diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-27 00:55:15 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:56:48 +0200 |
| commit | 4d127a038bf768eded68aae3cfa5c1b12241bdac (patch) | |
| tree | 16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/services/transcription | |
| parent | 82817cb50cb0487350573d6129130aec705f924b (diff) | |
| download | opencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz | |
chore: normalize line endings
Diffstat (limited to 'apps/web/src/services/transcription')
| -rw-r--r-- | apps/web/src/services/transcription/service.ts | 372 | ||||
| -rw-r--r-- | apps/web/src/services/transcription/worker.ts | 352 |
2 files changed, 362 insertions, 362 deletions
diff --git a/apps/web/src/services/transcription/service.ts b/apps/web/src/services/transcription/service.ts index ab88106c..fa657032 100644 --- a/apps/web/src/services/transcription/service.ts +++ b/apps/web/src/services/transcription/service.ts @@ -1,186 +1,186 @@ -import type { - TranscriptionLanguage, - TranscriptionResult, - TranscriptionProgress, - TranscriptionModelId, -} from "@/lib/transcription/types"; -import { - DEFAULT_TRANSCRIPTION_MODEL, - TRANSCRIPTION_MODELS, -} from "@/constants/transcription-constants"; -import type { WorkerMessage, WorkerResponse } from "./worker"; - -type ProgressCallback = (progress: TranscriptionProgress) => void; - -class TranscriptionService { - private worker: Worker | null = null; - private currentModelId: TranscriptionModelId | null = null; - private isInitialized = false; - private isInitializing = false; - - async transcribe({ - audioData, - language = "auto", - modelId = DEFAULT_TRANSCRIPTION_MODEL, - onProgress, - }: { - audioData: Float32Array; - language?: TranscriptionLanguage; - modelId?: TranscriptionModelId; - onProgress?: ProgressCallback; - }): Promise<TranscriptionResult> { - await this.ensureWorker({ modelId, onProgress }); - - return new Promise((resolve, reject) => { - if (!this.worker) { - reject(new Error("Worker not initialized")); - return; - } - - const handleMessage = (event: MessageEvent<WorkerResponse>) => { - const response = event.data; - - switch (response.type) { - case "transcribe-progress": - onProgress?.({ - status: "transcribing", - progress: response.progress, - message: "Transcribing audio...", - }); - break; - - case "transcribe-complete": - this.worker?.removeEventListener("message", handleMessage); - resolve({ - text: response.text, - segments: response.segments, - language, - }); - break; - - case "transcribe-error": - this.worker?.removeEventListener("message", handleMessage); - reject(new Error(response.error)); - break; - - case "cancelled": - this.worker?.removeEventListener("message", handleMessage); - reject(new Error("Transcription cancelled")); - break; - } - }; - - this.worker.addEventListener("message", handleMessage); - - this.worker.postMessage({ - type: "transcribe", - audio: audioData, - language, - } satisfies WorkerMessage); - }); - } - - cancel() { - this.worker?.postMessage({ type: "cancel" } satisfies WorkerMessage); - } - - private async ensureWorker({ - modelId, - onProgress, - }: { - modelId: TranscriptionModelId; - onProgress?: ProgressCallback; - }): Promise<void> { - const needsNewModel = this.currentModelId !== modelId; - - if (this.worker && this.isInitialized && !needsNewModel) { - return; - } - - if (this.isInitializing && !needsNewModel) { - await this.waitForInit(); - return; - } - - this.terminate(); - this.isInitializing = true; - this.isInitialized = false; - - const model = TRANSCRIPTION_MODELS.find((m) => m.id === modelId); - if (!model) { - throw new Error(`Unknown model: ${modelId}`); - } - - this.worker = new Worker(new URL("./worker.ts", import.meta.url), { - type: "module", - }); - - return new Promise((resolve, reject) => { - if (!this.worker) { - reject(new Error("Failed to create worker")); - return; - } - - const handleMessage = (event: MessageEvent<WorkerResponse>) => { - const response = event.data; - - switch (response.type) { - case "init-progress": - onProgress?.({ - status: "loading-model", - progress: response.progress, - message: `Loading ${model.name} model...`, - }); - break; - - case "init-complete": - this.worker?.removeEventListener("message", handleMessage); - this.isInitialized = true; - this.isInitializing = false; - this.currentModelId = modelId; - resolve(); - break; - - case "init-error": - this.worker?.removeEventListener("message", handleMessage); - this.isInitializing = false; - this.terminate(); - reject(new Error(response.error)); - break; - } - }; - - this.worker.addEventListener("message", handleMessage); - - this.worker.postMessage({ - type: "init", - modelId: model.huggingFaceId, - } satisfies WorkerMessage); - }); - } - - private waitForInit(): Promise<void> { - return new Promise((resolve) => { - const checkInit = () => { - if (this.isInitialized) { - resolve(); - } else if (!this.isInitializing) { - resolve(); - } else { - setTimeout(checkInit, 100); - } - }; - checkInit(); - }); - } - - terminate() { - this.worker?.terminate(); - this.worker = null; - this.isInitialized = false; - this.isInitializing = false; - this.currentModelId = null; - } -} - -export const transcriptionService = new TranscriptionService(); +import type {
+ TranscriptionLanguage,
+ TranscriptionResult,
+ TranscriptionProgress,
+ TranscriptionModelId,
+} from "@/lib/transcription/types";
+import {
+ DEFAULT_TRANSCRIPTION_MODEL,
+ TRANSCRIPTION_MODELS,
+} from "@/constants/transcription-constants";
+import type { WorkerMessage, WorkerResponse } from "./worker";
+
+type ProgressCallback = (progress: TranscriptionProgress) => void;
+
+class TranscriptionService {
+ private worker: Worker | null = null;
+ private currentModelId: TranscriptionModelId | null = null;
+ private isInitialized = false;
+ private isInitializing = false;
+
+ async transcribe({
+ audioData,
+ language = "auto",
+ modelId = DEFAULT_TRANSCRIPTION_MODEL,
+ onProgress,
+ }: {
+ audioData: Float32Array;
+ language?: TranscriptionLanguage;
+ modelId?: TranscriptionModelId;
+ onProgress?: ProgressCallback;
+ }): Promise<TranscriptionResult> {
+ await this.ensureWorker({ modelId, onProgress });
+
+ return new Promise((resolve, reject) => {
+ if (!this.worker) {
+ reject(new Error("Worker not initialized"));
+ return;
+ }
+
+ const handleMessage = (event: MessageEvent<WorkerResponse>) => {
+ const response = event.data;
+
+ switch (response.type) {
+ case "transcribe-progress":
+ onProgress?.({
+ status: "transcribing",
+ progress: response.progress,
+ message: "Transcribing audio...",
+ });
+ break;
+
+ case "transcribe-complete":
+ this.worker?.removeEventListener("message", handleMessage);
+ resolve({
+ text: response.text,
+ segments: response.segments,
+ language,
+ });
+ break;
+
+ case "transcribe-error":
+ this.worker?.removeEventListener("message", handleMessage);
+ reject(new Error(response.error));
+ break;
+
+ case "cancelled":
+ this.worker?.removeEventListener("message", handleMessage);
+ reject(new Error("Transcription cancelled"));
+ break;
+ }
+ };
+
+ this.worker.addEventListener("message", handleMessage);
+
+ this.worker.postMessage({
+ type: "transcribe",
+ audio: audioData,
+ language,
+ } satisfies WorkerMessage);
+ });
+ }
+
+ cancel() {
+ this.worker?.postMessage({ type: "cancel" } satisfies WorkerMessage);
+ }
+
+ private async ensureWorker({
+ modelId,
+ onProgress,
+ }: {
+ modelId: TranscriptionModelId;
+ onProgress?: ProgressCallback;
+ }): Promise<void> {
+ const needsNewModel = this.currentModelId !== modelId;
+
+ if (this.worker && this.isInitialized && !needsNewModel) {
+ return;
+ }
+
+ if (this.isInitializing && !needsNewModel) {
+ await this.waitForInit();
+ return;
+ }
+
+ this.terminate();
+ this.isInitializing = true;
+ this.isInitialized = false;
+
+ const model = TRANSCRIPTION_MODELS.find((m) => m.id === modelId);
+ if (!model) {
+ throw new Error(`Unknown model: ${modelId}`);
+ }
+
+ this.worker = new Worker(new URL("./worker.ts", import.meta.url), {
+ type: "module",
+ });
+
+ return new Promise((resolve, reject) => {
+ if (!this.worker) {
+ reject(new Error("Failed to create worker"));
+ return;
+ }
+
+ const handleMessage = (event: MessageEvent<WorkerResponse>) => {
+ const response = event.data;
+
+ switch (response.type) {
+ case "init-progress":
+ onProgress?.({
+ status: "loading-model",
+ progress: response.progress,
+ message: `Loading ${model.name} model...`,
+ });
+ break;
+
+ case "init-complete":
+ this.worker?.removeEventListener("message", handleMessage);
+ this.isInitialized = true;
+ this.isInitializing = false;
+ this.currentModelId = modelId;
+ resolve();
+ break;
+
+ case "init-error":
+ this.worker?.removeEventListener("message", handleMessage);
+ this.isInitializing = false;
+ this.terminate();
+ reject(new Error(response.error));
+ break;
+ }
+ };
+
+ this.worker.addEventListener("message", handleMessage);
+
+ this.worker.postMessage({
+ type: "init",
+ modelId: model.huggingFaceId,
+ } satisfies WorkerMessage);
+ });
+ }
+
+ private waitForInit(): Promise<void> {
+ return new Promise((resolve) => {
+ const checkInit = () => {
+ if (this.isInitialized) {
+ resolve();
+ } else if (!this.isInitializing) {
+ resolve();
+ } else {
+ setTimeout(checkInit, 100);
+ }
+ };
+ checkInit();
+ });
+ }
+
+ terminate() {
+ this.worker?.terminate();
+ this.worker = null;
+ this.isInitialized = false;
+ this.isInitializing = false;
+ this.currentModelId = null;
+ }
+}
+
+export const transcriptionService = new TranscriptionService();
diff --git a/apps/web/src/services/transcription/worker.ts b/apps/web/src/services/transcription/worker.ts index e583b2ab..e22fe796 100644 --- a/apps/web/src/services/transcription/worker.ts +++ b/apps/web/src/services/transcription/worker.ts @@ -1,176 +1,176 @@ -import { - pipeline, - type AutomaticSpeechRecognitionPipeline, - type AutomaticSpeechRecognitionOutput, -} from "@huggingface/transformers"; -import type { TranscriptionSegment } from "@/lib/transcription/types"; -import { - DEFAULT_CHUNK_LENGTH_SECONDS, - DEFAULT_STRIDE_SECONDS, -} from "@/constants/transcription-constants"; - -export type WorkerMessage = - | { type: "init"; modelId: string } - | { type: "transcribe"; audio: Float32Array; language: string } - | { type: "cancel" }; - -export type WorkerResponse = - | { type: "init-progress"; progress: number } - | { type: "init-complete" } - | { type: "init-error"; error: string } - | { type: "transcribe-progress"; progress: number } - | { - type: "transcribe-complete"; - text: string; - segments: TranscriptionSegment[]; - } - | { type: "transcribe-error"; error: string } - | { type: "cancelled" }; - -let transcriber: AutomaticSpeechRecognitionPipeline | null = null; -let cancelled = false; -let lastReportedProgress = -1; -const fileBytes = new Map<string, { loaded: number; total: number }>(); - -self.onmessage = async (event: MessageEvent<WorkerMessage>) => { - const message = event.data; - - switch (message.type) { - case "init": - await handleInit({ modelId: message.modelId }); - break; - case "transcribe": - await handleTranscribe({ - audio: message.audio, - language: message.language, - }); - break; - case "cancel": - cancelled = true; - self.postMessage({ type: "cancelled" } satisfies WorkerResponse); - break; - } -}; - -async function handleInit({ modelId }: { modelId: string }) { - lastReportedProgress = -1; - fileBytes.clear(); - - try { - transcriber = (await pipeline("automatic-speech-recognition", modelId, { - dtype: "q4", - device: "auto", - progress_callback: (progressInfo: { - status?: string; - file?: string; - loaded?: number; - total?: number; - }) => { - const file = progressInfo.file; - if (!file) return; - - const loaded = progressInfo.loaded ?? 0; - const total = progressInfo.total ?? 0; - - if (progressInfo.status === "progress" && total > 0) { - fileBytes.set(file, { loaded, total }); - } else if (progressInfo.status === "done") { - const existing = fileBytes.get(file); - if (existing) { - fileBytes.set(file, { - loaded: existing.total, - total: existing.total, - }); - } - } - - // sum all bytes - let totalLoaded = 0; - let totalSize = 0; - for (const { loaded, total } of fileBytes.values()) { - totalLoaded += loaded; - totalSize += total; - } - - if (totalSize === 0) return; - - const overallProgress = (totalLoaded / totalSize) * 100; - const roundedProgress = Math.floor(overallProgress); - - if (roundedProgress !== lastReportedProgress) { - lastReportedProgress = roundedProgress; - self.postMessage({ - type: "init-progress", - progress: roundedProgress, - } satisfies WorkerResponse); - } - }, - })) as unknown as AutomaticSpeechRecognitionPipeline; - - self.postMessage({ type: "init-complete" } satisfies WorkerResponse); - } catch (error) { - self.postMessage({ - type: "init-error", - error: error instanceof Error ? error.message : "Failed to load model", - } satisfies WorkerResponse); - } -} - -async function handleTranscribe({ - audio, - language, -}: { - audio: Float32Array; - language: string; -}) { - if (!transcriber) { - self.postMessage({ - type: "transcribe-error", - error: "Model not initialized", - } satisfies WorkerResponse); - return; - } - - cancelled = false; - - try { - const rawResult = await transcriber(audio, { - chunk_length_s: DEFAULT_CHUNK_LENGTH_SECONDS, - stride_length_s: DEFAULT_STRIDE_SECONDS, - language: language === "auto" ? undefined : language, - return_timestamps: true, - }); - - if (cancelled) return; - - const result: AutomaticSpeechRecognitionOutput = Array.isArray(rawResult) - ? rawResult[0] - : rawResult; - - const segments: TranscriptionSegment[] = []; - - if (result.chunks) { - for (const chunk of result.chunks) { - if (chunk.timestamp && chunk.timestamp.length >= 2) { - segments.push({ - text: chunk.text, - start: chunk.timestamp[0] ?? 0, - end: chunk.timestamp[1] ?? chunk.timestamp[0] ?? 0, - }); - } - } - } - - self.postMessage({ - type: "transcribe-complete", - text: result.text, - segments, - } satisfies WorkerResponse); - } catch (error) { - if (cancelled) return; - self.postMessage({ - type: "transcribe-error", - error: error instanceof Error ? error.message : "Transcription failed", - } satisfies WorkerResponse); - } -} +import {
+ pipeline,
+ type AutomaticSpeechRecognitionPipeline,
+ type AutomaticSpeechRecognitionOutput,
+} from "@huggingface/transformers";
+import type { TranscriptionSegment } from "@/lib/transcription/types";
+import {
+ DEFAULT_CHUNK_LENGTH_SECONDS,
+ DEFAULT_STRIDE_SECONDS,
+} from "@/constants/transcription-constants";
+
+export type WorkerMessage =
+ | { type: "init"; modelId: string }
+ | { type: "transcribe"; audio: Float32Array; language: string }
+ | { type: "cancel" };
+
+export type WorkerResponse =
+ | { type: "init-progress"; progress: number }
+ | { type: "init-complete" }
+ | { type: "init-error"; error: string }
+ | { type: "transcribe-progress"; progress: number }
+ | {
+ type: "transcribe-complete";
+ text: string;
+ segments: TranscriptionSegment[];
+ }
+ | { type: "transcribe-error"; error: string }
+ | { type: "cancelled" };
+
+let transcriber: AutomaticSpeechRecognitionPipeline | null = null;
+let cancelled = false;
+let lastReportedProgress = -1;
+const fileBytes = new Map<string, { loaded: number; total: number }>();
+
+self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
+ const message = event.data;
+
+ switch (message.type) {
+ case "init":
+ await handleInit({ modelId: message.modelId });
+ break;
+ case "transcribe":
+ await handleTranscribe({
+ audio: message.audio,
+ language: message.language,
+ });
+ break;
+ case "cancel":
+ cancelled = true;
+ self.postMessage({ type: "cancelled" } satisfies WorkerResponse);
+ break;
+ }
+};
+
+async function handleInit({ modelId }: { modelId: string }) {
+ lastReportedProgress = -1;
+ fileBytes.clear();
+
+ try {
+ transcriber = (await pipeline("automatic-speech-recognition", modelId, {
+ dtype: "q4",
+ device: "auto",
+ progress_callback: (progressInfo: {
+ status?: string;
+ file?: string;
+ loaded?: number;
+ total?: number;
+ }) => {
+ const file = progressInfo.file;
+ if (!file) return;
+
+ const loaded = progressInfo.loaded ?? 0;
+ const total = progressInfo.total ?? 0;
+
+ if (progressInfo.status === "progress" && total > 0) {
+ fileBytes.set(file, { loaded, total });
+ } else if (progressInfo.status === "done") {
+ const existing = fileBytes.get(file);
+ if (existing) {
+ fileBytes.set(file, {
+ loaded: existing.total,
+ total: existing.total,
+ });
+ }
+ }
+
+ // sum all bytes
+ let totalLoaded = 0;
+ let totalSize = 0;
+ for (const { loaded, total } of fileBytes.values()) {
+ totalLoaded += loaded;
+ totalSize += total;
+ }
+
+ if (totalSize === 0) return;
+
+ const overallProgress = (totalLoaded / totalSize) * 100;
+ const roundedProgress = Math.floor(overallProgress);
+
+ if (roundedProgress !== lastReportedProgress) {
+ lastReportedProgress = roundedProgress;
+ self.postMessage({
+ type: "init-progress",
+ progress: roundedProgress,
+ } satisfies WorkerResponse);
+ }
+ },
+ })) as unknown as AutomaticSpeechRecognitionPipeline;
+
+ self.postMessage({ type: "init-complete" } satisfies WorkerResponse);
+ } catch (error) {
+ self.postMessage({
+ type: "init-error",
+ error: error instanceof Error ? error.message : "Failed to load model",
+ } satisfies WorkerResponse);
+ }
+}
+
+async function handleTranscribe({
+ audio,
+ language,
+}: {
+ audio: Float32Array;
+ language: string;
+}) {
+ if (!transcriber) {
+ self.postMessage({
+ type: "transcribe-error",
+ error: "Model not initialized",
+ } satisfies WorkerResponse);
+ return;
+ }
+
+ cancelled = false;
+
+ try {
+ const rawResult = await transcriber(audio, {
+ chunk_length_s: DEFAULT_CHUNK_LENGTH_SECONDS,
+ stride_length_s: DEFAULT_STRIDE_SECONDS,
+ language: language === "auto" ? undefined : language,
+ return_timestamps: true,
+ });
+
+ if (cancelled) return;
+
+ const result: AutomaticSpeechRecognitionOutput = Array.isArray(rawResult)
+ ? rawResult[0]
+ : rawResult;
+
+ const segments: TranscriptionSegment[] = [];
+
+ if (result.chunks) {
+ for (const chunk of result.chunks) {
+ if (chunk.timestamp && chunk.timestamp.length >= 2) {
+ segments.push({
+ text: chunk.text,
+ start: chunk.timestamp[0] ?? 0,
+ end: chunk.timestamp[1] ?? chunk.timestamp[0] ?? 0,
+ });
+ }
+ }
+ }
+
+ self.postMessage({
+ type: "transcribe-complete",
+ text: result.text,
+ segments,
+ } satisfies WorkerResponse);
+ } catch (error) {
+ if (cancelled) return;
+ self.postMessage({
+ type: "transcribe-error",
+ error: error instanceof Error ? error.message : "Transcription failed",
+ } satisfies WorkerResponse);
+ }
+}
|
