implemented the lower priority features; haven't tested them
This commit is contained in:
@ -1,10 +1,10 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Download, Loader2, Zap, Cog, Info, Volume2, FileText } from 'lucide-react';
|
||||
import { Download, Loader2, Zap, Cog, Info, Volume2, FileText, ZoomIn, Video, Music } from 'lucide-react';
|
||||
import type { ExportOptions } from '../types/project';
|
||||
|
||||
export default function ExportDialog() {
|
||||
const { videoPath, words, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments } =
|
||||
const { videoPath, words, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments, additionalClips, backgroundMusic } =
|
||||
useEditorStore();
|
||||
|
||||
const hasCuts = cutRanges.length > 0;
|
||||
@ -22,6 +22,10 @@ export default function ExportDialog() {
|
||||
captions: 'none',
|
||||
normalizeAudio: false,
|
||||
normalizeTarget: -14,
|
||||
zoom: { enabled: false, zoomFactor: 1.25, panX: 0, panY: 0 },
|
||||
removeBackground: false,
|
||||
backgroundReplacement: 'blur',
|
||||
backgroundReplacementValue: '',
|
||||
});
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
const [transcriptFormat, setTranscriptFormat] = useState<'txt' | 'srt'>('txt');
|
||||
@ -147,27 +151,51 @@ export default function ExportDialog() {
|
||||
speed: r.speed,
|
||||
}));
|
||||
|
||||
const body: Record<string, any> = {
|
||||
input_path: videoPath,
|
||||
output_path: outputPath,
|
||||
keep_segments: keepSegments,
|
||||
mute_ranges: muteRanges.length > 0 ? muteRanges.map((r) => ({ start: r.start, end: r.end })) : undefined,
|
||||
gain_ranges: backendGainRanges.length > 0 ? backendGainRanges : undefined,
|
||||
speed_ranges: backendSpeedRanges.length > 0 ? backendSpeedRanges : undefined,
|
||||
global_gain_db: globalGainDb,
|
||||
words: options.captions !== 'none' ? words : undefined,
|
||||
deleted_indices: options.captions !== 'none' ? [...deletedSet] : undefined,
|
||||
mode: options.mode,
|
||||
resolution: options.resolution,
|
||||
format: options.format,
|
||||
enhanceAudio: options.enhanceAudio,
|
||||
normalize_loudness: options.normalizeAudio,
|
||||
normalize_target_lufs: options.normalizeTarget,
|
||||
captions: options.captions,
|
||||
};
|
||||
|
||||
// Zoom
|
||||
if (options.zoom?.enabled) {
|
||||
body.zoom = options.zoom;
|
||||
}
|
||||
|
||||
// Additional clips
|
||||
if (additionalClips.length > 0) {
|
||||
body.additional_clips = additionalClips.map((c) => c.path);
|
||||
}
|
||||
|
||||
// Background music
|
||||
if (backgroundMusic) {
|
||||
body.background_music = backgroundMusic;
|
||||
}
|
||||
|
||||
// Background removal
|
||||
if (options.removeBackground) {
|
||||
body.remove_background = true;
|
||||
body.background_replacement = options.backgroundReplacement || 'blur';
|
||||
body.background_replacement_value = options.backgroundReplacementValue || '';
|
||||
}
|
||||
|
||||
const res = await fetch(`${backendUrl}/export`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
input_path: videoPath,
|
||||
output_path: outputPath,
|
||||
keep_segments: keepSegments,
|
||||
mute_ranges: muteRanges.length > 0 ? muteRanges.map((r) => ({ start: r.start, end: r.end })) : undefined,
|
||||
gain_ranges: backendGainRanges.length > 0 ? backendGainRanges : undefined,
|
||||
speed_ranges: backendSpeedRanges.length > 0 ? backendSpeedRanges : undefined,
|
||||
global_gain_db: globalGainDb,
|
||||
words: options.captions !== 'none' ? words : undefined,
|
||||
deleted_indices: options.captions !== 'none' ? [...deletedSet] : undefined,
|
||||
mode: options.mode,
|
||||
resolution: options.resolution,
|
||||
format: options.format,
|
||||
enhanceAudio: options.enhanceAudio,
|
||||
normalize_loudness: options.normalizeAudio,
|
||||
normalize_target_lufs: options.normalizeTarget,
|
||||
captions: options.captions,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let detail = res.statusText;
|
||||
@ -185,7 +213,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, HANDLE_EXPORT_filters]);
|
||||
}, [videoPath, options, backendUrl, setExporting, getKeepSegments, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, words, HANDLE_EXPORT_filters, additionalClips, backgroundMusic]);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-5">
|
||||
@ -239,6 +267,139 @@ export default function ExportDialog() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Video zoom / punch-in */}
|
||||
<div className="space-y-2 pt-1 border-t border-editor-border">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.zoom?.enabled || false}
|
||||
onChange={(e) => setOptions((o) => ({ ...o, zoom: { ...o.zoom!, enabled: e.target.checked } }))}
|
||||
className="w-4 h-4 rounded bg-editor-surface border-editor-border accent-editor-accent"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-medium flex items-center gap-1">
|
||||
<ZoomIn className="w-3 h-3" />
|
||||
Video zoom / punch-in
|
||||
</span>
|
||||
<p className="text-[10px] text-editor-text-muted">
|
||||
Crop and zoom into the center of the video. Requires re-encode.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
{options.zoom?.enabled && (
|
||||
<div className="pl-6 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-editor-text-muted w-16">Zoom:</span>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={3}
|
||||
step={0.05}
|
||||
value={options.zoom?.zoomFactor || 1}
|
||||
onChange={(e) => setOptions((o) => ({ ...o, zoom: { ...o.zoom!, zoomFactor: Number(e.target.value) } }))}
|
||||
className="flex-1 h-1.5"
|
||||
/>
|
||||
<span className="text-xs text-editor-text w-10 text-right">{options.zoom?.zoomFactor?.toFixed(2)}x</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-editor-text-muted w-16">Pan X:</span>
|
||||
<input
|
||||
type="range"
|
||||
min={-1}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={options.zoom?.panX || 0}
|
||||
onChange={(e) => setOptions((o) => ({ ...o, zoom: { ...o.zoom!, panX: Number(e.target.value) } }))}
|
||||
className="flex-1 h-1.5"
|
||||
/>
|
||||
<span className="text-xs text-editor-text w-10 text-right">{((options.zoom?.panX || 0) * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-editor-text-muted w-16">Pan Y:</span>
|
||||
<input
|
||||
type="range"
|
||||
min={-1}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={options.zoom?.panY || 0}
|
||||
onChange={(e) => setOptions((o) => ({ ...o, zoom: { ...o.zoom!, panY: Number(e.target.value) } }))}
|
||||
className="flex-1 h-1.5"
|
||||
/>
|
||||
<span className="text-xs text-editor-text w-10 text-right">{((options.zoom?.panY || 0) * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Background removal */}
|
||||
{!isAudioOnly && (
|
||||
<div className="space-y-2 pt-1 border-t border-editor-border">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.removeBackground || false}
|
||||
onChange={(e) => setOptions((o) => ({ ...o, removeBackground: e.target.checked }))}
|
||||
className="w-4 h-4 rounded bg-editor-surface border-editor-border accent-editor-accent"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-medium flex items-center gap-1">
|
||||
<Video className="w-3 h-3" />
|
||||
Remove background
|
||||
</span>
|
||||
<p className="text-[10px] text-editor-text-muted">
|
||||
Replace or blur the background. Uses MediaPipe if available.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
{options.removeBackground && (
|
||||
<div className="pl-6 space-y-2">
|
||||
<SelectField
|
||||
label="Background replacement"
|
||||
value={options.backgroundReplacement || 'blur'}
|
||||
onChange={(v) => setOptions((o) => ({ ...o, backgroundReplacement: v as 'blur' | 'color' | 'image' }))}
|
||||
options={[
|
||||
{ value: 'blur', label: 'Blur background' },
|
||||
{ value: 'color', label: 'Solid color' },
|
||||
{ value: 'image', label: 'Custom image' },
|
||||
]}
|
||||
/>
|
||||
{options.backgroundReplacement === 'color' && (
|
||||
<input
|
||||
type="text"
|
||||
value={options.backgroundReplacementValue || '#00FF00'}
|
||||
onChange={(e) => setOptions((o) => ({ ...o, backgroundReplacementValue: e.target.value }))}
|
||||
placeholder="#00FF00"
|
||||
className="w-full 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]"
|
||||
/>
|
||||
)}
|
||||
{options.backgroundReplacement === 'image' && (
|
||||
<p className="text-[10px] text-editor-text-muted">Place a background image file path above.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Background music track info */}
|
||||
{backgroundMusic && (
|
||||
<div className="pt-1 border-t border-editor-border">
|
||||
<div className="flex items-center gap-1.5 text-xs text-editor-accent">
|
||||
<Music className="w-3 h-3" />
|
||||
Background music: {backgroundMusic.path.split(/[/\\]/).pop()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Append clips info */}
|
||||
{additionalClips.length > 0 && (
|
||||
<div className="pt-1 border-t border-editor-border">
|
||||
<div className="flex items-center gap-1.5 text-xs text-editor-accent">
|
||||
<Video className="w-3 h-3" />
|
||||
{additionalClips.length} additional clip{additionalClips.length > 1 ? 's' : ''} appended
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio normalization — integrated into export */}
|
||||
<div className="space-y-2 pt-1 border-t border-editor-border">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user