From dd4ce589200851ee311b1f2f16855a7ec91d2d79 Mon Sep 17 00:00:00 2001 From: dillonj Date: Mon, 4 May 2026 18:39:36 -0600 Subject: [PATCH] fixed dropdown bar visibility --- backend/services/video_editor.py | 38 +++++++++++++----------- frontend/src/components/ExportDialog.tsx | 36 +++++++++++++++------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/backend/services/video_editor.py b/backend/services/video_editor.py index 4098cdd..894cd6d 100644 --- a/backend/services/video_editor.py +++ b/backend/services/video_editor.py @@ -13,6 +13,20 @@ from typing import List logger = logging.getLogger(__name__) +def _get_codec_args(format_hint: str, has_video: bool = True) -> list: + """Return FFmpeg codec arguments for the given format.""" + if format_hint == "wav": + return ["-c:a", "pcm_s16le"] + if format_hint == "webm": + if has_video: + return ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"] + return ["-c:a", "libopus", "-b:a", "160k"] + # Default: MP4 + if has_video: + return ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"] + return ["-c:a", "aac", "-b:a", "192k"] + + def _input_has_video_stream(ffmpeg_cmd: str, input_path: str) -> bool: """Return True if the input contains at least one video stream.""" ffprobe = ffmpeg_cmd.replace("ffmpeg", "ffprobe") @@ -135,7 +149,7 @@ def export_stream_copy( output_path on success """ if mute_ranges: - # Mute ranges require audio filtering, so fall back to re-encoding + # Mute ranges require audio filtering, so fall back to re-encode return export_reencode(input_path, output_path, keep_segments, "1080p", "mp4", mute_ranges) ffmpeg = _find_ffmpeg() if not _input_has_video_stream(ffmpeg, input_path): @@ -283,16 +297,14 @@ def export_reencode( filter_complex = "".join(filter_parts) - audio_codec_args = ["-c:a", "aac", "-b:a", "192k"] - if format_hint == "webm": - audio_codec_args = ["-c:a", "libopus", "-b:a", "160k"] + codec_args = _get_codec_args(format_hint, has_video=False) cmd = [ ffmpeg, "-y", "-i", input_path, "-filter_complex", filter_complex, "-map", audio_map, - *audio_codec_args, + *codec_args, output_path, ] @@ -324,9 +336,7 @@ def export_reencode( filter_complex = f"[0:a]{audio_filter}[a];[0:v]{video_filter}{video_map}" - codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"] - if format_hint == "webm": - codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"] + codec_args = _get_codec_args(format_hint, has_video) cmd = [ ffmpeg, "-y", @@ -385,9 +395,7 @@ def export_reencode( else: video_map = "[outv]" - codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"] - if format_hint == "webm": - codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"] + codec_args = _get_codec_args(format_hint, has_video) cmd = [ ffmpeg, "-y", @@ -489,9 +497,7 @@ def export_reencode_with_subs( filter_complex = f"[0:a]{audio_filter}[a];[0:v]{video_filter}[v]" - codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"] - if format_hint == "webm": - codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"] + codec_args = _get_codec_args(format_hint, has_video=True) cmd = [ ffmpeg, "-y", @@ -547,9 +553,7 @@ def export_reencode_with_subs( filter_complex += f";[outv]ass='{escaped_sub}'[outv_final]" video_map = "[outv_final]" - codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"] - if format_hint == "webm": - codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"] + codec_args = _get_codec_args(format_hint, has_video=True) cmd = [ ffmpeg, "-y", diff --git a/frontend/src/components/ExportDialog.tsx b/frontend/src/components/ExportDialog.tsx index 58dfdcf..5111aac 100644 --- a/frontend/src/components/ExportDialog.tsx +++ b/frontend/src/components/ExportDialog.tsx @@ -9,10 +9,15 @@ export default function ExportDialog() { const hasCuts = cutRanges.length > 0; + // Detect if input is audio-only by its extension + const audioExtensions = new Set(['.wav', '.mp3', '.flac', '.m4a', '.ogg', '.aac', '.wma']); + const inputExt = videoPath ? '.' + videoPath.split('.').pop()?.toLowerCase() : ''; + const isAudioOnly = videoPath ? audioExtensions.has(inputExt) : false; + const [options, setOptions] = useState & { normalizeAudio: boolean; normalizeTarget: number }>({ - mode: 'fast', + mode: isAudioOnly ? 'reencode' : 'fast', resolution: '1080p', - format: 'mp4', + format: isAudioOnly ? 'wav' : 'mp4', enhanceAudio: false, captions: 'none', normalizeAudio: false, @@ -20,16 +25,24 @@ export default function ExportDialog() { }); const [exportError, setExportError] = useState(null); + const HANDLE_EXPORT_filters = useCallback(() => { + const ext = options.format; + const nameMap: Record = { + mp4: 'MP4', + mov: 'MOV', + webm: 'WebM', + wav: 'WAV Audio', + }; + return [{ name: nameMap[ext] || 'File', extensions: [ext] }]; + }, [options.format]); + const handleExport = useCallback(async () => { if (!videoPath) return; + const defaultExt = options.format === 'wav' ? 'wav' : 'mp4'; const outputPath = await window.electronAPI?.saveFile({ - defaultPath: videoPath.replace(/\.[^.]+$/, '_edited.mp4'), - filters: [ - { name: 'MP4', extensions: ['mp4'] }, - { name: 'MOV', extensions: ['mov'] }, - { name: 'WebM', extensions: ['webm'] }, - ], + defaultPath: videoPath.replace(/\.[^.]+$/, `_edited.${defaultExt}`), + filters: HANDLE_EXPORT_filters(), }); if (!outputPath) return; @@ -96,7 +109,7 @@ export default function ExportDialog() { setExportError(err instanceof Error ? err.message : 'Export failed'); setExporting(false); } - }, [videoPath, options, backendUrl, setExporting, getKeepSegments, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, words]); + }, [videoPath, options, backendUrl, setExporting, getKeepSegments, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, words, HANDLE_EXPORT_filters]); return (
@@ -146,6 +159,7 @@ export default function ExportDialog() { { value: 'mp4', label: 'MP4 (H.264)' }, { value: 'mov', label: 'MOV (QuickTime)' }, { value: 'webm', label: 'WebM (VP9)' }, + ...(isAudioOnly ? [{ value: 'wav' as const, label: 'WAV (Uncompressed)' }] : []), ]} /> @@ -171,7 +185,7 @@ export default function ExportDialog() { onChange(e.target.value)} - className="w-full px-3 py-2 bg-editor-surface border border-editor-border rounded-lg text-xs text-editor-text focus:outline-none focus:border-editor-accent" + className="w-full px-3 py-2 bg-editor-surface border border-editor-border rounded-lg text-xs text-editor-text focus:outline-none focus:border-editor-accent [color-scheme:dark]" > {options.map((opt) => (