Media-Converter / app.py
Nymbo's picture
Update app.py
04d4e85 verified
raw
history blame
12.7 kB
# 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)