able to re-transcribe
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Download, Loader2, Zap, Cog, Info, Volume2 } from 'lucide-react';
|
||||
import { Download, Loader2, Zap, Cog, Info, Volume2, FileText } from 'lucide-react';
|
||||
import type { ExportOptions } from '../types/project';
|
||||
|
||||
export default function ExportDialog() {
|
||||
@ -24,6 +24,82 @@ export default function ExportDialog() {
|
||||
normalizeTarget: -14,
|
||||
});
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
const [transcriptFormat, setTranscriptFormat] = useState<'txt' | 'srt'>('txt');
|
||||
const [isTranscribingTranscript, setIsTranscribingTranscript] = useState(false);
|
||||
|
||||
const handleTranscriptExport = useCallback(async () => {
|
||||
if (!videoPath || words.length === 0) return;
|
||||
|
||||
const defaultExt = transcriptFormat === 'srt' ? 'srt' : 'txt';
|
||||
const outputPath = await window.electronAPI?.saveFile({
|
||||
defaultPath: videoPath.replace(/\.[^.]+$/, `_transcript.${defaultExt}`),
|
||||
filters: transcriptFormat === 'srt'
|
||||
? [{ name: 'SRT Subtitles', extensions: ['srt'] }]
|
||||
: [{ name: 'Text File', extensions: ['txt'] }],
|
||||
});
|
||||
if (!outputPath) return;
|
||||
|
||||
setIsTranscribingTranscript(true);
|
||||
try {
|
||||
// Compute deleted word set
|
||||
const deletedSet = new Set<number>();
|
||||
for (const range of cutRanges) {
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
if (words[i].start >= range.start && words[i].end <= range.end) {
|
||||
deletedSet.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate content entirely on the frontend — no backend needed
|
||||
let content: string;
|
||||
if (transcriptFormat === 'srt') {
|
||||
const lines: string[] = [];
|
||||
let counter = 1;
|
||||
const activeWords: Array<[number, typeof words[0]]> = [];
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
if (!deletedSet.has(i)) activeWords.push([i, words[i]]);
|
||||
}
|
||||
const wordsPerLine = 8;
|
||||
for (let ci = 0; ci < activeWords.length; ci += wordsPerLine) {
|
||||
const chunk = activeWords.slice(ci, ci + wordsPerLine);
|
||||
if (chunk.length === 0) continue;
|
||||
const startTime = chunk[0][1].start;
|
||||
const endTime = chunk[chunk.length - 1][1].end;
|
||||
|
||||
const fmt = (s: number) => {
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = Math.floor(s % 60);
|
||||
const ms = Math.floor((s % 1) * 1000);
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')},${String(ms).padStart(3, '0')}`;
|
||||
};
|
||||
|
||||
lines.push(String(counter));
|
||||
lines.push(`${fmt(startTime)} --> ${fmt(endTime)}`);
|
||||
lines.push(chunk.map(([, w]) => w.word).join(' '));
|
||||
lines.push('');
|
||||
counter++;
|
||||
}
|
||||
content = lines.join('\n');
|
||||
} else {
|
||||
// Plain text
|
||||
const activeWords: string[] = [];
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
if (!deletedSet.has(i)) activeWords.push(words[i].word);
|
||||
}
|
||||
content = activeWords.join(' ');
|
||||
}
|
||||
|
||||
// Write directly via Tauri — instant, no backend round-trip
|
||||
await window.electronAPI?.writeFile(outputPath, content);
|
||||
} catch (err) {
|
||||
console.error('Transcript export error:', err);
|
||||
setExportError(err instanceof Error ? err.message : 'Transcript export failed');
|
||||
} finally {
|
||||
setIsTranscribingTranscript(false);
|
||||
}
|
||||
}, [videoPath, words, cutRanges, transcriptFormat]);
|
||||
|
||||
const HANDLE_EXPORT_filters = useCallback(() => {
|
||||
const ext = options.format;
|
||||
@ -220,7 +296,40 @@ export default function ExportDialog() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Export button */}
|
||||
{/* Transcript-only export */}
|
||||
<div className="space-y-2 pt-1 border-t border-editor-border">
|
||||
<h4 className="text-xs font-semibold flex items-center gap-1.5">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
Export Transcript Only
|
||||
</h4>
|
||||
<p className="text-[10px] text-editor-text-muted leading-relaxed">
|
||||
Export the edited transcript as plain text or SRT without rendering video.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={transcriptFormat}
|
||||
onChange={(e) => setTranscriptFormat(e.target.value as 'txt' | 'srt')}
|
||||
className="flex-1 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]"
|
||||
>
|
||||
<option value="txt">Plain Text (.txt)</option>
|
||||
<option value="srt">Subtitles (.srt)</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={handleTranscriptExport}
|
||||
disabled={isTranscribingTranscript || words.length === 0}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-editor-accent/20 text-editor-accent hover:bg-editor-accent/30 disabled:opacity-40 transition-colors"
|
||||
>
|
||||
{isTranscribingTranscript ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<FileText className="w-3 h-3" />
|
||||
)}
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export video button */}
|
||||
<button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || !videoPath}
|
||||
@ -234,7 +343,7 @@ export default function ExportDialog() {
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
Export
|
||||
Export Video
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user