mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
187 lines
4.5 KiB
TypeScript
187 lines
4.5 KiB
TypeScript
import type {
|
|
TranscriptionLanguage,
|
|
TranscriptionResult,
|
|
TranscriptionProgress,
|
|
TranscriptionModelId,
|
|
} from "@/transcription/types";
|
|
import {
|
|
DEFAULT_TRANSCRIPTION_MODEL,
|
|
TRANSCRIPTION_MODELS,
|
|
} from "@/transcription/models";
|
|
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();
|