import type { Transcript, TranscriptionCapabilities, TranscriptionProvider, TranscriptionRequest, } from "@rakazo/adapter-kit"; /** What the emulators "hear" in any audio: two timed sentences. */ export const EMULATED_TRANSCRIPT: Transcript = { text: "Welcome to the studio. Today we cut a vertical clip.", language: "en", segments: [ { startMs: 0, endMs: 2400, text: "Welcome to the studio." }, { startMs: 2400, endMs: 5100, text: "Today we cut a vertical clip." }, ], }; const EMULATED_MAX_BYTES = 25 * 1024 * 1024; /** Offline transcription provider for tests and conformance; no speech server involved. */ export class EmulatorTranscriptionProvider implements TranscriptionProvider { describe() { return { id: "emulator", contractVersion: "1", adapterVersion: "0.1.0", capabilities: { segments: true, languageDetection: true, maxBytes: EMULATED_MAX_BYTES, } satisfies TranscriptionCapabilities, }; } async transcribe(request: TranscriptionRequest): Promise { request.signal?.throwIfAborted(); if (request.audio.byteLength === 0) throw new Error("The audio is empty"); if (request.audio.byteLength > EMULATED_MAX_BYTES) { throw new Error("The audio is too large for the transcription server"); } return { ...EMULATED_TRANSCRIPT, ...(request.language ? { language: request.language } : {}), segments: EMULATED_TRANSCRIPT.segments.map((segment) => ({ ...segment })), }; } } export interface EmulatedTranscriptionRequest { url: string; authorization: string | null; model: string | null; responseFormat: string | null; language: string | null; fileName: string | null; fileType: string | null; fileSize: number; } /** * In-process substitute for an OpenAI-compatible `POST /v1/audio/transcriptions` server, so * the real HTTP adapter runs in offline tests. `openai` answers every response format like * OpenAI and whisper.cpp; `plain` mimics servers without timed output that reject * `verbose_json`; `text` answers with a bare `text/plain` body whatever was asked. */ export class OpenAiCompatibleTranscriptionServerEmulator { readonly requests: EmulatedTranscriptionRequest[] = []; /** Answer the next request with this HTTP status instead of a transcript. */ failNextStatus: number | undefined; constructor( private readonly options: { apiKey?: string; flavor?: "openai" | "plain" | "text" } = {}, ) {} readonly fetch: typeof fetch = async (input, init) => { init?.signal?.throwIfAborted(); const url = new URL(input instanceof Request ? input.url : String(input)); if (init?.method !== "POST" || !url.pathname.endsWith("/audio/transcriptions")) { return Response.json({ error: { message: "Not found" } }, { status: 404 }); } const headers = new Headers(init.headers); const authorization = headers.get("authorization"); let form: FormData; try { form = await new Request(url, { method: "POST", headers, body: init.body }).formData(); } catch { return Response.json({ error: { message: "Expected multipart/form-data" } }, { status: 400 }); } const file = form.get("file"); const field = (name: string) => { const value = form.get(name); return typeof value === "string" ? value : null; }; const responseFormat = field("response_format"); this.requests.push({ url: url.href, authorization, model: field("model"), responseFormat, language: field("language"), fileName: file instanceof File ? file.name : null, fileType: file instanceof File ? file.type : null, fileSize: file instanceof File ? file.size : 0, }); if (this.failNextStatus) { const status = this.failNextStatus; this.failNextStatus = undefined; return Response.json({ error: { message: "Upstream failure" } }, { status }); } if (this.options.apiKey && authorization !== `Bearer ${this.options.apiKey}`) { return Response.json({ error: { message: "Invalid API key" } }, { status: 401 }); } if (!(file instanceof File) || file.size === 0) { return Response.json({ error: { message: "file is required" } }, { status: 400 }); } const flavor = this.options.flavor ?? "openai"; if (flavor === "text" || responseFormat === "text") { return new Response(EMULATED_TRANSCRIPT.text, { headers: { "content-type": "text/plain; charset=utf-8" }, }); } if (responseFormat === "verbose_json") { if (flavor === "plain") { return Response.json( { error: { message: "Unsupported response_format: verbose_json" } }, { status: 400 }, ); } return Response.json({ task: "transcribe", language: "english", duration: EMULATED_TRANSCRIPT.segments.at(-1)!.endMs / 1000, text: EMULATED_TRANSCRIPT.text, segments: EMULATED_TRANSCRIPT.segments.map((segment, id) => ({ id, start: segment.startMs / 1000, end: segment.endMs / 1000, // Whisper keeps the leading space of each segment. text: ` ${segment.text}`, })), }); } return Response.json({ text: EMULATED_TRANSCRIPT.text }); }; }