import { OPENROUTER_STT_MODEL, STT_MAX_AUDIO_BYTES, STT_SAMPLE_RATE, WAV_HEADER_BYTES } from "../core/stt.ts"; import { isSttLanguage } from "../core/i18n.ts"; import { keyValue } from "./keys.ts"; const fail = (status: number, message: string) => Response.json({ message }, { status }); /** One complete recording per request. Credentials and audio never go to disk. */ export async function openRouterTranscription(request: Request, key = keyValue("OPENROUTER_API_KEY")): Promise { if (!key) return fail(503, "Dictation needs an OpenRouter key"); const params = new URL(request.url).searchParams; if (params.get("model") !== OPENROUTER_STT_MODEL) return fail(400, "Unknown speech recognition model"); const language = params.get("language"); if (!isSttLanguage(language)) return fail(400, "Invalid speech recognition language"); if (request.headers.get("content-type") !== "audio/wav") return fail(415, "Invalid dictation audio"); if (Number(request.headers.get("content-length")) > STT_MAX_AUDIO_BYTES) return fail(413, "Recording exceeds OpenRouter's 25 MB audio limit"); if (!request.body) return fail(400, "Invalid dictation audio"); const reader = request.body.getReader(); const frames: Uint8Array[] = []; let size = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; if (size > STT_MAX_AUDIO_BYTES) { await reader.cancel(); return fail(413, "Recording exceeds OpenRouter's 25 MB audio limit"); } frames.push(value); } } catch { return fail(request.signal.aborted ? 499 : 400, "Could not read dictation audio"); } finally { reader.releaseLock(); } if (request.signal.aborted) return fail(499, "Transcription cancelled"); const audio = Buffer.concat(frames, size); // Accept exactly the lossless PCM container produced by our capture graph. if (size <= WAV_HEADER_BYTES || size % 2 !== 0 || audio.toString("ascii", 0, 4) !== "RIFF" || audio.toString("ascii", 8, 12) !== "WAVE" || audio.toString("ascii", 12, 16) !== "fmt " || audio.readUInt32LE(4) !== size - 8 || audio.readUInt32LE(16) !== 16 || audio.readUInt16LE(20) !== 1 || audio.readUInt16LE(22) !== 1 || audio.readUInt32LE(24) !== STT_SAMPLE_RATE || audio.readUInt32LE(28) !== STT_SAMPLE_RATE * 2 || audio.readUInt16LE(32) !== 2 || audio.readUInt16LE(34) !== 16 || audio.toString("ascii", 36, 40) !== "data" || audio.readUInt32LE(40) !== size - WAV_HEADER_BYTES) { return fail(400, "Invalid dictation audio"); } const signal = AbortSignal.any([request.signal, AbortSignal.timeout(65_000)]); try { const response = await fetch("https://openrouter.ai/api/v1/audio/transcriptions", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: OPENROUTER_STT_MODEL, input_audio: { data: audio.toString("base64"), format: "wav" }, // MAI allows one forced language; automatic detection supports mixed speech. ...(language === "multi" ? {} : { language }), response_format: "json", }), signal, }); if (!response.ok) { // Provider diagnostics can contain credentials or echoed audio. Return only app-authored errors. await response.body?.cancel(); if (response.status === 401 || response.status === 403) return fail(502, "OpenRouter rejected the API key"); if (response.status === 402) return fail(502, "OpenRouter credits are insufficient"); if (response.status === 429) return fail(429, "OpenRouter rate limit reached; try again later"); return fail(502, `OpenRouter transcription failed (${response.status})`); } const result = await response.json() as { text?: unknown }; if (typeof result.text !== "string") return fail(502, "OpenRouter returned an invalid transcript"); return Response.json({ text: result.text }); } catch (error) { if (request.signal.aborted) return fail(499, "Transcription cancelled"); if (signal.aborted || (error instanceof Error && error.name === "TimeoutError")) return fail(504, "OpenRouter transcription timed out"); return fail(502, "OpenRouter transcription is unavailable"); } }