able to process audio with different model; new project button
This commit is contained in:
@ -20,6 +20,8 @@ import {
|
||||
Save,
|
||||
Scissors,
|
||||
VolumeX,
|
||||
FilePlus2,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
|
||||
const IS_ELECTRON = !!window.electronAPI;
|
||||
@ -31,12 +33,14 @@ export default function App() {
|
||||
const {
|
||||
videoPath,
|
||||
words,
|
||||
transcriptionModel,
|
||||
isTranscribing,
|
||||
transcriptionProgress,
|
||||
transcriptionStatus,
|
||||
loadVideo,
|
||||
setBackendUrl,
|
||||
setTranscription,
|
||||
setTranscriptionModel,
|
||||
setTranscribing,
|
||||
backendUrl,
|
||||
selectedWordIndices,
|
||||
@ -49,6 +53,7 @@ export default function App() {
|
||||
const [whisperModel, setWhisperModel] = useState('base');
|
||||
const [cutMode, setCutMode] = useState(false);
|
||||
const [muteMode, setMuteMode] = useState(false);
|
||||
const [showReprocessConfirm, setShowReprocessConfirm] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
const path = manualPath.trim();
|
||||
@ -177,6 +193,7 @@ export default function App() {
|
||||
setTranscribing(true, 20, 'Transcribing audio...');
|
||||
const data = await window.electronAPI.transcribe(path, whisperModel);
|
||||
setTranscription(data);
|
||||
setTranscriptionModel(whisperModel);
|
||||
} catch (err) {
|
||||
console.error('Transcription error:', 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) => {
|
||||
setActivePanel((prev) => (prev === panel ? null : panel));
|
||||
};
|
||||
@ -326,8 +359,18 @@ export default function App() {
|
||||
<span className="text-sm font-medium truncate max-w-[300px]">
|
||||
{videoPath.split(/[\\/]/).pop()}
|
||||
</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 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
|
||||
icon={<FolderOpen className="w-4 h-4" />}
|
||||
label="Open"
|
||||
@ -367,6 +410,42 @@ export default function App() {
|
||||
onClick={() => togglePanel('silence')}
|
||||
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
|
||||
icon={<Sparkles className="w-4 h-4" />}
|
||||
label="AI"
|
||||
@ -449,6 +528,37 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ interface EditorState {
|
||||
cutRanges: CutRange[];
|
||||
muteRanges: MuteRange[];
|
||||
silenceTrimGroups: SilenceTrimGroup[];
|
||||
transcriptionModel: string | null;
|
||||
language: string;
|
||||
|
||||
currentTime: number;
|
||||
@ -45,6 +46,7 @@ interface EditorActions {
|
||||
setBackendUrl: (url: string) => void;
|
||||
loadVideo: (path: string) => void;
|
||||
setExportedAudioPath: (path: string | null) => void;
|
||||
setTranscriptionModel: (model: string | null) => void;
|
||||
saveProject: () => ProjectFile;
|
||||
setTranscription: (result: TranscriptionResult) => void;
|
||||
setCurrentTime: (time: number) => void;
|
||||
@ -85,6 +87,7 @@ const initialState: EditorState = {
|
||||
cutRanges: [],
|
||||
muteRanges: [],
|
||||
silenceTrimGroups: [],
|
||||
transcriptionModel: null,
|
||||
language: '',
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
@ -136,8 +139,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
|
||||
setExportedAudioPath: (path) => set({ exportedAudioPath: path }),
|
||||
|
||||
setTranscriptionModel: (model) => set({ transcriptionModel: model }),
|
||||
|
||||
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');
|
||||
const now = new Date().toISOString();
|
||||
// Strip globalStartIndex (runtime-only field) before persisting
|
||||
@ -146,6 +151,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
version: 1,
|
||||
videoPath,
|
||||
exportedAudioPath: exportedAudioPath ?? undefined,
|
||||
transcriptionModel: transcriptionModel ?? undefined,
|
||||
words,
|
||||
segments: persistSegments as unknown as Segment[],
|
||||
deletedRanges,
|
||||
@ -410,6 +416,7 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
cutRanges: data.cutRanges || [],
|
||||
muteRanges: data.muteRanges || [],
|
||||
silenceTrimGroups: data.silenceTrimGroups || [],
|
||||
transcriptionModel: data.transcriptionModel ?? null,
|
||||
language: data.language || '',
|
||||
exportedAudioPath: data.exportedAudioPath ?? null,
|
||||
});
|
||||
|
||||
@ -58,6 +58,7 @@ export interface ProjectFile {
|
||||
version: 1;
|
||||
videoPath: string;
|
||||
exportedAudioPath?: string; // path to modified/processed audio if it exists
|
||||
transcriptionModel?: string;
|
||||
words: Word[];
|
||||
segments: Segment[];
|
||||
deletedRanges: DeletedRange[];
|
||||
|
||||
Reference in New Issue
Block a user