Spaces:
Sleeping
Sleeping
# File: app.py | |
import logging | |
import subprocess | |
from pprint import pprint | |
from tempfile import _TemporaryFileWrapper | |
from ffmpy import FFmpeg, FFRuntimeError | |
import gradio as gr | |
from functions import ( | |
Clear, | |
CommandBuilder, | |
audio_channels, | |
audio_codecs, | |
audio_quality, | |
audio_sample_rates, | |
change_clipbox, | |
containers, | |
set_custom_bitrate, | |
media_change, | |
presets, | |
supported_codecs, | |
supported_presets, | |
video_codecs, | |
VF, | |
) | |
# βββ Configure logging level βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
logging.basicConfig(level=logging.INFO) | |
def convert( | |
file: _TemporaryFileWrapper, | |
container_format: str, | |
prev_state: str | |
) -> tuple[str, str, str, str, str]: | |
""" | |
Convert the given media file to a new container format using FFmpeg. | |
Args: | |
file (_TemporaryFileWrapper): Uploaded input file wrapper (has .name). | |
container_format (str): Target container format (e.g., "mp4", "mp3"). | |
prev_state (str): Previous state token (for UI chaining). | |
Returns: | |
Tuple containing: | |
- audio_output_path (str) | |
- file_output_path (str) | |
- video_output_path (str) | |
- ffmpeg command run (str) | |
- new state token (str) | |
""" | |
# If no file was uploaded, bail out immediately | |
if file is None: | |
logging.error("No file provided for conversion.") | |
return [None, None, None, "No file provided", prev_state] | |
try: | |
# Extract base filename (without extension) | |
logging.info("File name: %s", file.name) | |
new_name, _ = file.name.rsplit(".", 1) | |
output_file = f"{new_name}_converted.{container_format.lower()}" | |
# Build FFmpeg command | |
ffmpeg_cmd_builder = FFmpeg( | |
inputs={file.name: None}, | |
outputs={output_file: None}, | |
global_options=["-y", "-hide_banner"], | |
) | |
print(ffmpeg_cmd_builder) # Debug: FFmpeg object | |
print(ffmpeg_cmd_builder.cmd) # Debug: actual shell command | |
# Run conversion | |
ffmpeg_cmd_builder.run(stderr=subprocess.PIPE) | |
ffmpeg_command_str = ffmpeg_cmd_builder.cmd | |
except FFRuntimeError as e: | |
# On error, decode stderr and return it as output_text | |
error_msg = e.stderr.decode() | |
print(error_msg, flush=True) | |
return [None, None, None, error_msg, prev_state] | |
# Update state and return all paths/outputs | |
new_state = output_file | |
return [ | |
output_file, # audio_output_path | |
output_file, # file_output_path | |
output_file, # video_output_path | |
ffmpeg_command_str, # ffmpeg command run | |
new_state # new state token | |
] | |
# βββ Custom CSS for the app ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
css = """ | |
body { | |
background: var(--body-background-fill); | |
} | |
""" | |
# βββ Build the Gradio interface ββββββββββββββββββββββββββββββββββββββββββββββββ | |
with gr.Blocks( | |
css=css, | |
theme=gr.themes.Soft( | |
primary_hue=gr.themes.colors.green, | |
secondary_hue=gr.themes.colors.amber, | |
neutral_hue=gr.themes.colors.slate, | |
font=["sans-serif"], | |
), | |
) as demo: | |
with gr.Tabs(selected="format", elem_classes="tabs"): | |
# ββ Format Tab ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
with gr.Tab("Format", id="format"): | |
with gr.Row(): | |
with gr.Column() as inputs: | |
file_input = gr.File(label="Upload File") # upload control | |
options = gr.Radio( | |
label="Container Format", | |
choices=containers, | |
value=containers[0], | |
) | |
with gr.Row() as inputs_clip: | |
clip = gr.Dropdown( | |
choices=["None", "Enabled"], label="Clip:", value="None" | |
) | |
start_time = gr.Textbox( | |
label="Start Time", placeholder="00:00", visible=False | |
) | |
stop_time = gr.Textbox( | |
label="Stop Time", placeholder="00:00", visible=False | |
) | |
with gr.Row(): | |
clearBtn = gr.Button("Clear") # reset inputs | |
convertBtn = gr.Button("Convert", variant="primary") # run conversion | |
with gr.Column(): | |
# Output modality buttons | |
video_button = gr.Button("Video") | |
audio_button = gr.Button("Audio") | |
file_button = gr.Button("File") | |
# Output components, initially hidden/shown appropriately | |
media_output_audio = gr.Audio(type="filepath", label="Audio", visible=False) | |
media_output_video = gr.Video(label="Video", visible=True, height=300) | |
media_output_file = gr.File(label="File", visible=False) | |
output_textbox = gr.Code( | |
value="$ echo 'Hello, World!'", | |
label="FFmpeg Command", | |
language="shell", | |
elem_id="outputtext", | |
) | |
# Instantiate and wire up clear/reset behavior | |
resetFormat = Clear(inputs, inputs_clip) | |
clearBtn.click( | |
fn=resetFormat.clear, | |
inputs=resetFormat(), | |
outputs=resetFormat(), | |
api_name="reset_format_tab", | |
description="Clear all inputs in the Format tab" | |
) | |
# Persistent state token for chaining | |
state = gr.State() | |
# Convert button β convert_media tool | |
convertBtn.click( | |
fn=convert, | |
inputs=[file_input, options, state], | |
outputs=[ | |
media_output_audio, | |
media_output_file, | |
media_output_video, | |
output_textbox, | |
state, | |
], | |
api_name="convert_media", | |
description="Convert an uploaded file to the selected format via FFmpeg" | |
) | |
# ββ Video Tab βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
with gr.Tab("Video", id="video"): | |
with gr.Row() as video_inputs: | |
video_options = gr.Dropdown( | |
label="Video Codec", choices=video_codecs, value=video_codecs[-1] | |
) | |
preset_options = gr.Dropdown( | |
label="Preset", choices=presets, value=presets[-1] | |
) | |
clearVidBtn = gr.Button("Clear") # reset video-specific inputs | |
videoReset = Clear(video_inputs) | |
clearVidBtn.click( | |
fn=videoReset.clear, | |
inputs=videoReset(), | |
outputs=videoReset(), | |
api_name="reset_video_tab", | |
description="Clear all inputs in the Video tab" | |
) | |
# ββ Audio Tab βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
with gr.Tab("Audio", id="audio"): | |
with gr.Row() as audio_inputs: | |
audio_options = gr.Dropdown( | |
label="Audio Codec", choices=audio_codecs, value=audio_codecs[-1] | |
) | |
audio_bitrate = gr.Dropdown( | |
label="Audio Quality", choices=audio_quality, value=audio_quality[0] | |
) | |
custom_bitrate = gr.Number( | |
label="Custom Bitrate", visible=False | |
) | |
gr.Dropdown( | |
label="Audio Channels", choices=audio_channels, value=audio_channels[0] | |
) | |
gr.Dropdown( | |
label="Sample Rate", choices=audio_sample_rates, value=audio_sample_rates[0] | |
) | |
clearAudBtn = gr.Button("Clear") | |
audioReset = Clear(audio_inputs) | |
clearAudBtn.click( | |
fn=audioReset.clear, | |
inputs=audioReset(), | |
outputs=audioReset(), | |
api_name="reset_audio_tab", | |
description="Clear all inputs in the Audio tab" | |
) | |
# ββ Filters Tab ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
with gr.Tab("Filters", id="filters") as filter_inputs: | |
gr.Markdown("## Video Filters") | |
with gr.Row(equal_height=True): | |
for vf in VF: | |
# each filter group β dropdown | |
name = list(vf.keys())[0] | |
choices = [opt["name"] for opt in vf[name]] | |
gr.Dropdown(label=name, choices=choices, value=choices[0]) | |
gr.Markdown("## Audio Filters") | |
acontrastSlider = gr.Slider(label="Audio Contrast") | |
clearFltBtn = gr.Button("Clear") | |
filterReset = Clear(filter_inputs, acontrastSlider) | |
clearFltBtn.click( | |
fn=filterReset.clear, | |
inputs=filterReset(), | |
outputs=filterReset(), | |
api_name="reset_filters_tab", | |
description="Clear all inputs in the Filters tab" | |
) | |
# βββ Cross-tab event listeners βββββββββββββββββββββββββββββββββββββββββββββββ | |
clip.change( | |
fn=change_clipbox, | |
inputs=[clip], | |
outputs=[start_time, stop_time], | |
api_name="toggle_clip_inputs", | |
description="Show or hide clip time fields when Clip option changes" | |
) | |
options.change( | |
fn=supported_codecs, | |
inputs=[options], | |
outputs=[video_options, audio_options], | |
api_name="update_codecs", | |
description="Update available video/audio codecs when container format changes" | |
) | |
video_options.change( | |
fn=supported_presets, | |
inputs=[video_options], | |
outputs=[preset_options], | |
api_name="update_presets", | |
description="Refresh preset list when video codec changes" | |
) | |
audio_bitrate.change( | |
fn=set_custom_bitrate, | |
inputs=[audio_bitrate], | |
outputs=[custom_bitrate], | |
api_name="set_custom_bitrate", | |
description="Toggle custom bitrate field based on selected audio quality" | |
) | |
# Single media_change handler for all three buttons | |
for btn, mode in [ | |
(audio_button, "audio"), | |
(video_button, "video"), | |
(file_button, "file"), | |
]: | |
btn.click( | |
fn=media_change, | |
inputs=[btn, state], | |
outputs=[media_output_audio, media_output_video, media_output_file], | |
api_name=f"switch_to_{mode}", | |
description=f"Switch displayed output to {mode}" | |
) | |
# βββ FFmpeg command updater listener ββββββββββββββββββββββββββββββββββββββββ | |
ffmpeg_commands = CommandBuilder( | |
inputs_clip, video_inputs, audio_inputs, filter_inputs, acontrastSlider | |
) | |
ffmpeg_commands.setup_listener() # start listening to UI changes | |
# pprint(ffmpeg_commands.commands) # debug: show commands | |
ffmpeg_commands.update(output_textbox) # populate initial command | |
# βββ Launch the app with MCP enabled βββββββββββββββββββββββββββββββββββββββββββ | |
if __name__ == "__main__": | |
demo.launch(show_error=True, max_threads=300, mcp_server=True) | |