opencharm 0.8.0 → 0.8.2

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/main.mjs CHANGED
@@ -997,7 +997,7 @@ function sendAdmin(socketPath, request) {
997
997
  }
998
998
  //#endregion
999
999
  //#region package.json
1000
- var version = "0.8.0";
1000
+ var version = "0.8.2";
1001
1001
  function channelToCube(value) {
1002
1002
  if (value < 48) return 0;
1003
1003
  if (value < 115) return 1;
@@ -3361,6 +3361,10 @@ var TurnController = class {
3361
3361
  #generation = 0;
3362
3362
  #controller;
3363
3363
  #speaking = false;
3364
+ #questions = 0;
3365
+ #questionsEnd;
3366
+ #endQuestions = () => {};
3367
+ #questionStarts = /* @__PURE__ */ new Set();
3364
3368
  #playedBy = 0;
3365
3369
  #asking;
3366
3370
  #endFace;
@@ -3414,6 +3418,7 @@ var TurnController = class {
3414
3418
  this.#frames = [];
3415
3419
  this.#face("listening");
3416
3420
  this.#deps.agent.warm?.(this.#deps.sessionKey);
3421
+ if (this.#deps.speakAloud?.() ?? true) this.#deps.voice.prime?.();
3417
3422
  }
3418
3423
  audio(frame) {
3419
3424
  if (this.#state === "listening" && this.#frames.length < MAX_FRAMES) this.#frames.push(frame);
@@ -3506,6 +3511,12 @@ var TurnController = class {
3506
3511
  if (ask) this.#asking = async (text, options) => {
3507
3512
  asking = true;
3508
3513
  clearTimeout(timer);
3514
+ if (this.#questions++ === 0) {
3515
+ this.#questionsEnd = new Promise((resolve) => {
3516
+ this.#endQuestions = resolve;
3517
+ });
3518
+ for (const notify of this.#questionStarts) notify();
3519
+ }
3509
3520
  try {
3510
3521
  return await ask(text, {
3511
3522
  ...options,
@@ -3513,6 +3524,10 @@ var TurnController = class {
3513
3524
  });
3514
3525
  } finally {
3515
3526
  asking = false;
3527
+ if (--this.#questions === 0) {
3528
+ this.#questionsEnd = void 0;
3529
+ this.#endQuestions();
3530
+ }
3516
3531
  if (!signal.aborted) timer = startClock();
3517
3532
  }
3518
3533
  };
@@ -3721,14 +3736,36 @@ var TurnController = class {
3721
3736
  state: "start"
3722
3737
  });
3723
3738
  }
3739
+ const sleep = this.#deps.sleep ?? sleepMs;
3740
+ const now = this.#deps.now ?? Date.now;
3741
+ const aborted = new Promise((resolve) => signal.addEventListener("abort", () => resolve("aborted"), { once: true }));
3724
3742
  this.#deps.send({
3725
3743
  type: "tts",
3726
3744
  state: "sentence_start",
3727
3745
  text
3728
3746
  });
3729
- const sleep = this.#deps.sleep ?? sleepMs;
3730
3747
  clock.pause();
3731
- await Promise.race([sleep(readingMs(text)), new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }))]);
3748
+ let remaining = readingMs(text);
3749
+ while (going()) {
3750
+ if (this.#questionsEnd) {
3751
+ await Promise.race([this.#questionsEnd, aborted]);
3752
+ continue;
3753
+ }
3754
+ const from = now();
3755
+ let notify = () => {};
3756
+ const question = new Promise((resolve) => {
3757
+ notify = () => resolve("question");
3758
+ });
3759
+ this.#questionStarts.add(notify);
3760
+ const outcome = await Promise.race([
3761
+ sleep(remaining).then(() => "read"),
3762
+ aborted,
3763
+ question
3764
+ ]);
3765
+ this.#questionStarts.delete(notify);
3766
+ if (outcome !== "question") break;
3767
+ remaining = Math.max(0, remaining - (now() - from));
3768
+ }
3732
3769
  clock.resume();
3733
3770
  }
3734
3771
  #failureLine(error) {
@@ -4378,21 +4415,25 @@ function audioOf(frame) {
4378
4415
  function voiceFor(language, voices) {
4379
4416
  return voices[language] ?? voices.en ?? DEFAULT_VOICES.en;
4380
4417
  }
4381
- function connect(text, language, voices, signal, options) {
4382
- const timeoutMs = options.timeoutMs ?? 15e3;
4418
+ function openSocket(options) {
4383
4419
  const connection = randomUUID().replace(/-/g, "");
4384
4420
  const url = `${options.url ?? SERVICE_URL}?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGec()}&Sec-MS-GEC-Version=${GEC_VERSION}&ConnectionId=${connection}`;
4385
- const packets = createChannel();
4386
- const reader = new WebmOpusReader();
4387
- let sent = 0;
4388
- let settled = false;
4389
- const socket = new WebSocket(url, {
4421
+ return new WebSocket(url, {
4390
4422
  headers: {
4391
4423
  "User-Agent": USER_AGENT,
4392
4424
  Origin: ORIGIN
4393
4425
  },
4394
- handshakeTimeout: timeoutMs
4426
+ handshakeTimeout: options.timeoutMs ?? 15e3
4395
4427
  });
4428
+ }
4429
+ function connect(text, language, voices, signal, options, ready) {
4430
+ const timeoutMs = options.timeoutMs ?? 15e3;
4431
+ const connection = randomUUID().replace(/-/g, "");
4432
+ const packets = createChannel();
4433
+ const reader = new WebmOpusReader();
4434
+ let sent = 0;
4435
+ let settled = false;
4436
+ const socket = ready ?? openSocket(options);
4396
4437
  const finish = (error) => {
4397
4438
  if (settled) return;
4398
4439
  settled = true;
@@ -4407,7 +4448,7 @@ function connect(text, language, voices, signal, options) {
4407
4448
  const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("Microsoft's voice took too long")), timeoutMs);
4408
4449
  if (signal.aborted) onAbort();
4409
4450
  else signal.addEventListener("abort", onAbort, { once: true });
4410
- socket.on("open", () => {
4451
+ const request = () => {
4411
4452
  socket.send(message("speech.config", "application/json; charset=utf-8", JSON.stringify({ context: { synthesis: { audio: {
4412
4453
  metadataoptions: {
4413
4454
  sentenceBoundaryEnabled: "false",
@@ -4416,7 +4457,9 @@ function connect(text, language, voices, signal, options) {
4416
4457
  outputFormat: OUTPUT_FORMAT
4417
4458
  } } } })));
4418
4459
  socket.send(message("ssml", "application/ssml+xml", ssml(text, voiceFor(language, voices)), connection));
4419
- });
4460
+ };
4461
+ if (socket.readyState === WebSocket.OPEN) request();
4462
+ else socket.once("open", request);
4420
4463
  socket.on("message", (data, isBinary) => {
4421
4464
  const frame = Buffer.isBuffer(data) ? data : Buffer.concat(data);
4422
4465
  if (!isBinary) {
@@ -4444,9 +4487,48 @@ function createMicrosoftSpeaker(options = {}) {
4444
4487
  ...DEFAULT_VOICES,
4445
4488
  ...options.voices
4446
4489
  };
4447
- const stream = (text, signal, language = "en") => connect(text, language, voices, signal, options);
4490
+ const spareMs = options.spareMs ?? 2e4;
4491
+ let spare;
4492
+ const takeSpare = () => {
4493
+ const taken = spare;
4494
+ spare = void 0;
4495
+ if (!taken) return void 0;
4496
+ if ((taken.socket.readyState === WebSocket.OPEN || taken.socket.readyState === WebSocket.CONNECTING) && Date.now() < taken.until) return taken.socket;
4497
+ taken.socket.terminate();
4498
+ };
4499
+ const prime = () => {
4500
+ if (spare) return;
4501
+ const socket = openSocket(options);
4502
+ socket.on("error", () => void 0);
4503
+ spare = {
4504
+ socket,
4505
+ until: Date.now() + spareMs
4506
+ };
4507
+ setTimeout(() => {
4508
+ if (spare?.socket !== socket) return;
4509
+ spare = void 0;
4510
+ socket.terminate();
4511
+ }, spareMs).unref();
4512
+ };
4513
+ async function* stream(text, signal, language = "en") {
4514
+ const ready = takeSpare();
4515
+ if (ready) {
4516
+ let spoke = false;
4517
+ try {
4518
+ for await (const packet of connect(text, language, voices, signal, options, ready)) {
4519
+ spoke = true;
4520
+ yield packet;
4521
+ }
4522
+ return;
4523
+ } catch (error) {
4524
+ if (spoke || signal.aborted) throw error;
4525
+ }
4526
+ }
4527
+ yield* connect(text, language, voices, signal, options);
4528
+ }
4448
4529
  return {
4449
4530
  name: "microsoft",
4531
+ prime,
4450
4532
  stream,
4451
4533
  async synthesize(text, signal, language = "en") {
4452
4534
  const packets = [];
@@ -4691,6 +4773,9 @@ function speakWithFallback(chain, options = {}) {
4691
4773
  },
4692
4774
  warm: async () => {
4693
4775
  await Promise.allSettled(chain.map((speaker) => speaker.warm?.() ?? Promise.resolve()));
4776
+ },
4777
+ prime: () => {
4778
+ if ((resting.get(first) ?? 0) <= now()) first.prime?.();
4694
4779
  }
4695
4780
  };
4696
4781
  }
@@ -4750,7 +4835,8 @@ function createVoiceFromConfig(config) {
4750
4835
  language: config.voice.language,
4751
4836
  warm: async () => {
4752
4837
  await Promise.allSettled([listener.warm?.(), speaker.warm?.()]);
4753
- }
4838
+ },
4839
+ ...speaker.prime ? { prime: speaker.prime } : {}
4754
4840
  };
4755
4841
  }
4756
4842
  function charmToolsServers(config, tools) {
@@ -4969,7 +5055,7 @@ function formatIdentity(unit, version, commit) {
4969
5055
  let cached;
4970
5056
  function identity() {
4971
5057
  if (cached) return cached;
4972
- const commit = "bf6c38c";
5058
+ const commit = "da4ff95";
4973
5059
  cached = {
4974
5060
  version: `cli@${version}`,
4975
5061
  commit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencharm",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "An open-source body for the AI agent you already run: a face, a voice, one key. Claude Code, Codex, Gemini CLI, Hermes, OpenClaw or any ACP or OpenAI-compatible agent; on a small board, your Mac's notch or the browser.",
5
5
  "type": "module",
6
6
  "bin": {
Binary file