opencharm 0.4.0 → 0.6.0
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/README.md +1 -0
- package/dist/main.mjs +133 -27
- package/package.json +1 -1
- package/sim/charm_sim.js +1 -1
- package/sim/charm_sim.wasm +0 -0
- package/sim/sim.js +18 -2
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ npx opencharm
|
|
|
20
20
|
| `opencharm pair <code>` | Pair the charm showing that code; you choose its PIN (`--name <name>`) |
|
|
21
21
|
| `opencharm status` | What's running (charmd, each connected charm's build, the starter commit of the workspace; spec 015), then the paired charms and whether they are connected, locked or blocked |
|
|
22
22
|
| `opencharm lock` / `unlock` / `revoke <charm>` | Lock it now; clear a block and unlock; forget it |
|
|
23
|
+
| `opencharm replies on\|off` | The desktop charm speaks its replies, or shows them as text, from the next one; a charm on a board always speaks. To keep it, set `"speakReplies": false` in `opencharm.json` |
|
|
23
24
|
| `opencharm look` | Show the charm's look, or change it on every unlocked charm until charmd restarts (`--colour <colour>`, `--greeting <text>`, `--motion full\|calm`); to keep it, set `charm` in `opencharm.json` |
|
|
24
25
|
| `opencharm dev face <charm> <state> [text]` / `dev say <charm> <text>` / `dev ask <charm> <question>` | Show a face, speak, or ask a question (hold = yes, press = no) on a connected charm (testing) |
|
|
25
26
|
| `opencharm voice` / `voice install` | The local voice's models (Parakeet listening, Supertonic speaking): whether they're here, and download them now (charmd does it on its first start otherwise) |
|
package/dist/main.mjs
CHANGED
|
@@ -497,6 +497,11 @@ const unlock = z.object({
|
|
|
497
497
|
op: z.literal("unlock"),
|
|
498
498
|
pin: z.string().regex(/^\d{4,12}$/)
|
|
499
499
|
});
|
|
500
|
+
const typed = z.object({
|
|
501
|
+
type: z.literal("charm"),
|
|
502
|
+
op: z.literal("text"),
|
|
503
|
+
text: z.string().max(2e3).refine((text) => text.trim().length > 0, { message: "empty" })
|
|
504
|
+
});
|
|
500
505
|
const answer = z.object({
|
|
501
506
|
type: z.literal("charm"),
|
|
502
507
|
op: z.literal("answer"),
|
|
@@ -507,7 +512,8 @@ const serverHello = z.object({
|
|
|
507
512
|
type: z.literal("hello"),
|
|
508
513
|
transport: z.literal("websocket"),
|
|
509
514
|
session_id: z.string().min(1).max(128),
|
|
510
|
-
audio_params: audioParams
|
|
515
|
+
audio_params: audioParams,
|
|
516
|
+
features: z.object({ text: z.boolean().optional() }).optional()
|
|
511
517
|
});
|
|
512
518
|
const stt = z.object({
|
|
513
519
|
session_id: sessionId,
|
|
@@ -593,7 +599,8 @@ const CLIENT_SCHEMAS = {
|
|
|
593
599
|
listen,
|
|
594
600
|
abort,
|
|
595
601
|
"charm:unlock": unlock,
|
|
596
|
-
"charm:answer": answer
|
|
602
|
+
"charm:answer": answer,
|
|
603
|
+
"charm:text": typed
|
|
597
604
|
};
|
|
598
605
|
const SERVER_SCHEMAS = {
|
|
599
606
|
hello: serverHello,
|
|
@@ -828,6 +835,7 @@ const schema = z.object({
|
|
|
828
835
|
agent: agentSchema.default({ adapter: "fake" }),
|
|
829
836
|
turnTimeoutSeconds: z.number().int().min(5).max(600).default(60),
|
|
830
837
|
logTranscripts: z.boolean().default(false),
|
|
838
|
+
speakReplies: z.boolean().default(true),
|
|
831
839
|
charm: charmSchema.prefault({})
|
|
832
840
|
}).strict();
|
|
833
841
|
function defaultAdminSocket(statePath) {
|
|
@@ -961,7 +969,7 @@ function sendAdmin(socketPath, request) {
|
|
|
961
969
|
}
|
|
962
970
|
//#endregion
|
|
963
971
|
//#region package.json
|
|
964
|
-
var version = "0.
|
|
972
|
+
var version = "0.6.0";
|
|
965
973
|
function channelToCube(value) {
|
|
966
974
|
if (value < 48) return 0;
|
|
967
975
|
if (value < 115) return 1;
|
|
@@ -1225,6 +1233,19 @@ async function runAdminCommand(ctx, command, args, deps) {
|
|
|
1225
1233
|
await look(ctx, args, deps);
|
|
1226
1234
|
return;
|
|
1227
1235
|
}
|
|
1236
|
+
if (command === "replies") {
|
|
1237
|
+
const choice = firstPositional(args);
|
|
1238
|
+
if (choice !== "on" && choice !== "off") {
|
|
1239
|
+
fail$2(ctx, "Usage: opencharm replies on|off (spoken, or text on the desktop charm)");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
await deps.send(deps.socket, {
|
|
1243
|
+
cmd: "replies",
|
|
1244
|
+
speak: choice === "on"
|
|
1245
|
+
});
|
|
1246
|
+
ctx.out.write(choice === "on" ? "Replies are spoken, from the next one.\n" : "Replies show as text on the desktop charm, from the next one (a charm on a board still speaks).\n");
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1228
1249
|
if (command === "status") {
|
|
1229
1250
|
const answer = statusSchema.safeParse(await deps.send(deps.socket, { cmd: "status" }));
|
|
1230
1251
|
if (!answer.success) {
|
|
@@ -1452,6 +1473,7 @@ function runHelp(ctx) {
|
|
|
1452
1473
|
opencharm lock <charm> lock a charm now
|
|
1453
1474
|
opencharm unlock <charm> clear a block and unlock
|
|
1454
1475
|
opencharm revoke <charm> forget a charm (it must pair again)
|
|
1476
|
+
opencharm replies on|off the desktop charm speaks its replies, or shows them as text
|
|
1455
1477
|
opencharm look show or change the charm's look until charmd restarts [--colour <c>] [--greeting <text>] [--motion full|calm]
|
|
1456
1478
|
opencharm dev face <charm> <state> [text] show a face (testing)
|
|
1457
1479
|
opencharm dev say <charm> <text> speak through it (testing)
|
|
@@ -1796,6 +1818,7 @@ async function serveMcp(input, output, deps) {
|
|
|
1796
1818
|
}
|
|
1797
1819
|
//#endregion
|
|
1798
1820
|
//#region ../charmd/src/admin/admin-actions.ts
|
|
1821
|
+
const repliesInput = z.object({ speak: z.boolean() }).strict();
|
|
1799
1822
|
const pairInput = z.object({
|
|
1800
1823
|
code: z.string().regex(/^\d{6}$/, "the code has 6 digits"),
|
|
1801
1824
|
pin: z.string().regex(/^\d{4,12}$/, "the PIN is 4 to 12 digits"),
|
|
@@ -1829,7 +1852,7 @@ function parseInput(schema, input) {
|
|
|
1829
1852
|
if (!result.success) throw new Error(result.error.issues.map((i) => i.message).join("; "));
|
|
1830
1853
|
return result.data;
|
|
1831
1854
|
}
|
|
1832
|
-
function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook, identity, starter, voice }) {
|
|
1855
|
+
function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook, identity, starter, voice, replies }) {
|
|
1833
1856
|
function sessionsOf(charmId) {
|
|
1834
1857
|
return [...sessions()].filter((s) => s.charmId === charmId && s.state !== "closed");
|
|
1835
1858
|
}
|
|
@@ -1934,6 +1957,14 @@ function createAdminActions({ store, pairing, sessions, look, agentCanChangeLook
|
|
|
1934
1957
|
};
|
|
1935
1958
|
});
|
|
1936
1959
|
},
|
|
1960
|
+
replies(input) {
|
|
1961
|
+
return settled(() => {
|
|
1962
|
+
const { speak } = parseInput(repliesInput, input);
|
|
1963
|
+
if (!replies) throw new Error("This charmd can't change how replies are given.");
|
|
1964
|
+
replies.speak = speak;
|
|
1965
|
+
return { speak };
|
|
1966
|
+
});
|
|
1967
|
+
},
|
|
1937
1968
|
status() {
|
|
1938
1969
|
return {
|
|
1939
1970
|
charmd: identity ?? "unknown",
|
|
@@ -1967,7 +1998,8 @@ const COMMANDS = [
|
|
|
1967
1998
|
"devFace",
|
|
1968
1999
|
"devSay",
|
|
1969
2000
|
"devAsk",
|
|
1970
|
-
"look"
|
|
2001
|
+
"look",
|
|
2002
|
+
"replies"
|
|
1971
2003
|
];
|
|
1972
2004
|
function isCommand(value) {
|
|
1973
2005
|
return typeof value === "string" && COMMANDS.includes(value);
|
|
@@ -2248,7 +2280,8 @@ var Session = class {
|
|
|
2248
2280
|
sendAudio: (packet) => {
|
|
2249
2281
|
if (this.#state === "unlocked") this.#deps.outbound.sendAudio?.(packet);
|
|
2250
2282
|
},
|
|
2251
|
-
ask: (text, options) => this.ask(text, options)
|
|
2283
|
+
ask: (text, options) => this.ask(text, options),
|
|
2284
|
+
kind: () => this.#build?.kind
|
|
2252
2285
|
});
|
|
2253
2286
|
return this.#turn;
|
|
2254
2287
|
}
|
|
@@ -2327,7 +2360,8 @@ var Session = class {
|
|
|
2327
2360
|
type: "hello",
|
|
2328
2361
|
transport: "websocket",
|
|
2329
2362
|
session_id: this.sessionId,
|
|
2330
|
-
audio_params: AUDIO_DOWN
|
|
2363
|
+
audio_params: AUDIO_DOWN,
|
|
2364
|
+
...this.#build?.kind === "desktop" ? { features: { text: true } } : {}
|
|
2331
2365
|
});
|
|
2332
2366
|
const { token } = this.#deps;
|
|
2333
2367
|
if (token === void 0) {
|
|
@@ -2372,6 +2406,9 @@ var Session = class {
|
|
|
2372
2406
|
if (message.state === "start") turn.listenStart();
|
|
2373
2407
|
else if (message.state === "stop") turn.listenStop();
|
|
2374
2408
|
} else if (message.type === "abort") turn.abort();
|
|
2409
|
+
else if (message.type === "charm" && message.op === "text") {
|
|
2410
|
+
if (this.#build?.kind === "desktop") turn.typed(message.text);
|
|
2411
|
+
}
|
|
2375
2412
|
}
|
|
2376
2413
|
async #onUnlock(pin) {
|
|
2377
2414
|
if (this.#state === "unlocked") {
|
|
@@ -3220,6 +3257,10 @@ var SentenceSplitter = class {
|
|
|
3220
3257
|
#buffer = "";
|
|
3221
3258
|
#pending = "";
|
|
3222
3259
|
#started = false;
|
|
3260
|
+
#firstClause;
|
|
3261
|
+
constructor(options = {}) {
|
|
3262
|
+
this.#firstClause = options.firstClause ?? true;
|
|
3263
|
+
}
|
|
3223
3264
|
#take(piece) {
|
|
3224
3265
|
const text = piece.replace(/\s+/g, " ").trim();
|
|
3225
3266
|
if (!text) return [];
|
|
@@ -3234,7 +3275,7 @@ var SentenceSplitter = class {
|
|
|
3234
3275
|
this.#buffer += chunk;
|
|
3235
3276
|
const out = [];
|
|
3236
3277
|
let start = 0;
|
|
3237
|
-
for (const match of this.#buffer.matchAll(this.#started ? BOUNDARY : FIRST_BOUNDARY)) {
|
|
3278
|
+
for (const match of this.#buffer.matchAll(this.#started || !this.#firstClause ? BOUNDARY : FIRST_BOUNDARY)) {
|
|
3238
3279
|
const end = match.index + match[0].length;
|
|
3239
3280
|
if (/^[,;:]/.test(match[0]) && (this.#started || this.#pending.length + end - start < FIRST_CLAUSE_CHARS)) continue;
|
|
3240
3281
|
const lastWord = this.#buffer.slice(start, end).trim().split(/\s+/).at(-1)?.toLowerCase() ?? "";
|
|
@@ -3259,6 +3300,8 @@ function cleanForSpeech(text) {
|
|
|
3259
3300
|
//#endregion
|
|
3260
3301
|
//#region ../charmd/src/turn/turn.ts
|
|
3261
3302
|
const MAX_FRAMES = 1e3;
|
|
3303
|
+
const READING_WORDS_PER_SECOND = 3;
|
|
3304
|
+
const MIN_READING_MS = 2e3;
|
|
3262
3305
|
const LOOKAHEAD = 2;
|
|
3263
3306
|
const AGENT_LABELS = { ...Object.fromEntries(Object.entries(ACP_AGENTS).map(([name, agent]) => [name, agent.label])) };
|
|
3264
3307
|
var TurnTimeout = class extends Error {};
|
|
@@ -3269,6 +3312,10 @@ var StageError = class extends Error {
|
|
|
3269
3312
|
this.stage = stage;
|
|
3270
3313
|
}
|
|
3271
3314
|
};
|
|
3315
|
+
function readingMs(text) {
|
|
3316
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
3317
|
+
return Math.max(MIN_READING_MS, words / READING_WORDS_PER_SECOND * 1e3);
|
|
3318
|
+
}
|
|
3272
3319
|
function sleepMs(ms) {
|
|
3273
3320
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3274
3321
|
}
|
|
@@ -3356,9 +3403,13 @@ var TurnController = class {
|
|
|
3356
3403
|
this.#face("idle");
|
|
3357
3404
|
return Promise.resolve();
|
|
3358
3405
|
}
|
|
3359
|
-
return this.#run(frames);
|
|
3406
|
+
return this.#run({ frames });
|
|
3360
3407
|
}
|
|
3361
|
-
|
|
3408
|
+
typed(text) {
|
|
3409
|
+
if (this.busy) this.#cancel();
|
|
3410
|
+
return this.#run({ text: text.trim() });
|
|
3411
|
+
}
|
|
3412
|
+
async #run(input) {
|
|
3362
3413
|
const generation = ++this.#generation;
|
|
3363
3414
|
const live = () => this.#generation === generation;
|
|
3364
3415
|
const controller = new AbortController();
|
|
@@ -3374,6 +3425,7 @@ var TurnController = class {
|
|
|
3374
3425
|
let timer = startClock();
|
|
3375
3426
|
let speakingStarted = false;
|
|
3376
3427
|
let asking = false;
|
|
3428
|
+
let reading = false;
|
|
3377
3429
|
const restartClock = () => {
|
|
3378
3430
|
clearTimeout(timer);
|
|
3379
3431
|
timer = startClock();
|
|
@@ -3384,7 +3436,15 @@ var TurnController = class {
|
|
|
3384
3436
|
restartClock();
|
|
3385
3437
|
},
|
|
3386
3438
|
touch: () => {
|
|
3387
|
-
if (speakingStarted && !asking) restartClock();
|
|
3439
|
+
if (speakingStarted && !asking && !reading) restartClock();
|
|
3440
|
+
},
|
|
3441
|
+
pause: () => {
|
|
3442
|
+
reading = true;
|
|
3443
|
+
clearTimeout(timer);
|
|
3444
|
+
},
|
|
3445
|
+
resume: () => {
|
|
3446
|
+
reading = false;
|
|
3447
|
+
restartClock();
|
|
3388
3448
|
}
|
|
3389
3449
|
};
|
|
3390
3450
|
const timeoutGuard = new Promise((_, reject) => signal.addEventListener("abort", () => {
|
|
@@ -3394,14 +3454,17 @@ var TurnController = class {
|
|
|
3394
3454
|
this.#state = "thinking";
|
|
3395
3455
|
this.#face("thinking");
|
|
3396
3456
|
try {
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3457
|
+
if ("text" in input) heard = input.text;
|
|
3458
|
+
else {
|
|
3459
|
+
const ogg = writeOggOpus(input.frames, { inputSampleRate: 16e3 });
|
|
3460
|
+
heard = await Promise.race([inStage("stt", () => this.#deps.voice.transcribe(ogg, signal)), timeoutGuard]);
|
|
3461
|
+
timings.sttMs = now() - started;
|
|
3462
|
+
if (!live()) return;
|
|
3463
|
+
this.#deps.send({
|
|
3464
|
+
type: "stt",
|
|
3465
|
+
text: heard
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3405
3468
|
if (!heard) {
|
|
3406
3469
|
outcome = "nothing-heard";
|
|
3407
3470
|
this.#state = "idle";
|
|
@@ -3429,7 +3492,7 @@ var TurnController = class {
|
|
|
3429
3492
|
signal,
|
|
3430
3493
|
...ask ? { ask: (action) => this.ask(`${label} wants to ${action}.`) } : {}
|
|
3431
3494
|
});
|
|
3432
|
-
said = await this.#speak(reply, signal, live, timings, started, timeoutGuard, guessLanguage(heard) ?? this.#deps.voice.language ?? "en", clock);
|
|
3495
|
+
said = await this.#speak(reply, signal, live, timings, started, timeoutGuard, guessLanguage(heard) ?? this.#deps.voice.language ?? "en", clock, !("text" in input) && (this.#deps.speakAloud?.() ?? true));
|
|
3433
3496
|
if (!live()) return;
|
|
3434
3497
|
if (this.#speaking) this.#deps.send({
|
|
3435
3498
|
type: "tts",
|
|
@@ -3464,6 +3527,7 @@ var TurnController = class {
|
|
|
3464
3527
|
outcome: live() ? outcome : "cancelled",
|
|
3465
3528
|
...timings,
|
|
3466
3529
|
totalMs: now() - started,
|
|
3530
|
+
..."text" in input ? { typed: true } : {},
|
|
3467
3531
|
...this.#deps.logTranscripts ? {
|
|
3468
3532
|
heard,
|
|
3469
3533
|
said: said.trim()
|
|
@@ -3471,7 +3535,7 @@ var TurnController = class {
|
|
|
3471
3535
|
});
|
|
3472
3536
|
}
|
|
3473
3537
|
}
|
|
3474
|
-
async #speak(source, signal, live, timings, started, timeoutGuard, language, clock) {
|
|
3538
|
+
async #speak(source, signal, live, timings, started, timeoutGuard, language, clock, aloud) {
|
|
3475
3539
|
const now = this.#deps.now ?? Date.now;
|
|
3476
3540
|
const going = () => live() && !signal.aborted;
|
|
3477
3541
|
let speaking = language;
|
|
@@ -3497,11 +3561,11 @@ var TurnController = class {
|
|
|
3497
3561
|
text,
|
|
3498
3562
|
language: sentenceLanguage
|
|
3499
3563
|
});
|
|
3500
|
-
startUpTo(playing + LOOKAHEAD);
|
|
3564
|
+
if (aloud) startUpTo(playing + LOOKAHEAD);
|
|
3501
3565
|
wake();
|
|
3502
3566
|
};
|
|
3503
3567
|
const produce = (async () => {
|
|
3504
|
-
const splitter = new SentenceSplitter();
|
|
3568
|
+
const splitter = new SentenceSplitter({ firstClause: aloud });
|
|
3505
3569
|
await inStage("agent", async () => {
|
|
3506
3570
|
for await (const chunk of source) {
|
|
3507
3571
|
if (!going()) return;
|
|
@@ -3524,6 +3588,11 @@ var TurnController = class {
|
|
|
3524
3588
|
await new Promise((resolve) => wake = resolve);
|
|
3525
3589
|
continue;
|
|
3526
3590
|
}
|
|
3591
|
+
if (!aloud) {
|
|
3592
|
+
await this.#showForReading(queue[playing].text, signal, going, clock, timings, started);
|
|
3593
|
+
playing += 1;
|
|
3594
|
+
continue;
|
|
3595
|
+
}
|
|
3527
3596
|
startUpTo(playing + LOOKAHEAD);
|
|
3528
3597
|
const { text, packets } = queue[playing];
|
|
3529
3598
|
if (!packets) throw new Error("A sentence wasn't started");
|
|
@@ -3563,9 +3632,20 @@ var TurnController = class {
|
|
|
3563
3632
|
clearTimeout(timer);
|
|
3564
3633
|
timer = startClock();
|
|
3565
3634
|
};
|
|
3635
|
+
let reading = false;
|
|
3566
3636
|
const clock = {
|
|
3567
3637
|
started: restartClock,
|
|
3568
|
-
touch:
|
|
3638
|
+
touch: () => {
|
|
3639
|
+
if (!reading) restartClock();
|
|
3640
|
+
},
|
|
3641
|
+
pause: () => {
|
|
3642
|
+
reading = true;
|
|
3643
|
+
clearTimeout(timer);
|
|
3644
|
+
},
|
|
3645
|
+
resume: () => {
|
|
3646
|
+
reading = false;
|
|
3647
|
+
restartClock();
|
|
3648
|
+
}
|
|
3569
3649
|
};
|
|
3570
3650
|
const timeoutGuard = new Promise((_, reject) => controller.signal.addEventListener("abort", () => {
|
|
3571
3651
|
if (controller.signal.reason instanceof TurnTimeout) reject(controller.signal.reason);
|
|
@@ -3575,7 +3655,7 @@ var TurnController = class {
|
|
|
3575
3655
|
yield await Promise.resolve(text);
|
|
3576
3656
|
}
|
|
3577
3657
|
try {
|
|
3578
|
-
await this.#speak(once(), controller.signal, live, {}, Date.now(), timeoutGuard, guessLanguage(text) ?? this.#deps.voice.language ?? "en", clock);
|
|
3658
|
+
await this.#speak(once(), controller.signal, live, {}, Date.now(), timeoutGuard, guessLanguage(text) ?? this.#deps.voice.language ?? "en", clock, this.#deps.speakAloud?.() ?? true);
|
|
3579
3659
|
if (!live()) return;
|
|
3580
3660
|
if (this.#speaking) this.#deps.send({
|
|
3581
3661
|
type: "tts",
|
|
@@ -3598,6 +3678,28 @@ var TurnController = class {
|
|
|
3598
3678
|
clearTimeout(timer);
|
|
3599
3679
|
}
|
|
3600
3680
|
}
|
|
3681
|
+
async #showForReading(text, signal, going, clock, timings, started) {
|
|
3682
|
+
if (!going()) return;
|
|
3683
|
+
if (!this.#speaking) {
|
|
3684
|
+
this.#speaking = true;
|
|
3685
|
+
this.#state = "speaking";
|
|
3686
|
+
timings.firstTextMs = (this.#deps.now ?? Date.now)() - started;
|
|
3687
|
+
clock.started();
|
|
3688
|
+
this.#deps.send({
|
|
3689
|
+
type: "tts",
|
|
3690
|
+
state: "start"
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
this.#deps.send({
|
|
3694
|
+
type: "tts",
|
|
3695
|
+
state: "sentence_start",
|
|
3696
|
+
text
|
|
3697
|
+
});
|
|
3698
|
+
const sleep = this.#deps.sleep ?? sleepMs;
|
|
3699
|
+
clock.pause();
|
|
3700
|
+
await Promise.race([sleep(readingMs(text)), new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }))]);
|
|
3701
|
+
clock.resume();
|
|
3702
|
+
}
|
|
3601
3703
|
#failureLine(error) {
|
|
3602
3704
|
if (error instanceof TurnTimeout) return "That took too long.";
|
|
3603
3705
|
if (error instanceof StageError && error.cause instanceof VoiceNotReady) return error.cause.message;
|
|
@@ -4670,6 +4772,7 @@ async function startDaemon(config, options = {}) {
|
|
|
4670
4772
|
});
|
|
4671
4773
|
const sessions = /* @__PURE__ */ new Map();
|
|
4672
4774
|
const voice = createVoiceFromConfig(config);
|
|
4775
|
+
const replies = { speak: config.speakReplies };
|
|
4673
4776
|
voice.warm?.().catch(() => void 0);
|
|
4674
4777
|
const log = options.log ?? ((entry) => {
|
|
4675
4778
|
if (!options.quiet) console.log(JSON.stringify({
|
|
@@ -4691,6 +4794,7 @@ async function startDaemon(config, options = {}) {
|
|
|
4691
4794
|
agentCanChangeLook,
|
|
4692
4795
|
identity: options.identity,
|
|
4693
4796
|
starter: options.starter,
|
|
4797
|
+
replies,
|
|
4694
4798
|
voice: () => {
|
|
4695
4799
|
const downloading = ["parakeet", "supertonic"].flatMap((id) => {
|
|
4696
4800
|
const state = modelState(id);
|
|
@@ -4749,7 +4853,8 @@ async function startDaemon(config, options = {}) {
|
|
|
4749
4853
|
agent,
|
|
4750
4854
|
timeoutMs: config.turnTimeoutSeconds * 1e3,
|
|
4751
4855
|
logTranscripts: config.logTranscripts,
|
|
4752
|
-
log
|
|
4856
|
+
log,
|
|
4857
|
+
speakAloud: () => io.kind() !== "desktop" || replies.speak
|
|
4753
4858
|
})
|
|
4754
4859
|
});
|
|
4755
4860
|
sessions.set(session.connectionId, session);
|
|
@@ -4831,7 +4936,7 @@ function formatIdentity(unit, version, commit) {
|
|
|
4831
4936
|
let cached;
|
|
4832
4937
|
function identity() {
|
|
4833
4938
|
if (cached) return cached;
|
|
4834
|
-
const commit = "
|
|
4939
|
+
const commit = "cb4cbce";
|
|
4835
4940
|
cached = {
|
|
4836
4941
|
version: `cli@${version}`,
|
|
4837
4942
|
commit,
|
|
@@ -5311,6 +5416,7 @@ async function main(argv) {
|
|
|
5311
5416
|
case "status":
|
|
5312
5417
|
case "dev":
|
|
5313
5418
|
case "look":
|
|
5419
|
+
case "replies":
|
|
5314
5420
|
await runAdminCommand(ctx, command, rest, {
|
|
5315
5421
|
socket: loadConfig(flagValue(rest, "--config")).adminSocket,
|
|
5316
5422
|
send: sendAdmin,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencharm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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": {
|
package/sim/charm_sim.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
async function createCharm(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["r"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("charm_sim.wasm")}return new URL("charm_sim.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;var HEAP16;var HEAP32;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;function js_panel(open){charmSim.panel(!!open)}function js_send_text(json){charmSim.sendText(UTF8ToString(json))}function js_send_binary(data,size){charmSim.sendBinary(HEAPU8.slice(data,data+size))}function js_mic(on){charmSim.mic(!!on)}function js_play_pcm(pcm,samples){charmSim.playPcm(HEAPF32.slice(pcm>>2,(pcm>>2)+samples))}function js_stop_audio(){charmSim.stopAudio()}function js_brightness(percent){charmSim.brightness(percent)}function js_store_get(key){var value=localStorage.getItem("opencharm."+UTF8ToString(key));if(value===null)return 0;var size=lengthBytesUTF8(value)+1;var ptr=_malloc(size);stringToUTF8(value,ptr,size);return ptr}function js_store_set(key,value){localStorage.setItem("opencharm."+UTF8ToString(key),UTF8ToString(value))}function js_store_erase(key){localStorage.removeItem("opencharm."+UTF8ToString(key))}function js_reconnect(){charmSim.reconnect()}var _sim_set_build,_sim_commit,_sim_init,_sim_frame,_sim_pin_key,_sim_frame_box,_sim_frame_dirty,_sim_tick,_sim_pointer,_sim_key,_sim_connected,_sim_disconnected,_sim_text,_sim_audio,_sim_mic_pcm,_sim_token,_sim_forget,_free,_malloc,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_sim_set_build=Module["_sim_set_build"]=wasmExports["s"];_sim_commit=Module["_sim_commit"]=wasmExports["t"];_sim_init=Module["_sim_init"]=wasmExports["u"];_sim_frame=Module["_sim_frame"]=wasmExports["v"];_sim_pin_key=Module["_sim_pin_key"]=wasmExports["w"];_sim_frame_box=Module["_sim_frame_box"]=wasmExports["x"];_sim_frame_dirty=Module["_sim_frame_dirty"]=wasmExports["y"];_sim_tick=Module["_sim_tick"]=wasmExports["z"];_sim_pointer=Module["_sim_pointer"]=wasmExports["A"];_sim_key=Module["_sim_key"]=wasmExports["B"];_sim_connected=Module["_sim_connected"]=wasmExports["C"];_sim_disconnected=Module["_sim_disconnected"]=wasmExports["D"];_sim_text=Module["_sim_text"]=wasmExports["E"];_sim_audio=Module["_sim_audio"]=wasmExports["F"];_sim_mic_pcm=Module["_sim_mic_pcm"]=wasmExports["G"];_sim_token=Module["_sim_token"]=wasmExports["H"];_sim_forget=Module["_sim_forget"]=wasmExports["I"];_free=Module["_free"]=wasmExports["J"];_malloc=Module["_malloc"]=wasmExports["K"];memory=wasmMemory=wasmExports["q"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={o:___cxa_throw,l:__abort_js,j:_emscripten_resize_heap,k:_fd_close,b:_fd_write,e:js_brightness,a:js_mic,p:js_panel,g:js_play_pcm,m:js_reconnect,h:js_send_binary,i:js_send_text,f:js_stop_audio,n:js_store_erase,d:js_store_get,c:js_store_set};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
|
|
1
|
+
async function createCharm(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var programArgs=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["r"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("charm_sim.wasm")}return new URL("charm_sim.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var __Unwind_RaiseException=ex=>{abort()};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;__Unwind_RaiseException(ptr)};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var HEAPU8;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _fd_close=fd=>52;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};var HEAPF32;var HEAP16;var HEAP32;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;function js_panel(open){charmSim.panel(!!open)}function js_send_text(json){charmSim.sendText(UTF8ToString(json))}function js_send_binary(data,size){charmSim.sendBinary(HEAPU8.slice(data,data+size))}function js_mic(on){charmSim.mic(!!on)}function js_play_pcm(pcm,samples){charmSim.playPcm(HEAPF32.slice(pcm>>2,(pcm>>2)+samples))}function js_stop_audio(){charmSim.stopAudio()}function js_brightness(percent){charmSim.brightness(percent)}function js_store_get(key){var value=localStorage.getItem("opencharm."+UTF8ToString(key));if(value===null)return 0;var size=lengthBytesUTF8(value)+1;var ptr=_malloc(size);stringToUTF8(value,ptr,size);return ptr}function js_store_set(key,value){localStorage.setItem("opencharm."+UTF8ToString(key),UTF8ToString(value))}function js_store_erase(key){localStorage.removeItem("opencharm."+UTF8ToString(key))}function js_reconnect(){charmSim.reconnect()}var _sim_set_build,_sim_commit,_sim_init,_sim_frame,_sim_pin_key,_sim_frame_box,_sim_frame_dirty,_sim_tick,_sim_pointer,_sim_key,_sim_connected,_sim_disconnected,_sim_text,_sim_type,_sim_audio,_sim_mic_pcm,_sim_token,_sim_forget,_free,_malloc,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_sim_set_build=Module["_sim_set_build"]=wasmExports["s"];_sim_commit=Module["_sim_commit"]=wasmExports["t"];_sim_init=Module["_sim_init"]=wasmExports["u"];_sim_frame=Module["_sim_frame"]=wasmExports["v"];_sim_pin_key=Module["_sim_pin_key"]=wasmExports["w"];_sim_frame_box=Module["_sim_frame_box"]=wasmExports["x"];_sim_frame_dirty=Module["_sim_frame_dirty"]=wasmExports["y"];_sim_tick=Module["_sim_tick"]=wasmExports["z"];_sim_pointer=Module["_sim_pointer"]=wasmExports["A"];_sim_key=Module["_sim_key"]=wasmExports["B"];_sim_connected=Module["_sim_connected"]=wasmExports["C"];_sim_disconnected=Module["_sim_disconnected"]=wasmExports["D"];_sim_text=Module["_sim_text"]=wasmExports["E"];_sim_type=Module["_sim_type"]=wasmExports["F"];_sim_audio=Module["_sim_audio"]=wasmExports["G"];_sim_mic_pcm=Module["_sim_mic_pcm"]=wasmExports["H"];_sim_token=Module["_sim_token"]=wasmExports["I"];_sim_forget=Module["_sim_forget"]=wasmExports["J"];_free=Module["_free"]=wasmExports["K"];_malloc=Module["_malloc"]=wasmExports["L"];memory=wasmMemory=wasmExports["q"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={o:___cxa_throw,l:__abort_js,j:_emscripten_resize_heap,k:_fd_close,b:_fd_write,e:js_brightness,a:js_mic,p:js_panel,g:js_play_pcm,m:js_reconnect,h:js_send_binary,i:js_send_text,f:js_stop_audio,n:js_store_erase,d:js_store_get,c:js_store_set};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
|
|
2
2
|
;return Module}export default createCharm;
|
package/sim/charm_sim.wasm
CHANGED
|
Binary file
|
package/sim/sim.js
CHANGED
|
@@ -109,6 +109,14 @@ window.charmSim = {
|
|
|
109
109
|
M._sim_forget();
|
|
110
110
|
ws?.close();
|
|
111
111
|
},
|
|
112
|
+
// Typed text from the desktop charm's field (spec 013): "sent", "busy" (a question or a talk), or
|
|
113
|
+
// "unsupported" (an older charmd).
|
|
114
|
+
type: (text) => {
|
|
115
|
+
let result = 0;
|
|
116
|
+
withString(text, (ptr) => (result = M._sim_type(ptr)));
|
|
117
|
+
wake();
|
|
118
|
+
return ["busy", "sent", "unsupported"][result] ?? "busy";
|
|
119
|
+
},
|
|
112
120
|
// A PIN key from a keyboard or the desktop app ("0"…"9", "<", "OK").
|
|
113
121
|
pinKey: (key) => {
|
|
114
122
|
withString(key, (ptr) => M._sim_pin_key(ptr));
|
|
@@ -291,15 +299,22 @@ function key(down) {
|
|
|
291
299
|
wake();
|
|
292
300
|
}
|
|
293
301
|
|
|
302
|
+
// Keys typed into a text field (the desktop charm's typing field) are text, not the charm's key or PIN.
|
|
303
|
+
const typingInto = (e) =>
|
|
304
|
+
e.target instanceof HTMLElement &&
|
|
305
|
+
(e.target.isContentEditable ||
|
|
306
|
+
e.target.tagName === "INPUT" ||
|
|
307
|
+
e.target.tagName === "TEXTAREA");
|
|
308
|
+
|
|
294
309
|
let spaceDown = false;
|
|
295
310
|
addEventListener("keydown", (e) => {
|
|
296
|
-
if (e.code !== "Space" || e.repeat || spaceDown) return;
|
|
311
|
+
if (e.code !== "Space" || e.repeat || spaceDown || typingInto(e)) return;
|
|
297
312
|
e.preventDefault();
|
|
298
313
|
spaceDown = true;
|
|
299
314
|
key(true);
|
|
300
315
|
});
|
|
301
316
|
addEventListener("keyup", (e) => {
|
|
302
|
-
if (e.code !== "Space") return;
|
|
317
|
+
if (e.code !== "Space" || !spaceDown) return;
|
|
303
318
|
e.preventDefault();
|
|
304
319
|
spaceDown = false;
|
|
305
320
|
key(false);
|
|
@@ -349,6 +364,7 @@ document.getElementById("shape").addEventListener("click", () => {
|
|
|
349
364
|
|
|
350
365
|
// The PIN can be typed on the keyboard too (the notch has no room for a pad).
|
|
351
366
|
addEventListener("keydown", (e) => {
|
|
367
|
+
if (typingInto(e)) return;
|
|
352
368
|
const key = /^[0-9]$/.test(e.key)
|
|
353
369
|
? e.key
|
|
354
370
|
: e.key === "Backspace"
|