From 92fd8bac6f2fd1281297102119e5f8cf8fec53b6 Mon Sep 17 00:00:00 2001 From: kcfancher Date: Thu, 2 Apr 2026 07:44:40 -0400 Subject: [PATCH] Update Captions Panel UI Split captions panel into Generate and Import tabs as they are two different approaches for adding captions. --- .../editor/panels/assets/views/captions.tsx | 266 ++++++++++-------- 1 file changed, 146 insertions(+), 120 deletions(-) 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 137d1baf..be0d4108 100644 --- a/apps/web/src/components/editor/panels/assets/views/captions.tsx +++ b/apps/web/src/components/editor/panels/assets/views/captions.tsx @@ -1,19 +1,20 @@ -import { Button } from "@/components/ui/button"; -import { PanelView } from "@/components/editor/panels/assets/views/base-panel"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { useState, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { PanelView } from "@/components/editor/panels/assets/views/base-panel"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useRef, useState } 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 { + DEFAULT_TRANSCRIPTION_SAMPLE_RATE, + TRANSCRIPTION_LANGUAGES, +} from "@/constants/transcription-constants"; import type { CaptionChunk, TranscriptionLanguage, @@ -32,7 +33,10 @@ import { SectionFields, } from "@/components/section"; +type CaptionsView = "generate" | "import"; + export function Captions() { + const [view, setView] = useState("generate"); const [selectedLanguage, setSelectedLanguage] = useState("auto"); const [isProcessing, setIsProcessing] = useState(false); @@ -42,59 +46,55 @@ export function Captions() { const containerRef = useRef(null); const fileInputRef = useRef(null); const editor = useEditor(); - - const handleProgress = (progress: TranscriptionProgress) => { - if (progress.status === "loading-model") { - setProcessingStep(`Loading model ${Math.round(progress.progress)}%`); - } else if (progress.status === "transcribing") { - setProcessingStep("Transcribing..."); - } - }; - - const handleGenerateTranscript = async () => { + + const handleProgress = (progress: TranscriptionProgress) => { + if (progress.status === "loading-model") { + setProcessingStep(`Loading model ${Math.round(progress.progress)}%`); + } else if (progress.status === "transcribing") { + setProcessingStep("Transcribing..."); + } + }; + + const handleGenerateTranscript = async () => { try { setIsProcessing(true); setError(null); setWarning(null); setProcessingStep("Extracting audio..."); - - const audioBlob = await extractTimelineAudio({ - tracks: editor.timeline.getTracks(), - mediaAssets: editor.media.getAssets(), - totalDuration: editor.timeline.getTotalDuration(), - }); - - setProcessingStep("Preparing audio..."); - const { samples } = await decodeAudioToFloat32({ - audioBlob, - sampleRate: DEFAULT_TRANSCRIPTION_SAMPLE_RATE, - }); - - const result = await transcriptionService.transcribe({ - audioData: samples, - language: selectedLanguage === "auto" ? undefined : selectedLanguage, - onProgress: handleProgress, - }); + + const audioBlob = await extractTimelineAudio({ + tracks: editor.timeline.getTracks(), + mediaAssets: editor.media.getAssets(), + totalDuration: editor.timeline.getTotalDuration(), + }); + + setProcessingStep("Preparing audio..."); + const { samples } = await decodeAudioToFloat32({ + audioBlob, + sampleRate: DEFAULT_TRANSCRIPTION_SAMPLE_RATE, + }); + + const result = await transcriptionService.transcribe({ + audioData: samples, + language: selectedLanguage === "auto" ? undefined : selectedLanguage, + onProgress: handleProgress, + }); 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", - ); - } finally { - setIsProcessing(false); - setProcessingStep(""); + console.error("Transcription failed:", error); + setError( + error instanceof Error ? error.message : "An unexpected error occurred", + ); + } finally { + setIsProcessing(false); + setProcessingStep(""); } }; - const insertCaptionChunks = ({ - captions, - }: { - captions: CaptionChunk[]; - }) => { + const insertCaptionChunks = ({ captions }: { captions: CaptionChunk[] }) => { const trackId = insertCaptionChunksAsTextTrack({ editor, captions, @@ -109,11 +109,7 @@ export function Captions() { fileInputRef.current?.click(); }; - const handleImportFile = async ({ - file, - }: { - file: File; - }) => { + const handleImportFile = async ({ file }: { file: File }) => { try { setIsProcessing(true); setError(null); @@ -162,24 +158,34 @@ export function Captions() { await handleImportFile({ file }); }; - - const handleLanguageChange = ({ value }: { value: string }) => { - if (value === "auto") { - setSelectedLanguage("auto"); - return; - } - - const matchedLanguage = TRANSCRIPTION_LANGUAGES.find( - (language) => language.code === value, - ); - if (!matchedLanguage) return; - setSelectedLanguage(matchedLanguage.code); - }; - + + const handleLanguageChange = ({ value }: { value: string }) => { + if (value === "auto") { + setSelectedLanguage("auto"); + return; + } + + const matchedLanguage = TRANSCRIPTION_LANGUAGES.find( + (language) => language.code === value, + ); + if (!matchedLanguage) return; + setSelectedLanguage(matchedLanguage.code); + }; + return ( setView(value as CaptionsView)} + > + + Generate + Import + + + } ref={containerRef} > void handleFileChange({ event })} /> -
- - - - - - - - {error && ( -
-

{error}

-
- )} - {warning && ( -
-

{warning}

-
- )} -
-
-
- -
+ {view === "generate" && ( +
+ + + + + + + + {error && ( +
+

{error}

+
+ )} +
+
+ )} + {view === "import" && ( +
+ +

+ Import an existing .srt subtitle file into the + timeline. +

-
-
-
+ {error && ( +
+

{error}

+
+ )} + {warning && ( +
+

{warning}

+
+ )} + + + )}
- ); -} + ); +}