diff --git a/src/host/start-config.ts b/src/host/start-config.ts index 8422094..019484d 100644 --- a/src/host/start-config.ts +++ b/src/host/start-config.ts @@ -6,17 +6,21 @@ import { DEFAULT_EXTRAS_BUDGET_USD } from "@livecap/engine"; -import { languageByCode } from "../languages.ts"; +import { languageByCode, SOURCE_AUTO_CODE } from "../languages.ts"; import type { EnginePref, HostInbound } from "../protocol.ts"; type StartMessage = Extract; -/** The meeting itself is spoken English (PROPOSAL positioning): reply - * suggestions and quick translate output INTO the meeting, and the archive - * header's source label reflects it. Captions themselves are auto-detected - * per sentence by whisper — there is no source-language setting (§8.6). */ -const MEETING_LANGUAGE = "English"; -const SOURCE_LANG_CODE = "EN"; +/** Reply suggestions and quick-translate output go INTO the meeting's spoken + * language (§8.5), and the archive header's source label reflects it. #94 lets + * the user pick that spoken language; when they leave it on "auto" (per-utterance + * whisper detection, the default) there is no single spoken language, so reply/ + * quick-translate falls back to English — the pre-#94 behavior. */ +const AUTO_MEETING_LANGUAGE = "English"; +/** Archive header source label (§8.9) for auto-detect (#94/#175): a neutral + * "AUTO" — detection is per-utterance, so no one language is the source. A + * PICKED source language uses its own archiveLabel (e.g. "KO"). */ +const AUTO_SOURCE_LABEL = "AUTO"; export interface ResolvedStartConfig { /** Translation target for the engine system prompt, e.g. "Korean". */ @@ -45,11 +49,22 @@ export interface ResolvedStartConfig { export function resolveStartConfig(message: StartMessage): ResolvedStartConfig { const language = languageByCode(message.targetLanguageCode); + // Spoken/source language (#94/#175): "auto" — and a blank value from an older + // shell — keeps per-utterance whisper detection (no fixed spoken language), so + // reply/quick-translate falls back to English and the archive header shows a + // neutral "AUTO". A picked tag uses its own name + archive label (Korean → + // "Korean" / "KO"). (`languageByCode("")` would be the `ko` default, hence the + // explicit blank check.) + const sourceCode = message.sourceLanguageCode.trim().toLowerCase(); + const source = + sourceCode === SOURCE_AUTO_CODE || sourceCode === "" + ? null + : languageByCode(message.sourceLanguageCode); return { targetLanguage: language.name, summaryLanguage: language.name, - meetingLanguage: MEETING_LANGUAGE, - sourceLangCode: SOURCE_LANG_CODE, + meetingLanguage: source ? source.name : AUTO_MEETING_LANGUAGE, + sourceLangCode: source ? source.archiveLabel : AUTO_SOURCE_LABEL, targetLangCode: language.archiveLabel, enginePref: message.enginePref === "local" ? "local" : "cli", poolUsd: Number.isFinite(message.poolUsd) && message.poolUsd > 0 ? message.poolUsd : 20, diff --git a/test/start-config.test.ts b/test/start-config.test.ts index 24751b9..7761902 100644 --- a/test/start-config.test.ts +++ b/test/start-config.test.ts @@ -34,7 +34,7 @@ describe("resolveStartConfig — language plumbing (#12 goal 4)", () => { expect(resolved.targetLanguage).toBe("Korean"); // (a) translation system prompt expect(resolved.summaryLanguage).toBe("Korean"); // (b) extras/summary output expect(resolved.targetLangCode).toBe("KO"); // (c) archive header meta - expect(resolved.sourceLangCode).toBe("EN"); + expect(resolved.sourceLangCode).toBe("AUTO"); // default source is auto-detect (#94) }); it("supports EN as a target and arbitrary BCP-47 tags", () => { @@ -54,6 +54,45 @@ describe("resolveStartConfig — language plumbing (#12 goal 4)", () => { }); }); +describe("resolveStartConfig — source (spoken) language wiring (#94/#175)", () => { + it("wires a picked source language to the archive header AND the meeting/quick-translate language", () => { + // Spoken Korean, translate into English: the archive header is KO → EN, and + // reply/quick-translate output goes into Korean (the meeting's language). + const resolved = resolveStartConfig( + startMessage({ sourceLanguageCode: "ko", targetLanguageCode: "en" }), + ); + expect(resolved.sourceLangCode).toBe("KO"); // was hardcoded "EN" before #175 + expect(resolved.meetingLanguage).toBe("Korean"); // quick-translate/reply output + expect(resolved.targetLangCode).toBe("EN"); + expect(resolved.summaryLanguage).toBe("English"); // summary stays in the target + }); + + it("carries arbitrary BCP-47 source tags through to the header + meeting language", () => { + const resolved = resolveStartConfig( + startMessage({ sourceLanguageCode: "ja", targetLanguageCode: "ko" }), + ); + expect(resolved.sourceLangCode).toBe("JA"); + expect(resolved.meetingLanguage).toBe("Japanese"); + }); + + it("keeps auto-detect neutral: header label AUTO, reply falls back to English", () => { + const resolved = resolveStartConfig( + startMessage({ sourceLanguageCode: "auto", targetLanguageCode: "ko" }), + ); + // The spoken language is detected per-utterance by whisper, so no fixed source + // language: a neutral "AUTO" header (never a phantom language) and the pre-#94 + // English reply fallback are preserved. + expect(resolved.sourceLangCode).toBe("AUTO"); + expect(resolved.meetingLanguage).toBe("English"); + }); + + it("treats a blank source (older shell) as auto — never languageByCode('') = the default", () => { + const resolved = resolveStartConfig(startMessage({ sourceLanguageCode: "" })); + expect(resolved.sourceLangCode).toBe("AUTO"); + expect(resolved.meetingLanguage).toBe("English"); + }); +}); + describe("resolveStartConfig — gauge + router mapping", () => { it("passes pool/reset-day through and clamps invalid values", () => { const ok = resolveStartConfig(startMessage({ poolUsd: 100, resetDay: 15 }));