@burdenoff/fe-libs 2026.903.4 → 2026.903.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,4 +21,8 @@ export { parseConfirmationRefusal, findPendingWrite } from './pendingWrite';
21
21
  export type { PendingWrite, PendingWriteValue } from './pendingWrite';
22
22
  export { createSpeechEngines } from './speech/createSpeechEngine';
23
23
  export type { SpeechEngine, SpeechSpeakOptions, SpeechSpeakHandlers, SpeechErrorReason } from './speech/types';
24
+ export { buildVoiceTranscriptMarkdown, voiceTranscriptFilename, buildContinuedFromVoiceNote } from './voice/voiceTranscript';
25
+ export { resolveLanguagePair, replyFollowsInput } from './voice/languagePair';
26
+ export { ASSISTANT_LANGUAGES, resolveAssistantLanguage, findAssistantLanguage } from './voice/languages';
27
+ export type { AssistantLanguage } from './voice/languages';
24
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,OAAO,CAAC;AAEf,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,2BAA2B,EAC3B,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,cAAc,EACd,cAAc,EACd,yBAAyB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,sBAAsB,EACtB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,OAAO,CAAC;AAEf,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,sBAAsB,EACtB,yBAAyB,EACzB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,2BAA2B,EAC3B,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,cAAc,EACd,cAAc,EACd,yBAAyB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC/G,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAC7H,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACzG,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * How often to re-check for the assistant's reply while a turn is in flight.
3
+ *
4
+ * The assistant has no streaming transport: the prompt is a fire-and-forget POST
5
+ * and the reply is discovered by re-fetching the message list. So this interval
6
+ * is the granularity of the whole experience — it bounds how late the answer
7
+ * appears in chat AND how late speech can start, because auto-speak only fires
8
+ * once a poll observes the reply completed.
9
+ *
10
+ * A flat 1500ms was the previous behaviour, which meant up to 1.5s of dead time
11
+ * after the answer was already sitting on the server, and made streamed text
12
+ * arrive in visible jerks rather than flowing.
13
+ *
14
+ * ★ The schedule is a compromise, not free. Every poll re-fetches the newest
15
+ * messages in full rather than a delta, so a faster interval is a real increase
16
+ * in request rate. It is spent where it buys the most — the first few seconds,
17
+ * where most replies land — and given back afterwards, so a long-running turn
18
+ * costs no more than it used to.
19
+ *
20
+ * The proper fix is a delta fetch (`sinceMs`, which the shared client already
21
+ * supports) or a streaming transport; both are larger changes than this.
22
+ */
23
+ /** While the answer is most likely to land. */
24
+ export declare const POLL_FAST_MS = 600;
25
+ /** Still active, but the cheap wins are gone. */
26
+ export declare const POLL_STEADY_MS = 1000;
27
+ /** Long-running turn: back to the original cadence. */
28
+ export declare const POLL_SLOW_MS = 1500;
29
+ export declare const POLL_FAST_UNTIL_MS = 6000;
30
+ export declare const POLL_STEADY_UNTIL_MS = 20000;
31
+ /**
32
+ * Delay before the next poll, given how long this turn has been running.
33
+ *
34
+ * `elapsedMs` is injected rather than read from a clock so the schedule is
35
+ * testable and cannot drift with a mocked timer.
36
+ */
37
+ export declare function pollDelayMs(elapsedMs: number): number;
38
+ //# sourceMappingURL=assistantPollSchedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assistantPollSchedule.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/assistantPollSchedule.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,+CAA+C;AAC/C,eAAO,MAAM,YAAY,MAAM,CAAC;AAChC,iDAAiD;AACjD,eAAO,MAAM,cAAc,OAAO,CAAC;AACnC,uDAAuD;AACvD,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AACxC,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIrD"}
@@ -0,0 +1,36 @@
1
+ import { AssistantLanguage } from './languages';
2
+ export interface AssistantLanguagePair {
3
+ /** What the user speaks or types. Drives the recogniser. */
4
+ input: AssistantLanguage;
5
+ /** What the assistant answers in. Drives the prompt directive and the voice. */
6
+ reply: AssistantLanguage;
7
+ /**
8
+ * A human chose this reply language, rather than it being guessed.
9
+ *
10
+ * `buildLanguagePreamble` uses this to decide whether the language the user
11
+ * WROTE IN overrides their setting. For a guess it should — what they typed is
12
+ * better evidence than the interface language. For a choice it must not, or
13
+ * the preference is inert for everyone who types rather than dictates.
14
+ *
15
+ * ★ True when EITHER field was pinned, not only the reply field. Someone who
16
+ * picked Tamil before this setting existed has `replyLanguage` null and means
17
+ * "answer me in Tamil"; reading only the reply field would decide they never
18
+ * chose anything and hand their setting back to the mirror clause — silently
19
+ * re-breaking, for every existing user, the exact bug that clause was written
20
+ * to fix.
21
+ *
22
+ * False only when NOTHING was pinned and the language came from the interface.
23
+ *
24
+ * Resolved rather than merely non-null, so a stale persisted code from a build
25
+ * that offered more languages is not mistaken for a decision.
26
+ */
27
+ replyIsExplicit: boolean;
28
+ }
29
+ export declare function resolveLanguagePair(inputCode: string | null | undefined, replyCode: string | null | undefined, appLocale: string): AssistantLanguagePair;
30
+ /**
31
+ * Does the reply language follow the input, or has it been pinned?
32
+ *
33
+ * The selector shows "Match my language" for the former, and it is the default.
34
+ */
35
+ export declare function replyFollowsInput(replyCode: string | null | undefined): boolean;
36
+ //# sourceMappingURL=languagePair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"languagePair.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/languagePair.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAmD,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEtG,MAAM,WAAW,qBAAqB;IACpC,4DAA4D;IAC5D,KAAK,EAAE,iBAAiB,CAAC;IACzB,gFAAgF;IAChF,KAAK,EAAE,iBAAiB,CAAC;IACzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,SAAS,EAAE,MAAM,GAChB,qBAAqB,CAWvB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE/E"}
@@ -0,0 +1,15 @@
1
+ import { findAssistantLanguage as e, resolveAssistantLanguage as t } from "./languages.js";
2
+ //#region src/shared/assistant/voice/languagePair.ts
3
+ function n(n, r, i) {
4
+ let a = t(n, i), o = e(r);
5
+ return {
6
+ input: a,
7
+ reply: o ?? a,
8
+ replyIsExplicit: o !== null || e(n) !== null
9
+ };
10
+ }
11
+ function r(t) {
12
+ return e(t) === null;
13
+ }
14
+ //#endregion
15
+ export { r as replyFollowsInput, n as resolveLanguagePair };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The language the user is TALKING TO the assistant in (BOFF-7108).
3
+ *
4
+ * Separate from the app locale on purpose. A farmer whose phone and UI are in
5
+ * English may well speak Tamil, and until now the only way to dictate Tamil was
6
+ * to translate the entire interface — the recogniser language was derived from
7
+ * `useI18n().locale`, so the two could not be chosen independently.
8
+ *
9
+ * One selection drives three things, which is the point:
10
+ * 1. `recognition.lang` — what the recogniser is listening for;
11
+ * 2. the reply-language directive — what the model answers in;
12
+ * 3. the voice for reading that reply aloud.
13
+ *
14
+ * Deliberately NOT keyed on the app `Locale` union. That union is the set of
15
+ * languages the INTERFACE is translated into (9, from fe-libs); this is the set
16
+ * of languages a user might SPEAK, which is larger and grows for different
17
+ * reasons. Tying them together is what created the problem.
18
+ */
19
+ export interface AssistantLanguage {
20
+ /** Primary subtag. What we persist, and what the speech-locale helpers take. */
21
+ code: string;
22
+ /** BCP-47 tag handed to the Web Speech APIs — region matters to acoustic models. */
23
+ tag: string;
24
+ /**
25
+ * The ENDONYM — what a speaker calls their own language. Someone who reads
26
+ * only Tamil cannot find "Tamil" in a list; they can find "தமிழ்".
27
+ */
28
+ label: string;
29
+ /**
30
+ * English name, for the model-facing directive.
31
+ *
32
+ * Hardcoded rather than derived from `Intl.DisplayNames` because this string
33
+ * goes into a prompt: it must be stable and correct regardless of the reader's
34
+ * browser, and a missing `Intl` implementation must not silently degrade an
35
+ * instruction into a bare subtag the model may answer *about* instead of *in*.
36
+ */
37
+ english: string;
38
+ /**
39
+ * Speaking rate for this language, 1 = the engine's default.
40
+ *
41
+ * Nothing set a rate at all until now, so every user heard the spec default of
42
+ * 1.0 — reported as "it reads fast", which it is: a synthesised voice at 1.0 is
43
+ * harder to follow than a person at the same nominal speed, and harder again
44
+ * when the listener is outdoors or hearing a second language.
45
+ *
46
+ * Indic and Arabic get a further reduction. Their scripts pack more syllables
47
+ * per character than Latin, so the same nominal rate delivers noticeably more
48
+ * speech per second.
49
+ */
50
+ rate?: number;
51
+ }
52
+ /** Applied when a language does not override it. */
53
+ export declare const DEFAULT_SPEECH_RATE = 0.9;
54
+ /**
55
+ * India first and by speaker count, then the other app locales.
56
+ *
57
+ * Regions follow where HealthyBowl operates: `-IN` for the Indic languages,
58
+ * because a recogniser tuned for `ur-PK` or a voice for `bn-BD` is the wrong
59
+ * pronunciation model for this product's users.
60
+ */
61
+ export declare const ASSISTANT_LANGUAGES: readonly AssistantLanguage[];
62
+ export declare function findAssistantLanguage(code: string | null | undefined): AssistantLanguage | null;
63
+ /**
64
+ * Which language to use: the user's explicit choice, else the app locale, else
65
+ * English.
66
+ *
67
+ * The app-locale step is what keeps this backwards-compatible — a user who never
68
+ * touches the new control behaves exactly as before, because the value derived
69
+ * is the one the old code derived. An app locale outside this list (or a stale
70
+ * persisted code from a build that offered more) degrades to English rather than
71
+ * handing the recogniser a tag it will reject.
72
+ */
73
+ export declare function resolveAssistantLanguage(selectedCode: string | null | undefined, appLocale: string): AssistantLanguage;
74
+ //# sourceMappingURL=languages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"languages.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/languages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,oDAAoD;AACpD,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAGvC;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,iBAAiB,EAkB3D,CAAC;AAOF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,GAAG,IAAI,CAG/F;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,SAAS,EAAE,MAAM,GAChB,iBAAiB,CAEnB"}
@@ -0,0 +1,124 @@
1
+ //#region src/shared/assistant/voice/languages.ts
2
+ var e = .85, t = [
3
+ {
4
+ code: "en",
5
+ tag: "en-IN",
6
+ label: "English",
7
+ english: "English"
8
+ },
9
+ {
10
+ code: "hi",
11
+ tag: "hi-IN",
12
+ label: "हिन्दी",
13
+ english: "Hindi",
14
+ rate: e
15
+ },
16
+ {
17
+ code: "bn",
18
+ tag: "bn-IN",
19
+ label: "বাংলা",
20
+ english: "Bengali",
21
+ rate: e
22
+ },
23
+ {
24
+ code: "te",
25
+ tag: "te-IN",
26
+ label: "తెలుగు",
27
+ english: "Telugu",
28
+ rate: e
29
+ },
30
+ {
31
+ code: "mr",
32
+ tag: "mr-IN",
33
+ label: "मराठी",
34
+ english: "Marathi",
35
+ rate: e
36
+ },
37
+ {
38
+ code: "ta",
39
+ tag: "ta-IN",
40
+ label: "தமிழ்",
41
+ english: "Tamil",
42
+ rate: e
43
+ },
44
+ {
45
+ code: "gu",
46
+ tag: "gu-IN",
47
+ label: "ગુજરાતી",
48
+ english: "Gujarati",
49
+ rate: e
50
+ },
51
+ {
52
+ code: "kn",
53
+ tag: "kn-IN",
54
+ label: "ಕನ್ನಡ",
55
+ english: "Kannada",
56
+ rate: e
57
+ },
58
+ {
59
+ code: "ml",
60
+ tag: "ml-IN",
61
+ label: "മലയാളം",
62
+ english: "Malayalam",
63
+ rate: e
64
+ },
65
+ {
66
+ code: "pa",
67
+ tag: "pa-IN",
68
+ label: "ਪੰਜਾਬੀ",
69
+ english: "Punjabi",
70
+ rate: e
71
+ },
72
+ {
73
+ code: "ur",
74
+ tag: "ur-IN",
75
+ label: "اردو",
76
+ english: "Urdu",
77
+ rate: e
78
+ },
79
+ {
80
+ code: "ar",
81
+ tag: "ar-SA",
82
+ label: "العربية",
83
+ english: "Arabic",
84
+ rate: e
85
+ },
86
+ {
87
+ code: "es",
88
+ tag: "es-ES",
89
+ label: "Español",
90
+ english: "Spanish"
91
+ },
92
+ {
93
+ code: "fr",
94
+ tag: "fr-FR",
95
+ label: "Français",
96
+ english: "French"
97
+ },
98
+ {
99
+ code: "de",
100
+ tag: "de-DE",
101
+ label: "Deutsch",
102
+ english: "German"
103
+ },
104
+ {
105
+ code: "zh",
106
+ tag: "zh-CN",
107
+ label: "中文",
108
+ english: "Chinese"
109
+ },
110
+ {
111
+ code: "ja",
112
+ tag: "ja-JP",
113
+ label: "日本語",
114
+ english: "Japanese"
115
+ }
116
+ ], n = new Map(t.map((e) => [e.code, e])), r = n.get("en");
117
+ function i(e) {
118
+ return e ? n.get(e.trim().toLowerCase().split("-")[0]) ?? null : null;
119
+ }
120
+ function a(e, t) {
121
+ return i(e) ?? i(t) ?? r;
122
+ }
123
+ //#endregion
124
+ export { t as ASSISTANT_LANGUAGES, i as findAssistantLanguage, a as resolveAssistantLanguage };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Markdown → speakable plain text (BOFF-6290, assistant voice reply).
3
+ *
4
+ * An assistant reply is Markdown: headings, tables, fenced code, links, list
5
+ * markers, the odd emoji. Handing that string straight to a speech engine reads
6
+ * out "asterisk asterisk yield asterisk asterisk", spells URLs character by
7
+ * character, and grinds through a forty-row table one pipe at a time. This
8
+ * module flattens a reply into something a person would actually say.
9
+ *
10
+ * Design notes:
11
+ *
12
+ * - **Pure, and deliberately so.** This repo's vitest runs in the `node`
13
+ * environment (no jsdom), so audio and DOM behaviour are not testable here.
14
+ * The transformation is where the real logic lives, so it is isolated as a
15
+ * pure function with no imports and covered directly by `speakableText.test.ts`.
16
+ * - **Copy comes in through `labels`**, not `useTr`, so the function stays pure
17
+ * and the spoken placeholders still follow the app locale (the hook passes
18
+ * translated strings in). `{{rows}}` / `{{columns}}` interpolate the same way
19
+ * `useTr` params do.
20
+ * - **No lookbehind assertions.** Safari < 16.4 throws at *parse* time on a
21
+ * lookbehind, which would take down the whole module — the same trap the
22
+ * old hand-rolled markdown renderer fell into (see AssistantMarkdownRenderer).
23
+ * Every rule below uses a captured prefix or a lookahead instead.
24
+ */
25
+ export interface SpeakableLabels {
26
+ /** Spoken in place of a fenced code block, which is unlistenable read out. */
27
+ codeBlock: string;
28
+ /** Spoken for an image with no alt text. */
29
+ image: string;
30
+ /** Spoken in place of a URL — nobody wants "h t t p s colon slash slash". */
31
+ link: string;
32
+ /** Spoken in place of a table too large to read. Params: rows, columns. */
33
+ largeTable: string;
34
+ /** Appended when a very long reply is cut short. */
35
+ truncated: string;
36
+ /** Spoken for `@` in an address. English "at" inside a Tamil reply is jarring. */
37
+ emailAt: string;
38
+ /** Spoken for `.` in an address. */
39
+ emailDot: string;
40
+ /**
41
+ * Prefix for a numbered item nested under an UNnumbered one. Param: ordinal.
42
+ *
43
+ * Nested ordered items normally read as a dotted path ("2.1.") which already
44
+ * says where the sub-steps are; that is impossible when the parent is a
45
+ * bullet and has no number of its own, so the level is named instead.
46
+ */
47
+ nestedItem: string;
48
+ }
49
+ /**
50
+ * Sentence punctuation is deliberately absent: block-level labels get a full
51
+ * stop from `asSentence`, and the inline ones ("link", "image") have to read
52
+ * naturally mid-sentence — "see link for details", not "see Link. for details".
53
+ */
54
+ export declare const DEFAULT_SPEAKABLE_LABELS: SpeakableLabels;
55
+ /**
56
+ * Tables at or below this many body rows are flattened into "header: value"
57
+ * sentences, because a 2×3 table is usually THE answer. Anything larger is
58
+ * summarised — the point of the limit is that a long table is unlistenable, not
59
+ * that tables are unimportant.
60
+ */
61
+ export declare const MAX_SPOKEN_TABLE_ROWS = 3;
62
+ /** Roughly ten minutes of speech. Past this the reply is summarised as cut off. */
63
+ export declare const MAX_SPEAKABLE_CHARS = 8000;
64
+ /**
65
+ * Utterances are split at about this length before being queued.
66
+ *
67
+ * Chrome (and Edge) silently stop a single `SpeechSynthesisUtterance` after
68
+ * roughly fifteen seconds — a long-standing engine bug, not something we can
69
+ * feature-detect. Queueing sentence-sized utterances keeps every one of them
70
+ * well under that ceiling, so a long reply finishes instead of dying mid-word.
71
+ */
72
+ export declare const SPEECH_CHUNK_CHARS = 180;
73
+ /**
74
+ * Chunk ceiling for Chinese and Japanese, in CHARACTERS.
75
+ *
76
+ * The limit above is sized for Latin text, where ~180 characters is ~30 words
77
+ * and lands around ten seconds. A CJK character is a whole syllable — a zh voice
78
+ * reads roughly 4–6 of them a second — so 180 of them is a minute of speech,
79
+ * four times past the very cut-off the chunking exists to dodge. 40 characters
80
+ * is ~7–10 seconds, the same target the Latin limit is aiming at.
81
+ */
82
+ export declare const SPEECH_CHUNK_CJK_CHARS = 40;
83
+ export interface SpeakableOptions {
84
+ labels?: Partial<SpeakableLabels>;
85
+ maxChars?: number;
86
+ maxSpokenTableRows?: number;
87
+ }
88
+ /**
89
+ * Split a GFM table row into cells, honouring `\|` escapes.
90
+ *
91
+ * Written as a scan rather than `split(/(?<!\\)\|/)` on purpose — see the
92
+ * no-lookbehind note at the top of the file.
93
+ */
94
+ export declare function splitTableRow(line: string): string[];
95
+ /**
96
+ * Say an address the way a person reads one out.
97
+ *
98
+ * "support at healthybowl dot com" — an engine handed the raw string either
99
+ * spells it or swallows the punctuation, and expanding the dots here also keeps
100
+ * the sentence splitter from mistaking `.com` for the end of a sentence.
101
+ */
102
+ export declare function speakableEmail(address: string, at?: string, dot?: string): string;
103
+ /**
104
+ * Convert an assistant reply (Markdown) into plain text worth speaking.
105
+ *
106
+ * Returns `''` when there is nothing speakable — the caller should treat that
107
+ * as "no speak control", not as an error.
108
+ */
109
+ export declare function toSpeakableText(markdown: string, options?: SpeakableOptions): string;
110
+ /** Does this text need CJK treatment? True for mixed "土壤 pH 6.5" text too. */
111
+ export declare function containsCjk(text: string): boolean;
112
+ /**
113
+ * Split speakable text into utterance-sized chunks.
114
+ *
115
+ * See SPEECH_CHUNK_CHARS: this exists to dodge Chrome's ~15 second single
116
+ * utterance cut-off, so the split points are sentence boundaries (a pause the
117
+ * listener expects) rather than an arbitrary character offset. A CJK sentence
118
+ * gets the tighter CJK ceiling, because a character there is a whole syllable.
119
+ */
120
+ export declare function chunkForSpeech(text: string, maxChars?: number, cjkMaxChars?: number): string[];
121
+ //# sourceMappingURL=speakableText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speakableText.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/speakableText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,eAStC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA6CD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBpD;AAkBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAO,EAAE,GAAG,SAAQ,GAAG,MAAM,CAE9E;AAqJD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAiJxF;AAiMD,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAkFD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAA2B,EACrC,WAAW,GAAE,MAA+B,GAC3C,MAAM,EAAE,CAoEV"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What a voice says when you audition it.
3
+ *
4
+ * Picking a voice used to be silent: you chose one, nothing happened, and the
5
+ * first time you heard it was after asking the assistant a real question and
6
+ * waiting out a round trip. Auditioning a voice should not cost a model call.
7
+ *
8
+ * ★ The sample is in the SPOKEN language, not the interface language. A voice is
9
+ * being judged on how it reads Tamil, so making it read English tells the
10
+ * listener nothing about the decision they are making — and the two are
11
+ * independent settings here anyway.
12
+ *
13
+ * ★ Carried as a static table rather than through `tr()`, for the same reason
14
+ * `speechLabels` is: the i18n provider only ever fetches the ACTIVE locale, so
15
+ * there is nothing to ask for a Tamil string while the interface is English.
16
+ * These are one short sentence each and they are needed synchronously.
17
+ *
18
+ * Deliberately short. Long enough to hear timbre and cadence, short enough that
19
+ * auditioning four voices is not a chore, and it names the product so the
20
+ * listener hears the one word this voice will say most often.
21
+ */
22
+ /**
23
+ * The audition sentence for a locale.
24
+ *
25
+ * Falls back to English rather than to silence: a voice the listener can hear
26
+ * saying the wrong language still answers "what does this voice sound like",
27
+ * which is the question being asked. Region subtags are ignored — `ta-IN` and
28
+ * `ta-LK` want the same sentence.
29
+ */
30
+ export declare function voicePreviewText(locale: string): string;
31
+ /** Exposed for tests: which languages have their own audition sentence. */
32
+ export declare const PREVIEW_LANGUAGES: string[];
33
+ //# sourceMappingURL=voicePreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voicePreview.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/voicePreview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAsBH;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,UAAmC,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { VoiceTranscriptTurn } from './turnVisibility';
2
+ /**
3
+ * The spoken conversation as a Markdown document.
4
+ *
5
+ * ★ Sessions become headings rather than one flat list. A transcript is read
6
+ * later, when "which day was that?" is the actual question — running three
7
+ * separate exchanges together is precisely what the session id was added to
8
+ * stop, and a saved file is the copy most likely to outlive anyone's memory of
9
+ * the order.
10
+ *
11
+ * ★ Markdown, not JSON. The audience is a person forwarding a spoken exchange to
12
+ * an agronomist or an insurer; the whole-conversation JSON export already exists
13
+ * for anything machine-shaped.
14
+ */
15
+ export declare function buildVoiceTranscriptMarkdown(turns: readonly VoiceTranscriptTurn[], labels: {
16
+ title: string;
17
+ savedAt: string;
18
+ session: string;
19
+ earlier: string;
20
+ you: string;
21
+ assistant: string;
22
+ }, now: Date): string;
23
+ /** A filename that sorts by date and never collides across a single day. */
24
+ export declare function voiceTranscriptFilename(now: Date): string;
25
+ /**
26
+ * The note that "Continue in chat" leaves in the typed thread.
27
+ *
28
+ * ★ It carries the user's QUESTIONS and none of the assistant's answers, capped
29
+ * at three. The spec asks for "a concise summary rather than dumping every voice
30
+ * turn", and the answers are the long half — they are one click away in the
31
+ * transcript, and pasting them into the thread would be the exact leak the
32
+ * surfaces were split to prevent.
33
+ *
34
+ * ★ Built from the user's own words rather than by asking the model. A generated
35
+ * summary costs a round trip and can be wrong about what someone asked; their
36
+ * own sentences, clipped, cannot be. This also makes the action instant and
37
+ * free, which matters for a control people will press to get out of a voice
38
+ * session.
39
+ *
40
+ * ★ It tells the MODEL nothing, and does not need to. The client sends no
41
+ * history — the backend session already holds every spoken turn — so this note
42
+ * exists purely so the human can see, in the thread, what was discussed.
43
+ */
44
+ export declare function buildContinuedFromVoiceNote(turns: readonly VoiceTranscriptTurn[], labels: {
45
+ intro: string;
46
+ nothing: string;
47
+ more: (count: number) => string;
48
+ }): string;
49
+ //# sourceMappingURL=voiceTranscript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voiceTranscript.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/voiceTranscript.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAgCvF;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,SAAS,mBAAmB,EAAE,EACrC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAC5G,GAAG,EAAE,IAAI,GACR,MAAM,CA8BR;AAED,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,IAAI,GAAG,MAAM,CAIzD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,SAAS,mBAAmB,EAAE,EACrC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GAC1E,MAAM,CAaR"}
@@ -0,0 +1,42 @@
1
+ import { groupConsecutiveBySession as e } from "./turnVisibility.js";
2
+ //#region src/shared/assistant/voice/voiceTranscript.ts
3
+ var t = 3, n = 80;
4
+ function r(e, t) {
5
+ let n = e.replace(/\s+/g, " ").trim();
6
+ if (n.length <= t) return n;
7
+ let r = n.slice(0, t), i = r.lastIndexOf(" ");
8
+ return `${(i > t * .6 ? r.slice(0, i) : r).trimEnd()}…`;
9
+ }
10
+ function i(e) {
11
+ let t = new Date(e);
12
+ return Number.isNaN(t.getTime()) ? "" : t.toLocaleTimeString(void 0, {
13
+ hour: "2-digit",
14
+ minute: "2-digit"
15
+ });
16
+ }
17
+ function a(t, n, r) {
18
+ let a = e(t, (e) => e.sessionId), o = [
19
+ `# ${n.title}`,
20
+ "",
21
+ `_${n.savedAt}: ${r.toLocaleString()}_`,
22
+ ""
23
+ ], s = 0;
24
+ return a.forEach((e) => {
25
+ e.sessionId ? (s += 1, o.push(`## ${n.session} ${s}`, "")) : o.push(`## ${n.earlier}`, "");
26
+ for (let t of e.turns) {
27
+ let e = t.role === "user" ? n.you : n.assistant, r = i(t.timestamp);
28
+ o.push(r ? `**${e}** (${r})` : `**${e}**`, "", t.text.trim(), "");
29
+ }
30
+ }), `${o.join("\n").trimEnd()}\n`;
31
+ }
32
+ function o(e) {
33
+ return `healthybowl-voice-${`${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}`}-${`${String(e.getHours()).padStart(2, "0")}${String(e.getMinutes()).padStart(2, "0")}`}.md`;
34
+ }
35
+ function s(e, i) {
36
+ let a = e.filter((e) => e.role === "user" && e.text.trim()).map((e) => e.text);
37
+ if (a.length === 0) return i.nothing;
38
+ let o = a.slice(-t), s = a.length - o.length, c = [i.intro, ...o.map((e) => `• ${r(e, n)}`)];
39
+ return s > 0 && c.push(i.more(s)), c.join("\n");
40
+ }
41
+ //#endregion
42
+ export { s as buildContinuedFromVoiceNote, a as buildVoiceTranscriptMarkdown, o as voiceTranscriptFilename };
@@ -9,4 +9,7 @@ import { chatTurns as D, groupBySession as O, groupConsecutiveBySession as k, is
9
9
  import { toCaptionText as P } from "./shared/assistant/voice/captionText.js";
10
10
  import { findPendingWrite as F, parseConfirmationRefusal as I } from "./shared/assistant/pendingWrite.js";
11
11
  import { createSpeechEngines as L } from "./shared/assistant/speech/createSpeechEngine.js";
12
- export { v as AssistantRuntimeManager, y as buildProgress, n as buildPromptRequestBody, D as chatTurns, r as checkAssistantAiCredits, i as createAssistantSandbox, a as createAssistantSession, T as createSandboxAssistantTransport, L as createSpeechEngines, o as downloadAssistantArtifact, s as extendAssistantSandboxTTL, c as findExistingSandbox, F as findPendingWrite, l as getAssistantArtifacts, b as getAssistantMedia, u as getAssistantMessages, d as getAssistantSession, f as getAssistantStreamAccess, O as groupBySession, k as groupConsecutiveBySession, t as isQuotaExhaustedError, x as isRecoverable, A as isVoiceTurn, S as mapSessionToHistory, C as mergeMessagesById, I as parseConfirmationRefusal, p as reportAssistantAiTokenUsage, w as sanitize, m as sendAssistantPromptAsync, h as sendHeartbeat, P as toCaptionText, E as useSandboxAssistantTransport, e as useVoiceInput, j as voiceMarker, M as voiceSessionIdOf, N as voiceTurnsOnly, g as waitForAssistantServiceReady, _ as waitForSandboxRunning };
12
+ import { buildContinuedFromVoiceNote as R, buildVoiceTranscriptMarkdown as z, voiceTranscriptFilename as B } from "./shared/assistant/voice/voiceTranscript.js";
13
+ import { ASSISTANT_LANGUAGES as V, findAssistantLanguage as H, resolveAssistantLanguage as U } from "./shared/assistant/voice/languages.js";
14
+ import { replyFollowsInput as W, resolveLanguagePair as G } from "./shared/assistant/voice/languagePair.js";
15
+ export { V as ASSISTANT_LANGUAGES, v as AssistantRuntimeManager, R as buildContinuedFromVoiceNote, y as buildProgress, n as buildPromptRequestBody, z as buildVoiceTranscriptMarkdown, D as chatTurns, r as checkAssistantAiCredits, i as createAssistantSandbox, a as createAssistantSession, T as createSandboxAssistantTransport, L as createSpeechEngines, o as downloadAssistantArtifact, s as extendAssistantSandboxTTL, H as findAssistantLanguage, c as findExistingSandbox, F as findPendingWrite, l as getAssistantArtifacts, b as getAssistantMedia, u as getAssistantMessages, d as getAssistantSession, f as getAssistantStreamAccess, O as groupBySession, k as groupConsecutiveBySession, t as isQuotaExhaustedError, x as isRecoverable, A as isVoiceTurn, S as mapSessionToHistory, C as mergeMessagesById, I as parseConfirmationRefusal, W as replyFollowsInput, p as reportAssistantAiTokenUsage, U as resolveAssistantLanguage, G as resolveLanguagePair, w as sanitize, m as sendAssistantPromptAsync, h as sendHeartbeat, P as toCaptionText, E as useSandboxAssistantTransport, e as useVoiceInput, j as voiceMarker, M as voiceSessionIdOf, B as voiceTranscriptFilename, N as voiceTurnsOnly, g as waitForAssistantServiceReady, _ as waitForSandboxRunning };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/fe-libs",
3
- "version": "2026.903.4",
3
+ "version": "2026.903.5",
4
4
  "description": "Burdenoff frontend primitives and domain libraries",
5
5
  "type": "module",
6
6
  "bin": {