diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/components/editor/panels/assets/views/captions.tsx index f22c0211..304fbf43 100644 --- a/apps/web/src/components/editor/panels/assets/views/captions.tsx +++ b/apps/web/src/components/editor/panels/assets/views/captions.tsx @@ -7,42 +7,41 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useState, useRef } from "react"; -import { extractTimelineAudio } from "@/lib/media/mediabunny"; -import { useEditor } from "@/hooks/use-editor"; +import { useState, useRef } from "react"; +import { extractTimelineAudio } from "@/lib/media/mediabunny"; +import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_TRANSCRIPTION_SAMPLE_RATE, TRANSCRIPTION_LANGUAGES, } from "@/constants/transcription-constants"; -import type { - TranscriptionLanguage, - TranscriptionProgress, -} from "@/lib/transcription/types"; -import { transcriptionService } from "@/services/transcription/service"; -import { decodeAudioToFloat32 } from "@/lib/media/audio"; -import { buildCaptionChunks } from "@/lib/transcription/caption"; -import { Spinner } from "@/components/ui/spinner"; -import { - Section, - SectionContent, - SectionField, - SectionFields, -} from "@/components/section"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { - AddTrackCommand, - BatchCommand, - InsertElementCommand, -} from "@/lib/commands"; - -export function Captions() { - const [selectedLanguage, setSelectedLanguage] = - useState("auto"); - const [isProcessing, setIsProcessing] = useState(false); - const [processingStep, setProcessingStep] = useState(""); - const [error, setError] = useState(null); - const containerRef = useRef(null); - const editor = useEditor(); +import type { + CaptionChunk, + TranscriptionLanguage, + TranscriptionProgress, +} from "@/lib/transcription/types"; +import { transcriptionService } from "@/services/transcription/service"; +import { decodeAudioToFloat32 } from "@/lib/media/audio"; +import { buildCaptionChunks } from "@/lib/transcription/caption"; +import { insertCaptionChunksAsTextTrack } from "@/lib/subtitles/insert"; +import { parseSrt } from "@/lib/subtitles/srt"; +import { Spinner } from "@/components/ui/spinner"; +import { + Section, + SectionContent, + SectionField, + SectionFields, +} from "@/components/section"; + +export function Captions() { + const [selectedLanguage, setSelectedLanguage] = + useState("auto"); + const [isProcessing, setIsProcessing] = useState(false); + const [processingStep, setProcessingStep] = useState(""); + const [error, setError] = useState(null); + const [warning, setWarning] = useState(null); + const containerRef = useRef(null); + const fileInputRef = useRef(null); + const editor = useEditor(); const handleProgress = (progress: TranscriptionProgress) => { if (progress.status === "loading-model") { @@ -53,10 +52,11 @@ export function Captions() { }; const handleGenerateTranscript = async () => { - try { - setIsProcessing(true); - setError(null); - setProcessingStep("Extracting audio..."); + try { + setIsProcessing(true); + setError(null); + setWarning(null); + setProcessingStep("Extracting audio..."); const audioBlob = await extractTimelineAudio({ tracks: editor.timeline.getTracks(), @@ -75,34 +75,11 @@ export function Captions() { language: selectedLanguage === "auto" ? undefined : selectedLanguage, onProgress: handleProgress, }); - - setProcessingStep("Generating captions..."); - const captionChunks = buildCaptionChunks({ segments: result.segments }); - - const addTrackCommand = new AddTrackCommand("text", 0); - const insertCommands = captionChunks.map( - (caption, i) => - new InsertElementCommand({ - placement: { - mode: "explicit", - trackId: addTrackCommand.getTrackId(), - }, - element: { - ...DEFAULTS.text.element, - name: `Caption ${i + 1}`, - content: caption.text, - duration: caption.duration, - startTime: caption.startTime, - fontSize: 65, - fontWeight: "bold", - }, - }), - ); - - editor.command.execute({ - command: new BatchCommand([addTrackCommand, ...insertCommands]), - }); - } catch (error) { + + setProcessingStep("Generating captions..."); + const captionChunks = buildCaptionChunks({ segments: result.segments }); + insertCaptionChunks({ captions: captionChunks }); + } catch (error) { console.error("Transcription failed:", error); setError( error instanceof Error ? error.message : "An unexpected error occurred", @@ -110,8 +87,78 @@ export function Captions() { } finally { setIsProcessing(false); setProcessingStep(""); - } - }; + } + }; + + const insertCaptionChunks = ({ + captions, + }: { + captions: CaptionChunk[]; + }) => { + const trackId = insertCaptionChunksAsTextTrack({ + editor, + captions, + }); + + if (!trackId) { + throw new Error("No captions were generated"); + } + }; + + const handleImportClick = () => { + fileInputRef.current?.click(); + }; + + const handleImportFile = async ({ + file, + }: { + file: File; + }) => { + try { + setIsProcessing(true); + setError(null); + setWarning(null); + setProcessingStep("Reading subtitle file..."); + + const input = await file.text(); + const result = parseSrt({ input }); + + if (result.captions.length === 0) { + throw new Error("No valid subtitle cues were found in the .srt file"); + } + + setProcessingStep("Importing subtitles..."); + insertCaptionChunks({ captions: result.captions }); + + if (result.skippedCueCount > 0) { + setWarning( + `Imported ${result.captions.length} subtitle cue(s) and skipped ${result.skippedCueCount} malformed cue(s).`, + ); + } + } catch (error) { + console.error("Subtitle import failed:", error); + setError( + error instanceof Error ? error.message : "An unexpected error occurred", + ); + } finally { + setIsProcessing(false); + setProcessingStep(""); + } + }; + + const handleFileChange = async ({ + event, + }: { + event: React.ChangeEvent; + }) => { + const file = event.target.files?.[0]; + if (event.target) { + event.target.value = ""; + } + if (!file) return; + + await handleImportFile({ file }); + }; const handleLanguageChange = ({ value }: { value: string }) => { if (value === "auto") { @@ -126,14 +173,21 @@ export function Captions() { setSelectedLanguage(matchedLanguage.code); }; - return ( - -
- + return ( + + void handleFileChange({ event })} + /> +
+