Merge feat/vocabulary-hint: names-and-terms decoder prompt (spec 001 workstream 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/Architecture.md
#	docs/Decisions.md
#	docs/WorkBreakdown.md
#	server-py/sidecar.py
#	src/App.tsx
#	src/server/live.ts
#	src/server/transcribe.ts
#	tests/fixtures/fake_sidecar.py
#	tests/transcribe.test.ts
This commit is contained in:
Lucas Winther committed 2026-10-30 01:35:28 +01:00
commit 2c87471fdc
26 files changed
+1177 -21

No files matched your search

+24 -5
View File
@@ -26,6 +26,7 @@
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { TranscribeStreamLine } from "../lib/engine";
import { HINT_MAX_CHARS, normalizeHint } from "../lib/hint";
import { getFasterWhisperModel } from "../lib/models";
import type { TranscriptChunk, WhisperTask } from "../worker/protocol";
import { getSessionUser as appGetSessionUser, type SessionUser } from "./auth.ts";
@@ -52,6 +53,11 @@ export interface SidecarTranscribeRequest {
* chunks. "live": short server-live passes — sequential, no VAD (D-WS5a).
*/
mode?: SidecarMode;
/**
* Normalized "Names and terms" hint (D-WS2a); the sidecar truncates it to
* Whisper's prompt budget with faster-whisper's own tokenizer.
*/
prompt?: string;
}
export type SidecarMode = "file" | "live";
@@ -273,6 +279,7 @@ export function createSidecar(command?: readonly string[]): Sidecar {
language: request.language ?? null,
task: request.task ?? "transcribe",
mode: request.mode ?? "file",
prompt: request.prompt ?? null,
});
} catch (error) {
pending.delete(id);
@@ -332,18 +339,23 @@ interface TranscribeParams {
fasterWhisperModel: string;
language?: string;
task: WhisperTask;
prompt?: string;
}
/**
* Parse + validate the `language`/`task` query params shared by both the
* HTTP (`POST /api/transcribe`) and WebSocket (`GET /api/transcribe/live`,
* Parse + validate the `language`/`task`/`prompt` query params shared by both
* the HTTP (`POST /api/transcribe`) and WebSocket (`GET /api/transcribe/live`,
* src/server/live.ts) entry points, so a live session can't forward an
* unvalidated string into the sidecar protocol just because it skips
* `parseParams` below. A string return is the 400 error text.
* `parseParams` below (D-032). A string return is the 400 error text.
*
* `prompt` is the raw "Names and terms" hint (D-WS2a): at most
* HINT_MAX_CHARS (the UI's textarea limit), normalized exactly as the browser
* worker does; empty after normalization = no prompt.
*/
export function parseLanguageAndTask(
url: URL,
): { language?: string; task: WhisperTask } | string {
): { language?: string; task: WhisperTask; prompt?: string } | string {
const rawLanguage = url.searchParams.get("language");
let language: string | undefined;
if (rawLanguage !== null && rawLanguage !== "" && rawLanguage !== "auto") {
@@ -356,7 +368,13 @@ export function parseLanguageAndTask(
return `invalid task: ${rawTask}`;
}
return { language, task: rawTask };
const rawPrompt = url.searchParams.get("prompt") ?? "";
if (rawPrompt.length > HINT_MAX_CHARS) {
return `prompt too long (max ${HINT_MAX_CHARS} characters)`;
}
const prompt = normalizeHint(rawPrompt);
return { language, task: rawTask, ...(prompt && { prompt }) };
}
/** Parse + validate query params; a string is the 400 error text. */
@@ -472,6 +490,7 @@ export function createTranscribeHandlers(deps: TranscribeDeps): TranscribeHandle
audioPath,
language: params.language,
task: params.task,
prompt: params.prompt,
},
(segment) => {
chunks.push({