able to process audio with different model; new project button
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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[];
|
||||||
|
|||||||
Reference in New Issue
Block a user