@livx.cc/agentx 0.99.112 → 0.99.113

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/index.d.ts CHANGED
@@ -1703,6 +1703,8 @@ declare class VoiceCallOptions {
1703
1703
  channel: Partial<SecondChannelOptions>;
1704
1704
  onMetric: (m: CallMetric) => void;
1705
1705
  now: () => number;
1706
+ /** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
1707
+ asideGapMs: number;
1706
1708
  }
1707
1709
  declare class VoiceCall {
1708
1710
  options?: Partial<VoiceCallOptions> | undefined;
@@ -1712,6 +1714,7 @@ declare class VoiceCall {
1712
1714
  private judge;
1713
1715
  private cur?;
1714
1716
  private lang;
1717
+ private lastAside;
1715
1718
  private lastAudibleAt;
1716
1719
  private firstFinalAt;
1717
1720
  private progressTimer;
@@ -1742,6 +1745,11 @@ declare class VoiceCall {
1742
1745
  barge(): void;
1743
1746
  /** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
1744
1747
  overlap(text: string): Promise<OverlapVerdict>;
1748
+ /** The fill channel's spoken aside for an overlap judged 'keep' ("yeah" → "Oh yeah, definitely."): the host plays it
1749
+ * while the reply is held, then resumes the same reply (bod-voice decideOverlap's aside). ≤4 canned words, never a
1750
+ * claim, the cue's language; null = stay quiet (a plain listener cue, too soon after the last, or no line). It is a
1751
+ * fill line: never MAIN history. */
1752
+ aside(text: string): string | null;
1745
1753
  /** Did the last barge cut OUR fill line `text`? (hosts strip it from the cut reply — it is not MAIN's speech) */
1746
1754
  spoke(text: string): boolean;
1747
1755
  /** Resolves when the in-flight fill line finished — hosts queue MAIN audio behind it (stack, don't cut). */
package/dist/index.js CHANGED
@@ -9304,14 +9304,40 @@ var ACK = new Set("yeah yea yes yep yup ya ok okay k right sure true totally exa
9304
9304
  var LEAD = /* @__PURE__ */ new Set(["and", "so", "um", "uh", "er", "well", "oh"]);
9305
9305
  var LEADER = /* @__PURE__ */ new Set(["yeah", "yea", "yes", "yep", "yup", "ok", "okay", "right", "sure", "alright", "mm", "mhm", "hmm", "uh-huh", "totally", "exactly", "true"]);
9306
9306
  var PHRASES = ["got it", "i see", "makes sense", "of course", "for sure", "all right", "that's right", "that's true", "thats right", "thats true", "love it", "i know", "me too", "uh huh"];
9307
+ var CONTINUE = /^(?:(?:and|so|oh) )?(?:and|so|really|seriously|then what|and then|go on|keep going|ואז|באמת|נו)$/;
9308
+ var AGREE = new Set("yeah yea yes yep yup right sure true totally exactly definitely absolutely agreed correct indeed \u05DB\u05DF \u05E0\u05DB\u05D5\u05DF \u05D1\u05D3\u05D9\u05D5\u05E7 \u05D1\u05E8\u05D5\u05E8 \u05D1\u05D8\u05D7 \u05D0\u05DB\u05DF \u05DC\u05D2\u05DE\u05E8\u05D9 \u05DE\u05E1\u05DB\u05D9\u05DD \u05DE\u05E1\u05DB\u05D9\u05DE\u05D4 \u05DE\u05DE\u05E9".split(" "));
9309
+ var AGREE_PHRASES = ["of course", "for sure", "that's right", "that's true", "thats right", "thats true", "makes sense", "i agree"];
9310
+ var HE_ACK = new Set("\u05DB\u05DF \u05E0\u05DB\u05D5\u05DF \u05D1\u05D3\u05D9\u05D5\u05E7 \u05D1\u05E8\u05D5\u05E8 \u05D1\u05D8\u05D7 \u05D0\u05DB\u05DF \u05DC\u05D2\u05DE\u05E8\u05D9 \u05DE\u05DE\u05E9 \u05D0\u05D5\u05E7\u05D9\u05D9 \u05D0\u05D5\u05E7\u05D9 \u05E1\u05D1\u05D1\u05D4 \u05D9\u05E4\u05D4 \u05DE\u05E2\u05D5\u05DC\u05D4 \u05D5\u05D5\u05D0\u05D5 \u05D0\u05D4\u05D4 \u05D0\u05D4 \u05D4\u05DE\u05DE \u05DE\u05DE\u05DE".split(" "));
9311
+ function cueKind(text) {
9312
+ const t = norm2(text);
9313
+ if (!t) return null;
9314
+ if (CONTINUE.test(t)) return "continue";
9315
+ let r = t;
9316
+ for (const p of AGREE_PHRASES) if (r.includes(p)) return "agree";
9317
+ r = r.split(/[\s-]+/).filter((w) => !LEAD.has(w)).join(" ");
9318
+ return r.split(" ").some((w) => AGREE.has(w)) ? "agree" : null;
9319
+ }
9320
+ var ASIDES = {
9321
+ en: { agree: ["Oh yeah, definitely.", "Yeah, exactly.", "Right, yeah."], continue: ["Yeah, so\u2026", "So, yeah\u2026"] },
9322
+ he: { agree: ["\u05DB\u05DF, \u05D1\u05D3\u05D9\u05D5\u05E7.", "\u05DB\u05DF, \u05DE\u05DE\u05E9.", "\u05E0\u05DB\u05D5\u05DF, \u05DB\u05DF."], continue: ["\u05DB\u05DF, \u05D0\u05D6\u2026", "\u05D0\u05D6 \u05DB\u05DF\u2026"] }
9323
+ };
9324
+ function overlapAside(text, lang, avoid = "", pick = (n) => Math.floor(Math.random() * n)) {
9325
+ const kind = cueKind(text);
9326
+ const l = new RegExp("\\p{Script=Hebrew}", "u").test(text) ? "he" : /[a-z]/i.test(text) ? "en" : lang;
9327
+ const pool = kind && l ? ASIDES[l]?.[kind] : void 0;
9328
+ if (!pool?.length) return null;
9329
+ const fresh = pool.filter((x) => x !== avoid);
9330
+ return fresh[pick(fresh.length)] ?? null;
9331
+ }
9307
9332
  function overlapRule(text) {
9333
+ if (CONTINUE.test(norm2(text))) return "keep";
9308
9334
  if (/\?/.test(text)) return "cut";
9309
9335
  let t = norm2(text);
9310
9336
  if (!t) return void 0;
9311
9337
  if (HARD.test(t)) return "cut";
9312
9338
  for (const p of PHRASES) t = t.split(p).join(" ");
9313
9339
  const words3 = t.split(/[\s-]+/).filter(Boolean);
9314
- const ack = (w) => ACK.has(w) || /^(?:ha)+h?$|^(?:he)+h?$|^m+(?:hm+)?$|^h?m+$/.test(w);
9340
+ const ack = (w) => ACK.has(w) || HE_ACK.has(w) || /^(?:ha)+h?$|^(?:he)+h?$|^m+(?:hm+)?$|^h?m+$/.test(w);
9315
9341
  if (words3.every(ack)) return "keep";
9316
9342
  const lead2 = words3.findIndex((w) => !LEAD.has(w));
9317
9343
  if (lead2 >= 0 && LEADER.has(words3[lead2]) && words3.slice(lead2).filter((w) => !ack(w) && !LEAD.has(w)).length <= 1) return "keep";
@@ -13284,6 +13310,8 @@ var VoiceCallOptions = class {
13284
13310
  onMetric = () => {
13285
13311
  };
13286
13312
  now = () => performance.now();
13313
+ /** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
13314
+ asideGapMs = 8e3;
13287
13315
  };
13288
13316
  var VoiceCall = class {
13289
13317
  constructor(options) {
@@ -13327,6 +13355,7 @@ var VoiceCall = class {
13327
13355
  judge;
13328
13356
  cur;
13329
13357
  lang = null;
13358
+ lastAside = { text: "", at: -Infinity };
13330
13359
  lastAudibleAt = -Infinity;
13331
13360
  firstFinalAt = null;
13332
13361
  // the first final of the turn being formed (hold/merge)
@@ -13435,6 +13464,19 @@ var VoiceCall = class {
13435
13464
  if (d.verdict === "cut") this.barge();
13436
13465
  return d.verdict;
13437
13466
  }
13467
+ /** The fill channel's spoken aside for an overlap judged 'keep' ("yeah" → "Oh yeah, definitely."): the host plays it
13468
+ * while the reply is held, then resumes the same reply (bod-voice decideOverlap's aside). ≤4 canned words, never a
13469
+ * claim, the cue's language; null = stay quiet (a plain listener cue, too soon after the last, or no line). It is a
13470
+ * fill line: never MAIN history. */
13471
+ aside(text) {
13472
+ const now3 = this.o.now();
13473
+ if (now3 - this.lastAside.at < this.o.asideGapMs) return null;
13474
+ const line = overlapAside(text, this.lang, this.lastAside.text);
13475
+ if (!line) return null;
13476
+ this.lastAside = { text: line, at: now3 };
13477
+ this.o.onMetric({ name: "fill", kind: "aside", text: line, lang: this.lang });
13478
+ return line;
13479
+ }
13438
13480
  /** Did the last barge cut OUR fill line `text`? (hosts strip it from the cut reply — it is not MAIN's speech) */
13439
13481
  spoke(text) {
13440
13482
  return this.fill.spoke(text);