From 4d127a038bf768eded68aae3cfa5c1b12241bdac Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 27 Mar 2026 00:55:15 +0100 Subject: chore: normalize line endings --- apps/web/src/services/transcription/service.ts | 372 ++++++++++++------------- apps/web/src/services/transcription/worker.ts | 352 +++++++++++------------ 2 files changed, 362 insertions(+), 362 deletions(-) (limited to 'apps/web/src/services/transcription') 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 { - await this.ensureWorker({ modelId, onProgress }); - - return new Promise((resolve, reject) => { - if (!this.worker) { - reject(new Error("Worker not initialized")); - return; - } - - const handleMessage = (event: MessageEvent) => { - 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 { - 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) => { - 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 { - 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 { + await this.ensureWorker({ modelId, onProgress }); + + return new Promise((resolve, reject) => { + if (!this.worker) { + reject(new Error("Worker not initialized")); + return; + } + + const handleMessage = (event: MessageEvent) => { + 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 { + 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) => { + 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 { + 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(); - -self.onmessage = async (event: MessageEvent) => { - 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(); + +self.onmessage = async (event: MessageEvent) => { + 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); + } +} -- cgit v1.3.1