chore: normalize line endings

This commit is contained in:
Maze Winther
2026-03-29 15:56:48 +02:00
parent 82817cb50c
commit 4d127a038b
736 changed files with 73880 additions and 73782 deletions
+186 -186
View File
@@ -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();
+176 -176
View File
@@ -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);
}
}