agentar 0.1.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/LICENSE +21 -0
- package/README.md +101 -0
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/dist/agentar.js +1967 -0
- package/package.json +44 -0
- package/web/assets/index-DgcJBIKV.js +5165 -0
- package/web/assets/index-w9OTr571.css +1 -0
- package/web/favicon.svg +1 -0
- package/web/index.html +22 -0
package/dist/agentar.js
ADDED
|
@@ -0,0 +1,1967 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// packages/core/src/visemes.ts
|
|
18
|
+
function isViseme(value) {
|
|
19
|
+
return VISEMES.includes(value);
|
|
20
|
+
}
|
|
21
|
+
var VISEMES;
|
|
22
|
+
var init_visemes = __esm({
|
|
23
|
+
"packages/core/src/visemes.ts"() {
|
|
24
|
+
"use strict";
|
|
25
|
+
VISEMES = [
|
|
26
|
+
"sil",
|
|
27
|
+
// silence / neutral
|
|
28
|
+
"PP",
|
|
29
|
+
// p, b, m
|
|
30
|
+
"FF",
|
|
31
|
+
// f, v
|
|
32
|
+
"TH",
|
|
33
|
+
// th
|
|
34
|
+
"DD",
|
|
35
|
+
// t, d
|
|
36
|
+
"kk",
|
|
37
|
+
// k, g
|
|
38
|
+
"CH",
|
|
39
|
+
// ch, j, sh
|
|
40
|
+
"SS",
|
|
41
|
+
// s, z
|
|
42
|
+
"nn",
|
|
43
|
+
// n, l
|
|
44
|
+
"RR",
|
|
45
|
+
// r
|
|
46
|
+
"aa",
|
|
47
|
+
// a (father)
|
|
48
|
+
"E",
|
|
49
|
+
// e (bed)
|
|
50
|
+
"I",
|
|
51
|
+
// i (tip, see)
|
|
52
|
+
"O",
|
|
53
|
+
// o (toe)
|
|
54
|
+
"U"
|
|
55
|
+
// u (book, boot)
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// packages/core/src/moods.ts
|
|
61
|
+
function isMood(v) {
|
|
62
|
+
return typeof v === "string" && MOODS.includes(v);
|
|
63
|
+
}
|
|
64
|
+
var MOODS;
|
|
65
|
+
var init_moods = __esm({
|
|
66
|
+
"packages/core/src/moods.ts"() {
|
|
67
|
+
"use strict";
|
|
68
|
+
MOODS = ["neutral", "happy", "thinking", "surprised", "concerned", "amused"];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// packages/core/src/config.ts
|
|
73
|
+
function color(v, fallback) {
|
|
74
|
+
return typeof v === "string" && HEX_RE.test(v) ? v.toLowerCase() : fallback;
|
|
75
|
+
}
|
|
76
|
+
function colorOverride(v, fallback) {
|
|
77
|
+
if (v === null) return null;
|
|
78
|
+
return typeof v === "string" && HEX_RE.test(v) ? v.toLowerCase() : fallback;
|
|
79
|
+
}
|
|
80
|
+
function oneOf(v, allowed, fallback) {
|
|
81
|
+
return typeof v === "string" && allowed.includes(v) ? v : fallback;
|
|
82
|
+
}
|
|
83
|
+
function num(v, min, max, fallback) {
|
|
84
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
|
|
85
|
+
}
|
|
86
|
+
function str(v, maxLen, fallback) {
|
|
87
|
+
return typeof v === "string" ? v.slice(0, maxLen) : fallback;
|
|
88
|
+
}
|
|
89
|
+
function bool(v, fallback) {
|
|
90
|
+
return typeof v === "boolean" ? v : fallback;
|
|
91
|
+
}
|
|
92
|
+
function model(v, fallback) {
|
|
93
|
+
if (!isDict(v)) return fallback;
|
|
94
|
+
if (v.source === "builtin" && typeof v.id === "string" && BUILTIN_MODELS.some((m) => m.id === v.id)) {
|
|
95
|
+
return { source: "builtin", id: v.id };
|
|
96
|
+
}
|
|
97
|
+
if (v.source === "url" && typeof v.url === "string" && isAllowedModelUrl(v.url)) {
|
|
98
|
+
const format = v.format === "vrm" || v.format === "glb" ? v.format : void 0;
|
|
99
|
+
return format ? { source: "url", url: v.url, format } : { source: "url", url: v.url };
|
|
100
|
+
}
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
function isAllowedModelUrl(url) {
|
|
104
|
+
if (url.startsWith("/models/")) return !url.includes("..");
|
|
105
|
+
return /^https?:\/\/[^\s/?#]+[^\s]*$/i.test(url);
|
|
106
|
+
}
|
|
107
|
+
function mergeConfig(base, patch) {
|
|
108
|
+
const p = isDict(patch) ? patch : {};
|
|
109
|
+
const a = isDict(p.appearance) ? p.appearance : {};
|
|
110
|
+
const v = isDict(p.voice) ? p.voice : {};
|
|
111
|
+
const b = isDict(p.behavior) ? p.behavior : {};
|
|
112
|
+
const s = isDict(p.scene) ? p.scene : {};
|
|
113
|
+
const A = base.appearance;
|
|
114
|
+
const V = base.voice;
|
|
115
|
+
const B = base.behavior;
|
|
116
|
+
const S = base.scene;
|
|
117
|
+
return {
|
|
118
|
+
version: 1,
|
|
119
|
+
name: str(p.name, 64, base.name).trim() || base.name,
|
|
120
|
+
model: model(p.model, base.model),
|
|
121
|
+
appearance: {
|
|
122
|
+
skin: "skin" in a ? colorOverride(a.skin, A.skin) : A.skin,
|
|
123
|
+
hair: "hair" in a ? colorOverride(a.hair, A.hair) : A.hair,
|
|
124
|
+
eyes: "eyes" in a ? colorOverride(a.eyes, A.eyes) : A.eyes,
|
|
125
|
+
top: "top" in a ? colorOverride(a.top, A.top) : A.top,
|
|
126
|
+
bottom: "bottom" in a ? colorOverride(a.bottom, A.bottom) : A.bottom,
|
|
127
|
+
shoes: "shoes" in a ? colorOverride(a.shoes, A.shoes) : A.shoes,
|
|
128
|
+
glasses: oneOf(a.glasses, GLASSES, A.glasses),
|
|
129
|
+
hat: oneOf(a.hat, HATS, A.hat),
|
|
130
|
+
accessoryColor: color(a.accessoryColor, A.accessoryColor),
|
|
131
|
+
height: num(a.height, 0.85, 1.15, A.height)
|
|
132
|
+
},
|
|
133
|
+
voice: {
|
|
134
|
+
provider: oneOf(v.provider, VOICE_PROVIDERS, V.provider),
|
|
135
|
+
voice: str(v.voice, 128, V.voice),
|
|
136
|
+
rate: num(v.rate, 0.5, 2, V.rate),
|
|
137
|
+
pitch: num(v.pitch, 0.5, 2, V.pitch),
|
|
138
|
+
volume: num(v.volume, 0, 1, V.volume)
|
|
139
|
+
},
|
|
140
|
+
behavior: {
|
|
141
|
+
mood: oneOf(b.mood, MOODS, B.mood),
|
|
142
|
+
expressiveness: num(b.expressiveness, 0, 1, B.expressiveness),
|
|
143
|
+
idleMotion: num(b.idleMotion, 0, 1, B.idleMotion),
|
|
144
|
+
eyeContact: num(b.eyeContact, 0, 1, B.eyeContact)
|
|
145
|
+
},
|
|
146
|
+
scene: {
|
|
147
|
+
background: color(s.background, S.background),
|
|
148
|
+
framing: oneOf(s.framing, FRAMINGS, S.framing),
|
|
149
|
+
lighting: oneOf(s.lighting, LIGHTINGS, S.lighting),
|
|
150
|
+
showFloor: bool(s.showFloor, S.showFloor)
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function resolveConfig(input) {
|
|
155
|
+
return mergeConfig(DEFAULT_CONFIG, input);
|
|
156
|
+
}
|
|
157
|
+
var GLASSES, HATS, VOICE_PROVIDERS, FRAMINGS, LIGHTINGS, BUILTIN_MODELS, DEFAULT_CONFIG, isDict, HEX_RE;
|
|
158
|
+
var init_config = __esm({
|
|
159
|
+
"packages/core/src/config.ts"() {
|
|
160
|
+
"use strict";
|
|
161
|
+
init_moods();
|
|
162
|
+
GLASSES = ["none", "model", "round", "square"];
|
|
163
|
+
HATS = ["none", "beanie", "cap"];
|
|
164
|
+
VOICE_PROVIDERS = ["system", "browser", "edge", "openai", "elevenlabs", "xai"];
|
|
165
|
+
FRAMINGS = ["head", "bust", "full"];
|
|
166
|
+
LIGHTINGS = ["studio", "warm", "cool", "dramatic"];
|
|
167
|
+
BUILTIN_MODELS = [
|
|
168
|
+
{
|
|
169
|
+
id: "mpfb",
|
|
170
|
+
label: "Alex (MPFB, CC0)",
|
|
171
|
+
file: "mpfb.glb",
|
|
172
|
+
license: "CC0-1.0",
|
|
173
|
+
attribution: "Created with Blender + MPFB (MakeHuman). Distributed with TalkingHead.",
|
|
174
|
+
slots: {
|
|
175
|
+
"Human.body": "skin",
|
|
176
|
+
"Human.female_casualsuit01": "top",
|
|
177
|
+
"Human.ponytail01": "hair",
|
|
178
|
+
"Human.mindfront_eyebrows_02": "hair",
|
|
179
|
+
"Human.high-poly": "eyes"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "avaturn",
|
|
184
|
+
label: "Avaturn sample (non-commercial)",
|
|
185
|
+
file: "avaturn.glb",
|
|
186
|
+
license: "Non-commercial use",
|
|
187
|
+
attribution: "Created at avaturn.me. Distributed with TalkingHead.",
|
|
188
|
+
slots: {
|
|
189
|
+
Body: "skin",
|
|
190
|
+
Head: "skin",
|
|
191
|
+
Eyes: "eyes",
|
|
192
|
+
avaturn_hair_0_material: "hair",
|
|
193
|
+
avaturn_hair_1_material: "hair",
|
|
194
|
+
avaturn_look_0_material: "top",
|
|
195
|
+
avaturn_shoes_0_material: "shoes"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "brunette",
|
|
200
|
+
label: "Brunette (Ready Player Me, CC BY-NC 4.0)",
|
|
201
|
+
file: "brunette.glb",
|
|
202
|
+
license: "CC-BY-NC-4.0",
|
|
203
|
+
attribution: "Created at Ready Player Me. Distributed with TalkingHead.",
|
|
204
|
+
slots: {
|
|
205
|
+
Wolf3D_Skin: "skin",
|
|
206
|
+
Wolf3D_Body: "skin",
|
|
207
|
+
Wolf3D_Hair: "hair",
|
|
208
|
+
Wolf3D_Eye: "eyes",
|
|
209
|
+
Wolf3D_Outfit_Top: "top",
|
|
210
|
+
Wolf3D_Outfit_Bottom: "bottom",
|
|
211
|
+
Wolf3D_Outfit_Footwear: "shoes",
|
|
212
|
+
Wolf3D_Glasses: "eyewear"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "avatarsdk",
|
|
217
|
+
label: "AvatarSDK sample (non-commercial)",
|
|
218
|
+
file: "avatarsdk.glb",
|
|
219
|
+
license: "Non-commercial use",
|
|
220
|
+
attribution: "Created at avatarsdk.com. Distributed with TalkingHead."
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
DEFAULT_CONFIG = {
|
|
224
|
+
version: 1,
|
|
225
|
+
name: "Agentar",
|
|
226
|
+
model: { source: "builtin", id: "mpfb" },
|
|
227
|
+
appearance: {
|
|
228
|
+
skin: null,
|
|
229
|
+
hair: null,
|
|
230
|
+
eyes: null,
|
|
231
|
+
top: null,
|
|
232
|
+
bottom: null,
|
|
233
|
+
shoes: null,
|
|
234
|
+
glasses: "none",
|
|
235
|
+
hat: "none",
|
|
236
|
+
accessoryColor: "#222222",
|
|
237
|
+
height: 1
|
|
238
|
+
},
|
|
239
|
+
voice: { provider: "system", voice: "", rate: 1, pitch: 1, volume: 1 },
|
|
240
|
+
behavior: { mood: "neutral", expressiveness: 0.7, idleMotion: 0.6, eyeContact: 0.8 },
|
|
241
|
+
scene: { background: "#1b1f27", framing: "bust", lighting: "studio", showFloor: false }
|
|
242
|
+
};
|
|
243
|
+
isDict = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
244
|
+
HEX_RE = /^#[0-9a-fA-F]{6}$/;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// packages/core/src/protocol.ts
|
|
249
|
+
var DEFAULT_PORT, PROTOCOL_VERSION, GESTURES;
|
|
250
|
+
var init_protocol = __esm({
|
|
251
|
+
"packages/core/src/protocol.ts"() {
|
|
252
|
+
"use strict";
|
|
253
|
+
DEFAULT_PORT = 7777;
|
|
254
|
+
PROTOCOL_VERSION = 1;
|
|
255
|
+
GESTURES = [
|
|
256
|
+
"nod",
|
|
257
|
+
"shake",
|
|
258
|
+
"tilt",
|
|
259
|
+
"wave",
|
|
260
|
+
"high-five",
|
|
261
|
+
"thumbs-up",
|
|
262
|
+
"thumbs-down",
|
|
263
|
+
"idea",
|
|
264
|
+
"shrug",
|
|
265
|
+
"namaste",
|
|
266
|
+
"wink",
|
|
267
|
+
"laugh",
|
|
268
|
+
"surprised",
|
|
269
|
+
"bow"
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// packages/core/src/speakable.ts
|
|
275
|
+
function toSpeakable(markdown, opts = {}) {
|
|
276
|
+
const maxChars = opts.maxChars ?? 600;
|
|
277
|
+
let s = markdown.replace(/\r\n/g, "\n");
|
|
278
|
+
let codeBlocks = 0;
|
|
279
|
+
s = s.replace(/```[\s\S]*?(```|$)/g, () => {
|
|
280
|
+
codeBlocks++;
|
|
281
|
+
return "\n";
|
|
282
|
+
});
|
|
283
|
+
s = s.replace(/^\s*\|?\s*:?-{3,}.*$/gm, "").replace(/\|/g, ", ").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/<?https?:\/\/[^\s>)]+>?/g, "a link").replace(/`([^`]+)`/g, "$1").replace(/^\s{0,3}#{1,6}\s+(.*)$/gm, "$1.").replace(/^\s*>\s?/gm, "").replace(/^\s*(?:[-*+]|\d+[.)])\s+(.*)$/gm, "$1.").replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(^|\W)[*_](\S[^*_]*)[*_](?=\W|$)/g, "$1$2").replace(/<[^>]+>/g, "").replace(/(?:[\w.-]+\/)+([\w.-]+\.\w+)(?::\d+)?/g, "$1").replace(/\.{2,}\s*$/gm, ".").replace(/([.!?])\.+/g, "$1").replace(/[ \t]+/g, " ").replace(/\n{2,}/g, "\n").split("\n").map((l) => l.trim()).filter(Boolean).join(" ").trim();
|
|
284
|
+
if (codeBlocks > 0) {
|
|
285
|
+
const note = codeBlocks === 1 ? "I've included a code snippet." : `I've included ${codeBlocks} code snippets.`;
|
|
286
|
+
s = s ? `${s} ${note}` : note;
|
|
287
|
+
}
|
|
288
|
+
if (s.length > maxChars) {
|
|
289
|
+
const cut = s.slice(0, maxChars);
|
|
290
|
+
const lastStop = Math.max(cut.lastIndexOf(". "), cut.lastIndexOf("! "), cut.lastIndexOf("? "));
|
|
291
|
+
s = (lastStop > maxChars * 0.4 ? cut.slice(0, lastStop + 1) : cut.replace(/\s+\S*$/, "") + "\u2026").trim();
|
|
292
|
+
s += " The full details are in the chat.";
|
|
293
|
+
}
|
|
294
|
+
return s;
|
|
295
|
+
}
|
|
296
|
+
var init_speakable = __esm({
|
|
297
|
+
"packages/core/src/speakable.ts"() {
|
|
298
|
+
"use strict";
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// packages/core/src/lipsync/english.ts
|
|
303
|
+
function compile(rule) {
|
|
304
|
+
const posL = rule.indexOf("[");
|
|
305
|
+
const posR = rule.indexOf("]");
|
|
306
|
+
const posE = rule.indexOf("=");
|
|
307
|
+
const left = rule.substring(0, posL);
|
|
308
|
+
const letters = [...rule.substring(posL + 1, posR)];
|
|
309
|
+
const right = rule.substring(posR + 1, posE);
|
|
310
|
+
const out = rule.substring(posE + 1);
|
|
311
|
+
letters[0] = letters[0].toLowerCase();
|
|
312
|
+
const exp = [...left].map((c) => OPS[c] ?? c).join("") + letters.join("") + [...right].map((c) => OPS[c] ?? c).join("");
|
|
313
|
+
return {
|
|
314
|
+
regex: new RegExp(exp),
|
|
315
|
+
move: letters.length,
|
|
316
|
+
visemes: out.length ? out.split(" ").filter(isViseme) : []
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
var RULES, OPS, COMPILED;
|
|
320
|
+
var init_english = __esm({
|
|
321
|
+
"packages/core/src/lipsync/english.ts"() {
|
|
322
|
+
"use strict";
|
|
323
|
+
init_visemes();
|
|
324
|
+
RULES = {
|
|
325
|
+
A: [
|
|
326
|
+
"[A] =aa",
|
|
327
|
+
" [ARE] =aa RR",
|
|
328
|
+
" [AR]O=aa RR",
|
|
329
|
+
"[AR]#=E RR",
|
|
330
|
+
" ^[AS]#=E SS",
|
|
331
|
+
"[A]WA=aa",
|
|
332
|
+
"[AW]=aa",
|
|
333
|
+
" :[ANY]=E nn I",
|
|
334
|
+
"[A]^+#=E",
|
|
335
|
+
"#:[ALLY]=aa nn I",
|
|
336
|
+
" [AL]#=aa nn",
|
|
337
|
+
"[AGAIN]=aa kk E nn",
|
|
338
|
+
"#:[AG]E=I kk",
|
|
339
|
+
"[A]^+:#=aa",
|
|
340
|
+
":[A]^+ =E",
|
|
341
|
+
"[A]^%=E",
|
|
342
|
+
" [ARR]=aa RR",
|
|
343
|
+
"[ARR]=aa RR",
|
|
344
|
+
" :[AR] =aa RR",
|
|
345
|
+
"[AR] =E",
|
|
346
|
+
"[AR]=aa RR",
|
|
347
|
+
"[AIR]=E RR",
|
|
348
|
+
"[AI]=E",
|
|
349
|
+
"[AY]=E",
|
|
350
|
+
"[AU]=aa",
|
|
351
|
+
"#:[AL] =aa nn",
|
|
352
|
+
"#:[ALS] =aa nn SS",
|
|
353
|
+
"[ALK]=aa kk",
|
|
354
|
+
"[AL]^=aa nn",
|
|
355
|
+
" :[ABLE]=E PP aa nn",
|
|
356
|
+
"[ABLE]=aa PP aa nn",
|
|
357
|
+
"[ANG]+=E nn kk",
|
|
358
|
+
"[A]=aa"
|
|
359
|
+
],
|
|
360
|
+
B: [" [BE]^#=PP I", "[BEING]=PP I I nn", " [BOTH] =PP O TH", " [BUS]#=PP I SS", "[BUIL]=PP I nn", "[B]=PP"],
|
|
361
|
+
C: [
|
|
362
|
+
" [CH]^=kk",
|
|
363
|
+
"^E[CH]=kk",
|
|
364
|
+
"[CH]=CH",
|
|
365
|
+
" S[CI]#=SS aa",
|
|
366
|
+
"[CI]A=SS",
|
|
367
|
+
"[CI]O=SS",
|
|
368
|
+
"[CI]EN=SS",
|
|
369
|
+
"[C]+=SS",
|
|
370
|
+
"[CK]=kk",
|
|
371
|
+
"[COM]%=kk aa PP",
|
|
372
|
+
"[C]=kk"
|
|
373
|
+
],
|
|
374
|
+
D: [
|
|
375
|
+
"#:[DED] =DD I DD",
|
|
376
|
+
".E[D] =DD",
|
|
377
|
+
"#^:E[D] =DD",
|
|
378
|
+
" [DE]^#=DD I",
|
|
379
|
+
" [DO] =DD U",
|
|
380
|
+
" [DOES]=DD aa SS",
|
|
381
|
+
" [DOING]=DD U I nn",
|
|
382
|
+
" [DOW]=DD aa",
|
|
383
|
+
"[DU]A=kk U",
|
|
384
|
+
"[D]=DD"
|
|
385
|
+
],
|
|
386
|
+
E: [
|
|
387
|
+
"#:[E] =",
|
|
388
|
+
"'^:[E] =",
|
|
389
|
+
" :[E] =I",
|
|
390
|
+
"#[ED] =DD",
|
|
391
|
+
"#:[E]D =",
|
|
392
|
+
"[EV]ER=E FF",
|
|
393
|
+
"[E]^%=I",
|
|
394
|
+
"[ERI]#=I RR I",
|
|
395
|
+
"[ERI]=E RR I",
|
|
396
|
+
"#:[ER]#=E",
|
|
397
|
+
"[ER]#=E RR",
|
|
398
|
+
"[ER]=E",
|
|
399
|
+
" [EVEN]=I FF E nn",
|
|
400
|
+
"#:[E]W=",
|
|
401
|
+
"@[EW]=U",
|
|
402
|
+
"[EW]=I U",
|
|
403
|
+
"[E]O=I",
|
|
404
|
+
"#:&[ES] =I SS",
|
|
405
|
+
"#:[E]S =",
|
|
406
|
+
"#:[ELY] =nn I",
|
|
407
|
+
"#:[EMENT]=PP E nn DD",
|
|
408
|
+
"[EFUL]=FF U nn",
|
|
409
|
+
"[EE]=I",
|
|
410
|
+
"[EARN]=E nn",
|
|
411
|
+
" [EAR]^=E",
|
|
412
|
+
"[EAD]=E DD",
|
|
413
|
+
"#:[EA] =I aa",
|
|
414
|
+
"[EA]SU=E",
|
|
415
|
+
"[EA]=I",
|
|
416
|
+
"[EIGH]=E",
|
|
417
|
+
"[EI]=I",
|
|
418
|
+
" [EYE]=aa",
|
|
419
|
+
"[EY]=I",
|
|
420
|
+
"[EU]=I U",
|
|
421
|
+
"[E]=E"
|
|
422
|
+
],
|
|
423
|
+
F: ["[FUL]=FF U nn", "[F]=FF"],
|
|
424
|
+
G: [
|
|
425
|
+
"[GIV]=kk I FF",
|
|
426
|
+
" [G]I^=kk",
|
|
427
|
+
"[GE]T=kk E",
|
|
428
|
+
"SU[GGES]=kk kk E SS",
|
|
429
|
+
"[GG]=kk",
|
|
430
|
+
" B#[G]=kk",
|
|
431
|
+
"[G]+=kk",
|
|
432
|
+
"[GREAT]=kk RR E DD",
|
|
433
|
+
"#[GH]=",
|
|
434
|
+
"[G]=kk"
|
|
435
|
+
],
|
|
436
|
+
H: [" [HAV]=I aa FF", " [HERE]=I I RR", " [HOUR]=aa E", "[HOW]=I aa", "[H]#=I", "[H]="],
|
|
437
|
+
I: [
|
|
438
|
+
" [IN]=I nn",
|
|
439
|
+
" [I] =aa",
|
|
440
|
+
"[IN]D=aa nn",
|
|
441
|
+
"[IER]=I E",
|
|
442
|
+
"#:R[IED] =I DD",
|
|
443
|
+
"[IED] =aa DD",
|
|
444
|
+
"[IEN]=I E nn",
|
|
445
|
+
"[IE]T=aa E",
|
|
446
|
+
" :[I]%=aa",
|
|
447
|
+
"[I]%=I",
|
|
448
|
+
"[IE]=I",
|
|
449
|
+
"[I]^+:#=I",
|
|
450
|
+
"[IR]#=aa RR",
|
|
451
|
+
"[IZ]%=aa SS",
|
|
452
|
+
"[IS]%=aa SS",
|
|
453
|
+
"[I]D%=aa",
|
|
454
|
+
"+^[I]^+=I",
|
|
455
|
+
"[I]T%=aa",
|
|
456
|
+
"#^:[I]^+=I",
|
|
457
|
+
"[I]^+=aa",
|
|
458
|
+
"[IR]=E",
|
|
459
|
+
"[IGH]=aa",
|
|
460
|
+
"[ILD]=aa nn DD",
|
|
461
|
+
"[IGN] =aa nn",
|
|
462
|
+
"[IGN]^=aa nn",
|
|
463
|
+
"[IGN]%=aa nn",
|
|
464
|
+
"[IQUE]=I kk",
|
|
465
|
+
"[I]=I"
|
|
466
|
+
],
|
|
467
|
+
J: ["[J]=kk"],
|
|
468
|
+
K: [" [K]N=", "[K]=kk"],
|
|
469
|
+
L: ["[LO]C#=nn O", "L[L]=", "#^:[L]%=aa nn", "[LEAD]=nn I DD", "[L]=nn"],
|
|
470
|
+
M: ["[MOV]=PP U FF", "[M]=PP"],
|
|
471
|
+
N: [
|
|
472
|
+
"E[NG]+=nn kk",
|
|
473
|
+
"[NG]R=nn kk",
|
|
474
|
+
"[NG]#=nn kk",
|
|
475
|
+
"[NGL]%=nn kk aa nn",
|
|
476
|
+
"[NG]=nn",
|
|
477
|
+
"[NK]=nn kk",
|
|
478
|
+
" [NOW] =nn aa",
|
|
479
|
+
"[N]=nn"
|
|
480
|
+
],
|
|
481
|
+
O: [
|
|
482
|
+
"[OF] =aa FF",
|
|
483
|
+
"[OROUGH]=E O",
|
|
484
|
+
"#:[OR] =E",
|
|
485
|
+
"#:[ORS] =E SS",
|
|
486
|
+
"[OR]=aa RR",
|
|
487
|
+
" [ONE]=FF aa nn",
|
|
488
|
+
"[OW]=O",
|
|
489
|
+
" [OVER]=O FF E",
|
|
490
|
+
"[OV]=aa FF",
|
|
491
|
+
"[O]^%=O",
|
|
492
|
+
"[O]^EN=O",
|
|
493
|
+
"[O]^I#=O",
|
|
494
|
+
"[OL]D=O nn",
|
|
495
|
+
"[OUGHT]=aa DD",
|
|
496
|
+
"[OUGH]=aa FF",
|
|
497
|
+
" [OU]=aa",
|
|
498
|
+
"H[OU]S#=aa",
|
|
499
|
+
"[OUS]=aa SS",
|
|
500
|
+
"[OUR]=aa RR",
|
|
501
|
+
"[OULD]=U DD",
|
|
502
|
+
"^[OU]^L=aa",
|
|
503
|
+
"[OUP]=U PP",
|
|
504
|
+
"[OU]=aa",
|
|
505
|
+
"[OY]=O",
|
|
506
|
+
"[OING]=O I nn",
|
|
507
|
+
"[OI]=O",
|
|
508
|
+
"[OOR]=aa RR",
|
|
509
|
+
"[OOK]=U kk",
|
|
510
|
+
"[OOD]=U DD",
|
|
511
|
+
"[OO]=U",
|
|
512
|
+
"[O]E=O",
|
|
513
|
+
"[O] =O",
|
|
514
|
+
"[OA]=O",
|
|
515
|
+
" [ONLY]=O nn nn I",
|
|
516
|
+
" [ONCE]=FF aa nn SS",
|
|
517
|
+
"[ON'T]=O nn DD",
|
|
518
|
+
"C[O]N=aa",
|
|
519
|
+
"[O]NG=aa",
|
|
520
|
+
" ^:[O]N=aa",
|
|
521
|
+
"I[ON]=aa nn",
|
|
522
|
+
"#:[ON] =aa nn",
|
|
523
|
+
"#^[ON]=aa nn",
|
|
524
|
+
"[O]ST =O",
|
|
525
|
+
"[OF]^=aa FF",
|
|
526
|
+
"[OTHER]=aa TH E",
|
|
527
|
+
"[OSS] =aa SS",
|
|
528
|
+
"#^:[OM]=aa PP",
|
|
529
|
+
"[O]=aa"
|
|
530
|
+
],
|
|
531
|
+
P: ["[PH]=FF", "[PEOP]=PP I PP", "[POW]=PP aa", "[PUT] =PP U DD", "[P]=PP"],
|
|
532
|
+
Q: ["[QUAR]=kk FF aa RR", "[QU]=kk FF", "[Q]=kk"],
|
|
533
|
+
R: [" [RE]^#=RR I", "[R]=RR"],
|
|
534
|
+
S: [
|
|
535
|
+
"[SH]=SS",
|
|
536
|
+
"#[SION]=SS aa nn",
|
|
537
|
+
"[SOME]=SS aa PP",
|
|
538
|
+
"#[SUR]#=SS E",
|
|
539
|
+
"[SUR]#=SS E",
|
|
540
|
+
"#[SU]#=SS U",
|
|
541
|
+
"#[SSU]#=SS U",
|
|
542
|
+
"#[SED] =SS DD",
|
|
543
|
+
"#[S]#=SS",
|
|
544
|
+
"[SAID]=SS E DD",
|
|
545
|
+
"^[SION]=SS aa nn",
|
|
546
|
+
"[S]S=",
|
|
547
|
+
".[S] =SS",
|
|
548
|
+
"#:.E[S] =SS",
|
|
549
|
+
"#^:##[S] =SS",
|
|
550
|
+
"#^:#[S] =SS",
|
|
551
|
+
"U[S] =SS",
|
|
552
|
+
" :#[S] =SS",
|
|
553
|
+
" [SCH]=SS kk",
|
|
554
|
+
"[S]C+=",
|
|
555
|
+
"#[SM]=SS PP",
|
|
556
|
+
"#[SN]'=SS aa nn",
|
|
557
|
+
"[S]=SS"
|
|
558
|
+
],
|
|
559
|
+
T: [
|
|
560
|
+
" [THE] =TH aa",
|
|
561
|
+
"[TO] =DD U",
|
|
562
|
+
"[THAT] =TH aa DD",
|
|
563
|
+
" [THIS] =TH I SS",
|
|
564
|
+
" [THEY]=TH E",
|
|
565
|
+
" [THERE]=TH E RR",
|
|
566
|
+
"[THER]=TH E",
|
|
567
|
+
"[THEIR]=TH E RR",
|
|
568
|
+
" [THAN] =TH aa nn",
|
|
569
|
+
" [THEM] =TH E PP",
|
|
570
|
+
"[THESE] =TH I SS",
|
|
571
|
+
" [THEN]=TH E nn",
|
|
572
|
+
"[THROUGH]=TH RR U",
|
|
573
|
+
"[THOSE]=TH O SS",
|
|
574
|
+
"[THOUGH] =TH O",
|
|
575
|
+
" [THUS]=TH aa SS",
|
|
576
|
+
"[TH]=TH",
|
|
577
|
+
"#:[TED] =DD I DD",
|
|
578
|
+
"S[TI]#N=CH",
|
|
579
|
+
"[TI]O=SS",
|
|
580
|
+
"[TI]A=SS",
|
|
581
|
+
"[TIEN]=SS aa nn",
|
|
582
|
+
"[TUR]#=CH E",
|
|
583
|
+
"[TU]A=CH U",
|
|
584
|
+
" [TWO]=DD U",
|
|
585
|
+
"[T]=DD"
|
|
586
|
+
],
|
|
587
|
+
U: [
|
|
588
|
+
" [UN]I=I U nn",
|
|
589
|
+
" [UN]=aa nn",
|
|
590
|
+
" [UPON]=aa PP aa nn",
|
|
591
|
+
"@[UR]#=U RR",
|
|
592
|
+
"[UR]#=I U RR",
|
|
593
|
+
"[UR]=E",
|
|
594
|
+
"[U]^ =aa",
|
|
595
|
+
"[U]^^=aa",
|
|
596
|
+
"[UY]=aa",
|
|
597
|
+
" G[U]#=",
|
|
598
|
+
"G[U]%=",
|
|
599
|
+
"G[U]#=FF",
|
|
600
|
+
"#N[U]=I U",
|
|
601
|
+
"@[U]=I",
|
|
602
|
+
"[U]=I U"
|
|
603
|
+
],
|
|
604
|
+
V: ["[VIEW]=FF I U", "[V]=FF"],
|
|
605
|
+
W: [
|
|
606
|
+
" [WERE]=FF E",
|
|
607
|
+
"[WA]S=FF aa",
|
|
608
|
+
"[WA]T=FF aa",
|
|
609
|
+
"[WHERE]=FF E RR",
|
|
610
|
+
"[WHAT]=FF aa DD",
|
|
611
|
+
"[WHOL]=I O nn",
|
|
612
|
+
"[WHO]=I U",
|
|
613
|
+
"[WH]=FF",
|
|
614
|
+
"[WAR]=FF aa RR",
|
|
615
|
+
"[WOR]^=FF E",
|
|
616
|
+
"[WR]=RR",
|
|
617
|
+
"[W]=FF"
|
|
618
|
+
],
|
|
619
|
+
X: [" [X]=SS", "[X]=kk SS"],
|
|
620
|
+
Y: [
|
|
621
|
+
"[YOUNG]=I aa nn",
|
|
622
|
+
" [YOU]=I U",
|
|
623
|
+
" [YES]=I E SS",
|
|
624
|
+
" [Y]=I",
|
|
625
|
+
"#^:[Y] =I",
|
|
626
|
+
"#^:[Y]I=I",
|
|
627
|
+
" :[Y] =aa",
|
|
628
|
+
" :[Y]#=aa",
|
|
629
|
+
" :[Y]^+:#=I",
|
|
630
|
+
" :[Y]^#=I",
|
|
631
|
+
"[Y]=I"
|
|
632
|
+
],
|
|
633
|
+
Z: ["[Z]=SS"]
|
|
634
|
+
};
|
|
635
|
+
OPS = {
|
|
636
|
+
"#": "[AEIOUY]+",
|
|
637
|
+
// one or more vowels
|
|
638
|
+
".": "[BDVGJLMNRWZ]",
|
|
639
|
+
// one voiced consonant
|
|
640
|
+
"%": "(?:ER|E|ES|ED|ING|ELY)",
|
|
641
|
+
// common suffixes
|
|
642
|
+
"&": "(?:[SCGZXJ]|CH|SH)",
|
|
643
|
+
"@": "(?:[TSRDLZNJ]|TH|CH|SH)",
|
|
644
|
+
"^": "[BCDFGHJKLMNPQRSTVWXZ]",
|
|
645
|
+
// one consonant
|
|
646
|
+
"+": "[EIY]",
|
|
647
|
+
":": "[BCDFGHJKLMNPQRSTVWXZ]*",
|
|
648
|
+
// zero or more consonants
|
|
649
|
+
" ": "\\b"
|
|
650
|
+
// word boundary
|
|
651
|
+
};
|
|
652
|
+
COMPILED = Object.fromEntries(
|
|
653
|
+
Object.entries(RULES).map(([k, rules]) => [k, rules.map(compile)])
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// packages/core/src/lipsync/timeline.ts
|
|
659
|
+
var init_timeline = __esm({
|
|
660
|
+
"packages/core/src/lipsync/timeline.ts"() {
|
|
661
|
+
"use strict";
|
|
662
|
+
init_english();
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// packages/core/src/lipsync/audio.ts
|
|
667
|
+
var init_audio = __esm({
|
|
668
|
+
"packages/core/src/lipsync/audio.ts"() {
|
|
669
|
+
"use strict";
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// packages/core/src/index.ts
|
|
674
|
+
var init_src = __esm({
|
|
675
|
+
"packages/core/src/index.ts"() {
|
|
676
|
+
"use strict";
|
|
677
|
+
init_visemes();
|
|
678
|
+
init_moods();
|
|
679
|
+
init_config();
|
|
680
|
+
init_protocol();
|
|
681
|
+
init_speakable();
|
|
682
|
+
init_english();
|
|
683
|
+
init_timeline();
|
|
684
|
+
init_audio();
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// apps/bridge/src/store.ts
|
|
689
|
+
import { mkdir as mkdir2, readFile as readFile3, rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
690
|
+
import { homedir as homedir2 } from "node:os";
|
|
691
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
692
|
+
function agentarHome() {
|
|
693
|
+
return process.env.AGENTAR_HOME ?? join2(homedir2(), ".agentar");
|
|
694
|
+
}
|
|
695
|
+
var ConfigStore;
|
|
696
|
+
var init_store = __esm({
|
|
697
|
+
"apps/bridge/src/store.ts"() {
|
|
698
|
+
"use strict";
|
|
699
|
+
init_src();
|
|
700
|
+
ConfigStore = class _ConfigStore {
|
|
701
|
+
constructor(file, initial) {
|
|
702
|
+
this.file = file;
|
|
703
|
+
this.config = initial;
|
|
704
|
+
}
|
|
705
|
+
file;
|
|
706
|
+
config;
|
|
707
|
+
static async open(file = join2(agentarHome(), "config.json")) {
|
|
708
|
+
let raw;
|
|
709
|
+
try {
|
|
710
|
+
raw = JSON.parse(await readFile3(file, "utf8"));
|
|
711
|
+
} catch {
|
|
712
|
+
raw = void 0;
|
|
713
|
+
}
|
|
714
|
+
return new _ConfigStore(file, resolveConfig(raw));
|
|
715
|
+
}
|
|
716
|
+
get() {
|
|
717
|
+
return this.config;
|
|
718
|
+
}
|
|
719
|
+
async update(patch) {
|
|
720
|
+
this.config = mergeConfig(this.config, patch);
|
|
721
|
+
await mkdir2(dirname2(this.file), { recursive: true });
|
|
722
|
+
const tmp = `${this.file}.tmp`;
|
|
723
|
+
await writeFile2(tmp, JSON.stringify(this.config, null, 2) + "\n", "utf8");
|
|
724
|
+
await rename(tmp, this.file);
|
|
725
|
+
return this.config;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// apps/bridge/src/models.ts
|
|
732
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
733
|
+
import { mkdir as mkdir3, rename as rename2, rm, stat } from "node:fs/promises";
|
|
734
|
+
import { dirname as dirname3, join as join3, resolve } from "node:path";
|
|
735
|
+
import { Readable } from "node:stream";
|
|
736
|
+
import { pipeline } from "node:stream/promises";
|
|
737
|
+
import { fileURLToPath } from "node:url";
|
|
738
|
+
function defaultWebDir() {
|
|
739
|
+
const candidates = [join3(REPO_ROOT, "apps/web/dist"), resolve(HERE, "../web")];
|
|
740
|
+
return candidates.find((d) => existsSync(join3(d, "index.html"))) ?? candidates[0];
|
|
741
|
+
}
|
|
742
|
+
function defaultModelsDir(home = agentarHome()) {
|
|
743
|
+
const repo = join3(REPO_ROOT, "assets/models");
|
|
744
|
+
return existsSync(join3(repo, "README.md")) ? repo : join3(home, "builtin-models");
|
|
745
|
+
}
|
|
746
|
+
function defaultCliPath() {
|
|
747
|
+
return join3(REPO_ROOT, "packages/cli/dist/index.js");
|
|
748
|
+
}
|
|
749
|
+
async function fetchModels(dir, opts = {}) {
|
|
750
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
751
|
+
const defaultId = DEFAULT_CONFIG.model.source === "builtin" ? DEFAULT_CONFIG.model.id : "";
|
|
752
|
+
await mkdir3(dir, { recursive: true });
|
|
753
|
+
let ok = true;
|
|
754
|
+
for (const m of BUILTIN_MODELS) {
|
|
755
|
+
if (!opts.all && m.id !== defaultId) continue;
|
|
756
|
+
const dest = join3(dir, m.file);
|
|
757
|
+
try {
|
|
758
|
+
if ((await stat(dest)).size > 1e3) {
|
|
759
|
+
if (!opts.quiet) log(`\u2713 ${m.file} already present`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
log(`\u2193 ${m.file} (${m.license}) \u2026`);
|
|
765
|
+
try {
|
|
766
|
+
const res = await fetch(`${MODELS_URL}/${m.file}`);
|
|
767
|
+
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
768
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(`${dest}.part`));
|
|
769
|
+
await rename2(`${dest}.part`, dest);
|
|
770
|
+
log(`\u2713 ${m.file} downloaded`);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
await rm(`${dest}.part`, { force: true });
|
|
773
|
+
log(`\u2717 ${m.file} failed: ${err.message}`);
|
|
774
|
+
ok = false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return ok;
|
|
778
|
+
}
|
|
779
|
+
var MODELS_URL, HERE, REPO_ROOT;
|
|
780
|
+
var init_models = __esm({
|
|
781
|
+
"apps/bridge/src/models.ts"() {
|
|
782
|
+
"use strict";
|
|
783
|
+
init_src();
|
|
784
|
+
init_store();
|
|
785
|
+
MODELS_URL = "https://raw.githubusercontent.com/met4citizen/TalkingHead/main/avatars";
|
|
786
|
+
HERE = dirname3(fileURLToPath(import.meta.url));
|
|
787
|
+
REPO_ROOT = resolve(HERE, "../../..");
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// apps/bridge/src/speech-queue.ts
|
|
792
|
+
import { randomUUID } from "node:crypto";
|
|
793
|
+
function deferred() {
|
|
794
|
+
let resolve2;
|
|
795
|
+
const promise = new Promise((r) => resolve2 = r);
|
|
796
|
+
return { promise, resolve: resolve2 };
|
|
797
|
+
}
|
|
798
|
+
var SpeechQueue;
|
|
799
|
+
var init_speech_queue = __esm({
|
|
800
|
+
"apps/bridge/src/speech-queue.ts"() {
|
|
801
|
+
"use strict";
|
|
802
|
+
SpeechQueue = class {
|
|
803
|
+
constructor(deps) {
|
|
804
|
+
this.deps = deps;
|
|
805
|
+
}
|
|
806
|
+
deps;
|
|
807
|
+
queue = [];
|
|
808
|
+
current = null;
|
|
809
|
+
get busy() {
|
|
810
|
+
return this.current !== null || this.queue.length > 0;
|
|
811
|
+
}
|
|
812
|
+
async say(req) {
|
|
813
|
+
const id = randomUUID();
|
|
814
|
+
if (this.deps.clientCount() === 0) {
|
|
815
|
+
return { id, status: "no-clients", error: "No avatar is connected. Open the agentar page in a browser first." };
|
|
816
|
+
}
|
|
817
|
+
if (req.interrupt ?? true) this.stop();
|
|
818
|
+
const abort = new AbortController();
|
|
819
|
+
const item = {
|
|
820
|
+
id,
|
|
821
|
+
text: req.text,
|
|
822
|
+
mood: req.mood,
|
|
823
|
+
abort,
|
|
824
|
+
audio: this.deps.render(id, req.text, abort.signal),
|
|
825
|
+
dispatched: deferred(),
|
|
826
|
+
finished: deferred(),
|
|
827
|
+
audience: 0,
|
|
828
|
+
failures: 0
|
|
829
|
+
};
|
|
830
|
+
item.audio.catch(() => void 0);
|
|
831
|
+
const isNext = this.current === null && this.queue.length === 0;
|
|
832
|
+
this.queue.push(item);
|
|
833
|
+
void this.pump();
|
|
834
|
+
if (req.wait) return item.finished.promise;
|
|
835
|
+
return isNext ? item.dispatched.promise : { id, status: "queued" };
|
|
836
|
+
}
|
|
837
|
+
/** Stop the current utterance and drop everything queued. */
|
|
838
|
+
stop() {
|
|
839
|
+
const dropped = [...this.queue.splice(0), ...this.current ? [this.current] : []];
|
|
840
|
+
const hadCurrent = this.current !== null;
|
|
841
|
+
this.current = null;
|
|
842
|
+
for (const item of dropped) this.settle(item, { id: item.id, status: "interrupted" });
|
|
843
|
+
if (hadCurrent) this.deps.broadcast({ type: "stop" });
|
|
844
|
+
}
|
|
845
|
+
/** Called when an avatar client reports that it finished an utterance. */
|
|
846
|
+
speechEnded(id, interrupted, error) {
|
|
847
|
+
const item = this.current;
|
|
848
|
+
if (!item || item.id !== id) return;
|
|
849
|
+
if (error && ++item.failures < item.audience) return;
|
|
850
|
+
this.current = null;
|
|
851
|
+
this.settle(item, error ? { id, status: "error", error } : { id, status: interrupted ? "interrupted" : "spoken" });
|
|
852
|
+
void this.pump();
|
|
853
|
+
}
|
|
854
|
+
async pump() {
|
|
855
|
+
if (this.current || this.queue.length === 0) return;
|
|
856
|
+
const item = this.queue.shift();
|
|
857
|
+
this.current = item;
|
|
858
|
+
let audio;
|
|
859
|
+
try {
|
|
860
|
+
audio = await item.audio;
|
|
861
|
+
} catch (err) {
|
|
862
|
+
if (this.current === item) this.current = null;
|
|
863
|
+
this.settle(item, { id: item.id, status: "error", error: err.message });
|
|
864
|
+
void this.pump();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (this.current !== item) return;
|
|
868
|
+
const utterance = { id: item.id, text: item.text };
|
|
869
|
+
if (item.mood) utterance.mood = item.mood;
|
|
870
|
+
if (audio) utterance.audio = audio;
|
|
871
|
+
this.deps.broadcast({ type: "speak", utterance });
|
|
872
|
+
item.dispatched.resolve({ id: item.id, status: "queued" });
|
|
873
|
+
const words = item.text.split(/\s+/).length;
|
|
874
|
+
const rate = Math.max(0.25, this.deps.speechRate?.() ?? 1);
|
|
875
|
+
item.audience = this.deps.clientCount();
|
|
876
|
+
item.timer = setTimeout(() => this.speechEnded(item.id, false), (words / (2.5 * rate) + 15) * 1e3);
|
|
877
|
+
}
|
|
878
|
+
settle(item, res) {
|
|
879
|
+
if (item.timer) clearTimeout(item.timer);
|
|
880
|
+
item.abort.abort();
|
|
881
|
+
item.dispatched.resolve(res);
|
|
882
|
+
item.finished.resolve(res);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// apps/bridge/src/tts/types.ts
|
|
889
|
+
var TtsError;
|
|
890
|
+
var init_types = __esm({
|
|
891
|
+
"apps/bridge/src/tts/types.ts"() {
|
|
892
|
+
"use strict";
|
|
893
|
+
TtsError = class extends Error {
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// apps/bridge/src/tts/cloud.ts
|
|
899
|
+
var OPENAI_VOICES, OpenAiTts, ElevenLabsTts, XaiTts;
|
|
900
|
+
var init_cloud = __esm({
|
|
901
|
+
"apps/bridge/src/tts/cloud.ts"() {
|
|
902
|
+
"use strict";
|
|
903
|
+
init_types();
|
|
904
|
+
OPENAI_VOICES = ["alloy", "ash", "ballad", "coral", "echo", "fable", "nova", "onyx", "sage", "shimmer", "verse"];
|
|
905
|
+
OpenAiTts = class {
|
|
906
|
+
constructor(apiKey = process.env.OPENAI_API_KEY, model2 = process.env.AGENTAR_OPENAI_TTS_MODEL ?? "gpt-4o-mini-tts", baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1") {
|
|
907
|
+
this.apiKey = apiKey;
|
|
908
|
+
this.model = model2;
|
|
909
|
+
this.baseUrl = baseUrl;
|
|
910
|
+
}
|
|
911
|
+
apiKey;
|
|
912
|
+
model;
|
|
913
|
+
baseUrl;
|
|
914
|
+
id = "openai";
|
|
915
|
+
async unavailableReason() {
|
|
916
|
+
return this.apiKey ? null : "Set OPENAI_API_KEY in the environment that runs `agentar start`.";
|
|
917
|
+
}
|
|
918
|
+
async listVoices() {
|
|
919
|
+
return OPENAI_VOICES.map((v) => ({ id: v, name: v[0].toUpperCase() + v.slice(1) }));
|
|
920
|
+
}
|
|
921
|
+
async synthesize(text2, voice, signal) {
|
|
922
|
+
if (!this.apiKey) throw new TtsError("OPENAI_API_KEY is not set");
|
|
923
|
+
const res = await fetch(`${this.baseUrl}/audio/speech`, {
|
|
924
|
+
method: "POST",
|
|
925
|
+
signal,
|
|
926
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
|
|
927
|
+
body: JSON.stringify({
|
|
928
|
+
model: this.model,
|
|
929
|
+
voice: voice.voice || "coral",
|
|
930
|
+
input: text2,
|
|
931
|
+
response_format: "mp3",
|
|
932
|
+
speed: voice.rate
|
|
933
|
+
})
|
|
934
|
+
});
|
|
935
|
+
if (!res.ok) throw new TtsError(`OpenAI TTS failed (${res.status}): ${(await res.text()).slice(0, 300)}`);
|
|
936
|
+
return { data: Buffer.from(await res.arrayBuffer()), mime: "audio/mpeg" };
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
ElevenLabsTts = class {
|
|
940
|
+
constructor(apiKey = process.env.ELEVENLABS_API_KEY, model2 = process.env.AGENTAR_ELEVENLABS_MODEL ?? "eleven_flash_v2_5") {
|
|
941
|
+
this.apiKey = apiKey;
|
|
942
|
+
this.model = model2;
|
|
943
|
+
}
|
|
944
|
+
apiKey;
|
|
945
|
+
model;
|
|
946
|
+
id = "elevenlabs";
|
|
947
|
+
async unavailableReason() {
|
|
948
|
+
return this.apiKey ? null : "Set ELEVENLABS_API_KEY in the environment that runs `agentar start`.";
|
|
949
|
+
}
|
|
950
|
+
async listVoices() {
|
|
951
|
+
if (!this.apiKey) return [];
|
|
952
|
+
const res = await fetch("https://api.elevenlabs.io/v1/voices", { headers: { "xi-api-key": this.apiKey } });
|
|
953
|
+
if (!res.ok) throw new TtsError(`ElevenLabs voices failed (${res.status})`);
|
|
954
|
+
const body = await res.json();
|
|
955
|
+
return (body.voices ?? []).map((v) => ({ id: v.voice_id, name: v.name, language: v.labels?.accent }));
|
|
956
|
+
}
|
|
957
|
+
async synthesize(text2, voice, signal) {
|
|
958
|
+
if (!this.apiKey) throw new TtsError("ELEVENLABS_API_KEY is not set");
|
|
959
|
+
const voiceId = encodeURIComponent(voice.voice || "21m00Tcm4TlvDq8ikWAM");
|
|
960
|
+
const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}?output_format=mp3_44100_128`, {
|
|
961
|
+
method: "POST",
|
|
962
|
+
signal,
|
|
963
|
+
headers: { "xi-api-key": this.apiKey, "Content-Type": "application/json", Accept: "audio/mpeg" },
|
|
964
|
+
body: JSON.stringify({
|
|
965
|
+
text: text2,
|
|
966
|
+
model_id: this.model,
|
|
967
|
+
voice_settings: { stability: 0.5, similarity_boost: 0.75, speed: Math.min(1.2, Math.max(0.7, voice.rate)) }
|
|
968
|
+
})
|
|
969
|
+
});
|
|
970
|
+
if (!res.ok) throw new TtsError(`ElevenLabs TTS failed (${res.status}): ${(await res.text()).slice(0, 300)}`);
|
|
971
|
+
return { data: Buffer.from(await res.arrayBuffer()), mime: "audio/mpeg" };
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
XaiTts = class {
|
|
975
|
+
constructor(apiKey = process.env.XAI_API_KEY, baseUrl = process.env.XAI_BASE_URL ?? "https://api.x.ai/v1") {
|
|
976
|
+
this.apiKey = apiKey;
|
|
977
|
+
this.baseUrl = baseUrl;
|
|
978
|
+
}
|
|
979
|
+
apiKey;
|
|
980
|
+
baseUrl;
|
|
981
|
+
id = "xai";
|
|
982
|
+
async unavailableReason() {
|
|
983
|
+
return this.apiKey ? null : "Set XAI_API_KEY in the environment that runs `agentar start`.";
|
|
984
|
+
}
|
|
985
|
+
async listVoices() {
|
|
986
|
+
if (!this.apiKey) return [];
|
|
987
|
+
const res = await fetch(`${this.baseUrl}/tts/voices`, { headers: { Authorization: `Bearer ${this.apiKey}` } });
|
|
988
|
+
if (!res.ok) throw new TtsError(`xAI voices failed (${res.status})`);
|
|
989
|
+
const body = await res.json();
|
|
990
|
+
return (body.voices ?? []).map((v) => ({ id: v.voice_id, name: v.name, language: v.language ?? void 0 }));
|
|
991
|
+
}
|
|
992
|
+
async synthesize(text2, voice, signal) {
|
|
993
|
+
if (!this.apiKey) throw new TtsError("XAI_API_KEY is not set");
|
|
994
|
+
const res = await fetch(`${this.baseUrl}/tts`, {
|
|
995
|
+
method: "POST",
|
|
996
|
+
signal,
|
|
997
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
|
|
998
|
+
body: JSON.stringify({
|
|
999
|
+
text: text2,
|
|
1000
|
+
voice_id: voice.voice || "eve",
|
|
1001
|
+
language: "auto",
|
|
1002
|
+
output_format: { codec: "mp3" },
|
|
1003
|
+
speed: Math.min(1.5, Math.max(0.7, voice.rate))
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
if (!res.ok) throw new TtsError(`xAI TTS failed (${res.status}): ${(await res.text()).slice(0, 300)}`);
|
|
1007
|
+
return { data: Buffer.from(await res.arrayBuffer()), mime: "audio/mpeg" };
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// apps/bridge/src/tts/edge.ts
|
|
1014
|
+
import { execFile } from "node:child_process";
|
|
1015
|
+
import { access, mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1016
|
+
import { homedir as homedir3, tmpdir } from "node:os";
|
|
1017
|
+
import { join as join4 } from "node:path";
|
|
1018
|
+
import { promisify } from "node:util";
|
|
1019
|
+
async function isExecutable(path) {
|
|
1020
|
+
try {
|
|
1021
|
+
await access(
|
|
1022
|
+
path,
|
|
1023
|
+
1
|
|
1024
|
+
/* X_OK */
|
|
1025
|
+
);
|
|
1026
|
+
return true;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
function edgeRate(rate) {
|
|
1032
|
+
const pct = Math.round((rate - 1) * 100);
|
|
1033
|
+
return `${pct < 0 ? "-" : "+"}${Math.abs(pct)}%`;
|
|
1034
|
+
}
|
|
1035
|
+
function parseEdgeVoices(stdout) {
|
|
1036
|
+
const voices = [];
|
|
1037
|
+
for (const line of stdout.split("\n")) {
|
|
1038
|
+
const m = /^((?:[a-z]{2,3}-)(?:[A-Za-z]+-)+)([A-Za-z]+?)(Multilingual)?Neural\s+(\S+)/.exec(line.trim());
|
|
1039
|
+
if (!m) continue;
|
|
1040
|
+
const [, locale = "", short = "", multi, gender = ""] = m;
|
|
1041
|
+
const id = line.trim().split(/\s+/)[0];
|
|
1042
|
+
const label = multi ? `${short} Multilingual` : short;
|
|
1043
|
+
voices.push({ id, name: `${label}, ${gender.toLowerCase()}`, language: locale.slice(0, -1) });
|
|
1044
|
+
}
|
|
1045
|
+
return voices;
|
|
1046
|
+
}
|
|
1047
|
+
var run, EdgeTts;
|
|
1048
|
+
var init_edge = __esm({
|
|
1049
|
+
"apps/bridge/src/tts/edge.ts"() {
|
|
1050
|
+
"use strict";
|
|
1051
|
+
init_types();
|
|
1052
|
+
run = promisify(execFile);
|
|
1053
|
+
EdgeTts = class {
|
|
1054
|
+
constructor(binary = process.env.AGENTAR_EDGE_TTS) {
|
|
1055
|
+
this.binary = binary;
|
|
1056
|
+
}
|
|
1057
|
+
binary;
|
|
1058
|
+
id = "edge";
|
|
1059
|
+
voicesCache = null;
|
|
1060
|
+
async unavailableReason() {
|
|
1061
|
+
return await this.findBinary() ? null : "Install edge-tts (`pipx install edge-tts` or `uv tool install edge-tts`).";
|
|
1062
|
+
}
|
|
1063
|
+
async listVoices() {
|
|
1064
|
+
if (this.voicesCache) return this.voicesCache;
|
|
1065
|
+
const { stdout } = await run(await this.requireBinary(), ["--list-voices"], { timeout: 3e4 });
|
|
1066
|
+
this.voicesCache = parseEdgeVoices(stdout);
|
|
1067
|
+
return this.voicesCache;
|
|
1068
|
+
}
|
|
1069
|
+
async synthesize(text2, voice, signal) {
|
|
1070
|
+
const bin = await this.requireBinary();
|
|
1071
|
+
const dir = await mkdtemp(join4(tmpdir(), "agentar-edge-"));
|
|
1072
|
+
const input = join4(dir, "input.txt");
|
|
1073
|
+
const out = join4(dir, "speech.mp3");
|
|
1074
|
+
try {
|
|
1075
|
+
await writeFile3(input, text2, "utf8");
|
|
1076
|
+
const args = [`--file=${input}`, `--write-media=${out}`, `--rate=${edgeRate(voice.rate)}`];
|
|
1077
|
+
if (voice.voice) args.push(`--voice=${voice.voice}`);
|
|
1078
|
+
await run(bin, args, { timeout: 6e4, signal });
|
|
1079
|
+
return { data: await readFile4(out), mime: "audio/mpeg" };
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
throw new TtsError(`Edge TTS failed: ${err.message}`);
|
|
1082
|
+
} finally {
|
|
1083
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
async requireBinary() {
|
|
1087
|
+
const bin = await this.findBinary();
|
|
1088
|
+
if (!bin) throw new TtsError("edge-tts not found");
|
|
1089
|
+
return bin;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* The bridge may be started from a hook or app with a short PATH, so also
|
|
1093
|
+
* look where pipx, uv, and Homebrew put their binaries.
|
|
1094
|
+
*/
|
|
1095
|
+
async findBinary() {
|
|
1096
|
+
if (this.binary) return await isExecutable(this.binary) ? this.binary : null;
|
|
1097
|
+
try {
|
|
1098
|
+
const { stdout } = await run("which", ["edge-tts"]);
|
|
1099
|
+
if (stdout.trim()) return stdout.trim();
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
for (const dir of [join4(homedir3(), ".local/bin"), "/opt/homebrew/bin", "/usr/local/bin"]) {
|
|
1103
|
+
const bin = join4(dir, "edge-tts");
|
|
1104
|
+
if (await isExecutable(bin)) return bin;
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// apps/bridge/src/tts/system.ts
|
|
1113
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1114
|
+
import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
1115
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1116
|
+
import { join as join5 } from "node:path";
|
|
1117
|
+
import { promisify as promisify2 } from "node:util";
|
|
1118
|
+
function parseSayVoices(stdout) {
|
|
1119
|
+
const voices = [];
|
|
1120
|
+
for (const line of stdout.split("\n")) {
|
|
1121
|
+
const m = /^(.+?)\s+([a-z]{2,3}[_-][A-Za-z0-9]+)\s+#/.exec(line);
|
|
1122
|
+
if (m) voices.push({ id: m[1].trim(), name: m[1].trim(), language: m[2].replace("_", "-") });
|
|
1123
|
+
}
|
|
1124
|
+
return voices;
|
|
1125
|
+
}
|
|
1126
|
+
var run2, SystemTts;
|
|
1127
|
+
var init_system = __esm({
|
|
1128
|
+
"apps/bridge/src/tts/system.ts"() {
|
|
1129
|
+
"use strict";
|
|
1130
|
+
init_types();
|
|
1131
|
+
run2 = promisify2(execFile2);
|
|
1132
|
+
SystemTts = class {
|
|
1133
|
+
constructor(platform = process.platform) {
|
|
1134
|
+
this.platform = platform;
|
|
1135
|
+
}
|
|
1136
|
+
platform;
|
|
1137
|
+
id = "system";
|
|
1138
|
+
voicesCache = null;
|
|
1139
|
+
async unavailableReason() {
|
|
1140
|
+
try {
|
|
1141
|
+
if (this.platform === "darwin") await run2("which", ["say"]);
|
|
1142
|
+
else if (this.platform === "win32") await run2("powershell", ["-NoProfile", "-Command", "Add-Type -AssemblyName System.Speech"]);
|
|
1143
|
+
else await this.linuxBinary();
|
|
1144
|
+
return null;
|
|
1145
|
+
} catch {
|
|
1146
|
+
return this.platform === "linux" ? "Install espeak-ng (e.g. `sudo apt install espeak-ng`) or choose another voice provider." : "The system speech engine is not available.";
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async listVoices() {
|
|
1150
|
+
if (this.voicesCache) return this.voicesCache;
|
|
1151
|
+
let voices = [];
|
|
1152
|
+
if (this.platform === "darwin") {
|
|
1153
|
+
const { stdout } = await run2("say", ["-v", "?"]);
|
|
1154
|
+
voices = parseSayVoices(stdout);
|
|
1155
|
+
} else if (this.platform === "win32") {
|
|
1156
|
+
const { stdout } = await run2("powershell", [
|
|
1157
|
+
"-NoProfile",
|
|
1158
|
+
"-Command",
|
|
1159
|
+
"Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name + '|' + $_.VoiceInfo.Culture }"
|
|
1160
|
+
]);
|
|
1161
|
+
voices = stdout.split(/\r?\n/).filter(Boolean).map((l) => {
|
|
1162
|
+
const [name = "", language] = l.split("|");
|
|
1163
|
+
return { id: name, name, language };
|
|
1164
|
+
});
|
|
1165
|
+
} else {
|
|
1166
|
+
const bin = await this.linuxBinary();
|
|
1167
|
+
const { stdout } = await run2(bin, ["--voices"]);
|
|
1168
|
+
voices = stdout.split("\n").slice(1).map((l) => l.trim().split(/\s+/)).filter((p) => p.length >= 4).map((p) => ({ id: p[3], name: p[3], language: p[1] }));
|
|
1169
|
+
}
|
|
1170
|
+
this.voicesCache = voices;
|
|
1171
|
+
return voices;
|
|
1172
|
+
}
|
|
1173
|
+
async synthesize(text2, voice) {
|
|
1174
|
+
const dir = await mkdtemp2(join5(tmpdir2(), "agentar-tts-"));
|
|
1175
|
+
const input = join5(dir, "input.txt");
|
|
1176
|
+
const out = join5(dir, "speech.wav");
|
|
1177
|
+
try {
|
|
1178
|
+
await writeFile4(input, text2, "utf8");
|
|
1179
|
+
if (this.platform === "darwin") {
|
|
1180
|
+
const args = ["-f", input, "-o", out, "--file-format=WAVE", "--data-format=LEI16@22050", "-r", String(Math.round(185 * voice.rate))];
|
|
1181
|
+
if (voice.voice) args.unshift("-v", voice.voice);
|
|
1182
|
+
await run2("say", args, { timeout: 6e4 });
|
|
1183
|
+
} else if (this.platform === "win32") {
|
|
1184
|
+
const script = [
|
|
1185
|
+
"Add-Type -AssemblyName System.Speech",
|
|
1186
|
+
"$s = New-Object System.Speech.Synthesis.SpeechSynthesizer",
|
|
1187
|
+
voice.voice ? `$s.SelectVoice($env:AGENTAR_VOICE)` : "",
|
|
1188
|
+
`$s.Rate = ${Math.max(-10, Math.min(10, Math.round((voice.rate - 1) * 10)))}`,
|
|
1189
|
+
"$s.SetOutputToWaveFile($env:AGENTAR_OUT)",
|
|
1190
|
+
"$s.Speak([IO.File]::ReadAllText($env:AGENTAR_IN))",
|
|
1191
|
+
"$s.Dispose()"
|
|
1192
|
+
].join("; ");
|
|
1193
|
+
await run2("powershell", ["-NoProfile", "-Command", script], {
|
|
1194
|
+
timeout: 6e4,
|
|
1195
|
+
env: { ...process.env, AGENTAR_VOICE: voice.voice, AGENTAR_OUT: out, AGENTAR_IN: input }
|
|
1196
|
+
});
|
|
1197
|
+
} else {
|
|
1198
|
+
const bin = await this.linuxBinary();
|
|
1199
|
+
const args = ["-w", out, "-s", String(Math.round(170 * voice.rate)), "-f", input];
|
|
1200
|
+
if (voice.voice) args.unshift("-v", voice.voice);
|
|
1201
|
+
await run2(bin, args, { timeout: 6e4 });
|
|
1202
|
+
}
|
|
1203
|
+
return { data: await readFile5(out), mime: "audio/wav" };
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
throw new TtsError(`System TTS failed: ${err.message}`);
|
|
1206
|
+
} finally {
|
|
1207
|
+
await rm3(dir, { recursive: true, force: true });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async linuxBinary() {
|
|
1211
|
+
for (const bin of ["espeak-ng", "espeak"]) {
|
|
1212
|
+
try {
|
|
1213
|
+
await run2("which", [bin]);
|
|
1214
|
+
return bin;
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
throw new TtsError("espeak-ng not found");
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// apps/bridge/src/server.ts
|
|
1225
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1226
|
+
import { createReadStream } from "node:fs";
|
|
1227
|
+
import { mkdir as mkdir4, readdir, stat as stat2, writeFile as writeFile5 } from "node:fs/promises";
|
|
1228
|
+
import { createServer } from "node:http";
|
|
1229
|
+
import { extname, join as join6, normalize, sep } from "node:path";
|
|
1230
|
+
import { WebSocketServer } from "ws";
|
|
1231
|
+
async function startBridge(opts = {}) {
|
|
1232
|
+
const port = opts.port ?? Number(process.env.AGENTAR_PORT ?? DEFAULT_PORT);
|
|
1233
|
+
const host = opts.host ?? process.env.AGENTAR_HOST ?? "127.0.0.1";
|
|
1234
|
+
const home = opts.homeDir ?? agentarHome();
|
|
1235
|
+
const webDir = opts.webDir ?? process.env.AGENTAR_WEB_DIR ?? defaultWebDir();
|
|
1236
|
+
const modelsDir = opts.modelsDir ?? process.env.AGENTAR_MODELS_DIR ?? defaultModelsDir(home);
|
|
1237
|
+
const cliPath = opts.cliPath ?? defaultCliPath();
|
|
1238
|
+
const userModelsDir = join6(home, "models");
|
|
1239
|
+
const log = opts.log ?? ((m) => console.log(`[agentar] ${m}`));
|
|
1240
|
+
const store = await ConfigStore.open(join6(home, "config.json"));
|
|
1241
|
+
const providers = {
|
|
1242
|
+
system: opts.providers?.system ?? new SystemTts(),
|
|
1243
|
+
edge: opts.providers?.edge ?? new EdgeTts(),
|
|
1244
|
+
openai: opts.providers?.openai ?? new OpenAiTts(),
|
|
1245
|
+
elevenlabs: opts.providers?.elevenlabs ?? new ElevenLabsTts(),
|
|
1246
|
+
xai: opts.providers?.xai ?? new XaiTts()
|
|
1247
|
+
};
|
|
1248
|
+
let publicUrl = "";
|
|
1249
|
+
const audio = /* @__PURE__ */ new Map();
|
|
1250
|
+
const clients = /* @__PURE__ */ new Map();
|
|
1251
|
+
const broadcast = (msg) => {
|
|
1252
|
+
const data = JSON.stringify(msg);
|
|
1253
|
+
for (const ws of clients.keys()) if (ws.readyState === ws.OPEN) ws.send(data);
|
|
1254
|
+
};
|
|
1255
|
+
const queue = new SpeechQueue({
|
|
1256
|
+
clientCount: () => clients.size,
|
|
1257
|
+
speechRate: () => store.get().voice.rate,
|
|
1258
|
+
broadcast,
|
|
1259
|
+
render: async (id, text2, signal) => {
|
|
1260
|
+
const voice = store.get().voice;
|
|
1261
|
+
if (voice.provider === "browser") return null;
|
|
1262
|
+
const { data, mime } = await providers[voice.provider].synthesize(text2, voice, signal);
|
|
1263
|
+
const now = Date.now();
|
|
1264
|
+
for (const [k, v] of audio) if (v.expires < now) audio.delete(k);
|
|
1265
|
+
audio.set(id, { data, mime, expires: now + AUDIO_TTL_MS });
|
|
1266
|
+
return { url: `/api/audio/${id}`, mime };
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
const server = createServer((req, res) => {
|
|
1270
|
+
handle(req, res).catch((err) => {
|
|
1271
|
+
const status = err.status;
|
|
1272
|
+
if (!status) log(`error: ${err.stack ?? String(err)}`);
|
|
1273
|
+
if (!res.headersSent) sendJson(res, status ?? 500, { error: status ? err.message : "Internal error" });
|
|
1274
|
+
else res.end();
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
async function handle(req, res) {
|
|
1278
|
+
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
1279
|
+
const path = url2.pathname;
|
|
1280
|
+
const method = req.method ?? "GET";
|
|
1281
|
+
if (!originAllowed(req)) return sendJson(res, 403, { error: "Cross-origin requests are not allowed" });
|
|
1282
|
+
if (path === "/api/health" && method === "GET") {
|
|
1283
|
+
const body = { ok: true, name: "agentar", version: VERSION, clients: clients.size };
|
|
1284
|
+
return sendJson(res, 200, body);
|
|
1285
|
+
}
|
|
1286
|
+
if (path === "/api/info" && method === "GET") {
|
|
1287
|
+
return sendJson(res, 200, { version: VERSION, url: publicUrl, cliPath });
|
|
1288
|
+
}
|
|
1289
|
+
if (path === "/api/config") {
|
|
1290
|
+
if (method === "GET") return sendJson(res, 200, store.get());
|
|
1291
|
+
if (method === "PUT" || method === "PATCH") {
|
|
1292
|
+
const patch = await readJson(req);
|
|
1293
|
+
const config = await store.update(patch);
|
|
1294
|
+
broadcast({ type: "config", config });
|
|
1295
|
+
return sendJson(res, 200, config);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (path === "/api/say" && method === "POST") {
|
|
1299
|
+
const body = await readSayBody(req);
|
|
1300
|
+
if (!body) return sendJson(res, 400, { error: "Provide non-empty `text` (max 5000 chars)." });
|
|
1301
|
+
const result = await queue.say(body);
|
|
1302
|
+
const status = result.status === "error" ? 502 : result.status === "no-clients" ? 409 : 200;
|
|
1303
|
+
return sendJson(res, status, result);
|
|
1304
|
+
}
|
|
1305
|
+
if (path === "/api/stop" && method === "POST") {
|
|
1306
|
+
queue.stop();
|
|
1307
|
+
return sendJson(res, 200, { ok: true });
|
|
1308
|
+
}
|
|
1309
|
+
if (path === "/api/mood" && method === "POST") {
|
|
1310
|
+
const body = await readJson(req);
|
|
1311
|
+
if (!isMood(body.mood)) return sendJson(res, 400, { error: "Unknown mood" });
|
|
1312
|
+
if (body.persist) {
|
|
1313
|
+
broadcast({ type: "config", config: await store.update({ behavior: { mood: body.mood } }) });
|
|
1314
|
+
} else {
|
|
1315
|
+
broadcast({ type: "mood", mood: body.mood });
|
|
1316
|
+
}
|
|
1317
|
+
return sendJson(res, 200, { ok: true });
|
|
1318
|
+
}
|
|
1319
|
+
if (path === "/api/gesture" && method === "POST") {
|
|
1320
|
+
const body = await readJson(req);
|
|
1321
|
+
if (typeof body.gesture !== "string" || !GESTURES.includes(body.gesture)) {
|
|
1322
|
+
return sendJson(res, 400, { error: `gesture must be one of ${GESTURES.join(", ")}` });
|
|
1323
|
+
}
|
|
1324
|
+
broadcast({ type: "gesture", gesture: body.gesture });
|
|
1325
|
+
return sendJson(res, 200, { ok: true });
|
|
1326
|
+
}
|
|
1327
|
+
if (path === "/api/voices" && method === "GET") {
|
|
1328
|
+
const p = url2.searchParams.get("provider") ?? store.get().voice.provider;
|
|
1329
|
+
if (!VOICE_PROVIDERS.includes(p)) return sendJson(res, 400, { error: "Unknown provider" });
|
|
1330
|
+
if (p === "browser") return sendJson(res, 200, { provider: p, available: true, voices: [] });
|
|
1331
|
+
const provider = providers[p];
|
|
1332
|
+
const reason = await provider.unavailableReason();
|
|
1333
|
+
if (reason) return sendJson(res, 200, { provider: p, available: false, reason, voices: [] });
|
|
1334
|
+
try {
|
|
1335
|
+
return sendJson(res, 200, { provider: p, available: true, voices: await provider.listVoices() });
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
return sendJson(res, 200, { provider: p, available: false, reason: err.message, voices: [] });
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const audioMatch = /^\/api\/audio\/([\w-]+)$/.exec(path);
|
|
1341
|
+
if (audioMatch && method === "GET") {
|
|
1342
|
+
const clip = audio.get(audioMatch[1]);
|
|
1343
|
+
if (!clip) return sendJson(res, 404, { error: "Audio expired" });
|
|
1344
|
+
res.writeHead(200, { "Content-Type": clip.mime, "Content-Length": clip.data.length, "Cache-Control": "no-store" });
|
|
1345
|
+
res.end(clip.data);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (path === "/api/models" && method === "GET") {
|
|
1349
|
+
const builtin = await Promise.all(
|
|
1350
|
+
BUILTIN_MODELS.map(async (m) => ({
|
|
1351
|
+
id: m.id,
|
|
1352
|
+
label: m.label,
|
|
1353
|
+
license: m.license,
|
|
1354
|
+
url: `/models/${m.file}`,
|
|
1355
|
+
available: await exists(join6(modelsDir, m.file))
|
|
1356
|
+
}))
|
|
1357
|
+
);
|
|
1358
|
+
let user = [];
|
|
1359
|
+
try {
|
|
1360
|
+
user = (await readdir(userModelsDir)).filter((f) => /\.(glb|vrm)$/i.test(f)).map((f) => ({ name: f, url: `/models/user/${encodeURIComponent(f)}` }));
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
return sendJson(res, 200, { builtin, user });
|
|
1364
|
+
}
|
|
1365
|
+
if (path === "/api/models" && method === "POST") {
|
|
1366
|
+
const name = sanitizeFileName(url2.searchParams.get("name") ?? "");
|
|
1367
|
+
if (!name) return sendJson(res, 400, { error: "Pass ?name=<file>.glb or .vrm" });
|
|
1368
|
+
const data = await readBody(req, MAX_MODEL);
|
|
1369
|
+
if (data.subarray(0, 4).toString("latin1") !== "glTF") return sendJson(res, 400, { error: "Not a binary glTF (.glb/.vrm) file" });
|
|
1370
|
+
await mkdir4(userModelsDir, { recursive: true });
|
|
1371
|
+
await writeFile5(join6(userModelsDir, name), data);
|
|
1372
|
+
log(`saved custom model ${name} (${(data.length / 1e6).toFixed(1)} MB)`);
|
|
1373
|
+
return sendJson(res, 201, { name, url: `/models/user/${encodeURIComponent(name)}` });
|
|
1374
|
+
}
|
|
1375
|
+
if (path.startsWith("/models/user/")) {
|
|
1376
|
+
await serveFile(res, userModelsDir, decodeURIComponent(path.slice("/models/user/".length)));
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (path.startsWith("/models/")) {
|
|
1380
|
+
await serveFile(res, modelsDir, decodeURIComponent(path.slice("/models/".length)));
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (path.startsWith("/api/")) return sendJson(res, 404, { error: "Not found" });
|
|
1384
|
+
if (method === "GET") {
|
|
1385
|
+
const rel = path === "/" ? "index.html" : decodeURIComponent(path.slice(1));
|
|
1386
|
+
if (await serveFile(res, webDir, rel, true)) return;
|
|
1387
|
+
if (await serveFile(res, webDir, "index.html", true)) return;
|
|
1388
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1389
|
+
res.end(
|
|
1390
|
+
`<!doctype html><title>agentar</title><body style="font-family:system-ui;background:#111;color:#eee;padding:2rem"><h1>agentar bridge is running</h1><p>The web UI is not built yet. Run <code>npm run build</code>, or use <code>npm run dev</code> and open <a style="color:#8cf" href="http://localhost:5173">http://localhost:5173</a>.</p>`
|
|
1391
|
+
);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
sendJson(res, 405, { error: "Method not allowed" });
|
|
1395
|
+
}
|
|
1396
|
+
async function serveFile(res, root, rel, quiet = false) {
|
|
1397
|
+
const full = normalize(join6(root, rel));
|
|
1398
|
+
if (!full.startsWith(normalize(root) + sep)) {
|
|
1399
|
+
if (!quiet) sendJson(res, 403, { error: "Forbidden" });
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
let info;
|
|
1403
|
+
try {
|
|
1404
|
+
info = await stat2(full);
|
|
1405
|
+
} catch {
|
|
1406
|
+
if (!quiet) {
|
|
1407
|
+
sendJson(res, 404, {
|
|
1408
|
+
error: rel.endsWith(".glb") ? "Model not found. Run `agentar fetch-models` to download the built-in avatars." : "Not found"
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
if (!info.isFile()) {
|
|
1414
|
+
if (!quiet) sendJson(res, 404, { error: "Not found" });
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
const type = MIME[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
1418
|
+
const immutable = full.includes(`${sep}assets${sep}`) && full.startsWith(normalize(webDir));
|
|
1419
|
+
res.writeHead(200, {
|
|
1420
|
+
"Content-Type": type,
|
|
1421
|
+
"Content-Length": info.size,
|
|
1422
|
+
"Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache"
|
|
1423
|
+
});
|
|
1424
|
+
createReadStream(full).pipe(res);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
|
|
1428
|
+
server.on("upgrade", (req, socket, head) => {
|
|
1429
|
+
const { pathname } = new URL(req.url ?? "/", "http://localhost");
|
|
1430
|
+
if (pathname !== "/ws" || !originAllowed(req)) {
|
|
1431
|
+
socket.destroy();
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
1435
|
+
});
|
|
1436
|
+
wss.on("connection", (ws) => {
|
|
1437
|
+
const clientId = randomUUID2();
|
|
1438
|
+
clients.set(ws, { id: clientId });
|
|
1439
|
+
log(`avatar connected (${clients.size} total)`);
|
|
1440
|
+
ws.send(JSON.stringify({ type: "hello", protocol: PROTOCOL_VERSION, config: store.get(), clientId }));
|
|
1441
|
+
ws.on("message", (raw) => {
|
|
1442
|
+
let msg;
|
|
1443
|
+
try {
|
|
1444
|
+
msg = JSON.parse(String(raw));
|
|
1445
|
+
} catch {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (typeof msg !== "object" || msg === null) return;
|
|
1449
|
+
if (msg.type === "speech-end" && typeof msg.id === "string") {
|
|
1450
|
+
queue.speechEnded(msg.id, Boolean(msg.interrupted), typeof msg.error === "string" ? msg.error : void 0);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
ws.on("close", () => {
|
|
1454
|
+
clients.delete(ws);
|
|
1455
|
+
log(`avatar disconnected (${clients.size} left)`);
|
|
1456
|
+
});
|
|
1457
|
+
});
|
|
1458
|
+
await new Promise((resolvePromise, reject) => {
|
|
1459
|
+
server.once("error", reject);
|
|
1460
|
+
server.listen(port, host, () => {
|
|
1461
|
+
server.off("error", reject);
|
|
1462
|
+
resolvePromise();
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
const actualPort = server.address().port;
|
|
1466
|
+
const url = publicUrl = `http://${host === "0.0.0.0" || host === "127.0.0.1" ? "localhost" : host}:${actualPort}`;
|
|
1467
|
+
log(`bridge listening on ${url}`);
|
|
1468
|
+
return {
|
|
1469
|
+
server,
|
|
1470
|
+
url,
|
|
1471
|
+
port: actualPort,
|
|
1472
|
+
store,
|
|
1473
|
+
queue,
|
|
1474
|
+
close: () => new Promise((r) => {
|
|
1475
|
+
queue.stop();
|
|
1476
|
+
for (const ws of clients.keys()) ws.terminate();
|
|
1477
|
+
wss.close();
|
|
1478
|
+
server.close(() => r());
|
|
1479
|
+
})
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
function originAllowed(req) {
|
|
1483
|
+
const origin = req.headers.origin;
|
|
1484
|
+
if (!origin || origin === "null") return !origin;
|
|
1485
|
+
try {
|
|
1486
|
+
const { hostname } = new URL(origin);
|
|
1487
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
1488
|
+
} catch {
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
function sendJson(res, status, body) {
|
|
1493
|
+
const data = JSON.stringify(body);
|
|
1494
|
+
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) });
|
|
1495
|
+
res.end(data);
|
|
1496
|
+
}
|
|
1497
|
+
async function readBody(req, limit) {
|
|
1498
|
+
const chunks = [];
|
|
1499
|
+
let size = 0;
|
|
1500
|
+
for await (const chunk of req) {
|
|
1501
|
+
size += chunk.length;
|
|
1502
|
+
if (size > limit) throw Object.assign(new Error("Body too large"), { status: 413 });
|
|
1503
|
+
chunks.push(chunk);
|
|
1504
|
+
}
|
|
1505
|
+
return Buffer.concat(chunks);
|
|
1506
|
+
}
|
|
1507
|
+
async function readJson(req) {
|
|
1508
|
+
const raw = (await readBody(req, MAX_JSON)).toString("utf8");
|
|
1509
|
+
if (!raw.trim()) return {};
|
|
1510
|
+
try {
|
|
1511
|
+
return JSON.parse(raw);
|
|
1512
|
+
} catch {
|
|
1513
|
+
return {};
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async function readSayBody(req) {
|
|
1517
|
+
const type = req.headers["content-type"] ?? "";
|
|
1518
|
+
let body;
|
|
1519
|
+
if (type.startsWith("text/plain")) {
|
|
1520
|
+
body = { text: (await readBody(req, MAX_JSON)).toString("utf8") };
|
|
1521
|
+
} else {
|
|
1522
|
+
const parsed = await readJson(req);
|
|
1523
|
+
body = typeof parsed === "object" && parsed ? parsed : {};
|
|
1524
|
+
}
|
|
1525
|
+
const text2 = typeof body.text === "string" ? body.text.trim() : "";
|
|
1526
|
+
if (!text2 || text2.length > MAX_TEXT) return null;
|
|
1527
|
+
const out = { text: text2 };
|
|
1528
|
+
if (isMood(body.mood)) out.mood = body.mood;
|
|
1529
|
+
if (typeof body.wait === "boolean") out.wait = body.wait;
|
|
1530
|
+
if (typeof body.interrupt === "boolean") out.interrupt = body.interrupt;
|
|
1531
|
+
return out;
|
|
1532
|
+
}
|
|
1533
|
+
function sanitizeFileName(name) {
|
|
1534
|
+
const base = name.split(/[\\/]/).pop() ?? "";
|
|
1535
|
+
const clean = base.replace(/[^\w.\- ]/g, "_").slice(0, 100);
|
|
1536
|
+
return /\.(glb|vrm)$/i.test(clean) && !clean.startsWith(".") ? clean : null;
|
|
1537
|
+
}
|
|
1538
|
+
async function exists(file) {
|
|
1539
|
+
try {
|
|
1540
|
+
return (await stat2(file)).isFile();
|
|
1541
|
+
} catch {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
var VERSION, MIME, MAX_JSON, MAX_MODEL, MAX_TEXT, AUDIO_TTL_MS;
|
|
1546
|
+
var init_server = __esm({
|
|
1547
|
+
"apps/bridge/src/server.ts"() {
|
|
1548
|
+
"use strict";
|
|
1549
|
+
init_src();
|
|
1550
|
+
init_models();
|
|
1551
|
+
init_speech_queue();
|
|
1552
|
+
init_store();
|
|
1553
|
+
init_cloud();
|
|
1554
|
+
init_edge();
|
|
1555
|
+
init_system();
|
|
1556
|
+
VERSION = "0.1.0";
|
|
1557
|
+
MIME = {
|
|
1558
|
+
".html": "text/html; charset=utf-8",
|
|
1559
|
+
".js": "text/javascript; charset=utf-8",
|
|
1560
|
+
".css": "text/css; charset=utf-8",
|
|
1561
|
+
".json": "application/json",
|
|
1562
|
+
".svg": "image/svg+xml",
|
|
1563
|
+
".png": "image/png",
|
|
1564
|
+
".jpg": "image/jpeg",
|
|
1565
|
+
".ico": "image/x-icon",
|
|
1566
|
+
".glb": "model/gltf-binary",
|
|
1567
|
+
".vrm": "model/gltf-binary",
|
|
1568
|
+
".wasm": "application/wasm",
|
|
1569
|
+
".wav": "audio/wav",
|
|
1570
|
+
".mp3": "audio/mpeg"
|
|
1571
|
+
};
|
|
1572
|
+
MAX_JSON = 256 * 1024;
|
|
1573
|
+
MAX_MODEL = 150 * 1024 * 1024;
|
|
1574
|
+
MAX_TEXT = 5e3;
|
|
1575
|
+
AUDIO_TTL_MS = 10 * 60 * 1e3;
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
// apps/bridge/src/index.ts
|
|
1580
|
+
var src_exports = {};
|
|
1581
|
+
__export(src_exports, {
|
|
1582
|
+
ConfigStore: () => ConfigStore,
|
|
1583
|
+
EdgeTts: () => EdgeTts,
|
|
1584
|
+
ElevenLabsTts: () => ElevenLabsTts,
|
|
1585
|
+
OpenAiTts: () => OpenAiTts,
|
|
1586
|
+
SpeechQueue: () => SpeechQueue,
|
|
1587
|
+
SystemTts: () => SystemTts,
|
|
1588
|
+
VERSION: () => VERSION,
|
|
1589
|
+
XaiTts: () => XaiTts,
|
|
1590
|
+
agentarHome: () => agentarHome,
|
|
1591
|
+
defaultModelsDir: () => defaultModelsDir,
|
|
1592
|
+
defaultWebDir: () => defaultWebDir,
|
|
1593
|
+
edgeRate: () => edgeRate,
|
|
1594
|
+
fetchModels: () => fetchModels,
|
|
1595
|
+
parseEdgeVoices: () => parseEdgeVoices,
|
|
1596
|
+
parseSayVoices: () => parseSayVoices,
|
|
1597
|
+
startBridge: () => startBridge
|
|
1598
|
+
});
|
|
1599
|
+
var init_src2 = __esm({
|
|
1600
|
+
"apps/bridge/src/index.ts"() {
|
|
1601
|
+
"use strict";
|
|
1602
|
+
init_server();
|
|
1603
|
+
init_speech_queue();
|
|
1604
|
+
init_models();
|
|
1605
|
+
init_store();
|
|
1606
|
+
init_system();
|
|
1607
|
+
init_cloud();
|
|
1608
|
+
init_edge();
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// packages/mcp/src/index.ts
|
|
1613
|
+
var src_exports2 = {};
|
|
1614
|
+
__export(src_exports2, {
|
|
1615
|
+
bridgeUrl: () => bridgeUrl,
|
|
1616
|
+
createMcpServer: () => createMcpServer,
|
|
1617
|
+
runMcpServer: () => runMcpServer
|
|
1618
|
+
});
|
|
1619
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1620
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1621
|
+
import { z } from "zod";
|
|
1622
|
+
function bridgeUrl() {
|
|
1623
|
+
return process.env.AGENTAR_URL ?? `http://localhost:${process.env.AGENTAR_PORT ?? DEFAULT_PORT}`;
|
|
1624
|
+
}
|
|
1625
|
+
async function post(path, body) {
|
|
1626
|
+
try {
|
|
1627
|
+
const res = await fetch(`${bridgeUrl()}${path}`, {
|
|
1628
|
+
method: "POST",
|
|
1629
|
+
headers: { "Content-Type": "application/json" },
|
|
1630
|
+
body: JSON.stringify(body)
|
|
1631
|
+
});
|
|
1632
|
+
return { status: res.status, json: await res.json() };
|
|
1633
|
+
} catch {
|
|
1634
|
+
throw new Error(`agentar bridge is not running at ${bridgeUrl()}. Start it with \`agentar start\`.`);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function createMcpServer() {
|
|
1638
|
+
const server = new McpServer(
|
|
1639
|
+
{ name: "agentar", version: "0.1.0" },
|
|
1640
|
+
{
|
|
1641
|
+
instructions: "agentar gives you a 3D talking body that the user can see and hear. Use `speak` for short, conversational spoken summaries (one to three sentences) \u2014 not for code, file paths or long lists, which belong in the chat. Use `set_mood` and `gesture` sparingly to match the tone."
|
|
1642
|
+
}
|
|
1643
|
+
);
|
|
1644
|
+
server.registerTool(
|
|
1645
|
+
"speak",
|
|
1646
|
+
{
|
|
1647
|
+
title: "Speak aloud",
|
|
1648
|
+
description: "Say something out loud through the user's agentar avatar (text-to-speech with a lip-synced 3D face). Keep it short and conversational; markdown is stripped automatically.",
|
|
1649
|
+
inputSchema: {
|
|
1650
|
+
text: z.string().min(1).max(5e3).describe("What to say, in plain conversational language."),
|
|
1651
|
+
mood: z.enum(MOODS).optional().describe("Facial expression while speaking."),
|
|
1652
|
+
wait: z.boolean().optional().describe("Wait until the avatar finishes speaking before returning. Default false.")
|
|
1653
|
+
}
|
|
1654
|
+
},
|
|
1655
|
+
async ({ text: raw, mood, wait }) => {
|
|
1656
|
+
const spoken = toSpeakable(raw, { maxChars: 1200 });
|
|
1657
|
+
const { json } = await post("/api/say", { text: spoken, mood, wait: wait ?? false });
|
|
1658
|
+
const res = json;
|
|
1659
|
+
if (res.status === "no-clients") return { ...text(`Not spoken: ${res.error}`), isError: true };
|
|
1660
|
+
if (res.status === "error") return { ...text(`Speech failed: ${res.error}`), isError: true };
|
|
1661
|
+
return text(res.status === "spoken" ? "Spoken." : "Speaking.");
|
|
1662
|
+
}
|
|
1663
|
+
);
|
|
1664
|
+
server.registerTool(
|
|
1665
|
+
"set_mood",
|
|
1666
|
+
{
|
|
1667
|
+
title: "Set facial mood",
|
|
1668
|
+
description: "Change the avatar's resting facial expression.",
|
|
1669
|
+
inputSchema: { mood: z.enum(MOODS) }
|
|
1670
|
+
},
|
|
1671
|
+
async ({ mood }) => {
|
|
1672
|
+
await post("/api/mood", { mood });
|
|
1673
|
+
return text(`Mood set to ${mood}.`);
|
|
1674
|
+
}
|
|
1675
|
+
);
|
|
1676
|
+
server.registerTool(
|
|
1677
|
+
"gesture",
|
|
1678
|
+
{
|
|
1679
|
+
title: "Gesture",
|
|
1680
|
+
description: "Play a short gesture. Head: nod (yes/agree), shake (no), tilt (curious). Hands: wave (hello/bye), high-five (celebrate a win), thumbs-up (approve), thumbs-down (disapprove), idea (one finger up), shrug (not sure), namaste (thanks). Face and body: wink, laugh, surprised, bow.",
|
|
1681
|
+
inputSchema: { gesture: z.enum(GESTURES) }
|
|
1682
|
+
},
|
|
1683
|
+
async ({ gesture }) => {
|
|
1684
|
+
await post("/api/gesture", { gesture });
|
|
1685
|
+
return text(`Played ${gesture}.`);
|
|
1686
|
+
}
|
|
1687
|
+
);
|
|
1688
|
+
server.registerTool(
|
|
1689
|
+
"stop_speaking",
|
|
1690
|
+
{ title: "Stop speaking", description: "Stop the avatar mid-sentence and clear queued speech.", inputSchema: {} },
|
|
1691
|
+
async () => {
|
|
1692
|
+
await post("/api/stop", {});
|
|
1693
|
+
return text("Stopped.");
|
|
1694
|
+
}
|
|
1695
|
+
);
|
|
1696
|
+
return server;
|
|
1697
|
+
}
|
|
1698
|
+
async function runMcpServer() {
|
|
1699
|
+
await createMcpServer().connect(new StdioServerTransport());
|
|
1700
|
+
}
|
|
1701
|
+
var text;
|
|
1702
|
+
var init_src3 = __esm({
|
|
1703
|
+
"packages/mcp/src/index.ts"() {
|
|
1704
|
+
"use strict";
|
|
1705
|
+
init_src();
|
|
1706
|
+
text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
// packages/cli/src/index.ts
|
|
1711
|
+
init_src();
|
|
1712
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1713
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1714
|
+
|
|
1715
|
+
// packages/cli/src/hooks.ts
|
|
1716
|
+
import { readFile } from "node:fs/promises";
|
|
1717
|
+
async function claudeCodeReply(stdinJson) {
|
|
1718
|
+
let input;
|
|
1719
|
+
try {
|
|
1720
|
+
input = JSON.parse(stdinJson);
|
|
1721
|
+
} catch {
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
if (input.stop_hook_active === true) return null;
|
|
1725
|
+
if (typeof input.last_assistant_message === "string" && input.last_assistant_message.trim()) {
|
|
1726
|
+
return input.last_assistant_message;
|
|
1727
|
+
}
|
|
1728
|
+
if (typeof input.transcript_path !== "string") return null;
|
|
1729
|
+
try {
|
|
1730
|
+
return lastAssistantText(await readFile(input.transcript_path, "utf8"));
|
|
1731
|
+
} catch {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function lastAssistantText(jsonl) {
|
|
1736
|
+
const lines = jsonl.split("\n");
|
|
1737
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1738
|
+
const line = lines[i].trim();
|
|
1739
|
+
if (!line) continue;
|
|
1740
|
+
let entry;
|
|
1741
|
+
try {
|
|
1742
|
+
entry = JSON.parse(line);
|
|
1743
|
+
} catch {
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
if (entry.type !== "assistant" && entry.message?.role !== "assistant") continue;
|
|
1747
|
+
const content = entry.message?.content;
|
|
1748
|
+
const text2 = typeof content === "string" ? content : Array.isArray(content) ? content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n") : "";
|
|
1749
|
+
if (text2.trim()) return text2;
|
|
1750
|
+
}
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
function codexReply(arg) {
|
|
1754
|
+
if (!arg) return null;
|
|
1755
|
+
try {
|
|
1756
|
+
const payload = JSON.parse(arg);
|
|
1757
|
+
if (payload.type !== "agent-turn-complete") return null;
|
|
1758
|
+
const msg = payload["last-assistant-message"];
|
|
1759
|
+
return typeof msg === "string" && msg.trim() ? msg : null;
|
|
1760
|
+
} catch {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// packages/cli/src/install.ts
|
|
1766
|
+
import { copyFile, mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
1767
|
+
import { homedir } from "node:os";
|
|
1768
|
+
import { dirname, join } from "node:path";
|
|
1769
|
+
async function installClaudeCode(cliPath, file = join(homedir(), ".claude", "settings.json")) {
|
|
1770
|
+
const command = `node ${JSON.stringify(cliPath)} hook claude-code`;
|
|
1771
|
+
let settings = {};
|
|
1772
|
+
try {
|
|
1773
|
+
settings = JSON.parse(await readFile2(file, "utf8"));
|
|
1774
|
+
await copyFile(file, `${file}.bak`);
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
if (err.code !== "ENOENT") throw new Error(`Could not read ${file}: ${err.message}`);
|
|
1777
|
+
}
|
|
1778
|
+
const hooks = settings.hooks ??= {};
|
|
1779
|
+
const stop = hooks.Stop ??= [];
|
|
1780
|
+
if (JSON.stringify(stop).includes("hook claude-code")) return `Already installed in ${file}`;
|
|
1781
|
+
stop.push({ hooks: [{ type: "command", command }] });
|
|
1782
|
+
await mkdir(dirname(file), { recursive: true });
|
|
1783
|
+
await writeFile(file, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1784
|
+
return `Added a Stop hook to ${file}`;
|
|
1785
|
+
}
|
|
1786
|
+
async function installCodex(cliPath, file = join(homedir(), ".codex", "config.toml")) {
|
|
1787
|
+
let toml = "";
|
|
1788
|
+
try {
|
|
1789
|
+
toml = await readFile2(file, "utf8");
|
|
1790
|
+
await copyFile(file, `${file}.bak`);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
if (err.code !== "ENOENT") throw err;
|
|
1793
|
+
}
|
|
1794
|
+
const notes = [];
|
|
1795
|
+
const q = (s) => JSON.stringify(s);
|
|
1796
|
+
if (/^\s*notify\s*=/m.test(toml)) {
|
|
1797
|
+
notes.push(`${file} already has a notify setting. Add agentar manually: notify = ["node", ${q(cliPath)}, "hook", "codex"]`);
|
|
1798
|
+
} else {
|
|
1799
|
+
toml = `notify = ["node", ${q(cliPath)}, "hook", "codex"]
|
|
1800
|
+
` + toml;
|
|
1801
|
+
notes.push("Added notify hook (speaks each finished turn).");
|
|
1802
|
+
}
|
|
1803
|
+
if (/^\s*\[mcp_servers\.agentar\]/m.test(toml)) {
|
|
1804
|
+
notes.push("MCP server already configured.");
|
|
1805
|
+
} else {
|
|
1806
|
+
toml += `${toml.endsWith("\n") || !toml ? "" : "\n"}
|
|
1807
|
+
[mcp_servers.agentar]
|
|
1808
|
+
command = "node"
|
|
1809
|
+
args = [${q(cliPath)}, "mcp"]
|
|
1810
|
+
`;
|
|
1811
|
+
notes.push("Added [mcp_servers.agentar] (speak / set_mood / gesture tools).");
|
|
1812
|
+
}
|
|
1813
|
+
await mkdir(dirname(file), { recursive: true });
|
|
1814
|
+
await writeFile(file, toml, "utf8");
|
|
1815
|
+
return notes;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
// packages/cli/src/index.ts
|
|
1819
|
+
var CLI_PATH = fileURLToPath2(import.meta.url);
|
|
1820
|
+
var BRIDGE = process.env.AGENTAR_URL ?? `http://localhost:${process.env.AGENTAR_PORT ?? DEFAULT_PORT}`;
|
|
1821
|
+
var HELP = `agentar (agent + avatar) \u2014 give your AI agent a body
|
|
1822
|
+
|
|
1823
|
+
Usage:
|
|
1824
|
+
agentar start [--port N] [--no-open] Start the bridge and open the avatar in your browser
|
|
1825
|
+
agentar say <text> [--mood M] [--wait] Make the avatar speak
|
|
1826
|
+
agentar stop Stop speaking
|
|
1827
|
+
agentar fetch-models [--all] Download the built-in avatars (--all adds non-commercial samples)
|
|
1828
|
+
agentar mcp Run the MCP server (stdio) for Claude Code / Codex
|
|
1829
|
+
agentar hook claude-code Claude Code Stop hook (reads JSON on stdin)
|
|
1830
|
+
agentar hook codex <json> Codex notify hook
|
|
1831
|
+
agentar install claude-code Speak every Claude Code reply (adds a Stop hook)
|
|
1832
|
+
agentar install codex Configure Codex notify + MCP server
|
|
1833
|
+
|
|
1834
|
+
Environment:
|
|
1835
|
+
AGENTAR_PORT (default ${DEFAULT_PORT}), AGENTAR_URL, AGENTAR_HOME (default ~/.agentar),
|
|
1836
|
+
OPENAI_API_KEY, ELEVENLABS_API_KEY, XAI_API_KEY for cloud voices,
|
|
1837
|
+
AGENTAR_EDGE_TTS (path to edge-tts, if it is not on PATH).`;
|
|
1838
|
+
async function main(argv) {
|
|
1839
|
+
const [cmd, ...rest] = argv;
|
|
1840
|
+
switch (cmd) {
|
|
1841
|
+
case "start":
|
|
1842
|
+
return start(rest);
|
|
1843
|
+
case "say":
|
|
1844
|
+
return say(rest);
|
|
1845
|
+
case "stop":
|
|
1846
|
+
await fetch(`${BRIDGE}/api/stop`, { method: "POST" });
|
|
1847
|
+
return 0;
|
|
1848
|
+
case "fetch-models": {
|
|
1849
|
+
const { defaultModelsDir: defaultModelsDir2, fetchModels: fetchModels2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
|
|
1850
|
+
return await fetchModels2(process.env.AGENTAR_MODELS_DIR ?? defaultModelsDir2(), { all: rest.includes("--all") }) ? 0 : 1;
|
|
1851
|
+
}
|
|
1852
|
+
case "mcp": {
|
|
1853
|
+
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_src3(), src_exports2));
|
|
1854
|
+
await runMcpServer2();
|
|
1855
|
+
return -1;
|
|
1856
|
+
}
|
|
1857
|
+
case "hook":
|
|
1858
|
+
return hook(rest);
|
|
1859
|
+
case "install":
|
|
1860
|
+
return install(rest);
|
|
1861
|
+
case void 0:
|
|
1862
|
+
case "help":
|
|
1863
|
+
case "--help":
|
|
1864
|
+
case "-h":
|
|
1865
|
+
console.log(HELP);
|
|
1866
|
+
return 0;
|
|
1867
|
+
default:
|
|
1868
|
+
console.error(`Unknown command: ${cmd}
|
|
1869
|
+
|
|
1870
|
+
${HELP}`);
|
|
1871
|
+
return 1;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
async function start(args) {
|
|
1875
|
+
const portIdx = args.indexOf("--port");
|
|
1876
|
+
const port = portIdx >= 0 ? Number(args[portIdx + 1]) : void 0;
|
|
1877
|
+
const { defaultModelsDir: defaultModelsDir2, fetchModels: fetchModels2, startBridge: startBridge2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
|
|
1878
|
+
const modelsDir = process.env.AGENTAR_MODELS_DIR ?? defaultModelsDir2();
|
|
1879
|
+
await fetchModels2(modelsDir, { quiet: true });
|
|
1880
|
+
const bridge = await startBridge2({ ...port ? { port } : {}, modelsDir, cliPath: CLI_PATH });
|
|
1881
|
+
console.log(`
|
|
1882
|
+
agentar is running \u2192 ${bridge.url}
|
|
1883
|
+
OBS / virtual camera view \u2192 ${bridge.url}/?stage=1
|
|
1884
|
+
`);
|
|
1885
|
+
if (!args.includes("--no-open")) openBrowser(bridge.url);
|
|
1886
|
+
const shutdown = () => void bridge.close().then(() => process.exit(0));
|
|
1887
|
+
process.on("SIGINT", shutdown);
|
|
1888
|
+
process.on("SIGTERM", shutdown);
|
|
1889
|
+
return -1;
|
|
1890
|
+
}
|
|
1891
|
+
async function say(args) {
|
|
1892
|
+
const words = [];
|
|
1893
|
+
let mood;
|
|
1894
|
+
let wait = false;
|
|
1895
|
+
for (let i = 0; i < args.length; i++) {
|
|
1896
|
+
if (args[i] === "--mood") mood = args[++i];
|
|
1897
|
+
else if (args[i] === "--wait") wait = true;
|
|
1898
|
+
else words.push(args[i]);
|
|
1899
|
+
}
|
|
1900
|
+
const text2 = words.join(" ").trim();
|
|
1901
|
+
if (!text2) {
|
|
1902
|
+
console.error("Usage: agentar say <text> [--mood happy] [--wait]");
|
|
1903
|
+
return 1;
|
|
1904
|
+
}
|
|
1905
|
+
const res = await speak(text2, isMood(mood) ? mood : void 0, wait);
|
|
1906
|
+
if (!res.ok) console.error(res.error);
|
|
1907
|
+
return res.ok ? 0 : 1;
|
|
1908
|
+
}
|
|
1909
|
+
async function speak(text2, mood, wait = false) {
|
|
1910
|
+
try {
|
|
1911
|
+
const res = await fetch(`${BRIDGE}/api/say`, {
|
|
1912
|
+
method: "POST",
|
|
1913
|
+
headers: { "Content-Type": "application/json" },
|
|
1914
|
+
body: JSON.stringify({ text: text2, mood, wait })
|
|
1915
|
+
});
|
|
1916
|
+
const body = await res.json();
|
|
1917
|
+
return res.ok ? { ok: true } : { ok: false, error: body.error ?? body.status };
|
|
1918
|
+
} catch {
|
|
1919
|
+
return { ok: false, error: `agentar is not running at ${BRIDGE}. Start it with: agentar start` };
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
async function hook(args) {
|
|
1923
|
+
const [kind, payload] = args;
|
|
1924
|
+
let reply = null;
|
|
1925
|
+
if (kind === "claude-code") reply = await claudeCodeReply(await readStdin());
|
|
1926
|
+
else if (kind === "codex") reply = codexReply(payload ?? args[args.length - 1]);
|
|
1927
|
+
if (reply) await speak(toSpeakable(reply));
|
|
1928
|
+
return 0;
|
|
1929
|
+
}
|
|
1930
|
+
async function install(args) {
|
|
1931
|
+
const [target] = args;
|
|
1932
|
+
if (target === "claude-code") {
|
|
1933
|
+
console.log(await installClaudeCode(CLI_PATH));
|
|
1934
|
+
console.log(`
|
|
1935
|
+
For tools Claude can call itself (speak, set_mood, gesture), also run:
|
|
1936
|
+
claude mcp add agentar -- node ${JSON.stringify(CLI_PATH)} mcp`);
|
|
1937
|
+
return 0;
|
|
1938
|
+
}
|
|
1939
|
+
if (target === "codex") {
|
|
1940
|
+
for (const note of await installCodex(CLI_PATH)) console.log(note);
|
|
1941
|
+
return 0;
|
|
1942
|
+
}
|
|
1943
|
+
console.error("Usage: agentar install <claude-code|codex>");
|
|
1944
|
+
return 1;
|
|
1945
|
+
}
|
|
1946
|
+
function readStdin() {
|
|
1947
|
+
if (process.stdin.isTTY) return Promise.resolve("");
|
|
1948
|
+
return new Promise((resolve2) => {
|
|
1949
|
+
let data = "";
|
|
1950
|
+
process.stdin.setEncoding("utf8");
|
|
1951
|
+
process.stdin.on("data", (c) => data += c);
|
|
1952
|
+
process.stdin.on("end", () => resolve2(data));
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
function openBrowser(url) {
|
|
1956
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
1957
|
+
execFile3(cmd, [url], () => void 0);
|
|
1958
|
+
}
|
|
1959
|
+
main(process.argv.slice(2)).then(
|
|
1960
|
+
(code) => {
|
|
1961
|
+
if (code >= 0) process.exit(code);
|
|
1962
|
+
},
|
|
1963
|
+
(err) => {
|
|
1964
|
+
console.error(err.message);
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1967
|
+
);
|