summaryrefslogtreecommitdiff
path: root/apps/web/src/services/transcription/worker.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-27 00:55:15 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:56:48 +0200
commit4d127a038bf768eded68aae3cfa5c1b12241bdac (patch)
tree16bbb801caf6770546cff4fdb37b6408ec9ead98 /apps/web/src/services/transcription/worker.ts
parent82817cb50cb0487350573d6129130aec705f924b (diff)
downloadopencut-4d127a038bf768eded68aae3cfa5c1b12241bdac.tar.gz
chore: normalize line endings
Diffstat (limited to 'apps/web/src/services/transcription/worker.ts')
-rw-r--r--apps/web/src/services/transcription/worker.ts352
1 files changed, 176 insertions, 176 deletions
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);
+ }
+}