Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions src/host/start-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostInbound, { type: "start" }>;

/** 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". */
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 40 additions & 1 deletion test/start-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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 }));
Expand Down
Loading