From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- apps/web/src/transcription/audio.ts | 4 ++ apps/web/src/transcription/caption-defaults.ts | 2 + apps/web/src/transcription/caption.ts | 49 +++++++++++++++++++++++ apps/web/src/transcription/diagnostics.ts | 25 ++++++++++++ apps/web/src/transcription/languages.ts | 14 +++++++ apps/web/src/transcription/models.ts | 34 ++++++++++++++++ apps/web/src/transcription/supported-languages.ts | 1 + apps/web/src/transcription/types.ts | 47 ++++++++++++++++++++++ 8 files changed, 176 insertions(+) create mode 100644 apps/web/src/transcription/audio.ts create mode 100644 apps/web/src/transcription/caption-defaults.ts create mode 100644 apps/web/src/transcription/caption.ts create mode 100644 apps/web/src/transcription/diagnostics.ts create mode 100644 apps/web/src/transcription/languages.ts create mode 100644 apps/web/src/transcription/models.ts create mode 100644 apps/web/src/transcription/supported-languages.ts create mode 100644 apps/web/src/transcription/types.ts (limited to 'apps/web/src/transcription') 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; +} -- cgit v1.3.1