summaryrefslogtreecommitdiff
path: root/apps/web/src/services/transcription/service.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-01-31 22:19:11 +0100
committerMaze Winther <mazewinther@gmail.com>2026-01-31 22:19:11 +0100
commitf820d9a8948b97460ebdfdaa6b42b6d510e2b3cf (patch)
tree460b4cd9ac403fe5591bb0b5b4064e1662d08077 /apps/web/src/services/transcription/service.ts
parent7878c8bb7b83bce568ced990903b8195a9dd4605 (diff)
downloadopencut-f820d9a8948b97460ebdfdaa6b42b6d510e2b3cf.tar.gz
refactor services structure
Diffstat (limited to 'apps/web/src/services/transcription/service.ts')
-rw-r--r--apps/web/src/services/transcription/service.ts186
1 files changed, 186 insertions, 0 deletions
diff --git a/apps/web/src/services/transcription/service.ts b/apps/web/src/services/transcription/service.ts
new file mode 100644
index 00000000..c34a82ce
--- /dev/null
+++ b/apps/web/src/services/transcription/service.ts
@@ -0,0 +1,186 @@
+import type {
+ TranscriptionLanguage,
+ TranscriptionResult,
+ TranscriptionProgress,
+ TranscriptionModelId,
+} from "@/types/transcription";
+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();