able to process audio with different model; new project button

This commit is contained in:
2026-04-11 19:42:30 -06:00
parent b8ec396ebd
commit 0df967507f
3 changed files with 120 additions and 2 deletions

View File

@ -20,6 +20,8 @@ import {
Save, Save,
Scissors, Scissors,
VolumeX, VolumeX,
FilePlus2,
RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
const IS_ELECTRON = !!window.electronAPI; const IS_ELECTRON = !!window.electronAPI;
@ -31,12 +33,14 @@ export default function App() {
const { const {
videoPath, videoPath,
words, words,
transcriptionModel,
isTranscribing, isTranscribing,
transcriptionProgress, transcriptionProgress,
transcriptionStatus, transcriptionStatus,
loadVideo, loadVideo,
setBackendUrl, setBackendUrl,
setTranscription, setTranscription,
setTranscriptionModel,
setTranscribing, setTranscribing,
backendUrl, backendUrl,
selectedWordIndices, selectedWordIndices,
@ -49,6 +53,7 @@ export default function App() {
const [whisperModel, setWhisperModel] = useState('base'); const [whisperModel, setWhisperModel] = useState('base');
const [cutMode, setCutMode] = useState(false); const [cutMode, setCutMode] = useState(false);
const [muteMode, setMuteMode] = useState(false); const [muteMode, setMuteMode] = useState(false);
const [showReprocessConfirm, setShowReprocessConfirm] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
useKeyboardShortcuts(); useKeyboardShortcuts();
@ -137,6 +142,17 @@ export default function App() {
} }
}; };
const handleNewProject = () => {
const shouldReset = window.confirm('Start a new project? Unsaved changes in the current session will be lost.');
if (!shouldReset) return;
useEditorStore.getState().reset();
setActivePanel(null);
setManualPath('');
setCutMode(false);
setMuteMode(false);
};
const handleManualSubmit = async (e: React.FormEvent) => { const handleManualSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const path = manualPath.trim(); const path = manualPath.trim();
@ -177,6 +193,7 @@ export default function App() {
setTranscribing(true, 20, 'Transcribing audio...'); setTranscribing(true, 20, 'Transcribing audio...');
const data = await window.electronAPI.transcribe(path, whisperModel); const data = await window.electronAPI.transcribe(path, whisperModel);
setTranscription(data); setTranscription(data);
setTranscriptionModel(whisperModel);
} catch (err) { } catch (err) {
console.error('Transcription error:', err); console.error('Transcription error:', err);
alert(`Transcription failed. Check the console for details.\n\n${err}`); alert(`Transcription failed. Check the console for details.\n\n${err}`);
@ -185,6 +202,22 @@ export default function App() {
} }
}; };
const handleReprocessProject = async () => {
if (!videoPath) return;
if (!window.electronAPI?.transcribe) {
alert('Reprocessing is only available in desktop mode.');
return;
}
setShowReprocessConfirm(true);
};
const confirmReprocessProject = async () => {
if (!videoPath) return;
setShowReprocessConfirm(false);
await transcribeVideo(videoPath);
};
const togglePanel = (panel: Panel) => { const togglePanel = (panel: Panel) => {
setActivePanel((prev) => (prev === panel ? null : panel)); setActivePanel((prev) => (prev === panel ? null : panel));
}; };
@ -326,8 +359,18 @@ export default function App() {
<span className="text-sm font-medium truncate max-w-[300px]"> <span className="text-sm font-medium truncate max-w-[300px]">
{videoPath.split(/[\\/]/).pop()} {videoPath.split(/[\\/]/).pop()}
</span> </span>
{transcriptionModel && (
<span className="px-2 py-0.5 rounded border border-editor-border bg-editor-surface text-[10px] uppercase tracking-wide text-editor-text-muted">
Model: {transcriptionModel}
</span>
)}
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-2">
<ToolbarButton
icon={<FilePlus2 className="w-4 h-4" />}
label="New"
onClick={handleNewProject}
/>
<ToolbarButton <ToolbarButton
icon={<FolderOpen className="w-4 h-4" />} icon={<FolderOpen className="w-4 h-4" />}
label="Open" label="Open"
@ -367,6 +410,42 @@ export default function App() {
onClick={() => togglePanel('silence')} onClick={() => togglePanel('silence')}
disabled={!videoPath} disabled={!videoPath}
/> />
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-editor-surface border border-editor-border">
<select
value={whisperModel}
onChange={(e) => setWhisperModel(e.target.value)}
className="bg-transparent text-xs text-editor-text focus:outline-none"
title="Transcription model"
>
<optgroup label="Multilingual">
<option value="tiny">tiny</option>
<option value="base">base</option>
<option value="small">small</option>
<option value="medium">medium</option>
<option value="large-v2">large-v2</option>
<option value="large-v3">large-v3</option>
<option value="large-v3-turbo">large-v3-turbo</option>
<option value="distil-large-v3">distil-large-v3</option>
</optgroup>
<optgroup label="English">
<option value="tiny.en">tiny.en</option>
<option value="base.en">base.en</option>
<option value="small.en">small.en</option>
<option value="medium.en">medium.en</option>
<option value="distil-small.en">distil-small.en</option>
<option value="distil-medium.en">distil-medium.en</option>
</optgroup>
</select>
<button
onClick={handleReprocessProject}
disabled={isTranscribing || !videoPath}
title="Reprocess transcript with selected model"
className="flex items-center gap-1 px-2 py-1 rounded text-xs text-editor-text-muted hover:text-editor-text hover:bg-editor-bg disabled:opacity-40 disabled:cursor-not-allowed"
>
<RefreshCw className={`w-3 h-3 ${isTranscribing ? 'animate-spin' : ''}`} />
Reprocess
</button>
</div>
<ToolbarButton <ToolbarButton
icon={<Sparkles className="w-4 h-4" />} icon={<Sparkles className="w-4 h-4" />}
label="AI" label="AI"
@ -449,6 +528,37 @@ export default function App() {
)} )}
</div> </div>
{import.meta.env.DEV && <DevPanel />} {import.meta.env.DEV && <DevPanel />}
{showReprocessConfirm && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 px-4"
onClick={() => setShowReprocessConfirm(false)}
>
<div
className="w-full max-w-md rounded-xl border border-editor-border bg-editor-bg p-4 space-y-3"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-sm font-semibold">Reprocess transcript?</h3>
<p className="text-xs text-editor-text-muted leading-relaxed">
This will reprocess the current file with <span className="text-editor-text font-medium">{whisperModel}</span> and replace the current transcript words and timings.
</p>
<div className="flex items-center justify-end gap-2 pt-1">
<button
onClick={() => setShowReprocessConfirm(false)}
className="px-3 py-1.5 rounded-md text-xs text-editor-text-muted hover:text-editor-text hover:bg-editor-surface"
>
Cancel
</button>
<button
onClick={confirmReprocessProject}
className="px-3 py-1.5 rounded-md text-xs bg-editor-accent hover:bg-editor-accent-hover text-white"
>
Reprocess Now
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -23,6 +23,7 @@ interface EditorState {
cutRanges: CutRange[]; cutRanges: CutRange[];
muteRanges: MuteRange[]; muteRanges: MuteRange[];
silenceTrimGroups: SilenceTrimGroup[]; silenceTrimGroups: SilenceTrimGroup[];
transcriptionModel: string | null;
language: string; language: string;
currentTime: number; currentTime: number;
@ -45,6 +46,7 @@ interface EditorActions {
setBackendUrl: (url: string) => void; setBackendUrl: (url: string) => void;
loadVideo: (path: string) => void; loadVideo: (path: string) => void;
setExportedAudioPath: (path: string | null) => void; setExportedAudioPath: (path: string | null) => void;
setTranscriptionModel: (model: string | null) => void;
saveProject: () => ProjectFile; saveProject: () => ProjectFile;
setTranscription: (result: TranscriptionResult) => void; setTranscription: (result: TranscriptionResult) => void;
setCurrentTime: (time: number) => void; setCurrentTime: (time: number) => void;
@ -85,6 +87,7 @@ const initialState: EditorState = {
cutRanges: [], cutRanges: [],
muteRanges: [], muteRanges: [],
silenceTrimGroups: [], silenceTrimGroups: [],
transcriptionModel: null,
language: '', language: '',
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
@ -136,8 +139,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
setExportedAudioPath: (path) => set({ exportedAudioPath: path }), setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
setTranscriptionModel: (model) => set({ transcriptionModel: model }),
saveProject: (): ProjectFile => { saveProject: (): ProjectFile => {
const { videoPath, words, segments, deletedRanges, cutRanges, muteRanges, silenceTrimGroups, language, exportedAudioPath } = get(); const { videoPath, words, segments, deletedRanges, cutRanges, muteRanges, silenceTrimGroups, transcriptionModel, language, exportedAudioPath } = get();
if (!videoPath) throw new Error('No video loaded'); if (!videoPath) throw new Error('No video loaded');
const now = new Date().toISOString(); const now = new Date().toISOString();
// Strip globalStartIndex (runtime-only field) before persisting // Strip globalStartIndex (runtime-only field) before persisting
@ -146,6 +151,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
version: 1, version: 1,
videoPath, videoPath,
exportedAudioPath: exportedAudioPath ?? undefined, exportedAudioPath: exportedAudioPath ?? undefined,
transcriptionModel: transcriptionModel ?? undefined,
words, words,
segments: persistSegments as unknown as Segment[], segments: persistSegments as unknown as Segment[],
deletedRanges, deletedRanges,
@ -410,6 +416,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
cutRanges: data.cutRanges || [], cutRanges: data.cutRanges || [],
muteRanges: data.muteRanges || [], muteRanges: data.muteRanges || [],
silenceTrimGroups: data.silenceTrimGroups || [], silenceTrimGroups: data.silenceTrimGroups || [],
transcriptionModel: data.transcriptionModel ?? null,
language: data.language || '', language: data.language || '',
exportedAudioPath: data.exportedAudioPath ?? null, exportedAudioPath: data.exportedAudioPath ?? null,
}); });

View File

@ -58,6 +58,7 @@ export interface ProjectFile {
version: 1; version: 1;
videoPath: string; videoPath: string;
exportedAudioPath?: string; // path to modified/processed audio if it exists exportedAudioPath?: string; // path to modified/processed audio if it exists
transcriptionModel?: string;
words: Word[]; words: Word[];
segments: Segment[]; segments: Segment[];
deletedRanges: DeletedRange[]; deletedRanges: DeletedRange[];