mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { Button } from "@/components/ui/button";
|
|
import { PropertyGroup } from "../../properties-panel/property-item";
|
|
import { PanelBaseView as BaseView } from "@/components/editor/panel-base-view";
|
|
import { LanguageSelect } from "@/components/language-select";
|
|
import { useState, useRef } from "react";
|
|
import { extractTimelineAudio } from "@/lib/media/mediabunny";
|
|
import { useEditor } from "@/hooks/use-editor";
|
|
import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
|
|
import { LANGUAGES } from "@/constants/captions-constants";
|
|
import { Loader2 } from "lucide-react";
|
|
import type { TranscriptionProgress } from "@/types/transcription";
|
|
import { transcriptionService } from "@/services/transcription";
|
|
import { decodeAudioToFloat32 } from "@/lib/media/audio";
|
|
import { buildCaptionChunks } from "@/lib/transcription/caption";
|
|
|
|
export function Captions() {
|
|
const [selectedLanguage, setSelectedLanguage] = useState("auto");
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [processingStep, setProcessingStep] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const containerRef = useRef<HTMLDivElement>(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 () => {
|
|
try {
|
|
setIsProcessing(true);
|
|
setError(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 });
|
|
|
|
const result = await transcriptionService.transcribe({
|
|
audioData: samples,
|
|
language:
|
|
selectedLanguage === "auto" ? "auto" : selectedLanguage.toLowerCase(),
|
|
onProgress: handleProgress,
|
|
});
|
|
|
|
setProcessingStep("Generating captions...");
|
|
const captionChunks = buildCaptionChunks({ segments: result.segments });
|
|
|
|
const captionTrackId = editor.timeline.addTrack({
|
|
type: "text",
|
|
index: 0,
|
|
});
|
|
|
|
for (let i = 0; i < captionChunks.length; i++) {
|
|
const caption = captionChunks[i];
|
|
editor.timeline.insertElement({
|
|
placement: { mode: "explicit", trackId: captionTrackId },
|
|
element: {
|
|
...DEFAULT_TEXT_ELEMENT,
|
|
name: `Caption ${i + 1}`,
|
|
content: caption.text,
|
|
duration: caption.duration,
|
|
startTime: caption.startTime,
|
|
fontSize: 65,
|
|
fontWeight: "bold",
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Transcription failed:", error);
|
|
setError(
|
|
error instanceof Error ? error.message : "An unexpected error occurred",
|
|
);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
setProcessingStep("");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<BaseView
|
|
ref={containerRef}
|
|
className="flex h-full flex-col justify-between"
|
|
>
|
|
<PropertyGroup title="Language">
|
|
<LanguageSelect
|
|
selectedCountry={selectedLanguage}
|
|
onSelect={setSelectedLanguage}
|
|
containerRef={containerRef}
|
|
languages={LANGUAGES}
|
|
/>
|
|
</PropertyGroup>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
{error && (
|
|
<div className="bg-destructive/10 border-destructive/20 rounded-md border p-3">
|
|
<p className="text-destructive text-sm">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
className="w-full"
|
|
onClick={handleGenerateTranscript}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing && <Loader2 className="mr-1 animate-spin" />}
|
|
{isProcessing ? processingStep : "Generate transcript"}
|
|
</Button>
|
|
</div>
|
|
</BaseView>
|
|
);
|
|
}
|