Built for people who think better out loud.
1type TranscriptionWord = {
2 word: string;
3 start: number;
4 end: number;
5};
6
7type TranscriptionSegment = {
8 id: number;
9 start: number;
10 end: number;
11 text: string;
12};
13
14export type TranscriptionResult = {
15 text: string;
16 language?: string;
17 duration?: number;
18 segments?: TranscriptionSegment[] | null;
19 words?: TranscriptionWord[] | null;
20};
21
22type TranscriptionRequest = {
23 endpointUrl: string;
24 blob: Blob;
25 signal?: AbortSignal;
26};
27
28type ApiErrorPayload = {
29 error?: {
30 message?: string;
31 };
32};
33
34function extensionFromMimeType(mimeType: string) {
35 const match = mimeType.match(/audio\/([^;]+)/);
36 return match?.[1] || "webm";
37}
38
39async function parseErrorMessage(response: Response) {
40 const payload = (await response.json().catch(() => ({}))) as ApiErrorPayload;
41 return payload?.error?.message;
42}
43
44export async function transcribeWithBackend({
45 endpointUrl,
46 blob,
47 signal,
48}: TranscriptionRequest): Promise<TranscriptionResult> {
49 const extension = extensionFromMimeType(blob.type || "audio/webm");
50 const file = new File([blob], `recording.${extension}`, {
51 type: blob.type || "audio/webm",
52 });
53
54 const formData = new FormData();
55 formData.append("file", file);
56
57 const response = await fetch(endpointUrl, {
58 method: "POST",
59 body: formData,
60 signal,
61 });
62
63 if (!response.ok) {
64 const message = await parseErrorMessage(response);
65 throw new Error(message || `Transcription request failed (${response.status}).`);
66 }
67
68 const payload = (await response.json()) as TranscriptionResult;
69 if (!payload || typeof payload.text !== "string") {
70 throw new Error("Backend did not return a transcription.");
71 }
72
73 return payload;
74}