From 4d3d8a221869245b15fec3eb1d1c244aeeb42d6d Mon Sep 17 00:00:00 2001 From: dillonj Date: Wed, 15 Apr 2026 20:17:05 -0600 Subject: [PATCH] speed zones work now --- frontend/src/App.tsx | 17 ++- frontend/src/components/ExportDialog.tsx | 13 +- frontend/src/components/TranscriptEditor.tsx | 43 +----- frontend/src/components/WaveformTimeline.tsx | 15 +-- frontend/src/hooks/useKeyboardShortcuts.ts | 1 - frontend/src/hooks/useVideoSync.ts | 131 +++++++++++++------ frontend/src/store/editorStore.ts | 56 ++------ frontend/src/types/project.ts | 6 - 8 files changed, 127 insertions(+), 155 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d4b30a..35e5bbc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -37,7 +37,6 @@ export default function App() { exportedAudioPath, words, segments, - deletedRanges, cutRanges, muteRanges, gainRanges, @@ -61,6 +60,7 @@ export default function App() { } = useEditorStore(); const [activePanel, setActivePanel] = useState(null); + const [projectName, setProjectName] = useState(null); const [whisperModel, setWhisperModel] = useState('base'); useEffect(() => { if (transcriptionModel) setWhisperModel(transcriptionModel); }, [transcriptionModel]); const [cutMode, setCutMode] = useState(false); @@ -81,7 +81,6 @@ export default function App() { exportedAudioPath, words, segments, - deletedRanges, cutRanges, muteRanges, gainRanges, @@ -96,7 +95,6 @@ export default function App() { exportedAudioPath, words, segments, - deletedRanges, cutRanges, muteRanges, gainRanges, @@ -116,8 +114,7 @@ export default function App() { exportedAudioPath: data.exportedAudioPath ?? null, words: data.words || [], segments: data.segments || [], - deletedRanges: data.deletedRanges || [], - cutRanges: data.cutRanges || [], + cutRanges: [...(data.cutRanges || []), ...(data.deletedRanges || []).map((r: any) => ({ id: r.id, start: r.start, end: r.end }))], muteRanges: data.muteRanges || [], gainRanges: data.gainRanges || [], speedRanges: data.speedRanges || [], @@ -183,6 +180,7 @@ export default function App() { const content = await window.electronAPI!.readFile(projectPath); const data = JSON.parse(content); loadProjectFromData(data); + setProjectName(projectPath.split(/[/\\]/).pop()?.replace(/\.aive$/i, '') ?? null); } catch (err) { console.error('Failed to load project:', err); alert(`Failed to load project: ${err}`); @@ -197,6 +195,7 @@ export default function App() { const data = useEditorStore.getState().saveProject(); const path = savePath.endsWith('.aive') ? savePath : `${savePath}.aive`; await window.electronAPI!.writeFile(path, JSON.stringify(data, null, 2)); + setProjectName(path.split(/[/\\]/).pop()?.replace(/\.aive$/i, '') ?? null); if (projectSignature) { setLastSavedSignature(projectSignature); } @@ -599,7 +598,13 @@ export default function App() {
{videoPath && (
- {videoPath.split(/[\/]/).pop()} + {projectName && ( + {projectName} + )} + {videoPath.split(/[/\\]/).pop()} + + {words.length} words · {cutRanges.length} cuts · {muteRanges.length} mutes · {gainRanges.length} gains · {speedRanges.length} speeds + {transcriptionModel && ( {transcriptionModel} diff --git a/frontend/src/components/ExportDialog.tsx b/frontend/src/components/ExportDialog.tsx index 4e22de9..1d530ae 100644 --- a/frontend/src/components/ExportDialog.tsx +++ b/frontend/src/components/ExportDialog.tsx @@ -4,10 +4,10 @@ import { Download, Loader2, Zap, Cog, Info } from 'lucide-react'; import type { ExportOptions } from '../types/project'; export default function ExportDialog() { - const { videoPath, words, deletedRanges, muteRanges, gainRanges, speedRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments } = + const { videoPath, words, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments } = useEditorStore(); - const hasCuts = deletedRanges.length > 0; + const hasCuts = cutRanges.length > 0; const [options, setOptions] = useState>({ mode: 'fast', @@ -35,8 +35,11 @@ export default function ExportDialog() { const keepSegments = getKeepSegments(); const deletedSet = new Set(); - for (const range of deletedRanges) { - for (const idx of range.wordIndices) deletedSet.add(idx); + for (const range of cutRanges) { + for (let i = 0; i < words.length; i++) { + const w = words[i]; + if (w.start >= range.start && w.end <= range.end) deletedSet.add(i); + } } const res = await fetch(`${backendUrl}/export`, { @@ -61,7 +64,7 @@ export default function ExportDialog() { console.error('Export error:', err); setExporting(false); } - }, [videoPath, options, backendUrl, setExporting, getKeepSegments, deletedRanges, muteRanges, gainRanges, speedRanges, globalGainDb, words]); + }, [videoPath, options, backendUrl, setExporting, getKeepSegments, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, words]); return (
diff --git a/frontend/src/components/TranscriptEditor.tsx b/frontend/src/components/TranscriptEditor.tsx index 04de44d..ae38c04 100644 --- a/frontend/src/components/TranscriptEditor.tsx +++ b/frontend/src/components/TranscriptEditor.tsx @@ -22,7 +22,6 @@ export default function TranscriptEditor({ }: TranscriptEditorProps) { const words = useEditorStore((s) => s.words); const segments = useEditorStore((s) => s.segments); - const deletedRanges = useEditorStore((s) => s.deletedRanges); const cutRanges = useEditorStore((s) => s.cutRanges); const muteRanges = useEditorStore((s) => s.muteRanges); const gainRanges = useEditorStore((s) => s.gainRanges); @@ -31,7 +30,6 @@ export default function TranscriptEditor({ const hoveredWordIndex = useEditorStore((s) => s.hoveredWordIndex); const setSelectedWordIndices = useEditorStore((s) => s.setSelectedWordIndices); const setHoveredWordIndex = useEditorStore((s) => s.setHoveredWordIndex); - const restoreRange = useEditorStore((s) => s.restoreRange); const removeCutRange = useEditorStore((s) => s.removeCutRange); const removeMuteRange = useEditorStore((s) => s.removeMuteRange); const removeGainRange = useEditorStore((s) => s.removeGainRange); @@ -48,14 +46,6 @@ export default function TranscriptEditor({ const zoneDragStart = useRef(null); const [zoneDragRange, setZoneDragRange] = useState<{ start: number; end: number } | null>(null); - const deletedSet = useMemo(() => { - const s = new Set(); - for (const range of deletedRanges) { - for (const idx of range.wordIndices) s.add(idx); - } - return s; - }, [deletedRanges]); - const selectedSet = useMemo(() => new Set(selectedWordIndices), [selectedWordIndices]); const [activeWordIndex, setActiveWordIndex] = useState(-1); @@ -170,11 +160,6 @@ export default function TranscriptEditor({ [setSelectedWordIndices], ); - const getRangeForWord = useCallback( - (wordIndex: number) => deletedRanges.find((r) => r.wordIndices.includes(wordIndex)), - [deletedRanges], - ); - const cutSelectedWords = useCallback(() => { if (selectedWordIndices.length === 0) return; const sorted = [...selectedWordIndices].sort((a, b) => a - b); @@ -257,14 +242,12 @@ export default function TranscriptEditor({

{segment.words.map((word, localIndex) => { const globalIndex = (segment.globalStartIndex ?? 0) + localIndex; - const isDeleted = deletedSet.has(globalIndex); const isSelected = selectedSet.has(globalIndex); const isActive = globalIndex === activeWordIndex; const isHovered = globalIndex === hoveredWordIndex; const isZoneDragSelected = zoneDragRange ? globalIndex >= zoneDragRange.start && globalIndex <= zoneDragRange.end : false; - const deletedRange = isDeleted ? getRangeForWord(globalIndex) : null; const cutRange = getCutRangeForWord(globalIndex); const muteRange = getMuteRangeForWord(globalIndex); const gainRange = getGainRangeForWord(globalIndex); @@ -281,7 +264,6 @@ export default function TranscriptEditor({ onMouseLeave={() => setHoveredWordIndex(null)} className={` relative px-[2px] py-[1px] rounded cursor-pointer transition-colors - ${isDeleted ? 'line-through text-editor-text-muted/40 bg-editor-word-deleted' : ''} ${cutRange ? 'bg-red-500/20 text-red-100' : ''} ${muteRange ? 'bg-blue-500/20 text-blue-100' : ''} ${gainRange ? 'bg-amber-500/20 text-amber-100' : ''} @@ -290,23 +272,12 @@ export default function TranscriptEditor({ ${isZoneDragSelected && muteMode ? 'bg-blue-500/30 ring-1 ring-blue-400/60' : ''} ${isZoneDragSelected && gainMode ? 'bg-amber-500/30 ring-1 ring-amber-400/60' : ''} ${isZoneDragSelected && speedMode ? 'bg-emerald-500/30 ring-1 ring-emerald-400/60' : ''} - ${isSelected && !isDeleted && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-selected text-white' : ''} - ${isActive && !isDeleted && !isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/20 text-editor-accent' : ''} - ${isHovered && !isDeleted && !isSelected && !isActive && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-hover' : ''} + ${isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-selected text-white' : ''} + ${isActive && !isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/20 text-editor-accent' : ''} + ${isHovered && !isSelected && !isActive && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-hover' : ''} `} > {word.word}{' '} - {isDeleted && isHovered && deletedRange && ( - - )} {(cutRange || muteRange || gainRange || speedRange) && isHovered && (

); }, - [segments, deletedSet, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getRangeForWord, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, getSpeedRangeForWord, restoreRange, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange, zoneDragRange, cutMode, muteMode, gainMode, speedMode], + [segments, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, getSpeedRangeForWord, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange, zoneDragRange, cutMode, muteMode, gainMode, speedMode], ); return (
-
- - {words.length} words · {deletedRanges.length} cuts · {cutRanges.length} cut ranges · {muteRanges.length} mute ranges · {gainRanges.length} gain ranges - · {speedRanges.length} speed ranges - +
{selectedWordIndices.length > 0 && (