summaryrefslogtreecommitdiff
path: root/apps/web/src/transcription
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/transcription
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/transcription')
-rw-r--r--apps/web/src/transcription/audio.ts4
-rw-r--r--apps/web/src/transcription/caption-defaults.ts2
-rw-r--r--apps/web/src/transcription/caption.ts49
-rw-r--r--apps/web/src/transcription/diagnostics.ts25
-rw-r--r--apps/web/src/transcription/languages.ts14
-rw-r--r--apps/web/src/transcription/models.ts34
-rw-r--r--apps/web/src/transcription/supported-languages.ts1
-rw-r--r--apps/web/src/transcription/types.ts47
8 files changed, 176 insertions, 0 deletions
diff --git a/apps/web/src/transcription/audio.ts b/apps/web/src/transcription/audio.ts
new file mode 100644
index 00000000..e2dc96d9
--- /dev/null
+++ b/apps/web/src/transcription/audio.ts
@@ -0,0 +1,4 @@
+export const DEFAULT_TRANSCRIPTION_SAMPLE_RATE = 16000;
+
+export const DEFAULT_CHUNK_LENGTH_SECONDS = 30;
+export const DEFAULT_STRIDE_SECONDS = 5;
diff --git a/apps/web/src/transcription/caption-defaults.ts b/apps/web/src/transcription/caption-defaults.ts
new file mode 100644
index 00000000..fc277158
--- /dev/null
+++ b/apps/web/src/transcription/caption-defaults.ts
@@ -0,0 +1,2 @@
+export const DEFAULT_WORDS_PER_CAPTION = 3;
+export const MIN_CAPTION_DURATION_SECONDS = 0.8;
diff --git a/apps/web/src/transcription/caption.ts b/apps/web/src/transcription/caption.ts
new file mode 100644
index 00000000..8e6ae864
--- /dev/null
+++ b/apps/web/src/transcription/caption.ts
@@ -0,0 +1,49 @@
+import type { TranscriptionSegment, CaptionChunk } from "@/transcription/types";
+import {
+ DEFAULT_WORDS_PER_CAPTION,
+ MIN_CAPTION_DURATION_SECONDS,
+} from "@/transcription/caption-defaults";
+
+export function buildCaptionChunks({
+ segments,
+ wordsPerChunk = DEFAULT_WORDS_PER_CAPTION,
+ minDuration = MIN_CAPTION_DURATION_SECONDS,
+}: {
+ segments: TranscriptionSegment[];
+ wordsPerChunk?: number;
+ minDuration?: number;
+}): CaptionChunk[] {
+ const captions: CaptionChunk[] = [];
+ let globalEndTime = 0;
+
+ for (const segment of segments) {
+ const words = segment.text.trim().split(/\s+/);
+ if (words.length === 0 || (words.length === 1 && words[0] === "")) continue;
+
+ const segmentDuration = segment.end - segment.start;
+ const wordsPerSecond = words.length / segmentDuration;
+
+ const chunks: string[] = [];
+ for (let i = 0; i < words.length; i += wordsPerChunk) {
+ chunks.push(words.slice(i, i + wordsPerChunk).join(" "));
+ }
+
+ let chunkStartTime = segment.start;
+ for (const chunk of chunks) {
+ const chunkWords = chunk.split(/\s+/).length;
+ const chunkDuration = Math.max(minDuration, chunkWords / wordsPerSecond);
+ const adjustedStartTime = Math.max(chunkStartTime, globalEndTime);
+
+ captions.push({
+ text: chunk,
+ startTime: adjustedStartTime,
+ duration: chunkDuration,
+ });
+
+ globalEndTime = adjustedStartTime + chunkDuration;
+ chunkStartTime += chunkDuration;
+ }
+ }
+
+ return captions;
+}
diff --git a/apps/web/src/transcription/diagnostics.ts b/apps/web/src/transcription/diagnostics.ts
new file mode 100644
index 00000000..6765d07f
--- /dev/null
+++ b/apps/web/src/transcription/diagnostics.ts
@@ -0,0 +1,25 @@
+import type { DiagnosticsManager } from "@/core/managers/diagnostics-manager";
+import { timelineHasAudio } from "@/media/audio";
+
+export const TRANSCRIPTION_DIAGNOSTICS_SCOPE = "transcription";
+
+export function registerTranscriptionDiagnostics({
+ diagnostics,
+}: {
+ diagnostics: DiagnosticsManager;
+}): void {
+ diagnostics.register({
+ id: "transcription.no_audio",
+ scope: TRANSCRIPTION_DIAGNOSTICS_SCOPE,
+ severity: "caution",
+ message: "No audio detected. Add a clip with audio to the timeline first.",
+ check: (editor) => {
+ const scene = editor.scenes.getActiveSceneOrNull();
+ if (!scene) return false;
+ return !timelineHasAudio({
+ tracks: scene.tracks,
+ mediaAssets: editor.media.getAssets(),
+ });
+ },
+ });
+}
diff --git a/apps/web/src/transcription/languages.ts b/apps/web/src/transcription/languages.ts
new file mode 100644
index 00000000..e5678145
--- /dev/null
+++ b/apps/web/src/transcription/languages.ts
@@ -0,0 +1,14 @@
+export const LANGUAGES = [
+ { code: "en", name: "English" },
+ { code: "es", name: "Spanish" },
+ { code: "it", name: "Italian" },
+ { code: "fr", name: "French" },
+ { code: "de", name: "German" },
+ { code: "pt", name: "Portuguese" },
+ { code: "ru", name: "Russian" },
+ { code: "ja", name: "Japanese" },
+ { code: "zh", name: "Chinese" },
+] as const;
+
+export type Language = (typeof LANGUAGES)[number];
+export type LanguageCode = Language["code"];
diff --git a/apps/web/src/transcription/models.ts b/apps/web/src/transcription/models.ts
new file mode 100644
index 00000000..ee9047e7
--- /dev/null
+++ b/apps/web/src/transcription/models.ts
@@ -0,0 +1,34 @@
+import type {
+ TranscriptionModel,
+ TranscriptionModelId,
+} from "./types";
+
+export const TRANSCRIPTION_MODELS: TranscriptionModel[] = [
+ {
+ id: "whisper-tiny",
+ name: "Tiny",
+ huggingFaceId: "onnx-community/whisper-tiny",
+ description: "Fastest, lower accuracy",
+ },
+ {
+ id: "whisper-small",
+ name: "Small",
+ huggingFaceId: "onnx-community/whisper-small",
+ description: "Good balance of speed and accuracy",
+ },
+ {
+ id: "whisper-medium",
+ name: "Medium",
+ huggingFaceId: "onnx-community/whisper-medium",
+ description: "Higher accuracy, slower",
+ },
+ {
+ id: "whisper-large-v3-turbo",
+ name: "Large v3 Turbo",
+ huggingFaceId: "onnx-community/whisper-large-v3-turbo",
+ description: "Best accuracy, requires WebGPU for good performance",
+ },
+];
+
+export const DEFAULT_TRANSCRIPTION_MODEL: TranscriptionModelId =
+ "whisper-small";
diff --git a/apps/web/src/transcription/supported-languages.ts b/apps/web/src/transcription/supported-languages.ts
new file mode 100644
index 00000000..0adc1332
--- /dev/null
+++ b/apps/web/src/transcription/supported-languages.ts
@@ -0,0 +1 @@
+export { LANGUAGES as TRANSCRIPTION_LANGUAGES } from "./languages";
diff --git a/apps/web/src/transcription/types.ts b/apps/web/src/transcription/types.ts
new file mode 100644
index 00000000..7ab0da7f
--- /dev/null
+++ b/apps/web/src/transcription/types.ts
@@ -0,0 +1,47 @@
+import type { LanguageCode } from "./languages";
+
+export type TranscriptionLanguage = LanguageCode | "auto";
+
+export interface TranscriptionSegment {
+ text: string;
+ start: number;
+ end: number;
+}
+
+export interface TranscriptionResult {
+ text: string;
+ segments: TranscriptionSegment[];
+ language: string;
+}
+
+export type TranscriptionStatus =
+ | "idle"
+ | "loading-model"
+ | "transcribing"
+ | "complete"
+ | "error";
+
+export interface TranscriptionProgress {
+ status: TranscriptionStatus;
+ progress: number;
+ message?: string;
+}
+
+export type TranscriptionModelId =
+ | "whisper-tiny"
+ | "whisper-small"
+ | "whisper-medium"
+ | "whisper-large-v3-turbo";
+
+export interface TranscriptionModel {
+ id: TranscriptionModelId;
+ name: string;
+ huggingFaceId: string;
+ description: string;
+}
+
+export interface CaptionChunk {
+ text: string;
+ startTime: number;
+ duration: number;
+}