fixed dropdown bar visibility
This commit is contained in:
@ -13,6 +13,20 @@ from typing import List
|
|||||||
logger = logging.getLogger(__name__)
|
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:
|
def _input_has_video_stream(ffmpeg_cmd: str, input_path: str) -> bool:
|
||||||
"""Return True if the input contains at least one video stream."""
|
"""Return True if the input contains at least one video stream."""
|
||||||
ffprobe = ffmpeg_cmd.replace("ffmpeg", "ffprobe")
|
ffprobe = ffmpeg_cmd.replace("ffmpeg", "ffprobe")
|
||||||
@ -135,7 +149,7 @@ def export_stream_copy(
|
|||||||
output_path on success
|
output_path on success
|
||||||
"""
|
"""
|
||||||
if mute_ranges:
|
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)
|
return export_reencode(input_path, output_path, keep_segments, "1080p", "mp4", mute_ranges)
|
||||||
ffmpeg = _find_ffmpeg()
|
ffmpeg = _find_ffmpeg()
|
||||||
if not _input_has_video_stream(ffmpeg, input_path):
|
if not _input_has_video_stream(ffmpeg, input_path):
|
||||||
@ -283,16 +297,14 @@ def export_reencode(
|
|||||||
|
|
||||||
filter_complex = "".join(filter_parts)
|
filter_complex = "".join(filter_parts)
|
||||||
|
|
||||||
audio_codec_args = ["-c:a", "aac", "-b:a", "192k"]
|
codec_args = _get_codec_args(format_hint, has_video=False)
|
||||||
if format_hint == "webm":
|
|
||||||
audio_codec_args = ["-c:a", "libopus", "-b:a", "160k"]
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
ffmpeg, "-y",
|
ffmpeg, "-y",
|
||||||
"-i", input_path,
|
"-i", input_path,
|
||||||
"-filter_complex", filter_complex,
|
"-filter_complex", filter_complex,
|
||||||
"-map", audio_map,
|
"-map", audio_map,
|
||||||
*audio_codec_args,
|
*codec_args,
|
||||||
output_path,
|
output_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -324,9 +336,7 @@ def export_reencode(
|
|||||||
|
|
||||||
filter_complex = f"[0:a]{audio_filter}[a];[0:v]{video_filter}{video_map}"
|
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"]
|
codec_args = _get_codec_args(format_hint, has_video)
|
||||||
if format_hint == "webm":
|
|
||||||
codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"]
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
ffmpeg, "-y",
|
ffmpeg, "-y",
|
||||||
@ -385,9 +395,7 @@ def export_reencode(
|
|||||||
else:
|
else:
|
||||||
video_map = "[outv]"
|
video_map = "[outv]"
|
||||||
|
|
||||||
codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"]
|
codec_args = _get_codec_args(format_hint, has_video)
|
||||||
if format_hint == "webm":
|
|
||||||
codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"]
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
ffmpeg, "-y",
|
ffmpeg, "-y",
|
||||||
@ -489,9 +497,7 @@ def export_reencode_with_subs(
|
|||||||
|
|
||||||
filter_complex = f"[0:a]{audio_filter}[a];[0:v]{video_filter}[v]"
|
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"]
|
codec_args = _get_codec_args(format_hint, has_video=True)
|
||||||
if format_hint == "webm":
|
|
||||||
codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"]
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
ffmpeg, "-y",
|
ffmpeg, "-y",
|
||||||
@ -547,9 +553,7 @@ def export_reencode_with_subs(
|
|||||||
filter_complex += f";[outv]ass='{escaped_sub}'[outv_final]"
|
filter_complex += f";[outv]ass='{escaped_sub}'[outv_final]"
|
||||||
video_map = "[outv_final]"
|
video_map = "[outv_final]"
|
||||||
|
|
||||||
codec_args = ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-c:a", "aac", "-b:a", "192k"]
|
codec_args = _get_codec_args(format_hint, has_video=True)
|
||||||
if format_hint == "webm":
|
|
||||||
codec_args = ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"]
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
ffmpeg, "-y",
|
ffmpeg, "-y",
|
||||||
|
|||||||
@ -9,10 +9,15 @@ export default function ExportDialog() {
|
|||||||
|
|
||||||
const hasCuts = cutRanges.length > 0;
|
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<Omit<ExportOptions, 'outputPath'> & { normalizeAudio: boolean; normalizeTarget: number }>({
|
const [options, setOptions] = useState<Omit<ExportOptions, 'outputPath'> & { normalizeAudio: boolean; normalizeTarget: number }>({
|
||||||
mode: 'fast',
|
mode: isAudioOnly ? 'reencode' : 'fast',
|
||||||
resolution: '1080p',
|
resolution: '1080p',
|
||||||
format: 'mp4',
|
format: isAudioOnly ? 'wav' : 'mp4',
|
||||||
enhanceAudio: false,
|
enhanceAudio: false,
|
||||||
captions: 'none',
|
captions: 'none',
|
||||||
normalizeAudio: false,
|
normalizeAudio: false,
|
||||||
@ -20,16 +25,24 @@ export default function ExportDialog() {
|
|||||||
});
|
});
|
||||||
const [exportError, setExportError] = useState<string | null>(null);
|
const [exportError, setExportError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const HANDLE_EXPORT_filters = useCallback(() => {
|
||||||
|
const ext = options.format;
|
||||||
|
const nameMap: Record<string, string> = {
|
||||||
|
mp4: 'MP4',
|
||||||
|
mov: 'MOV',
|
||||||
|
webm: 'WebM',
|
||||||
|
wav: 'WAV Audio',
|
||||||
|
};
|
||||||
|
return [{ name: nameMap[ext] || 'File', extensions: [ext] }];
|
||||||
|
}, [options.format]);
|
||||||
|
|
||||||
const handleExport = useCallback(async () => {
|
const handleExport = useCallback(async () => {
|
||||||
if (!videoPath) return;
|
if (!videoPath) return;
|
||||||
|
|
||||||
|
const defaultExt = options.format === 'wav' ? 'wav' : 'mp4';
|
||||||
const outputPath = await window.electronAPI?.saveFile({
|
const outputPath = await window.electronAPI?.saveFile({
|
||||||
defaultPath: videoPath.replace(/\.[^.]+$/, '_edited.mp4'),
|
defaultPath: videoPath.replace(/\.[^.]+$/, `_edited.${defaultExt}`),
|
||||||
filters: [
|
filters: HANDLE_EXPORT_filters(),
|
||||||
{ name: 'MP4', extensions: ['mp4'] },
|
|
||||||
{ name: 'MOV', extensions: ['mov'] },
|
|
||||||
{ name: 'WebM', extensions: ['webm'] },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
if (!outputPath) return;
|
if (!outputPath) return;
|
||||||
|
|
||||||
@ -96,7 +109,7 @@ export default function ExportDialog() {
|
|||||||
setExportError(err instanceof Error ? err.message : 'Export failed');
|
setExportError(err instanceof Error ? err.message : 'Export failed');
|
||||||
setExporting(false);
|
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 (
|
return (
|
||||||
<div className="p-4 space-y-5">
|
<div className="p-4 space-y-5">
|
||||||
@ -146,6 +159,7 @@ export default function ExportDialog() {
|
|||||||
{ value: 'mp4', label: 'MP4 (H.264)' },
|
{ value: 'mp4', label: 'MP4 (H.264)' },
|
||||||
{ value: 'mov', label: 'MOV (QuickTime)' },
|
{ value: 'mov', label: 'MOV (QuickTime)' },
|
||||||
{ value: 'webm', label: 'WebM (VP9)' },
|
{ value: 'webm', label: 'WebM (VP9)' },
|
||||||
|
...(isAudioOnly ? [{ value: 'wav' as const, label: 'WAV (Uncompressed)' }] : []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -171,7 +185,7 @@ export default function ExportDialog() {
|
|||||||
<select
|
<select
|
||||||
value={options.normalizeTarget}
|
value={options.normalizeTarget}
|
||||||
onChange={(e) => setOptions((o) => ({ ...o, normalizeTarget: Number(e.target.value) }))}
|
onChange={(e) => setOptions((o) => ({ ...o, normalizeTarget: Number(e.target.value) }))}
|
||||||
className="flex-1 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:outline-none focus:border-editor-accent"
|
className="flex-1 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:outline-none focus:border-editor-accent [color-scheme:dark]"
|
||||||
>
|
>
|
||||||
<option value={-14}>YouTube (-14 LUFS)</option>
|
<option value={-14}>YouTube (-14 LUFS)</option>
|
||||||
<option value={-16}>Spotify (-16 LUFS)</option>
|
<option value={-16}>Spotify (-16 LUFS)</option>
|
||||||
@ -295,7 +309,7 @@ function SelectField({
|
|||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => 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) => (
|
{options.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>
|
<option key={opt.value} value={opt.value}>
|
||||||
|
|||||||
Reference in New Issue
Block a user