opencharm 0.7.1 → 0.8.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.
package/dist/main.mjs CHANGED
@@ -406,6 +406,25 @@ const ACP_AGENTS = {
406
406
  };
407
407
  const ACP_AGENT_NAMES = Object.keys(ACP_AGENTS);
408
408
  //#endregion
409
+ //#region ../design/src/own-colour.ts
410
+ const OWN_COLOUR = /^#[0-9A-Fa-f]{6}$/;
411
+ function linear(v) {
412
+ return v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
413
+ }
414
+ function ownColourProblem(hex) {
415
+ const [r, g, b] = [
416
+ 1,
417
+ 3,
418
+ 5
419
+ ].map((at) => Number.parseInt(hex.slice(at, at + 2), 16) / 255);
420
+ if (.2126 * linear(r) + .7152 * linear(g) + .0722 * linear(b) < .05) return "too dark to see on black";
421
+ const max = Math.max(r, g, b);
422
+ const spread = max - Math.min(r, g, b);
423
+ if (max !== r || max < .5 || spread / max < .5) return void 0;
424
+ const hue = 60 * (g - b) / spread;
425
+ return hue >= 6 && hue <= 40 ? "too close to the orange that means 'it needs you'" : void 0;
426
+ }
427
+ //#endregion
409
428
  //#region ../protocol/src/constants.ts
410
429
  const AUDIO_DOWN = {
411
430
  format: "opus",
@@ -629,22 +648,7 @@ const MAX_NAME_CHARS = 12;
629
648
  const MAX_NAME_BYTES = 24;
630
649
  const MAX_GREETING_BYTES = 40;
631
650
  const COLOUR_IDS = facesData.colours.map((colour) => colour.id);
632
- const OWN_COLOUR = /^#[0-9A-Fa-f]{6}$/;
633
651
  const lookName = z.string().min(1, "the name can't be empty").refine((name) => [...name].length <= MAX_NAME_CHARS && utf8Length(name) <= MAX_NAME_BYTES, { message: `at most ${MAX_NAME_CHARS} characters` });
634
- function ownColourProblem(hex) {
635
- const [r, g, b] = [
636
- 1,
637
- 3,
638
- 5
639
- ].map((at) => Number.parseInt(hex.slice(at, at + 2), 16) / 255);
640
- const linear = (v) => v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
641
- if (.2126 * linear(r) + .7152 * linear(g) + .0722 * linear(b) < .05) return "too dark to see on black";
642
- const max = Math.max(r, g, b);
643
- const spread = max - Math.min(r, g, b);
644
- if (max !== r || max < .5 || spread / max < .5) return void 0;
645
- const hue = 60 * (g - b) / spread;
646
- return hue >= 6 && hue <= 40 ? "too close to the orange that means 'it needs you'" : void 0;
647
- }
648
652
  const colour = z.string().superRefine((value, context) => {
649
653
  if (COLOUR_IDS.includes(value)) return;
650
654
  const problem = OWN_COLOUR.test(value) ? ownColourProblem(value) : `the colour is one of ${COLOUR_IDS.join(", ")}, or your own as #RRGGBB`;
@@ -993,7 +997,7 @@ function sendAdmin(socketPath, request) {
993
997
  }
994
998
  //#endregion
995
999
  //#region package.json
996
- var version = "0.7.1";
1000
+ var version = "0.8.1";
997
1001
  function channelToCube(value) {
998
1002
  if (value < 48) return 0;
999
1003
  if (value < 115) return 1;
@@ -3410,6 +3414,7 @@ var TurnController = class {
3410
3414
  this.#frames = [];
3411
3415
  this.#face("listening");
3412
3416
  this.#deps.agent.warm?.(this.#deps.sessionKey);
3417
+ if (this.#deps.speakAloud?.() ?? true) this.#deps.voice.prime?.();
3413
3418
  }
3414
3419
  audio(frame) {
3415
3420
  if (this.#state === "listening" && this.#frames.length < MAX_FRAMES) this.#frames.push(frame);
@@ -4374,21 +4379,25 @@ function audioOf(frame) {
4374
4379
  function voiceFor(language, voices) {
4375
4380
  return voices[language] ?? voices.en ?? DEFAULT_VOICES.en;
4376
4381
  }
4377
- function connect(text, language, voices, signal, options) {
4378
- const timeoutMs = options.timeoutMs ?? 15e3;
4382
+ function openSocket(options) {
4379
4383
  const connection = randomUUID().replace(/-/g, "");
4380
4384
  const url = `${options.url ?? SERVICE_URL}?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGec()}&Sec-MS-GEC-Version=${GEC_VERSION}&ConnectionId=${connection}`;
4381
- const packets = createChannel();
4382
- const reader = new WebmOpusReader();
4383
- let sent = 0;
4384
- let settled = false;
4385
- const socket = new WebSocket(url, {
4385
+ return new WebSocket(url, {
4386
4386
  headers: {
4387
4387
  "User-Agent": USER_AGENT,
4388
4388
  Origin: ORIGIN
4389
4389
  },
4390
- handshakeTimeout: timeoutMs
4390
+ handshakeTimeout: options.timeoutMs ?? 15e3
4391
4391
  });
4392
+ }
4393
+ function connect(text, language, voices, signal, options, ready) {
4394
+ const timeoutMs = options.timeoutMs ?? 15e3;
4395
+ const connection = randomUUID().replace(/-/g, "");
4396
+ const packets = createChannel();
4397
+ const reader = new WebmOpusReader();
4398
+ let sent = 0;
4399
+ let settled = false;
4400
+ const socket = ready ?? openSocket(options);
4392
4401
  const finish = (error) => {
4393
4402
  if (settled) return;
4394
4403
  settled = true;
@@ -4403,7 +4412,7 @@ function connect(text, language, voices, signal, options) {
4403
4412
  const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("Microsoft's voice took too long")), timeoutMs);
4404
4413
  if (signal.aborted) onAbort();
4405
4414
  else signal.addEventListener("abort", onAbort, { once: true });
4406
- socket.on("open", () => {
4415
+ const request = () => {
4407
4416
  socket.send(message("speech.config", "application/json; charset=utf-8", JSON.stringify({ context: { synthesis: { audio: {
4408
4417
  metadataoptions: {
4409
4418
  sentenceBoundaryEnabled: "false",
@@ -4412,7 +4421,9 @@ function connect(text, language, voices, signal, options) {
4412
4421
  outputFormat: OUTPUT_FORMAT
4413
4422
  } } } })));
4414
4423
  socket.send(message("ssml", "application/ssml+xml", ssml(text, voiceFor(language, voices)), connection));
4415
- });
4424
+ };
4425
+ if (socket.readyState === WebSocket.OPEN) request();
4426
+ else socket.once("open", request);
4416
4427
  socket.on("message", (data, isBinary) => {
4417
4428
  const frame = Buffer.isBuffer(data) ? data : Buffer.concat(data);
4418
4429
  if (!isBinary) {
@@ -4440,9 +4451,48 @@ function createMicrosoftSpeaker(options = {}) {
4440
4451
  ...DEFAULT_VOICES,
4441
4452
  ...options.voices
4442
4453
  };
4443
- const stream = (text, signal, language = "en") => connect(text, language, voices, signal, options);
4454
+ const spareMs = options.spareMs ?? 2e4;
4455
+ let spare;
4456
+ const takeSpare = () => {
4457
+ const taken = spare;
4458
+ spare = void 0;
4459
+ if (!taken) return void 0;
4460
+ if ((taken.socket.readyState === WebSocket.OPEN || taken.socket.readyState === WebSocket.CONNECTING) && Date.now() < taken.until) return taken.socket;
4461
+ taken.socket.terminate();
4462
+ };
4463
+ const prime = () => {
4464
+ if (spare) return;
4465
+ const socket = openSocket(options);
4466
+ socket.on("error", () => void 0);
4467
+ spare = {
4468
+ socket,
4469
+ until: Date.now() + spareMs
4470
+ };
4471
+ setTimeout(() => {
4472
+ if (spare?.socket !== socket) return;
4473
+ spare = void 0;
4474
+ socket.terminate();
4475
+ }, spareMs).unref();
4476
+ };
4477
+ async function* stream(text, signal, language = "en") {
4478
+ const ready = takeSpare();
4479
+ if (ready) {
4480
+ let spoke = false;
4481
+ try {
4482
+ for await (const packet of connect(text, language, voices, signal, options, ready)) {
4483
+ spoke = true;
4484
+ yield packet;
4485
+ }
4486
+ return;
4487
+ } catch (error) {
4488
+ if (spoke || signal.aborted) throw error;
4489
+ }
4490
+ }
4491
+ yield* connect(text, language, voices, signal, options);
4492
+ }
4444
4493
  return {
4445
4494
  name: "microsoft",
4495
+ prime,
4446
4496
  stream,
4447
4497
  async synthesize(text, signal, language = "en") {
4448
4498
  const packets = [];
@@ -4687,6 +4737,9 @@ function speakWithFallback(chain, options = {}) {
4687
4737
  },
4688
4738
  warm: async () => {
4689
4739
  await Promise.allSettled(chain.map((speaker) => speaker.warm?.() ?? Promise.resolve()));
4740
+ },
4741
+ prime: () => {
4742
+ if ((resting.get(first) ?? 0) <= now()) first.prime?.();
4690
4743
  }
4691
4744
  };
4692
4745
  }
@@ -4746,7 +4799,8 @@ function createVoiceFromConfig(config) {
4746
4799
  language: config.voice.language,
4747
4800
  warm: async () => {
4748
4801
  await Promise.allSettled([listener.warm?.(), speaker.warm?.()]);
4749
- }
4802
+ },
4803
+ ...speaker.prime ? { prime: speaker.prime } : {}
4750
4804
  };
4751
4805
  }
4752
4806
  function charmToolsServers(config, tools) {
@@ -4965,7 +5019,7 @@ function formatIdentity(unit, version, commit) {
4965
5019
  let cached;
4966
5020
  function identity() {
4967
5021
  if (cached) return cached;
4968
- const commit = "de16e27";
5022
+ const commit = "e06b6db";
4969
5023
  cached = {
4970
5024
  version: `cli@${version}`,
4971
5025
  commit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencharm",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
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