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