@burdenoff/fe-libs 2026.903.2 → 2026.903.3
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.
- package/dist/shared/assistant/index.d.ts +3 -0
- package/dist/shared/assistant/index.d.ts.map +1 -1
- package/dist/shared/assistant/pendingWrite.d.ts +73 -0
- package/dist/shared/assistant/pendingWrite.d.ts.map +1 -0
- package/dist/shared/assistant/pendingWrite.js +85 -0
- package/dist/shared/assistant/voice/captionText.d.ts +43 -0
- package/dist/shared/assistant/voice/captionText.d.ts.map +1 -0
- package/dist/shared/assistant/voice/captionText.js +49 -0
- package/dist/shared/assistant/voice/speechVoices.d.ts +142 -0
- package/dist/shared/assistant/voice/speechVoices.d.ts.map +1 -0
- package/dist/shared/assistant/voice/voiceSession.d.ts +184 -0
- package/dist/shared/assistant/voice/voiceSession.d.ts.map +1 -0
- package/dist/shared/assistant/voice/voiceStageState.d.ts +49 -0
- package/dist/shared/assistant/voice/voiceStageState.d.ts.map +1 -0
- package/dist/shared-assistant.js +3 -1
- package/package.json +1 -1
|
@@ -16,4 +16,7 @@ export { isVoiceTurn, voiceSessionIdOf, voiceMarker, chatTurns, voiceTurnsOnly,
|
|
|
16
16
|
export type { VoiceSessionGroup, VoiceTranscriptTurn } from './voice/turnVisibility';
|
|
17
17
|
export { useVoiceInput } from './voice/useVoiceInput';
|
|
18
18
|
export type { UseVoiceInputOptions, UseVoiceInputResult, VoiceInputError } from './voice/useVoiceInput';
|
|
19
|
+
export { toCaptionText } from './voice/captionText';
|
|
20
|
+
export { parseConfirmationRefusal, findPendingWrite } from './pendingWrite';
|
|
21
|
+
export type { PendingWrite, PendingWriteValue } from './pendingWrite';
|
|
19
22
|
//# 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"}
|
|
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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write the assistant is asking permission for — read from the request
|
|
3
|
+
* itself, not from the model's description of it.
|
|
4
|
+
*
|
|
5
|
+
* ★ WHY THIS EXISTS. In "Take action" mode the in-sandbox helper refuses every
|
|
6
|
+
* mutation on its first attempt and returns a `CONFIRMATION_REQUIRED` tool
|
|
7
|
+
* result naming the operation and the exact values it was about to send. The
|
|
8
|
+
* model is then instructed to describe that change in prose and ask the user.
|
|
9
|
+
*
|
|
10
|
+
* So today the user approves a PARAGRAPH. The paragraph is generated by the same
|
|
11
|
+
* model that wants the write to go through, and nothing checks it against the
|
|
12
|
+
* payload — a field can be omitted, rounded, or described as something milder
|
|
13
|
+
* than it is, and the approval still counts.
|
|
14
|
+
*
|
|
15
|
+
* The refusal itself is already in the message parts the panel receives, and the
|
|
16
|
+
* panel throws it away (`getToolProgress` reduces a tool part to
|
|
17
|
+
* "Completed: <tool>"). Reading it back means the confirmation card can show the
|
|
18
|
+
* operation and values that will actually be sent.
|
|
19
|
+
*
|
|
20
|
+
* ★ WHAT THIS IS NOT. It does not make the UI the authorization boundary. The
|
|
21
|
+
* approval still travels back through the model, which holds the single-use
|
|
22
|
+
* token and could in principle retry without asking — see the module docstring
|
|
23
|
+
* in `ai-assistant/src/write-confirmation.ts`, which names the remaining gap and
|
|
24
|
+
* the fix ("the UI holding the approval"). This narrows what the user can be
|
|
25
|
+
* misled about; it does not close that.
|
|
26
|
+
*/
|
|
27
|
+
export interface PendingWriteValue {
|
|
28
|
+
key: string;
|
|
29
|
+
/** Already stringified for display; objects and arrays are JSON. */
|
|
30
|
+
value: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PendingWrite {
|
|
33
|
+
/** GraphQL operation name, e.g. `createCrop`. */
|
|
34
|
+
operation: string;
|
|
35
|
+
/**
|
|
36
|
+
* The variables, flattened for display. `null` when the refusal reported no
|
|
37
|
+
* variables or the block could not be parsed — the card then falls back to
|
|
38
|
+
* naming the operation alone rather than implying there is nothing to send.
|
|
39
|
+
*/
|
|
40
|
+
values: PendingWriteValue[] | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A tool part as the transport delivers it. Deliberately loose: `state.output`
|
|
44
|
+
* is typed `unknown` upstream and different agent versions have wrapped the text
|
|
45
|
+
* differently, so this accepts anything and proves the shape at runtime.
|
|
46
|
+
*/
|
|
47
|
+
interface ToolPartLike {
|
|
48
|
+
type?: string;
|
|
49
|
+
state?: {
|
|
50
|
+
output?: unknown;
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
} | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse one refusal.
|
|
56
|
+
*
|
|
57
|
+
* ★ The single-use token is deliberately NOT extracted. It is a capability: the
|
|
58
|
+
* write goes through if it is echoed back, so putting it on screen would invite
|
|
59
|
+
* a user — or anything reading the page — to replay it, and would turn a
|
|
60
|
+
* confirmation card into a bypass. The card needs the operation and the values;
|
|
61
|
+
* it never needs the token.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseConfirmationRefusal(text: string): PendingWrite | null;
|
|
64
|
+
/**
|
|
65
|
+
* The pending write in a set of message parts, if the agent refused one.
|
|
66
|
+
*
|
|
67
|
+
* Takes the LAST match: within a turn the model may be told to stop after the
|
|
68
|
+
* first refusal, but a turn that somehow produced two is asking about the most
|
|
69
|
+
* recent one.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findPendingWrite(parts: readonly ToolPartLike[] | undefined): PendingWrite | null;
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=pendingWrite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pendingWrite.d.ts","sourceRoot":"","sources":["../../../src/shared/assistant/pendingWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAAC;CACpC;AAOD;;;;GAIG;AACH,UAAU,YAAY;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CAClE;AA+BD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAsD1E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAWhG"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region src/shared/assistant/pendingWrite.ts
|
|
2
|
+
var e = "CONFIRMATION_REQUIRED", t = /CONFIRMATION_REQUIRED: the write "([^"]+)" was NOT sent/, n = "Values you were about to send:";
|
|
3
|
+
function r(e) {
|
|
4
|
+
if (typeof e == "string") return e;
|
|
5
|
+
if (e && typeof e == "object" && "output" in e) {
|
|
6
|
+
let t = e.output;
|
|
7
|
+
if (typeof t == "string") return t;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function i(e) {
|
|
12
|
+
if (!e || typeof e != "object" || Array.isArray(e)) return null;
|
|
13
|
+
let t = Object.entries(e);
|
|
14
|
+
return t.length === 0 ? null : t.map(([e, t]) => ({
|
|
15
|
+
key: e,
|
|
16
|
+
value: typeof t == "string" ? t : t == null ? "—" : JSON.stringify(t)
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function a(r) {
|
|
20
|
+
if (!r.includes(e)) return null;
|
|
21
|
+
let a = t.exec(r)?.[1]?.trim();
|
|
22
|
+
if (!a) return null;
|
|
23
|
+
let o = r.indexOf(n);
|
|
24
|
+
if (o === -1) return {
|
|
25
|
+
operation: a,
|
|
26
|
+
values: null
|
|
27
|
+
};
|
|
28
|
+
let s = r.slice(o + 30).trim();
|
|
29
|
+
if (!s || s.startsWith("(no variables)")) return {
|
|
30
|
+
operation: a,
|
|
31
|
+
values: null
|
|
32
|
+
};
|
|
33
|
+
let c = s.indexOf("{");
|
|
34
|
+
if (c === -1) return {
|
|
35
|
+
operation: a,
|
|
36
|
+
values: null
|
|
37
|
+
};
|
|
38
|
+
let l = 0, u = -1, d = !1, f = !1;
|
|
39
|
+
for (let e = c; e < s.length; e += 1) {
|
|
40
|
+
let t = s[e];
|
|
41
|
+
if (f) {
|
|
42
|
+
f = !1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (t === "\\" && d) {
|
|
46
|
+
f = !0;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (t === "\"") d = !d;
|
|
50
|
+
else if (!d && t === "{") l += 1;
|
|
51
|
+
else if (!d && t === "}" && (--l, l === 0)) {
|
|
52
|
+
u = e + 1;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (u === -1) return {
|
|
57
|
+
operation: a,
|
|
58
|
+
values: null
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
return {
|
|
62
|
+
operation: a,
|
|
63
|
+
values: i(JSON.parse(s.slice(c, u)))
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
return {
|
|
67
|
+
operation: a,
|
|
68
|
+
values: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function o(e) {
|
|
73
|
+
if (!e) return null;
|
|
74
|
+
let t = null;
|
|
75
|
+
for (let n of e) {
|
|
76
|
+
if (n?.type !== "tool") continue;
|
|
77
|
+
let e = r(n.state?.output);
|
|
78
|
+
if (!e) continue;
|
|
79
|
+
let i = a(e);
|
|
80
|
+
i && (t = i);
|
|
81
|
+
}
|
|
82
|
+
return t;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { o as findPendingWrite, a as parseConfirmationRefusal };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → the words, for a caption someone READS.
|
|
3
|
+
*
|
|
4
|
+
* The live voice caption is the one surface where the model's raw Markdown was
|
|
5
|
+
* printed literally: bold stars, hash headings, table pipes, fence backticks.
|
|
6
|
+
* The audio path has been clean for a while (`toSpeakableText`), but that
|
|
7
|
+
* function is the wrong tool here and reusing it would be worse than the bug.
|
|
8
|
+
*
|
|
9
|
+
* ★ `toSpeakableText` is a SCRIPT FOR A VOICE, not a Markdown cleaner. It
|
|
10
|
+
* replaces a table with "Table with 40 rows and 2 columns", a fenced block with
|
|
11
|
+
* "Code block", a link with the word "link", and rewrites `x@y.com` as "x at y
|
|
12
|
+
* dot com" — sensible in an earpiece, destructive on a screen where the reader
|
|
13
|
+
* can simply read the thing. Worse, those replacements come from an English
|
|
14
|
+
* default list only localised inside the speech provider, so reusing it would
|
|
15
|
+
* print English chrome inside a Tamil or Hindi caption: the exact
|
|
16
|
+
* mixed-language defect that code exists to prevent.
|
|
17
|
+
*
|
|
18
|
+
* So this strips MARKERS and keeps CONTENT. Every cell value, all code text,
|
|
19
|
+
* bare URLs, email addresses, emoji and punctuation survive verbatim. Nothing
|
|
20
|
+
* is summarised, truncated or annotated: a caption that quietly drops the
|
|
21
|
+
* user's numbers is a worse failure than one showing a stray asterisk. Where
|
|
22
|
+
* this function is unsure, it leaves the text ALONE.
|
|
23
|
+
*
|
|
24
|
+
* ★ Newlines are PRESERVED, which only helps where the element renders them.
|
|
25
|
+
* `VoiceCaption` carries `whitespace-pre-wrap` for exactly that. The mini
|
|
26
|
+
* controller deliberately does NOT: it is `line-clamp-3`, and real newlines
|
|
27
|
+
* each burn one of the three lines — measured at the strip's true width, that
|
|
28
|
+
* cut a 116-character reply to 41 visible characters. Markers still go; the
|
|
29
|
+
* breaks are allowed to collapse on the clamped surface.
|
|
30
|
+
*
|
|
31
|
+
* ★ NO LOOKBEHIND ANYWHERE IN CODE. Safari < 16.4 throws on that assertion
|
|
32
|
+
* class at PARSE time, not match time — one literal takes down the whole chunk
|
|
33
|
+
* on those devices, and this ships to phones. Lookahead is fine; where a
|
|
34
|
+
* leading boundary is needed it is CAPTURED and put back. Same rule as
|
|
35
|
+
* `speakableText.ts`, and `captionText.test.ts` asserts it on the source.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Strip Markdown markers for display, keeping all of the content.
|
|
39
|
+
*
|
|
40
|
+
* Safe on plain text: a reply with no Markdown comes back unchanged.
|
|
41
|
+
*/
|
|
42
|
+
export declare function toCaptionText(markdown: string): string;
|
|
43
|
+
//# sourceMappingURL=captionText.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captionText.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/captionText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAkKH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA2CtD"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/shared/assistant/voice/captionText.ts
|
|
2
|
+
var e = "", t = "\\uE000", n = /^[\s:|-]*$/, r = /^\s*([-*_])\s*(?:\1\s*){2,}$/, i = /^\s*=+\s*$/;
|
|
3
|
+
function a(e) {
|
|
4
|
+
return e.includes("|") && e.includes("-") && n.test(e);
|
|
5
|
+
}
|
|
6
|
+
function o(e) {
|
|
7
|
+
let t = /* @__PURE__ */ new Set();
|
|
8
|
+
return e.forEach((n, r) => {
|
|
9
|
+
if (!a(n)) return;
|
|
10
|
+
let i = e[r - 1];
|
|
11
|
+
r > 0 && i !== void 0 && i.includes("|") && t.add(r - 1);
|
|
12
|
+
for (let n = r + 1; n < e.length; n += 1) {
|
|
13
|
+
let r = e[n];
|
|
14
|
+
if (r === void 0 || r.trim() === "" || !r.includes("|")) break;
|
|
15
|
+
t.add(n);
|
|
16
|
+
}
|
|
17
|
+
}), t;
|
|
18
|
+
}
|
|
19
|
+
function s(n, r) {
|
|
20
|
+
let i = (t) => (r.push(t), `${e}${r.length - 1}${e}`);
|
|
21
|
+
return n.replace(/^ {0,3}```[^\n]*\n([\s\S]*?)^ {0,3}```[^\n]*$/gm, (e, t) => i(t.replace(/\n$/, ""))).replace(/^ {0,3}```[^\n]*\n?([\s\S]*)$/m, (e, t) => i(t)).replace(/`([^`\n]+)`/g, (e, t) => i(t)).replace(RegExp(`<((?:https?://|mailto:)[^>\\s${t}]+)>`, "gi"), (e, t) => i(t)).replace(RegExp(`<([^@<>\\s${t}]+@[^@<>\\s${t}]+\\.[^@<>\\s${t}]+)>`, "g"), (e, t) => i(t)).replace(RegExp(`\\b(?:https?://|www\\.)[^\\s<>()\\[\\]${t}]+`, "gi"), (e) => i(e)).replace(/\b[\w.+-]{1,64}@[\w-]{1,63}\.[\w.-]{1,63}\b/g, (e) => i(e));
|
|
22
|
+
}
|
|
23
|
+
function c(t, n) {
|
|
24
|
+
let r = RegExp(`${e}(\\d+)${e}`, "g"), i = t;
|
|
25
|
+
for (let e = 0; e <= n.length; e += 1) {
|
|
26
|
+
let e = i.replace(r, (e, t) => n[Number(t)] ?? "");
|
|
27
|
+
if (e === i) break;
|
|
28
|
+
i = e;
|
|
29
|
+
}
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
function l(e) {
|
|
33
|
+
return e.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1").replace(/(^|[^\w~])~~(\S(?:[^\n]*?\S)?)~~(?=[^\w~]|$)/g, "$1$2").replace(/(^|[^\w*])\*\*(\S(?:[^\n]*?\S)?)\*\*(?=[^\w*]|$)/g, "$1$2").replace(/(^|[^\p{L}\p{N}_])__(\S(?:[^\n]*?\S)?)__(?=[^\p{L}\p{N}_]|$)/gu, "$1$2").replace(/(^|[^\w*])\*(\S(?:[^*\n]*\S)?)\*(?=[^\w*]|$)/g, "$1$2").replace(/(^|[^\p{L}\p{N}_])_(\S(?:[^_\n]*\S)?)_(?=[^\p{L}\p{N}_]|$)/gu, "$1$2");
|
|
34
|
+
}
|
|
35
|
+
function u(e) {
|
|
36
|
+
return e.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((e) => e.trim()).join(" · ");
|
|
37
|
+
}
|
|
38
|
+
function d(t) {
|
|
39
|
+
if (!t) return "";
|
|
40
|
+
let n = [], d = s(t.replace(/\r\n/g, "\n").replace(new RegExp(e, "g"), ""), n).split("\n"), f = o(d), p = [];
|
|
41
|
+
return d.forEach((e, t) => {
|
|
42
|
+
if (a(e) || r.test(e) || i.test(e) && (p[p.length - 1] ?? "").trim() !== "") return;
|
|
43
|
+
let n = e;
|
|
44
|
+
for (; /^\s*>\s?/.test(n);) n = n.replace(/^\s*>\s?/, "");
|
|
45
|
+
n = n.replace(/^\s*#{1,6}\s+/, ""), n = n.replace(/^(\s*)[-*+]\s+/, "$1"), n = n.replace(/^(\s*)\d{1,2}[.)]\s+/, "$1"), f.has(t) && (n = u(n)), p.push(l(n).replace(/\s+$/, ""));
|
|
46
|
+
}), c(p.join("\n").replace(/\n{3,}/g, "\n\n").trim(), n);
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { d as toCaptionText };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Locale } from '../../providers/shell/I18nProvider';
|
|
2
|
+
/**
|
|
3
|
+
* BCP-47 tag we ask the engine for, per app locale.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately identical to `useVoiceInput`'s RECOGNITION_LANG: regional choices
|
|
6
|
+
* follow where HealthyBowl actually operates (India for ta/hi), because a voice's
|
|
7
|
+
* pronunciation model is region-specific. Typed `Record<Locale, …>` so adding a
|
|
8
|
+
* locale to fe-libs fails the build here rather than silently falling back to
|
|
9
|
+
* English.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SPEECH_LANG: Record<Locale, string>;
|
|
12
|
+
/**
|
|
13
|
+
* The subset of `SpeechSynthesisVoice` this module needs. Structural on purpose:
|
|
14
|
+
* `selectVoiceForLocale` returns the caller's own array element, so the browser
|
|
15
|
+
* engine gets back the real `SpeechSynthesisVoice` instance it must assign to
|
|
16
|
+
* `utterance.voice`, while tests pass literals.
|
|
17
|
+
*/
|
|
18
|
+
export interface SpeechVoiceLike {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly lang: string;
|
|
21
|
+
/** The engine's default voice (usually the OS UI language). */
|
|
22
|
+
readonly default?: boolean;
|
|
23
|
+
/** On-device rather than server-side — no network, no latency, no vendor. */
|
|
24
|
+
readonly localService?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** `en_GB`, `EN-gb` and `en-GB` are the same tag as far as matching goes. */
|
|
27
|
+
export declare function normalizeLangTag(tag: string): string;
|
|
28
|
+
/** `ta-IN` → `ta`. The part that decides whether a voice is intelligible at all. */
|
|
29
|
+
export declare function primaryLanguageSubtag(tag: string): string;
|
|
30
|
+
export declare function speechLangForLocale(locale: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Pick the best installed voice for an app locale, or `null` when the device has
|
|
33
|
+
* no voice for that language at all.
|
|
34
|
+
*
|
|
35
|
+
* The ladder, in order:
|
|
36
|
+
* 1. exact tag (`ta-IN` for Tamil);
|
|
37
|
+
* 2. same language, any region (`en-GB` when only `en-US` was asked for) —
|
|
38
|
+
* an accent is a far smaller problem than the wrong language;
|
|
39
|
+
* 3. within whichever tier matched: the engine's default voice, then an
|
|
40
|
+
* on-device voice, then engine order.
|
|
41
|
+
*
|
|
42
|
+
* There is intentionally NO cross-language fallback. Reading a Tamil reply with
|
|
43
|
+
* an English voice produces confident nonsense; the caller disables the control
|
|
44
|
+
* and says why instead (see `AssistantSpeechProvider`).
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Every installed voice that can read this language, best first.
|
|
48
|
+
*
|
|
49
|
+
* Exposed so the UI can offer a CHOICE. Quality varies enormously between
|
|
50
|
+
* voices for the same language — Apple's "Enhanced"/"Premium" downloads and
|
|
51
|
+
* Chrome's network "Google …" voices are markedly better than the compact
|
|
52
|
+
* on-device defaults — and none of that is machine-readable, so the honest
|
|
53
|
+
* answer is to rank by the weak signals available and let the listener decide.
|
|
54
|
+
*/
|
|
55
|
+
export declare function voicesForLocale<T extends SpeechVoiceLike>(voices: readonly T[], locale: string): T[];
|
|
56
|
+
/**
|
|
57
|
+
* Best-effort quality ordering. Higher is better.
|
|
58
|
+
*
|
|
59
|
+
* Every signal here is a heuristic on a NAME, which is fragile and locale-
|
|
60
|
+
* dependent — hence the voice picker: this decides the DEFAULT, not the answer.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately NOT preferring `localService`. That is an offline-capability
|
|
63
|
+
* flag, not a quality one, and on desktop Chrome it actively selects the
|
|
64
|
+
* compact on-device voice over the better-sounding network one. The previous
|
|
65
|
+
* ladder preferred it and was therefore choosing the worse voice.
|
|
66
|
+
*/
|
|
67
|
+
export declare function voiceQualityRank(voice: SpeechVoiceLike): number;
|
|
68
|
+
/**
|
|
69
|
+
* The voice to read this language with: the user's explicit pick if it is still
|
|
70
|
+
* installed, otherwise the best-ranked one.
|
|
71
|
+
*
|
|
72
|
+
* `preferredName` is matched by name because that is the only identifier stable
|
|
73
|
+
* across sessions — voice objects are recreated on every page load, and
|
|
74
|
+
* `voiceURI` is not reliably unique across platforms.
|
|
75
|
+
*/
|
|
76
|
+
export declare function selectVoiceForLocale<T extends SpeechVoiceLike>(voices: readonly T[], locale: string, preferredName?: string | null): T | null;
|
|
77
|
+
/**
|
|
78
|
+
* What the UI should do with the speak control, given what the engine reports.
|
|
79
|
+
*
|
|
80
|
+
* `pending` matters: `speechSynthesis.getVoices()` famously returns `[]` on the
|
|
81
|
+
* first call in Chrome and only fills in after `voiceschanged`. Treating an
|
|
82
|
+
* empty list as "no voice" would disable the button for a moment on every load
|
|
83
|
+
* and, on engines that never fire the event, forever — so an empty list stays
|
|
84
|
+
* enabled and we let the engine resolve `utterance.lang` itself.
|
|
85
|
+
*/
|
|
86
|
+
export type VoiceAvailability = 'unsupported' | 'pending' | 'ready' | 'missing-voice' | 'no-voices';
|
|
87
|
+
/**
|
|
88
|
+
* `probeSettled` is what separates "the list has not arrived yet" from "this
|
|
89
|
+
* device genuinely has none".
|
|
90
|
+
*
|
|
91
|
+
* Both look like `voices.length === 0`, and the difference is not knowable from
|
|
92
|
+
* the list alone — only the caller, having waited out `VOICE_LIST_TIMEOUT_MS`
|
|
93
|
+
* and looked again, can tell them apart. Defaulting to `false` keeps the
|
|
94
|
+
* optimistic reading (`pending`, control stays enabled) for every caller that
|
|
95
|
+
* has not probed, which is the pre-existing behaviour.
|
|
96
|
+
*
|
|
97
|
+
* Without this distinction the empty list stays `pending` forever, `speechGate`
|
|
98
|
+
* keeps answering `wait-for-voices`, and the button is enabled but does nothing
|
|
99
|
+
* on every press — a dead control with no explanation, which is exactly how
|
|
100
|
+
* this feature looked on any Linux box or bare container.
|
|
101
|
+
*/
|
|
102
|
+
export declare function voiceAvailability(isSupported: boolean, voices: readonly SpeechVoiceLike[], locale: string, probeSettled?: boolean): VoiceAvailability;
|
|
103
|
+
/**
|
|
104
|
+
* Human name for the language we would have spoken, in the reader's own locale
|
|
105
|
+
* ("Tamil" for an English UI, "தமிழ்" for a Tamil one).
|
|
106
|
+
*
|
|
107
|
+
* Used to make the disabled reason actionable: "No Tamil voice is installed"
|
|
108
|
+
* tells a user what to go and install; "no voice is installed" does not.
|
|
109
|
+
* `Intl.DisplayNames` is unavailable in some embedded webviews, so the subtag
|
|
110
|
+
* itself is the fallback — degraded copy beats a thrown render.
|
|
111
|
+
*/
|
|
112
|
+
export declare function languageDisplayName(languageTag: string, inLocale: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* What a caller that wants to speak RIGHT NOW should do.
|
|
115
|
+
*
|
|
116
|
+
* The one decision every playback path has to make, in one place, because
|
|
117
|
+
* having it in three (the per-message button, the header toggle, the auto-speak
|
|
118
|
+
* subscription) is how "there is no cross-language fallback" stopped being true
|
|
119
|
+
* on two of them: an empty or unmatched voice list means the engine picks its
|
|
120
|
+
* own default, and a Tamil reply comes out in an English voice.
|
|
121
|
+
*
|
|
122
|
+
* - `speak` — a voice for this language exists; go.
|
|
123
|
+
* - `wait-for-voices` — the list has not arrived yet (Chrome's first
|
|
124
|
+
* `getVoices()` returns `[]`). Speaking now would gamble on the engine
|
|
125
|
+
* default, and refusing forever would disable the control on engines that
|
|
126
|
+
* never fire `voiceschanged`; so the caller waits for the list, briefly, and
|
|
127
|
+
* asks again. The control stays enabled throughout.
|
|
128
|
+
* - `block` — unsupported, or no voice for this language. Say nothing.
|
|
129
|
+
*/
|
|
130
|
+
export type SpeechGate = 'speak' | 'wait-for-voices' | 'block';
|
|
131
|
+
export declare function speechGate(availability: VoiceAvailability): SpeechGate;
|
|
132
|
+
/**
|
|
133
|
+
* Should the control be visible but inert?
|
|
134
|
+
*
|
|
135
|
+
* `pending` deliberately is NOT disabled — see `speechGate`. `no-voices` is,
|
|
136
|
+
* because by then we have waited and looked again: staying enabled would leave
|
|
137
|
+
* the user pressing a button that can never do anything.
|
|
138
|
+
*/
|
|
139
|
+
export declare function isSpeechBlocked(availability: VoiceAvailability): boolean;
|
|
140
|
+
/** How long to wait for a late voice list before giving up on one attempt. */
|
|
141
|
+
export declare const VOICE_LIST_TIMEOUT_MS = 2000;
|
|
142
|
+
//# sourceMappingURL=speechVoices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"speechVoices.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/speechVoices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oCAAoC,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAU9C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;;GAcG;AACH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAYpG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAa/D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,eAAe,EAC5D,MAAM,EAAE,SAAS,CAAC,EAAE,EACpB,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAC5B,CAAC,GAAG,IAAI,CAQV;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,SAAS,GAAG,OAAO,GAAG,eAAe,GAAG,WAAW,CAAC;AAEpG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,OAAO,EACpB,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,MAAM,EAAE,MAAM,EACd,YAAY,UAAQ,GACnB,iBAAiB,CAInB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAE/D,wBAAgB,UAAU,CAAC,YAAY,EAAE,iBAAiB,GAAG,UAAU,CAItE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,iBAAiB,GAAG,OAAO,CAExE;AAED,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,OAAQ,CAAC"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voice-conversation state machine (BOFF-7120).
|
|
3
|
+
*
|
|
4
|
+
* A live voice session is a loop — listen, submit, wait, speak, listen again —
|
|
5
|
+
* and every interesting decision in it is about TIMING and INTERRUPTION rather
|
|
6
|
+
* than about rendering. Those decisions live here, with no DOM, no audio and no
|
|
7
|
+
* React, for the same reason `browserSpeechEngine` pushed its logic into
|
|
8
|
+
* `speakableText`/`speechVoices`: this repo's vitest runs in the `node`
|
|
9
|
+
* environment, so anything touching `window` cannot be tested at all.
|
|
10
|
+
*
|
|
11
|
+
* ★ **The microphone is CLOSED while the assistant speaks.** It was open, so
|
|
12
|
+
* that talking over the assistant could interrupt it, guarded by matching the
|
|
13
|
+
* transcript against the text being spoken. That guard was too weak to survive
|
|
14
|
+
* contact with reality: speech recognition of synthesised speech mis-hears it
|
|
15
|
+
* ("sow ragi" -> "so ragi"), so exact containment fails, the mis-heard echo is
|
|
16
|
+
* accepted as a new question, submitted, answered, spoken — and round it goes.
|
|
17
|
+
* The operator hit this loop on a real device.
|
|
18
|
+
*
|
|
19
|
+
* Browsers do not give the Web Speech API the acoustic echo cancellation that a
|
|
20
|
+
* native duplex app has, so there is no text-level fix for this. Interruption is
|
|
21
|
+
* now an explicit tap instead, which is reliable, and the echo check survives
|
|
22
|
+
* only as a guard on the moments just after playback stops.
|
|
23
|
+
*
|
|
24
|
+
* ★ **Mute is NOT a phase.** It is a property of the microphone, orthogonal to
|
|
25
|
+
* where the conversation is. Modelling it as a state meant muting while the
|
|
26
|
+
* assistant was thinking discarded the `thinking` phase, so the reply — already
|
|
27
|
+
* in flight — was then ignored, and unmuting landed in `listening` with nothing
|
|
28
|
+
* pending and nothing coming. The session dead-ended and the user's question was
|
|
29
|
+
* silently lost.
|
|
30
|
+
*
|
|
31
|
+
* What this deliberately does NOT model: the assistant is turn-based
|
|
32
|
+
* (speech -> text -> LLM -> text -> speech), so `thinking` is a real, visible
|
|
33
|
+
* phase lasting seconds. A native speech-to-speech API would collapse
|
|
34
|
+
* listening/thinking/speaking into one duplex stream; we are not that, and
|
|
35
|
+
* pretending otherwise would hide latency the UI must show.
|
|
36
|
+
*/
|
|
37
|
+
/** Where the CONVERSATION is. Independent of whether the mic is capturing. */
|
|
38
|
+
export type VoiceSessionPhase = 'idle' | 'listening' | 'thinking' | 'speaking' | 'ended';
|
|
39
|
+
export interface VoiceSessionSnapshot {
|
|
40
|
+
phase: VoiceSessionPhase;
|
|
41
|
+
/** Microphone off. Orthogonal to `phase` — see the header. */
|
|
42
|
+
muted: boolean;
|
|
43
|
+
/** FINALISED speech for the turn not yet submitted, accumulated. */
|
|
44
|
+
pending: string;
|
|
45
|
+
/** The recogniser's current guess, replaced wholesale until it finalises. */
|
|
46
|
+
interim: string;
|
|
47
|
+
/** Why the session stopped, when it stopped abnormally. */
|
|
48
|
+
error: VoiceSessionError | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Deliberately does NOT include a "playback blocked" variant. Nothing can
|
|
52
|
+
* detect it — the speech provider does not surface a per-message playback
|
|
53
|
+
* refusal — and the watchdog in `useVoiceSession` already recovers by handing
|
|
54
|
+
* the turn back silently, which continues the conversation rather than killing
|
|
55
|
+
* it. An error the code cannot raise is dead UI copy.
|
|
56
|
+
*/
|
|
57
|
+
export type VoiceSessionError = 'no-speech-support' | 'no-voice-support' | 'mic-denied';
|
|
58
|
+
export type VoiceSessionEvent =
|
|
59
|
+
/** `final` distinguishes a committed phrase from a revisable guess. */
|
|
60
|
+
{
|
|
61
|
+
type: 'start';
|
|
62
|
+
} | {
|
|
63
|
+
type: 'speech';
|
|
64
|
+
transcript: string;
|
|
65
|
+
final: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Nothing heard for `SILENCE_SUBMIT_MS`. */
|
|
68
|
+
| {
|
|
69
|
+
type: 'silence';
|
|
70
|
+
} | {
|
|
71
|
+
type: 'reply';
|
|
72
|
+
text: string;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'speech-ended';
|
|
75
|
+
} | {
|
|
76
|
+
type: 'mute';
|
|
77
|
+
muted: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** The user tapped to cut the assistant off. Explicit, not inferred. */
|
|
80
|
+
| {
|
|
81
|
+
type: 'interrupt';
|
|
82
|
+
} | {
|
|
83
|
+
type: 'fail';
|
|
84
|
+
error: VoiceSessionError;
|
|
85
|
+
} | {
|
|
86
|
+
type: 'end';
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* How long the user must stop talking before the turn is submitted.
|
|
90
|
+
*
|
|
91
|
+
* Long enough to survive a mid-sentence breath — a farmer describing a problem
|
|
92
|
+
* pauses — and short enough that the session does not feel unresponsive. A
|
|
93
|
+
* premature send truncates the question, which costs a whole 2-5s round trip to
|
|
94
|
+
* recover from.
|
|
95
|
+
*
|
|
96
|
+
* ★ Raised from 1200ms because 1.2s does not survive an ordinary pause: the
|
|
97
|
+
* operator reported being cut off mid-sentence, and the cost is worse than a
|
|
98
|
+
* truncated prompt — once the turn is submitted the reducer discards every
|
|
99
|
+
* `speech` event outside `listening`, so the rest of the sentence is not
|
|
100
|
+
* queued, it is thrown away, and the assistant answers half a question.
|
|
101
|
+
*
|
|
102
|
+
* The asymmetry is what settles the number: waiting 800ms longer costs a moment
|
|
103
|
+
* of silence, while cutting someone off costs the whole round trip AND makes
|
|
104
|
+
* them repeat themselves.
|
|
105
|
+
*/
|
|
106
|
+
export declare const SILENCE_SUBMIT_MS = 2000;
|
|
107
|
+
/**
|
|
108
|
+
* Quiet needed before the pause is SHOWN as a pause.
|
|
109
|
+
*
|
|
110
|
+
* The submit timer above is armed the instant there is something submittable —
|
|
111
|
+
* which is mid-sentence — so it cannot itself say whether the user has stopped.
|
|
112
|
+
* This is the delay before the stage switches to `endpoint-grace`, leaving the
|
|
113
|
+
* remaining ~1.6s of the window visibly readable as "still listening, finish
|
|
114
|
+
* your thought" rather than as the session having already moved on.
|
|
115
|
+
*
|
|
116
|
+
* Short enough to be seen, long enough that an ordinary breath between words
|
|
117
|
+
* does not flicker the stage. ★800, not 400: this file already records that
|
|
118
|
+
* SILENCE_SUBMIT_MS had to go 1200 -> 2000 because ordinary pauses ran longer
|
|
119
|
+
* than expected, so a 400ms threshold sits inside normal inter-clause silence
|
|
120
|
+
* and would oscillate the stage mid-sentence. Still leaves ~1.2s visible.
|
|
121
|
+
*/
|
|
122
|
+
export declare const ENDPOINT_GRACE_MS = 800;
|
|
123
|
+
/**
|
|
124
|
+
* How long the `interrupting` stage may persist without the microphone coming
|
|
125
|
+
* back. A ceiling, not a duration: the state normally ends the moment capture
|
|
126
|
+
* resumes. It exists so that a session whose microphone never reopens — the
|
|
127
|
+
* failure case — cannot latch this on and hide its own error message.
|
|
128
|
+
*/
|
|
129
|
+
export declare const INTERRUPT_MAX_MS = 1500;
|
|
130
|
+
/**
|
|
131
|
+
* How long the `interrupting` stage is held even once the recogniser reports
|
|
132
|
+
* back.
|
|
133
|
+
*
|
|
134
|
+
* ★ `isListening` waits for the recogniser's own `onstart` as of #367, so it is
|
|
135
|
+
* now an honest capture signal. The dwell stays: `onstart` can still fire a beat
|
|
136
|
+
* before audio is usable, and a session whose microphone never opens must not
|
|
137
|
+
* latch this on and hide its own error.
|
|
138
|
+
*/
|
|
139
|
+
export declare const INTERRUPT_MIN_MS = 500;
|
|
140
|
+
/** A turn is submitted only when there is something worth sending. */
|
|
141
|
+
export declare function isSubmittable(text: string): boolean;
|
|
142
|
+
/** Everything heard this turn: committed phrases plus the current guess. */
|
|
143
|
+
export declare function spokenSoFar(snapshot: VoiceSessionSnapshot): string;
|
|
144
|
+
export declare function isEcho(transcript: string, spokenText: string | null): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* What to do with a reply the app has just observed.
|
|
147
|
+
*
|
|
148
|
+
* Pure, because the rule is subtle and the hook that used to hold it has no
|
|
149
|
+
* test file at all — which is how the original defect shipped.
|
|
150
|
+
*
|
|
151
|
+
* Three outcomes:
|
|
152
|
+
* - already claimed -> do nothing (a re-render, or two identical answers)
|
|
153
|
+
* - arrived while thinking -> claim it AND advance the turn
|
|
154
|
+
* - arrived any other time -> ★ CLAIM IT, but do not advance
|
|
155
|
+
*
|
|
156
|
+
* That last case is the one that matters. Interrupting stops playback but
|
|
157
|
+
* nothing cancels the model, so the abandoned answer still lands seconds later
|
|
158
|
+
* while the phase is `listening`. Leaving it unclaimed meant the NEXT question
|
|
159
|
+
* found it waiting and spoke the answer to the question the user had cut off.
|
|
160
|
+
*/
|
|
161
|
+
export declare function planReplyConsumption(replyId: string | null | undefined, phase: VoiceSessionPhase, consumedReplyId: string | null): {
|
|
162
|
+
claim: boolean;
|
|
163
|
+
advance: boolean;
|
|
164
|
+
};
|
|
165
|
+
export declare const INITIAL_SESSION: Readonly<VoiceSessionSnapshot>;
|
|
166
|
+
/**
|
|
167
|
+
* The whole session loop, as one pure reduction.
|
|
168
|
+
*
|
|
169
|
+
* `spokenText` is what the assistant is currently saying, supplied so echo can
|
|
170
|
+
* be told from interruption. It is an argument rather than snapshot state
|
|
171
|
+
* because it belongs to the speech engine, not to the session.
|
|
172
|
+
*/
|
|
173
|
+
export declare function voiceSessionReducer(snapshot: VoiceSessionSnapshot, event: VoiceSessionEvent, spokenText?: string | null): VoiceSessionSnapshot;
|
|
174
|
+
/**
|
|
175
|
+
* Is the microphone capturing right now?
|
|
176
|
+
*
|
|
177
|
+
* False while the assistant speaks, which is the whole point: an open
|
|
178
|
+
* microphone during playback is what created the feedback loop. False whenever
|
|
179
|
+
* muted, whatever the phase.
|
|
180
|
+
*/
|
|
181
|
+
export declare function isCapturing(snapshot: VoiceSessionSnapshot): boolean;
|
|
182
|
+
/** The orb's visual mode. Kept here so the UI has no state logic of its own. */
|
|
183
|
+
export declare function orbMode(snapshot: VoiceSessionSnapshot): 'idle' | 'listening' | 'thinking' | 'speaking';
|
|
184
|
+
//# sourceMappingURL=voiceSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voiceSession.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/voiceSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;AAEzF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,iBAAiB,CAAC;IACzB,8DAA8D;IAC9D,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,YAAY,CAAC;AAExF,MAAM,MAAM,iBAAiB;AAC3B,uEAAuE;AACrE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AACxD,6CAA6C;GAC3C;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AAClC,wEAAwE;GACtE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpB;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAElE;AAqCD,wBAAgB,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAY7E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,KAAK,EAAE,iBAAiB,EACxB,eAAe,EAAE,MAAM,GAAG,IAAI,GAC7B;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAItC;AAED,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,oBAAoB,CAMzD,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,oBAAoB,EAC9B,KAAK,EAAE,iBAAiB,EACxB,UAAU,GAAE,MAAM,GAAG,IAAW,GAC/B,oBAAoB,CAmFtB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAKnE;AAED,gFAAgF;AAChF,wBAAgB,OAAO,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAatG"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { VoiceSessionSnapshot } from './voiceSession';
|
|
2
|
+
export type VoiceStageState = 'connecting' | 'listening' | 'endpoint-grace' | 'thinking' | 'speaking' | 'interrupting' | 'mic-error' | 'ended';
|
|
3
|
+
export interface StageInputs {
|
|
4
|
+
snapshot: VoiceSessionSnapshot;
|
|
5
|
+
/** Is the recogniser genuinely running? False during start-up. */
|
|
6
|
+
capturing: boolean;
|
|
7
|
+
/** True from the moment the user cuts the assistant off until listening resumes. */
|
|
8
|
+
interrupting?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The user has stopped producing new words and the submit timer is running.
|
|
11
|
+
*
|
|
12
|
+
* Its own state because the checklist asks for it: the two-second correction
|
|
13
|
+
* window should look like a deliberate pause the user can still speak into,
|
|
14
|
+
* not like the session has already moved on.
|
|
15
|
+
*/
|
|
16
|
+
awaitingEndpoint?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Precedence is the whole design. A failure outranks a phase — a session that
|
|
20
|
+
* cannot hear must never render as "Listening…", which is precisely the lie the
|
|
21
|
+
* operator screenshotted.
|
|
22
|
+
*/
|
|
23
|
+
export declare function voiceStageState({ snapshot, capturing, interrupting, awaitingEndpoint, }: StageInputs): VoiceStageState;
|
|
24
|
+
/** Does this stage show the user's live transcript rather than the reply? */
|
|
25
|
+
export declare function showsUserCaption(state: VoiceStageState): boolean;
|
|
26
|
+
/** Does this stage show the assistant's caption? */
|
|
27
|
+
export declare function showsAssistantCaption(state: VoiceStageState): boolean;
|
|
28
|
+
/** Is the microphone control an INTERRUPT rather than a mute toggle? */
|
|
29
|
+
export declare function micIsInterrupt(state: VoiceStageState): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Does this stage show a LIVE waveform?
|
|
32
|
+
*
|
|
33
|
+
* ★ Shared rather than repeated, because the two surfaces had already drifted:
|
|
34
|
+
* the full stage and the docked strip each answered this question with their
|
|
35
|
+
* own inline expression, so teaching one about a new state silently left the
|
|
36
|
+
* other frozen. `endpoint-grace` in particular MUST animate — it is the window
|
|
37
|
+
* in which the user may still be talking, and a still waveform beside the words
|
|
38
|
+
* "still listening" says the opposite of the label.
|
|
39
|
+
*/
|
|
40
|
+
export declare function waveformIsActive(state: VoiceStageState): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Is the waveform open but WAITING rather than actively hearing speech?
|
|
43
|
+
*
|
|
44
|
+
* The correction window is not the same as mid-sentence listening, and a status
|
|
45
|
+
* line is a weak way to say so: a calmer, shorter profile makes the pause
|
|
46
|
+
* visible without implying the microphone closed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function waveformIsSettled(state: VoiceStageState): boolean;
|
|
49
|
+
//# sourceMappingURL=voiceStageState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voiceStageState.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/voiceStageState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,MAAM,eAAe,GACzB,YAAY,GAAG,WAAW,GAAG,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,cAAc,GAAG,WAAW,GAAG,OAAO,CAAC;AAEnH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,YAAoB,EACpB,gBAAwB,GACzB,EAAE,WAAW,GAAG,eAAe,CAwB/B;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAKhE;AAED,oDAAoD;AACpD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAErE;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAE9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAEjE"}
|
package/dist/shared-assistant.js
CHANGED
|
@@ -6,4 +6,6 @@ import { buildProgress as y, getAssistantMedia as b, isRecoverable as x, mapSess
|
|
|
6
6
|
import { createSandboxAssistantTransport as T } from "./shared/assistant/createSandboxAssistantTransport.js";
|
|
7
7
|
import { useSandboxAssistantTransport as E } from "./shared/assistant/useSandboxAssistantTransport.js";
|
|
8
8
|
import { chatTurns as D, groupBySession as O, groupConsecutiveBySession as k, isVoiceTurn as A, voiceMarker as j, voiceSessionIdOf as M, voiceTurnsOnly as N } from "./shared/assistant/voice/turnVisibility.js";
|
|
9
|
-
|
|
9
|
+
import { toCaptionText as P } from "./shared/assistant/voice/captionText.js";
|
|
10
|
+
import { findPendingWrite as F, parseConfirmationRefusal as I } from "./shared/assistant/pendingWrite.js";
|
|
11
|
+
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, 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 };
|