@burdenoff/website-sdk 2026.1105.3 → 2026.1106.1

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.
@@ -2,6 +2,146 @@
2
2
 
3
3
  var chunkZSTFX7FV_js = require('../chunk-ZSTFX7FV.js');
4
4
 
5
+ // src/avatar-mic/speech-words.ts
6
+ var FILLERS = /* @__PURE__ */ new Set([
7
+ "uh",
8
+ "um",
9
+ "uhm",
10
+ "umm",
11
+ "er",
12
+ "erm",
13
+ "eh",
14
+ "ah",
15
+ "hm",
16
+ "hmm",
17
+ "hmmm",
18
+ "mm",
19
+ "mmm",
20
+ "mhm",
21
+ "huh",
22
+ "oh",
23
+ "so",
24
+ "well",
25
+ "okay",
26
+ "ok",
27
+ "and",
28
+ "like",
29
+ "hey",
30
+ "hi",
31
+ "hello",
32
+ "actually"
33
+ ]);
34
+ var OPEN_ENDINGS = /* @__PURE__ */ new Set([
35
+ "the",
36
+ "a",
37
+ "an",
38
+ "of",
39
+ "to",
40
+ "for",
41
+ "about",
42
+ "and",
43
+ "or",
44
+ "but",
45
+ "what",
46
+ "how",
47
+ "which",
48
+ "is",
49
+ "are",
50
+ "can"
51
+ ]);
52
+ var PRONOUNS = /* @__PURE__ */ new Set([
53
+ "i",
54
+ "me",
55
+ "you",
56
+ "it",
57
+ "we",
58
+ "us",
59
+ "they",
60
+ "them",
61
+ "he",
62
+ "him",
63
+ "she",
64
+ "her",
65
+ "this",
66
+ "that",
67
+ "these",
68
+ "those"
69
+ ]);
70
+ var ECHO_MIN_WORDS = 3;
71
+ var SELF_CONTAINED_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
72
+ function tidyWords(text) {
73
+ return text.replace(/\s+/g, " ").trim();
74
+ }
75
+ function speechWords(text) {
76
+ const flat = text.toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, " ").trim();
77
+ return flat === "" ? [] : flat.split(" ");
78
+ }
79
+ function startsWithWords(whole, head) {
80
+ return head.length <= whole.length && head.every((word, index) => whole[index] === word);
81
+ }
82
+ function endsWithWords(whole, tail) {
83
+ return tail.length <= whole.length && tail.every(
84
+ (word, index) => whole[whole.length - tail.length + index] === word
85
+ );
86
+ }
87
+ function startsWithWordsOf(text, head) {
88
+ return startsWithWords(speechWords(text), speechWords(head));
89
+ }
90
+ function mergeTurnPhrase(turn, phrase) {
91
+ const heard = speechWords(turn);
92
+ const incoming = speechWords(phrase);
93
+ if (incoming.length === 0) return turn;
94
+ if (heard.length === 0) return tidyWords(phrase);
95
+ if (startsWithWords(incoming, heard)) {
96
+ return incoming.length === heard.length ? turn : tidyWords(phrase);
97
+ }
98
+ if (incoming.length >= 2 && endsWithWords(heard, incoming)) return turn;
99
+ return tidyWords(`${turn} ${phrase}`);
100
+ }
101
+ function mergeInterim(turn, interim) {
102
+ const said = tidyWords(interim);
103
+ if (said === "") return turn;
104
+ const head = speechWords(turn);
105
+ if (head.length === 0) return said;
106
+ const tokens = said.split(" ");
107
+ let matched = 0;
108
+ for (let at = 0; at < tokens.length; at += 1) {
109
+ const words = speechWords(tokens[at] ?? "");
110
+ for (const word of words) {
111
+ if (word !== head[matched]) return tidyWords(`${turn} ${said}`);
112
+ matched += 1;
113
+ if (matched === head.length) {
114
+ return tidyWords(`${turn} ${tokens.slice(at + 1).join(" ")}`);
115
+ }
116
+ }
117
+ }
118
+ return tidyWords(`${turn} ${said}`);
119
+ }
120
+ function saidWords(words) {
121
+ return words.filter((word) => !FILLERS.has(word));
122
+ }
123
+ function isGenuineSpeech(text) {
124
+ const said = saidWords(speechWords(text));
125
+ if (said.length === 1 && PRONOUNS.has(said[0] ?? "")) return false;
126
+ const content = said.filter((word) => !OPEN_ENDINGS.has(word));
127
+ if (content.length === 0) return false;
128
+ const letters = [...content.join("")];
129
+ if (letters.length >= 2) return true;
130
+ return letters.length === 1 && SELF_CONTAINED_SCRIPT.test(letters[0] ?? "");
131
+ }
132
+ function waitsLonger(text) {
133
+ const words = speechWords(text);
134
+ const last = words[words.length - 1];
135
+ if (last === void 0) return false;
136
+ return saidWords(words).length <= 1 || OPEN_ENDINGS.has(last) || FILLERS.has(last);
137
+ }
138
+ function isEchoOf(text, spoken) {
139
+ if (spoken === null) return false;
140
+ const heard = speechWords(text);
141
+ if (heard.length < ECHO_MIN_WORDS) return false;
142
+ return ` ${speechWords(spoken).join(" ")} `.includes(` ${heard.join(" ")} `);
143
+ }
144
+
5
145
  // src/avatar-mic/browser-press.ts
6
146
  var BROWSER_START_WAIT_MS = 15e3;
7
147
  var BROWSER_FINAL_WAIT_MS = 3e3;
@@ -11,6 +151,11 @@ var BROWSER_MAX_QUICK_ENDS = 8;
11
151
  var BROWSER_MAX_RESTARTS = 60;
12
152
  var BROWSER_PRIME_SETTLE_MS = 250;
13
153
  var BROWSER_GRANT_RETRY_MS = 350;
154
+ var BROWSER_PAUSE_MIN_MS = 2e3;
155
+ var BROWSER_PAUSE_MAX_MS = 2500;
156
+ var BROWSER_PAUSE_AFTER_START_MS = 1500;
157
+ var BROWSER_PAUSE_SHOWN_MS = 800;
158
+ var BROWSER_ECHO_WINDOW_MS = 2500;
14
159
  var QUICK_END_MS = 1e3;
15
160
  var NETWORK_FAILURES_FOR_GOOD = 2;
16
161
  var IDLE = { name: "idle" };
@@ -82,8 +227,7 @@ function readRecognitionResults(results) {
82
227
  continue;
83
228
  }
84
229
  const last = finals.length > 0 ? finals[finals.length - 1] : void 0;
85
- const lower = text.toLowerCase();
86
- if (last !== void 0 && (lower === last.toLowerCase() || lower.startsWith(`${last.toLowerCase()} `))) {
230
+ if (last !== void 0 && startsWithWordsOf(text, last)) {
87
231
  finals[finals.length - 1] = text;
88
232
  } else {
89
233
  finals.push(text);
@@ -93,6 +237,11 @@ function readRecognitionResults(results) {
93
237
  }
94
238
  function createBrowserAvatarMic(options, seams = {}) {
95
239
  const { createRecognition, now, primeMicrophone, watchMicPermission, touch } = { ...BROWSER, ...seams };
240
+ const { endpoint } = options;
241
+ const pauseMs = endpoint === void 0 || !Number.isFinite(endpoint.pauseMs) ? BROWSER_PAUSE_MIN_MS : Math.min(
242
+ BROWSER_PAUSE_MAX_MS,
243
+ Math.max(BROWSER_PAUSE_MIN_MS, endpoint.pauseMs)
244
+ );
96
245
  let press = null;
97
246
  let epoch = 0;
98
247
  let state = IDLE;
@@ -106,14 +255,23 @@ function createBrowserAvatarMic(options, seams = {}) {
106
255
  state = next;
107
256
  options.onState(next);
108
257
  };
109
- const heard = (p) => [...p.earlier, ...p.instance?.finals ?? [], p.instance?.interim ?? ""].filter((part) => part !== "").join(" ");
258
+ const turnOf = (earlier, instance) => {
259
+ let turn = earlier;
260
+ for (const final of instance?.finals ?? []) {
261
+ turn = mergeTurnPhrase(turn, final);
262
+ }
263
+ return mergeInterim(turn, instance?.interim ?? "");
264
+ };
265
+ const heard = (p) => turnOf(p.earlier, p.instance);
110
266
  const show = (p) => {
111
267
  if (p.stage === "listening") {
268
+ const text = heard(p);
112
269
  report({
113
270
  name: "listening",
114
271
  epoch: p.epoch,
115
272
  startedAt: p.startedAt,
116
- text: heard(p)
273
+ text,
274
+ ...endpoint === void 0 ? {} : { genuine: isGenuineSpeech(text), pausing: p.pausing }
117
275
  });
118
276
  } else if (p.stage === "finishing") {
119
277
  report({
@@ -140,7 +298,9 @@ function createBrowserAvatarMic(options, seams = {}) {
140
298
  p.startTimer,
141
299
  p.limitTimer,
142
300
  p.finalTimer,
143
- p.launchTimer
301
+ p.launchTimer,
302
+ p.pauseTimer,
303
+ p.pausingTimer
144
304
  ]) {
145
305
  if (timer !== null) clearTimeout(timer);
146
306
  }
@@ -148,6 +308,14 @@ function createBrowserAvatarMic(options, seams = {}) {
148
308
  p.limitTimer = null;
149
309
  p.finalTimer = null;
150
310
  p.launchTimer = null;
311
+ p.pauseTimer = null;
312
+ p.pausingTimer = null;
313
+ };
314
+ const clearPause = (p) => {
315
+ if (p.pauseTimer !== null) clearTimeout(p.pauseTimer);
316
+ if (p.pausingTimer !== null) clearTimeout(p.pausingTimer);
317
+ p.pauseTimer = null;
318
+ p.pausingTimer = null;
151
319
  };
152
320
  const settle = (p, next) => {
153
321
  if (p.stage === "ended") return;
@@ -162,23 +330,104 @@ function createBrowserAvatarMic(options, seams = {}) {
162
330
  const complete = (p) => {
163
331
  const text = heard(p);
164
332
  if (text !== "") networkFailures = 0;
165
- settle(p, { name: "done", epoch: p.epoch, text, limit: p.limit });
333
+ if (endpoint === void 0) {
334
+ settle(p, { name: "done", epoch: p.epoch, text, limit: p.limit });
335
+ return;
336
+ }
337
+ const by = p.stage === "finishing" ? p.by ?? "tap" : p.stage === "starting" ? "silence" : "failed";
338
+ const echo = by === "pause" && p.speechAt !== null && p.speechAt - p.listenedAt <= BROWSER_ECHO_WINDOW_MS && isEchoOf(text, endpoint.echoOf());
339
+ settle(p, {
340
+ name: "done",
341
+ epoch: p.epoch,
342
+ text,
343
+ limit: p.limit,
344
+ by,
345
+ echo
346
+ });
166
347
  };
167
348
  const fail = (p, reason) => settle(p, { name: "failed", epoch: p.epoch, reason, text: heard(p) });
168
349
  const current = (p, instance) => press === p && p.stage !== "ended" && p.instance === instance;
350
+ const armPause = (p, turn, speech) => {
351
+ clearPause(p);
352
+ if (!speech) return;
353
+ const at = now();
354
+ if (!p.pausing) {
355
+ p.pausingTimer = setTimeout(
356
+ () => {
357
+ p.pausingTimer = null;
358
+ if (press !== p || p.stage !== "listening") return;
359
+ p.pausing = true;
360
+ show(p);
361
+ },
362
+ Math.max(0, p.wordsAt + BROWSER_PAUSE_SHOWN_MS - at)
363
+ );
364
+ }
365
+ const instance = p.instance;
366
+ if (instance === null || !instance.started) return;
367
+ const wait = waitsLonger(turn) ? 2 * pauseMs : pauseMs;
368
+ const due = Math.max(
369
+ p.wordsAt + wait,
370
+ instance.startedAt + BROWSER_PAUSE_AFTER_START_MS
371
+ );
372
+ p.pauseTimer = setTimeout(
373
+ () => {
374
+ p.pauseTimer = null;
375
+ if (press === p && p.stage === "listening" && p.instance === instance) {
376
+ stopRecording(p, "pause");
377
+ }
378
+ },
379
+ Math.max(0, due - at)
380
+ );
381
+ };
382
+ const listenOn = (p) => {
383
+ if (endpoint === void 0 || p.stage !== "listening") return;
384
+ const turn = heard(p);
385
+ const words = speechWords(turn).join(" ");
386
+ const at = now();
387
+ if (words !== p.words) {
388
+ p.words = words;
389
+ p.wordsAt = at;
390
+ p.pausing = false;
391
+ }
392
+ const speech = isGenuineSpeech(turn);
393
+ if (speech && p.speechAt === null) {
394
+ p.speechAt = at;
395
+ if (p.limitTimer !== null) clearTimeout(p.limitTimer);
396
+ p.limitTimer = setTimeout(() => {
397
+ p.limitTimer = null;
398
+ if (press === p) stopRecording(p, "limit");
399
+ }, options.maxPressMs);
400
+ }
401
+ armPause(p, turn, speech);
402
+ };
169
403
  const began = (p, instance) => {
170
404
  instance.started = true;
171
405
  instance.startedAt = now();
172
406
  hadMicrophone = true;
173
- if (p.stage !== "starting") return;
174
- if (p.startTimer !== null) clearTimeout(p.startTimer);
175
- p.startTimer = null;
176
- p.stage = "listening";
177
- p.startedAt = Date.now();
178
- p.limitTimer = setTimeout(() => {
179
- p.limitTimer = null;
180
- if (press === p) stopRecording(p, true);
181
- }, options.maxPressMs);
407
+ if (p.stage === "starting") {
408
+ if (p.startTimer !== null) clearTimeout(p.startTimer);
409
+ p.startTimer = null;
410
+ p.stage = "listening";
411
+ p.startedAt = Date.now();
412
+ p.listenedAt = instance.startedAt;
413
+ p.limitTimer = setTimeout(() => {
414
+ p.limitTimer = null;
415
+ if (press !== p) return;
416
+ if (endpoint === void 0) {
417
+ stopRecording(p, "limit");
418
+ } else if (p.stage === "listening" && p.speechAt === null) {
419
+ settle(p, {
420
+ name: "done",
421
+ epoch: p.epoch,
422
+ text: "",
423
+ limit: false,
424
+ by: "silence",
425
+ echo: false
426
+ });
427
+ }
428
+ }, options.maxPressMs);
429
+ }
430
+ listenOn(p);
182
431
  };
183
432
  const launchIn = (p, delay) => {
184
433
  if (delay <= 0) {
@@ -204,9 +453,7 @@ function createBrowserAvatarMic(options, seams = {}) {
204
453
  );
205
454
  };
206
455
  const keep = (p, instance) => {
207
- for (const text of [...instance.finals, instance.interim]) {
208
- if (text !== "") p.earlier.push(text);
209
- }
456
+ p.earlier = turnOf(p.earlier, instance);
210
457
  };
211
458
  const onError = (p, instance, code) => {
212
459
  if (code === "no-speech") {
@@ -272,6 +519,8 @@ function createBrowserAvatarMic(options, seams = {}) {
272
519
  else fail(p, "failed");
273
520
  return;
274
521
  case "listening": {
522
+ if (p.pauseTimer !== null) clearTimeout(p.pauseTimer);
523
+ p.pauseTimer = null;
275
524
  const quick = !instance.started || now() - instance.startedAt < QUICK_END_MS && instance.finals.length === 0 && instance.interim === "";
276
525
  restart(p, quick ? "quick" : "listened");
277
526
  return;
@@ -318,6 +567,7 @@ function createBrowserAvatarMic(options, seams = {}) {
318
567
  const read = readRecognitionResults(event.results);
319
568
  instance.finals = read.finals;
320
569
  instance.interim = read.interim;
570
+ listenOn(p);
321
571
  show(p);
322
572
  };
323
573
  recognition.onerror = (event) => {
@@ -361,14 +611,16 @@ function createBrowserAvatarMic(options, seams = {}) {
361
611
  }
362
612
  );
363
613
  }
364
- function stopRecording(p, limit) {
614
+ function stopRecording(p, by) {
365
615
  if (p.stage !== "listening") return;
366
616
  if (p.limitTimer !== null) clearTimeout(p.limitTimer);
367
617
  if (p.launchTimer !== null) clearTimeout(p.launchTimer);
368
618
  p.limitTimer = null;
369
619
  p.launchTimer = null;
620
+ clearPause(p);
370
621
  p.stage = "finishing";
371
- p.limit = limit;
622
+ p.limit = by === "limit";
623
+ p.by = by;
372
624
  const instance = p.instance;
373
625
  if (instance === null || !instance.started) {
374
626
  complete(p);
@@ -416,7 +668,7 @@ function createBrowserAvatarMic(options, seams = {}) {
416
668
  epoch,
417
669
  stage: "starting",
418
670
  instance: null,
419
- earlier: [],
671
+ earlier: "",
420
672
  lang: options.lang,
421
673
  langRetried: false,
422
674
  restarts: 0,
@@ -429,7 +681,15 @@ function createBrowserAvatarMic(options, seams = {}) {
429
681
  startTimer: null,
430
682
  limitTimer: null,
431
683
  finalTimer: null,
432
- launchTimer: null
684
+ launchTimer: null,
685
+ by: null,
686
+ words: "",
687
+ wordsAt: 0,
688
+ speechAt: null,
689
+ listenedAt: 0,
690
+ pausing: false,
691
+ pauseTimer: null,
692
+ pausingTimer: null
433
693
  };
434
694
  press = p;
435
695
  report({ name: "starting", epoch });
@@ -446,7 +706,7 @@ function createBrowserAvatarMic(options, seams = {}) {
446
706
  const p = press;
447
707
  if (p === null) return;
448
708
  if (p.stage === "starting") cancel();
449
- else stopRecording(p, false);
709
+ else stopRecording(p, "tap");
450
710
  },
451
711
  cancel,
452
712
  state: () => state