@double_geste/agent-standup 1.27.0 → 1.29.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 -1
- package/kokoro.js +2 -2
- package/package.json +2 -2
- package/public/index.html +46 -28
- package/public/locales/da.json +7 -1
- package/public/locales/de.json +7 -1
- package/public/locales/en.json +7 -1
- package/public/locales/es.json +7 -1
- package/public/locales/ja.json +7 -1
- package/public/locales/ko.json +227 -0
- package/public/locales/pt-BR.json +7 -1
- package/public/locales/zh-CN.json +7 -1
- package/public/locales/zh-TW.json +7 -1
- package/server.js +34 -21
package/README.md
CHANGED
|
@@ -13,6 +13,6 @@ Install once for status bars (faster than npx): `npm i -g @double_geste/agent-st
|
|
|
13
13
|
- **tmux**: `set -g status-right '#(agent-standup status --here)'`
|
|
14
14
|
- **Anything else**: `agent-standup status --json`
|
|
15
15
|
|
|
16
|
-
Full documentation, the VS Code extension, the desktop app and the demo: https://
|
|
16
|
+
Full documentation, the VS Code extension, the desktop app and the demo: https://agent-standup.com · https://github.com/Atoll92/agent-standup.git
|
|
17
17
|
|
|
18
18
|
Unofficial: not affiliated with Anthropic or OpenAI.
|
package/kokoro.js
CHANGED
|
@@ -7,8 +7,8 @@ const DIR = process.env.AGENT_STANDUP_KOKORO_DIR || (WIN ? path.join(process.env
|
|
|
7
7
|
const PKG = 'kokoro-js@1.2.1', DTYPE = 'q4', SIZE_MB = 730, MAX_RESTARTS = 3;
|
|
8
8
|
const WORKER = process.env.AGENT_STANDUP_KOKORO_WORKER || path.join(__dirname, 'kokoro-worker.js'), LOG = path.join(DIR, 'install.log');
|
|
9
9
|
|
|
10
|
-
// In the order agents get them (the lead takes the first), so successive agents sound distinct.
|
|
11
|
-
const VOICES = [['af_heart', 'Heart', 'en_US'], ['am_fenrir', 'Fenrir', 'en_US'], ['bf_emma', 'Emma', 'en_GB'], ['am_michael', 'Michael', 'en_US'], ['af_bella', 'Bella', 'en_US'], ['bm_george', 'George', 'en_GB'], ['af_sarah', 'Sarah', 'en_US'], ['am_puck', 'Puck', 'en_US']
|
|
10
|
+
// In the order agents get them (the lead takes the first), so successive agents sound distinct. English only: kokoro-js has no French voice (it refuses ff_siwis), so French lines are read by the system voices.
|
|
11
|
+
const VOICES = [['af_heart', 'Heart', 'en_US'], ['am_fenrir', 'Fenrir', 'en_US'], ['bf_emma', 'Emma', 'en_GB'], ['am_michael', 'Michael', 'en_US'], ['af_bella', 'Bella', 'en_US'], ['bm_george', 'George', 'en_GB'], ['af_sarah', 'Sarah', 'en_US'], ['am_puck', 'Puck', 'en_US']].map(([name, label, locale]) => ({ name, label, locale }));
|
|
12
12
|
const voices = () => VOICES, hasVoice = (n) => VOICES.some((v) => v.name === n);
|
|
13
13
|
const speedFor = (wpm) => Math.max(0.5, Math.min(2, Math.round((wpm / 178) * 20) / 20)); // 178 words a minute is about Kokoro's natural pace
|
|
14
14
|
const exists = (f) => { try { fs.statSync(f); return true; } catch { return false; } };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Your Claude Code and Codex agents as a live video call, from the terminal: a local page in your browser, plus one-line status for status bars. Free, open source, zero token cost.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-standup": "bin/agent-standup.js"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "git+https://github.com/Atoll92/agent-standup.git"
|
|
29
29
|
},
|
|
30
|
-
"homepage": "https://
|
|
30
|
+
"homepage": "https://agent-standup.com",
|
|
31
31
|
"bugs": {
|
|
32
32
|
"url": "https://github.com/Atoll92/agent-standup/issues"
|
|
33
33
|
},
|
package/public/index.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
/* ---------- header ---------- */
|
|
19
19
|
header { height:48px; flex:none; display:flex; align-items:center; gap:10px; padding:0 12px; border-bottom:1px solid var(--line); background:var(--bg) }
|
|
20
20
|
.brand { display:flex; align-items:center; gap:9px; min-width:0; overflow:hidden } .brand .logo { flex:none } .brand > div { min-width:0 } .brand b { overflow:hidden; text-overflow:ellipsis }
|
|
21
|
-
header > * { flex-shrink:0 } header .brand { flex-shrink:100 } header #board { flex-shrink
|
|
21
|
+
header > * { flex-shrink:0 } header .brand { flex-shrink:100; min-width:26px } header #board { flex-shrink:.01 } .tok, .ctxp { flex-shrink:2; min-width:0; overflow:hidden; text-overflow:ellipsis } #pill { flex-shrink:.05; min-width:min-content; overflow:hidden; text-overflow:ellipsis } /* in a narrow window the buttons (the gear above all) stay whole: the name gives way first (down to the logo), then the counters; the chips only when nothing else can (.01, not 0: the header must never overflow) */
|
|
22
22
|
@media (max-width:720px) { .ctxp { display:none !important } } @media (max-width:620px) { .tok { display:none !important } } @media (max-width:560px) { #bCoffee { display:none !important } } @media (max-width:480px) { header { gap:6px; padding-left:8px; padding-right:8px } } /* narrow: the coffee link stays in ⚙ › Tools */
|
|
23
23
|
.logo { width:26px; height:26px; display:block; flex:none }
|
|
24
24
|
.brand b { font:700 14px "Comic Sans MS","Chalkboard SE",system-ui,sans-serif; white-space:nowrap }
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
/* the header's status chips are styled by id: "board" is also the class the Sessions view puts on <body>, and a rule on .board used to hide the whole page in a narrow window */
|
|
154
154
|
#board { display:flex; gap:6px; font-size:11.5px; flex-wrap:nowrap; min-width:0; overflow:hidden } #board em { font-style:normal } #board span { flex:none; padding:2px 8px; border-radius:99px; background:var(--panel2); border:1px solid var(--line); white-space:nowrap }
|
|
155
155
|
#board .working { color:var(--gold) } #board .waiting { color:#8fd3ff } #board .blocked { color:#fff; background:#c0392b; border-color:#c0392b } #board .stalled { color:#1b1d27; background:#ffb347; border-color:#ffb347 } #board .done { color:var(--ok) }
|
|
156
|
-
@media (max-width:1180px) { #board em { display:none } .meet { display:none } }
|
|
156
|
+
@media (max-width:1180px) { #board em { display:none } .meet { display:none } } #board.tight em { display:none } /* tight: set by fitBoard when the chips do not all fit with their words */
|
|
157
157
|
@media (max-width:640px) { #board { display:none } }
|
|
158
158
|
/* what is said sits above everything else on the tile (badge, prop, ticker) and is never cut: a line longer than its tile grows over the tiles below */
|
|
159
159
|
.bubble { display:none; position:absolute; z-index:6; top:6px; left:6px; width:max-content; max-width:calc(100% - 12px); background:#fff; color:#1b1d27; border-radius:11px; padding:6px 10px; font-size:12.5px; line-height:1.35; box-shadow:0 4px 14px rgba(0,0,0,.35); overflow-wrap:anywhere }
|
|
@@ -290,13 +290,14 @@
|
|
|
290
290
|
<button class="mi" data-t="vanish"><span data-fr="Retirer les agents terminés (2 min)" data-en="Remove finished agents (2 min)">Retirer les agents terminés (2 min)</span><i class="sw"></i></button>
|
|
291
291
|
<button class="mi" data-t="epi"><span data-fr="Réactions et blagues" data-en="Reactions & jokes">Réactions et blagues</span><i class="sw"></i></button>
|
|
292
292
|
<h4 data-fr="Source" data-en="Source">Source</h4>
|
|
293
|
-
<div class="box"><div class="seg" style="width:100%" id="modeSeg"><button data-m="live" style="flex:1">Live</button><button data-m="replay" style="flex:1">Replay</button><button data-m="demo" style="flex:1">Demo</button></div>
|
|
293
|
+
<div class="box"><div class="seg" style="width:100%" id="modeSeg"><button data-m="live" style="flex:1" data-fr="Live" data-en="Live">Live</button><button data-m="replay" style="flex:1" data-fr="Replay" data-en="Replay">Replay</button><button data-m="demo" style="flex:1" data-fr="Démo" data-en="Demo">Demo</button></div>
|
|
294
294
|
<select id="liveProj" style="display:none;margin-top:6px" title="Project to follow"></select>
|
|
295
295
|
<select id="sessions" style="display:none"></select></div>
|
|
296
296
|
<h4 data-fr="Personnages" data-en="Characters">Personnages</h4>
|
|
297
297
|
<div class="box"><select id="charPack" aria-label="Characters"><option value="none" data-fr="Personnages dessinés" data-en="Drawn characters">Personnages dessinés</option><option value="folder" data-fr="Mon dossier" data-en="My folder">Mon dossier</option><option value="cats" data-fr="Pack de chats" data-en="Cat pack">Pack de chats</option></select></div>
|
|
298
298
|
<div class="box"><div id="charInfo" class="dim" style="font-size:11.5px;line-height:1.5"></div><div style="display:flex;gap:6px;margin-top:6px"><button class="btn" id="charReload" data-fr="Recharger" data-en="Reload">Recharger</button><button class="btn" id="charOpen" style="display:none" data-fr="Ouvrir le dossier" data-en="Open folder">Ouvrir le dossier</button></div></div>
|
|
299
299
|
<button class="mi" data-t="customLaptop"><span data-fr="Ordinateur devant vos personnages" data-en="Laptop in front of your own characters">Ordinateur devant vos personnages</span><i class="sw"></i></button>
|
|
300
|
+
<button class="mi" data-t="ownNames"><span data-fr="Garder les noms de mes agents" data-en="Keep my agents’ own names">Garder les noms de mes agents</span><i class="sw"></i></button>
|
|
300
301
|
<h4 data-fr="Outils" data-en="Tools">Outils</h4>
|
|
301
302
|
<button class="mi" id="mCoffee" style="display:none"><span id="mCoffeeT"></span></button>
|
|
302
303
|
<button class="mi" id="mKeys"><span data-fr="⌨ Raccourcis clavier (?)" data-en="⌨ Keyboard shortcuts (?)">⌨ Raccourcis clavier (?)</span></button>
|
|
@@ -344,7 +345,7 @@ const CID = Math.random().toString(36).slice(2, 12); // identifies this window
|
|
|
344
345
|
const Q = new URLSearchParams(location.search);
|
|
345
346
|
const EMBED = Q.get('embed') === '1' || window.parent !== window;
|
|
346
347
|
|
|
347
|
-
let prefs = { lang: 'en', voice: true, roster: true, banter: true, reactions: false, office: false, officeVol: 0.8, vstyle: 'cartoon', notify: false, epi: false, vanish: true, music: false, station: 'fip', musicVol: 0.35, view: 'call', hideCode: false };
|
|
348
|
+
let prefs = { lang: 'en', voice: true, roster: true, banter: true, reactions: false, office: false, officeVol: 0.8, vstyle: 'cartoon', notify: false, epi: false, vanish: true, music: false, station: 'fip', musicVol: 0.35, view: 'call', hideCode: false, ownNames: true };
|
|
348
349
|
{ let loc = {}; try { loc = JSON.parse(localStorage.getItem('cc.prefs') || '{}') || {}; } catch {} // the settings file (shared by every window) and this origin's copy: the more recent one wins
|
|
349
350
|
const sv = window.SAVED_PREFS && typeof window.SAVED_PREFS === 'object' ? window.SAVED_PREFS : {}; Object.assign(prefs, (sv._t || 0) > (loc._t || 0) ? sv : loc); }
|
|
350
351
|
if (prefs.vstyle === 'babble') prefs.vstyle = 'animal', prefs.vstyleChosen = true;
|
|
@@ -354,7 +355,7 @@ let prefsTimer = null;
|
|
|
354
355
|
const savePrefs = () => { prefs._t = Date.now(); try { localStorage.setItem('cc.prefs', JSON.stringify(prefs)); } catch {} clearTimeout(prefsTimer); prefsTimer = setTimeout(() => { fetch('/api/prefs', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(prefs) }).catch(() => {}); }, 400); };
|
|
355
356
|
// Languages: French and English live in the code as pairs L(french, english); every other language is a catalogue public/locales/<code>.json mapping the English text to its translation.
|
|
356
357
|
// A missing string falls back to English, so a half-translated language still works. See public/locales/README.md to add one.
|
|
357
|
-
const LANGS = [['en', 'English', 'en'], ['fr', 'Français', 'fr'], ['zh-TW', '繁體中文', 'zh-Hant-TW'], ['zh-CN', '简体中文', 'zh-Hans-CN'], ['es', 'Español', 'es'], ['de', 'Deutsch', 'de'], ['da', 'Dansk', 'da'], ['pt-BR', 'Português (Brasil)', 'pt-BR'], ['ja', '日本語', 'ja']];
|
|
358
|
+
const LANGS = [['en', 'English', 'en'], ['fr', 'Français', 'fr'], ['zh-TW', '繁體中文', 'zh-Hant-TW'], ['zh-CN', '简体中文', 'zh-Hans-CN'], ['es', 'Español', 'es'], ['de', 'Deutsch', 'de'], ['da', 'Dansk', 'da'], ['pt-BR', 'Português (Brasil)', 'pt-BR'], ['ja', '日本語', 'ja'], ['ko', '한국어', 'ko']];
|
|
358
359
|
let LOC = {};
|
|
359
360
|
const FR = () => prefs.lang === 'fr';
|
|
360
361
|
const FR_W = /\b(le|la|les|des|une|un|et|est|pour|dans|que|qui|pas|sur|avec|je|tu|nous|vous|ce|cette|ça|du|au|aux|mais|sont|été|fait|plus|ne|ont|il|elle|ils|elles|très|aussi|donc|bien|tout|voici|voilà|merci|oui|non|comme|où|déjà|encore)\b/gi, EN_W = /\b(the|and|is|are|to|of|in|that|with|for|this|it|on|you|we|not|have|has|was|be|will|can|from|your|at|as|but|all|now|also|done|here|there|which|what|then|just|so)\b/gi;
|
|
@@ -411,11 +412,16 @@ function renderCharInfo() {
|
|
|
411
412
|
$('#charOpen').style.display = EMBED ? '' : 'none';
|
|
412
413
|
}
|
|
413
414
|
function persona(a) { const p = persona0(a); p.custom = customFor(a.id); return p; }
|
|
415
|
+
const subName = (a, h) => (a.own && prefs.ownNames ? (nameBook[a.id] = a.own) : a.given || uniqueName(a.id, h)); // own: the name the person gave this agent (see ownName in server.js); otherwise a first name from the list. a.own is tested first: the landing page reuses this section and has no settings
|
|
416
|
+
function renameAll() { // ⚙ → own names on or off: every sub-agent is named again, in the order it arrived
|
|
417
|
+
for (const k in nameBook) delete nameBook[k]; for (const p of Object.values(S.agents)) if (p.id !== 'main' && p.id !== 'user') p.name = subName(p, hash(p.id));
|
|
418
|
+
buildStage(); paintStatus(); renderLog(true); syncUI();
|
|
419
|
+
}
|
|
414
420
|
function persona0(a) {
|
|
415
|
-
const h = hash(a.id), base = { ...a };
|
|
421
|
+
const h = hash(a.id), base = { ...a, given: 'given' in a ? a.given : a.name };
|
|
416
422
|
if (a.id === 'main') return { ...base, name: a.name || 'Claude', role: L('Chef', 'Lead'), shirt: '#e8845f', shape: 0, acc: 1, eye: 0, glasses: false, scene: 6, prop: 1, acc2: '#f5c04a', lead: true, vi: 0, gid: 'main' };
|
|
417
423
|
if (a.id === 'user') return { ...base, name: L('Vous', 'You'), role: L('Humain', 'Human'), shirt: '#8a94c9', shape: 1, acc: 2, eye: 1, glasses: false, scene: 7, prop: 1, acc2: '#ff7a6b', vi: 3, gid: 'user' };
|
|
418
|
-
return { ...base, name:
|
|
424
|
+
return { ...base, name: subName(base, h), roleEn: a.roleEn || a.role, role: (FR() && a.roleFr) || a.roleEn || a.role, shirt: PAL[(h >>> 6) % 8], shape: (h >>> 3) % 5, acc: (h >>> 9) % 6, eye: (h >>> 12) % 3, glasses: ((h >>> 15) % 4) === 0,
|
|
419
425
|
scene: (h >>> 18) % 6, prop: (h >>> 21) % 3, acc2: PAL[(h >>> 24) % 8], vi: h >>> 4, gid: h.toString(36) };
|
|
420
426
|
}
|
|
421
427
|
|
|
@@ -756,33 +762,39 @@ const synth = window.speechSynthesis;
|
|
|
756
762
|
const estMs = (text) => Math.min(9000, 900 + text.length * 42) / spd();
|
|
757
763
|
|
|
758
764
|
const NOVELTY = /grandma|grandpa|rocko|bad news|bahh|bells|boing|bubbles|cellos|good news|jester|organ|superstar|trinoids|whisper|wobble|zarvox|albert|fred|junior|kathy|ralph|hysterical|deranged|pipe/i;
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
765
|
+
const ROBOT = /^(eddy|flo|reed|sandy|shelley)\b/i; /* Apple's synthetic voices: understandable, and plainly a machine. Kept for when nothing else is installed */
|
|
766
|
+
const GOOD = /premium|enhanced|natural|siri|neural|améli|amelie|thomas|jacques|audrey|aurélie|aurelie|marie|samantha|daniel|karen|moira|serena|oliver|google|hortense|julie|paul|denise|henri|eloise|zira|david|hazel|mark|aria|guy/i;
|
|
767
|
+
const voiceScore = (n) => (/premium|enhanced|natural|neural|siri/i.test(n) ? 100 : 0) + (GOOD.test(n) ? 30 : 0) - (ROBOT.test(n) ? 50 : 0) - (NOVELTY.test(n) ? 200 : 0);
|
|
768
|
+
const frHome = () => [...(navigator.languages || []), navigator.language || ''].map((l) => String(l).replace('-', '_')).find((l) => /^fr_[A-Z]{2}$/.test(l)) || 'fr_FR'; /* French from France unless the computer itself is set to another French */
|
|
769
|
+
function rankVoices(list, lang) { // [{ name, locale }] → names, best distinct voices first. The synthetic ones only when fewer than two others exist
|
|
770
|
+
const home = lang === 'fr' ? frHome() : '', sc = (v) => voiceScore(v.name) + (home && v.locale === home ? 10 : 0), seen = new Set(), out = [];
|
|
771
|
+
[...list].sort((a, b) => sc(b) - sc(a)).forEach((v) => { const base = v.name.replace(/\s*\(.*$/, ''), x = sc(v); if (x > -100 && (x >= 0 || out.length < 2) && !seen.has(base)) { seen.add(base); out.push(v.name); } });
|
|
764
772
|
return out.slice(0, 5);
|
|
765
773
|
}
|
|
766
774
|
const TTS = {
|
|
767
|
-
server: false, engine: 'system', platform: '', pool: { fr: [], en: [] }, cache: new Map(), made: new Map(), q: [], busy: false, now: 0,
|
|
775
|
+
server: false, engine: 'system', platform: '', pool: { fr: [], en: [] }, sys: { fr: [], en: [] }, kv: new Set(), cache: new Map(), made: new Map(), q: [], busy: false, now: 0,
|
|
768
776
|
async init() {
|
|
769
|
-
this.engine = 'system'; this.server = false; this.pool = { fr: [], en: [] }; this.cache.clear(); this.made.clear(); this.q = [];
|
|
777
|
+
this.engine = 'system'; this.server = false; this.pool = { fr: [], en: [] }; this.sys = { fr: [], en: [] }; this.kv = new Set(); this.cache.clear(); this.made.clear(); this.q = [];
|
|
770
778
|
try {
|
|
771
|
-
if (prefs.vstyle === 'kokoro') { // natural voices installed on demand; until they are, the system voices stay in use
|
|
772
|
-
|
|
773
|
-
|
|
779
|
+
let k = null; if (prefs.vstyle === 'kokoro') { try { k = await (await fetch('/api/voices?engine=kokoro')).json(); } catch {} if (!k || !k.available || !k.ready) k = null; } // natural voices installed on demand; until they are, the system voices stay in use
|
|
780
|
+
let d = { available: false, voices: [] }; try { d = await (await fetch('/api/voices')).json(); } catch {}
|
|
781
|
+
this.platform = d.platform || (k && k.platform) || ''; const sys = {}; for (const l of ['fr', 'en']) sys[l] = d.available ? rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')), l) : [];
|
|
782
|
+
if (k) { // Kokoro speaks the languages it has voices for (English); the others are read by the system voices, each character keeping one of its own
|
|
783
|
+
this.server = true; this.engine = 'kokoro'; this.sys = sys;
|
|
784
|
+
for (const l of ['fr', 'en']) { const kv = k.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name); kv.forEach((v) => this.kv.add(v)); this.pool[l] = kv.length ? kv : sys[l]; }
|
|
785
|
+
return;
|
|
774
786
|
}
|
|
775
|
-
|
|
776
|
-
for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
|
|
787
|
+
if (!d.available) return; this.server = true; this.pool = sys; } catch {}
|
|
777
788
|
},
|
|
778
789
|
voiceFor(p, fr = FR()) { const pool = this.pool[fr ? 'fr' : 'en']; return pool.length ? (p.id === 'main' ? pool[0] : pool[(p.vi >>> 2) % pool.length]) : null; },
|
|
779
790
|
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() * (S.boost > 1 ? 1.25 : 1) : +$('#speed').value))) / 10) * 10; },
|
|
780
791
|
rateAt(k, boost) { const i = S.idx, b = S.boost; S.idx = k; S.boost = boost; const r = this.rate(); S.idx = i; S.boost = b; return r; }, // the rate rate() will give once playback is at event k
|
|
781
792
|
buf(text, voice, rate, ahead) { // ahead: made in advance (see warmTTS); otherwise the line is wanted now
|
|
782
|
-
const
|
|
793
|
+
const eng = this.kv.has(voice) ? 'kokoro' : 'system', fb = eng === 'kokoro' && this.sys.en.length ? this.sys.en[Math.max(0, this.pool.en.indexOf(voice)) % this.sys.en.length] : ''; /* fb: the system voice that reads the line if Kokoro cannot */
|
|
794
|
+
const vt = eng + '|' + voice + '|' + text, K = (r) => eng + '|' + voice + '|' + r + '|' + text, key = K(rate), got = this.made.get(vt) || []; // got: the rates at which this text is ready in this voice
|
|
783
795
|
if (!ahead && S.live && !got.includes(rate)) { const r = got.find((x) => Math.abs(x - rate) <= rate * 0.2 && this.cache.has(K(x))); if (r) return this.cache.get(K(r)); } // made ahead at the rate of a moment ago (the call fell behind or caught up since): played as it is, a second of silence is worse than a slightly different pace
|
|
784
796
|
if (!this.cache.has(key)) {
|
|
785
|
-
this.cache.set(key, fetch(`/api/tts?engine=${
|
|
797
|
+
this.cache.set(key, fetch(`/api/tts?engine=${eng}&voice=${encodeURIComponent(voice)}&rate=${rate}${fb ? '&fb=' + encodeURIComponent(fb) : ''}&text=${encodeURIComponent(text)}`).then((r) => { if (!r.ok) throw new Error('tts'); return r.arrayBuffer(); }).then((b) => AUD.ctx.decodeAudioData(b)));
|
|
786
798
|
if (this.cache.size > 60) this.cache.delete(this.cache.keys().next().value);
|
|
787
799
|
this.cache.get(key).then(() => { this.made.set(vt, [...(this.made.get(vt) || []), rate]); if (this.made.size > 120) this.made.delete(this.made.keys().next().value); }, () => this.cache.delete(key));
|
|
788
800
|
if (!ahead) { this.now++; const end = () => { this.now--; this.pump(); }; this.cache.get(key).then(end, end); } // a line wanted now never waits behind the ones made ahead: they stop being started until it is ready
|
|
@@ -902,7 +914,7 @@ async function speakServer(text, p, gain, token, fr = FR()) {
|
|
|
902
914
|
function speakBrowser(text, p, token, fr = FR()) { // fallback where no server voice exists (Windows/Linux)
|
|
903
915
|
return new Promise((resolve) => {
|
|
904
916
|
const vs = synth.getVoices().filter((v) => v.lang.toLowerCase().startsWith(fr ? 'fr' : 'en')); if (!vs.length) return resolve(false);
|
|
905
|
-
const ranked = rankVoices(vs.map((v) => v.name)), voice = vs.find((v) => v.name === (p.id === 'main' ? ranked[0] : ranked[(p.vi >>> 2) % ranked.length])) || vs[0];
|
|
917
|
+
const ranked = rankVoices(vs.map((v) => ({ name: v.name, locale: v.lang.replace('-', '_') })), fr ? 'fr' : 'en'), voice = vs.find((v) => v.name === (p.id === 'main' ? ranked[0] : ranked[(p.vi >>> 2) % ranked.length])) || vs[0];
|
|
906
918
|
const parts = speakable(text).match(/[^.!?;:]+[.!?;:]*/g) || [text]; synth.cancel(); let i = 0, done = false, started = false, t0 = 0, said = 0; const all = parts.reduce((n, x) => n + x.length, 0);
|
|
907
919
|
const fin = (ok) => { if (!done) { done = true; resolve(ok); } };
|
|
908
920
|
setTimeout(() => { if (!started && !done) { synth.cancel(); fin(false); } }, 2500); // engine never started (e.g. inside a webview)
|
|
@@ -1032,7 +1044,7 @@ function paintStatus() {
|
|
|
1032
1044
|
const prev = $('.row.now'); if (prev) prev.classList.remove('now');
|
|
1033
1045
|
const now = $(`.row[data-i="${S.idx}"]`); if (now) { now.classList.add('now'); if (document.body.classList.contains('log')) now.scrollIntoView({ block: 'nearest' }); }
|
|
1034
1046
|
later('detail', renderDetail); later('tree', renderTree); later('meet', renderMeet, 400); drawGantt();
|
|
1035
|
-
$('#seek').value = S.idx; $('#pos').textContent = `${Math.min(S.idx + 1, S.events.length)} / ${S.events.length}`;
|
|
1047
|
+
const sk = $('#seek'); sk.max = Math.max(0, S.events.length - 1); sk.value = S.idx; $('#pos').textContent = `${Math.min(S.idx + 1, S.events.length)} / ${S.events.length}`;
|
|
1036
1048
|
}
|
|
1037
1049
|
const applyStatus = (ev) => {
|
|
1038
1050
|
if (ev.kind === 'order') { (S.waitOn[ev.agent] = S.waitOn[ev.agent] || new Set()).add(ev.to); S.orderTs[ev.to] = ev.ts || 0; S.status[ev.to] = 'working'; delete S.doneAt[ev.to]; S.gone.delete(ev.to); S.leaving.delete(ev.to); if (!ev.bg && ev.toolId) S.run[ev.agent] = { id: ev.toolId, ts: ev.ts, start: performance.now(), name: 'Agent' }; const t = $('#t-' + ev.to); if (t) t.classList.remove('vanishing'); }
|
|
@@ -1141,8 +1153,14 @@ function paintStates() {
|
|
|
1141
1153
|
paintPill();
|
|
1142
1154
|
const b = $('#board'), parts = [['working', '⚙', L('au travail', 'working')], ['waiting', '🔗', L('en attente', 'waiting')], ['blocked', '✋', L('vous attendent', 'need you')], ['stalled', '⚠', L('bloqués', 'stalled')], ['done', '✓', L('terminés', 'done')]]
|
|
1143
1155
|
.filter(([k]) => n[k] && !S.pillSess).map(([k, i, l]) => `<span class="${k}" title="${n[k]} ${l}">${i} ${n[k]}<em> ${l}</em></span>`).join('');
|
|
1144
|
-
if (b.innerHTML !== parts) b.innerHTML = parts; S.nBlocked = n.blocked + n.stalled; S.attn = attn; paintRoster(ros); return n;
|
|
1156
|
+
if (b.innerHTML !== parts) b.innerHTML = parts; fitBoard(); S.nBlocked = n.blocked + n.stalled; S.attn = attn; paintRoster(ros); return n;
|
|
1157
|
+
}
|
|
1158
|
+
function fitBoard(force) { // the chips lose their words when they do not all fit (a long language, a narrower window): a chip is never cut mid-word. Tried again with the words only when the window or the texts around have changed
|
|
1159
|
+
const b = $('#board'), key = innerWidth + '|' + b.textContent.length + '|' + $('#pill').textContent.length + '|' + $('#subtitle').textContent.length;
|
|
1160
|
+
if (b.classList.contains('tight')) { if (!force && key === fitBoard.key) return; b.classList.remove('tight'); }
|
|
1161
|
+
fitBoard.key = key; if (b.scrollWidth > b.clientWidth + 1) b.classList.add('tight');
|
|
1145
1162
|
}
|
|
1163
|
+
addEventListener('resize', () => fitBoard(true)); new ResizeObserver(() => requestAnimationFrame(() => fitBoard(true))).observe($('#board')); /* the room the chips have also changes when the texts beside them do (a title, a longer status) */
|
|
1146
1164
|
const RORDER = { blocked: 0, stalled: 1, working: 2, waiting: 3, idle: 4, done: 5 };
|
|
1147
1165
|
function paintRoster(ros) { // a fixed list beside the call: who is doing what, urgent first, readable even with dozens of agents
|
|
1148
1166
|
const box = $('#roster'); if (!box || !document.body.classList.contains('rost')) return;
|
|
@@ -2189,7 +2207,7 @@ function menuInit() {
|
|
|
2189
2207
|
$('#menuNone').style.display = q && !secs.some((sc) => sc.items.some((e) => !e.classList.contains('mhide'))) ? '' : 'none';
|
|
2190
2208
|
};
|
|
2191
2209
|
}
|
|
2192
|
-
$('#menu').onclick = (e) => { const b = e.target.closest('.mi[data-t]'); if (!b) return; prefs[b.dataset.t] = !prefs[b.dataset.t]; savePrefs(); if (prefs[b.dataset.t] && ['voice', 'office', 'music', 'notify'].includes(b.dataset.t)) claimAudio(); b.querySelector('.sw').classList.toggle('on', prefs[b.dataset.t]); AUD.apply(); if (b.dataset.t === 'office') $('#officeBox').style.display = prefs.office ? '' : 'none'; if (b.dataset.t === 'customLaptop') { for (const k in imgCache) delete imgCache[k]; buildStage(); paintStatus(); } if (b.dataset.t === 'reactions') document.body.classList.toggle('react', prefs.reactions); if (b.dataset.t === 'roster') { document.body.classList.toggle('rost', prefs.roster); paintStates(); } if (b.dataset.t === 'notify') { prefs.notifyChosen = true; savePrefs(); } if (b.dataset.t === 'notify' && prefs.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
|
|
2210
|
+
$('#menu').onclick = (e) => { const b = e.target.closest('.mi[data-t]'); if (!b) return; prefs[b.dataset.t] = !prefs[b.dataset.t]; savePrefs(); if (prefs[b.dataset.t] && ['voice', 'office', 'music', 'notify'].includes(b.dataset.t)) claimAudio(); b.querySelector('.sw').classList.toggle('on', prefs[b.dataset.t]); AUD.apply(); if (b.dataset.t === 'office') $('#officeBox').style.display = prefs.office ? '' : 'none'; if (b.dataset.t === 'customLaptop') { for (const k in imgCache) delete imgCache[k]; buildStage(); paintStatus(); } if (b.dataset.t === 'ownNames') renameAll(); if (b.dataset.t === 'reactions') document.body.classList.toggle('react', prefs.reactions); if (b.dataset.t === 'roster') { document.body.classList.toggle('rost', prefs.roster); paintStates(); } if (b.dataset.t === 'notify') { prefs.notifyChosen = true; savePrefs(); } if (b.dataset.t === 'notify' && prefs.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
|
|
2193
2211
|
if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
|
|
2194
2212
|
$('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
|
|
2195
2213
|
$('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
|
|
@@ -2205,8 +2223,8 @@ async function kokoroUI() {
|
|
|
2205
2223
|
b.disabled = st.installing || !st.npm; b.textContent = st.installing ? L('Installation…', 'Installing…') : L(`Installer (${st.sizeMb} Mo)`, `Install (${st.sizeMb} MB)`);
|
|
2206
2224
|
h.textContent = st.installing ? L('Téléchargement en cours, vous pouvez continuer : les voix du système servent en attendant.', 'Downloading, you can carry on: system voices are used meanwhile.')
|
|
2207
2225
|
: st.error ? st.error : !st.npm && !st.installed ? L('npm est introuvable : installez Node.js pour activer cette option.', 'npm not found: install Node.js to enable this option.')
|
|
2208
|
-
: st.installed ? (st.gaveUp ? L('Le moteur s’est arrêté : les voix du système servent à la place.', 'The engine stopped: system voices are used instead.') : st.ready ? L('Voix prêtes.', 'Voices ready.') : L('Chargement du modèle (téléchargé au premier usage, environ 305 Mo)…', 'Loading the model (downloaded on first use, about 305 MB)…'))
|
|
2209
|
-
: L('Voix locales de bonne qualité, en anglais
|
|
2226
|
+
: st.installed ? (st.gaveUp ? L('Le moteur s’est arrêté : les voix du système servent à la place.', 'The engine stopped: system voices are used instead.') : st.ready ? L('Voix prêtes.', 'Voices ready.') + ' ' + L('Le français est lu par les voix de votre système : en installer une « améliorée » ou « premium » change tout.', 'French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.') : L('Chargement du modèle (téléchargé au premier usage, environ 305 Mo)…', 'Loading the model (downloaded on first use, about 305 MB)…'))
|
|
2227
|
+
: L('Voix locales de bonne qualité, en anglais (le français passe par les voix de votre système). Gratuit, rien n’est envoyé nulle part.', 'Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.');
|
|
2210
2228
|
if ((st.installing || (st.installed && !st.ready && !st.gaveUp)) && !kokoroTimer) kokoroTimer = setInterval(async () => { await kokoroUI(); }, 2500);
|
|
2211
2229
|
if (!st.installing && (st.ready || st.gaveUp || !st.installed) && kokoroTimer) { clearInterval(kokoroTimer); kokoroTimer = null; if (st.ready && TTS.engine !== 'kokoro') TTS.init(); }
|
|
2212
2230
|
}
|
|
@@ -2255,7 +2273,7 @@ function lastWord() { // the last sentence the lead said: a closing question (
|
|
|
2255
2273
|
function paintPill() {
|
|
2256
2274
|
const pill = $('#pill'), now = Date.now(); let cls = '', txt = '', to = '', tip = '', sess = false;
|
|
2257
2275
|
if (S.offline) { cls = 'off'; txt = '⚠ ' + L('HORS LIGNE', 'OFFLINE'); }
|
|
2258
|
-
else if (!S.live) txt = 'REPLAY';
|
|
2276
|
+
else if (!S.live) txt = L('REPLAY', 'REPLAY');
|
|
2259
2277
|
else if (!S.cur) txt = L('En attente d’une session', 'Waiting for a session');
|
|
2260
2278
|
else if (S.locked) txt = L('PRÊT', 'READY');
|
|
2261
2279
|
else if (!S.playing) txt = L('PAUSE', 'PAUSED');
|
package/public/locales/da.json
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"Copy as Markdown": "Kopiér som Markdown",
|
|
43
43
|
"Copy failed": "Kopiering mislykkedes",
|
|
44
44
|
"custom character(s) loaded": "egne figurer indlæst",
|
|
45
|
+
"Demo": "Demo",
|
|
45
46
|
"Demo — a team fixes a slow login": "Demo — et team retter et langsomt login",
|
|
46
47
|
"done": "færdig",
|
|
47
48
|
"Done": "Færdig",
|
|
@@ -62,7 +63,8 @@
|
|
|
62
63
|
"Files touched": "Berørte filer",
|
|
63
64
|
"Final report": "Slutrapport",
|
|
64
65
|
"first error": "første fejl",
|
|
65
|
-
"
|
|
66
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "Fransk læses af dine systemstemmer: at installere en “forbedret” eller “premium” stemme gør en stor forskel.",
|
|
67
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Lokale stemmer af god kvalitet, på engelsk (fransk bruger dine systemstemmer). Gratis, intet forlader din computer.",
|
|
66
68
|
"Hide code & file names in videos": "Skjul kode og filnavne i videoer",
|
|
67
69
|
"Highlights": "Højdepunkter",
|
|
68
70
|
"Human": "Menneske",
|
|
@@ -71,6 +73,7 @@
|
|
|
71
73
|
"Installing…": "Installerer…",
|
|
72
74
|
"is sharing their screen": "deler sin skærm",
|
|
73
75
|
"Join the call": "Deltag i opkaldet",
|
|
76
|
+
"Keep my agents’ own names": "Behold mine agenters egne navne",
|
|
74
77
|
"Keyboard shortcuts": "Tastaturgenveje",
|
|
75
78
|
"Language": "Sprog",
|
|
76
79
|
"Lead": "Leder",
|
|
@@ -80,6 +83,7 @@
|
|
|
80
83
|
"lines": "linjer",
|
|
81
84
|
"Listing ": "Viser ",
|
|
82
85
|
"Little lines & animations": "Små replikker og animationer",
|
|
86
|
+
"Live": "Live",
|
|
83
87
|
"Loading the model (downloaded on first use, about 305 MB)…": "Indlæser modellen (hentes ved første brug, ca. 305 MB)…",
|
|
84
88
|
"longest wait ": "længste ventetid ",
|
|
85
89
|
"Looking for a tool": "Leder efter et værktøj",
|
|
@@ -121,6 +125,8 @@
|
|
|
121
125
|
"Reload": "Genindlæs",
|
|
122
126
|
"Remove finished agents (2 min)": "Fjern færdige agenter (2 min)",
|
|
123
127
|
"Repeated failures": "Gentagne fejl",
|
|
128
|
+
"Replay": "Afspilning",
|
|
129
|
+
"REPLAY": "AFSPILNING",
|
|
124
130
|
"report": "rapport",
|
|
125
131
|
"report to": "rapport til",
|
|
126
132
|
"Role": "Rolle",
|
package/public/locales/de.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"Files touched": "Bearbeitete Dateien",
|
|
59
59
|
"Final report": "Abschlussbericht",
|
|
60
60
|
"first error": "erster Fehler",
|
|
61
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "Französisch wird von deinen Systemstimmen gelesen: Eine „erweiterte“ oder „Premium“-Stimme zu installieren macht viel aus.",
|
|
61
62
|
"Hide code & file names in videos": "Code & Dateinamen in Videos ausblenden",
|
|
62
63
|
"Highlights": "Highlights",
|
|
63
64
|
"Human": "Mensch",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"input": "Eingabe",
|
|
66
67
|
"is sharing their screen": "teilt den Bildschirm",
|
|
67
68
|
"Join the call": "Dem Videoanruf beitreten",
|
|
69
|
+
"Keep my agents’ own names": "Eigene Namen meiner Agenten behalten",
|
|
68
70
|
"Language": "Sprache",
|
|
69
71
|
"Lead": "Hauptagent",
|
|
70
72
|
"Lead context": "Kontext Hauptagent",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"lines": "Zeilen",
|
|
73
75
|
"Listing ": "Liste auf ",
|
|
74
76
|
"Little lines & animations": "Kleine Sprüche & Animationen",
|
|
77
|
+
"Live": "Live",
|
|
75
78
|
"longest wait ": "längstes Warten ",
|
|
76
79
|
"Looking for a tool": "Sucht ein Werkzeug",
|
|
77
80
|
"looping?": "Schleife?",
|
|
@@ -106,6 +109,8 @@
|
|
|
106
109
|
"Recent activity": "Letzte Aktivität",
|
|
107
110
|
"Remove finished agents (2 min)": "Fertige Agenten entfernen (2 Min.)",
|
|
108
111
|
"Repeated failures": "Wiederholte Fehler",
|
|
112
|
+
"Replay": "Wiedergabe",
|
|
113
|
+
"REPLAY": "WIEDERGABE",
|
|
109
114
|
"report": "Bericht",
|
|
110
115
|
"report to": "Bericht an",
|
|
111
116
|
"Role": "Rolle",
|
|
@@ -167,10 +172,11 @@
|
|
|
167
172
|
"Reload": "Neu laden",
|
|
168
173
|
"Open folder": "Ordner öffnen",
|
|
169
174
|
"custom character(s) loaded": "eigene Figur(en) geladen",
|
|
175
|
+
"Demo": "Demo",
|
|
170
176
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Leg deine Pixel-Art (PNG) in diesen Ordner, um die Figuren zu ersetzen:",
|
|
171
177
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = Hauptagent · you.png = du · andere = Subagenten · name-talk.png = offener Mund",
|
|
172
178
|
"Downloading, you can carry on: system voices are used meanwhile.": "Wird heruntergeladen, du kannst weitermachen: bis dahin kommen die Systemstimmen zum Einsatz.",
|
|
173
|
-
"Good-quality local voices,
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Lokale Stimmen in guter Qualität, auf Englisch (Französisch nutzt deine Systemstimmen). Kostenlos, nichts verlässt deinen Rechner.",
|
|
174
180
|
"Installing…": "Installation läuft…",
|
|
175
181
|
"Loading the model (downloaded on first use, about 305 MB)…": "Modell wird geladen (beim ersten Mal heruntergeladen, etwa 305 MB)…",
|
|
176
182
|
"npm not found: install Node.js to enable this option.": "npm nicht gefunden: Installiere Node.js, um diese Option zu nutzen.",
|
package/public/locales/en.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"Copy as Markdown": "Copy as Markdown",
|
|
44
44
|
"Copy failed": "Copy failed",
|
|
45
45
|
"custom character(s) loaded": "custom character(s) loaded",
|
|
46
|
+
"Demo": "Demo",
|
|
46
47
|
"Demo — a team fixes a slow login": "Demo — a team fixes a slow login",
|
|
47
48
|
"Display": "Display",
|
|
48
49
|
"done": "done",
|
|
@@ -67,8 +68,9 @@
|
|
|
67
68
|
"Find a session": "Find a session",
|
|
68
69
|
"Find a session…": "Find a session…",
|
|
69
70
|
"first error": "first error",
|
|
71
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.",
|
|
70
72
|
"Go to a session": "Go to a session",
|
|
71
|
-
"Good-quality local voices,
|
|
73
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.",
|
|
72
74
|
"Hide code & file names in videos": "Hide code & file names in videos",
|
|
73
75
|
"Highlights": "Highlights",
|
|
74
76
|
"Human": "Human",
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
"Installing…": "Installing…",
|
|
78
80
|
"is sharing their screen": "is sharing their screen",
|
|
79
81
|
"Join the call": "Join the call",
|
|
82
|
+
"Keep my agents’ own names": "Keep my agents’ own names",
|
|
80
83
|
"Keyboard shortcuts": "Keyboard shortcuts",
|
|
81
84
|
"Language": "Language",
|
|
82
85
|
"Laptop in front of your own characters": "Laptop in front of your own characters",
|
|
@@ -87,6 +90,7 @@
|
|
|
87
90
|
"lines": "lines",
|
|
88
91
|
"Listing ": "Listing ",
|
|
89
92
|
"Little lines & animations": "Little lines & animations",
|
|
93
|
+
"Live": "Live",
|
|
90
94
|
"Lively": "Lively",
|
|
91
95
|
"Loading the model (downloaded on first use, about 305 MB)…": "Loading the model (downloaded on first use, about 305 MB)…",
|
|
92
96
|
"longest wait ": "longest wait ",
|
|
@@ -137,6 +141,8 @@
|
|
|
137
141
|
"Reload": "Reload",
|
|
138
142
|
"Remove finished agents (2 min)": "Remove finished agents (2 min)",
|
|
139
143
|
"Repeated failures": "Repeated failures",
|
|
144
|
+
"Replay": "Replay",
|
|
145
|
+
"REPLAY": "REPLAY",
|
|
140
146
|
"Replay: next / previous highlight": "Replay: next / previous highlight",
|
|
141
147
|
"Replay: previous / next event": "Replay: previous / next event",
|
|
142
148
|
"report": "report",
|
package/public/locales/es.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"Files touched": "Archivos tocados",
|
|
59
59
|
"Final report": "Informe final",
|
|
60
60
|
"first error": "primer error",
|
|
61
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "El francés lo leen las voces de tu sistema: instalar una «mejorada» o «premium» cambia mucho.",
|
|
61
62
|
"Hide code & file names in videos": "Ocultar código y archivos en los vídeos",
|
|
62
63
|
"Highlights": "Destacados",
|
|
63
64
|
"Human": "Humano",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"input": "entrada",
|
|
66
67
|
"is sharing their screen": "está compartiendo pantalla",
|
|
67
68
|
"Join the call": "Unirse a la videollamada",
|
|
69
|
+
"Keep my agents’ own names": "Mantener los nombres de mis agentes",
|
|
68
70
|
"Language": "Idioma",
|
|
69
71
|
"Lead": "Principal",
|
|
70
72
|
"Lead context": "Contexto del agente principal",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"lines": "líneas",
|
|
73
75
|
"Listing ": "Listando ",
|
|
74
76
|
"Little lines & animations": "Frases y animaciones",
|
|
77
|
+
"Live": "En vivo",
|
|
75
78
|
"longest wait ": "espera máxima ",
|
|
76
79
|
"Looking for a tool": "Buscando una herramienta",
|
|
77
80
|
"looping?": "¿en bucle?",
|
|
@@ -106,6 +109,8 @@
|
|
|
106
109
|
"Recent activity": "Actividad reciente",
|
|
107
110
|
"Remove finished agents (2 min)": "Quitar agentes terminados (2 min)",
|
|
108
111
|
"Repeated failures": "Fallos repetidos",
|
|
112
|
+
"Replay": "Repetición",
|
|
113
|
+
"REPLAY": "REPETICIÓN",
|
|
109
114
|
"report": "informe",
|
|
110
115
|
"report to": "informe a",
|
|
111
116
|
"Role": "Rol",
|
|
@@ -167,10 +172,11 @@
|
|
|
167
172
|
"Reload": "Recargar",
|
|
168
173
|
"Open folder": "Abrir carpeta",
|
|
169
174
|
"custom character(s) loaded": "personaje(s) propio(s) cargado(s)",
|
|
175
|
+
"Demo": "Demo",
|
|
170
176
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Suelta tu pixel art (PNG) en esta carpeta para reemplazar los personajes:",
|
|
171
177
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = el agente principal · you.png = tú · el resto = los subagentes · name-talk.png = boca abierta",
|
|
172
178
|
"Downloading, you can carry on: system voices are used meanwhile.": "Descargando, puedes seguir: mientras tanto se usan las voces del sistema.",
|
|
173
|
-
"Good-quality local voices,
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Voces locales de buena calidad, en inglés (el francés usa las voces de tu sistema). Gratis, nada sale de tu equipo.",
|
|
174
180
|
"Installing…": "Instalando…",
|
|
175
181
|
"Loading the model (downloaded on first use, about 305 MB)…": "Cargando el modelo (se descarga la primera vez, unos 305 MB)…",
|
|
176
182
|
"npm not found: install Node.js to enable this option.": "No se encontró npm: instala Node.js para activar esta opción.",
|
package/public/locales/ja.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"Files touched": "触れたファイル",
|
|
59
59
|
"Final report": "最終レポート",
|
|
60
60
|
"first error": "最初のエラー",
|
|
61
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "フランス語はシステム音声で読み上げます。“拡張”または“プレミアム”の音声を入れると大きく変わります。",
|
|
61
62
|
"Hide code & file names in videos": "動画でコードとファイル名を隠す",
|
|
62
63
|
"Highlights": "ハイライト",
|
|
63
64
|
"Human": "人間",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"input": "入力",
|
|
66
67
|
"is sharing their screen": "が画面を共有中",
|
|
67
68
|
"Join the call": "ビデオ通話に参加",
|
|
69
|
+
"Keep my agents’ own names": "自分のエージェント名をそのまま使う",
|
|
68
70
|
"Language": "言語",
|
|
69
71
|
"Lead": "メインエージェント",
|
|
70
72
|
"Lead context": "メインエージェントのコンテキスト",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"lines": "行",
|
|
73
75
|
"Listing ": "一覧表示中: ",
|
|
74
76
|
"Little lines & animations": "ひとことセリフとアニメーション",
|
|
77
|
+
"Live": "ライブ",
|
|
75
78
|
"longest wait ": "最長待ち時間 ",
|
|
76
79
|
"Looking for a tool": "ツールを探し中",
|
|
77
80
|
"looping?": "ループ中?",
|
|
@@ -106,6 +109,8 @@
|
|
|
106
109
|
"Recent activity": "最近のアクティビティ",
|
|
107
110
|
"Remove finished agents (2 min)": "完了したエージェントを削除(2分後)",
|
|
108
111
|
"Repeated failures": "くり返しの失敗",
|
|
112
|
+
"Replay": "リプレイ",
|
|
113
|
+
"REPLAY": "リプレイ",
|
|
109
114
|
"report": "報告",
|
|
110
115
|
"report to": "報告先",
|
|
111
116
|
"Role": "役割",
|
|
@@ -167,10 +172,11 @@
|
|
|
167
172
|
"Reload": "再読み込み",
|
|
168
173
|
"Open folder": "フォルダを開く",
|
|
169
174
|
"custom character(s) loaded": "件のカスタムキャラクターを読み込みました",
|
|
175
|
+
"Demo": "デモ",
|
|
170
176
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "このフォルダにドット絵(PNG)を入れると、キャラクターを差し替えられます:",
|
|
171
177
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = メインエージェント · you.png = あなた · それ以外 = サブエージェント · name-talk.png = 口を開けた状態",
|
|
172
178
|
"Downloading, you can carry on: system voices are used meanwhile.": "ダウンロード中です。そのまま作業を続けられます。それまではシステム音声を使います。",
|
|
173
|
-
"Good-quality local voices,
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "品質の良いローカル音声(英語。フランス語はシステム音声を使います)。無料で、データがPCの外に出ることはありません。",
|
|
174
180
|
"Installing…": "インストール中…",
|
|
175
181
|
"Loading the model (downloaded on first use, about 305 MB)…": "モデルを読み込み中(初回使用時にダウンロード、約305 MB)…",
|
|
176
182
|
"npm not found: install Node.js to enable this option.": "npm が見つかりません。このオプションを使うには Node.js をインストールしてください。",
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"language": "ko",
|
|
4
|
+
"reviewed": false,
|
|
5
|
+
"note": "AI draft, native review welcome"
|
|
6
|
+
},
|
|
7
|
+
" full — compaction soon": " 사용 — 곧 압축됩니다",
|
|
8
|
+
"”": "”",
|
|
9
|
+
"↗ Open in browser": "↗ 브라우저에서 열기",
|
|
10
|
+
"▶ Join": "▶ 참가",
|
|
11
|
+
"▶ Watch the demo": "▶ 데모 보기",
|
|
12
|
+
"☕ Buy the (replaced) dev a coffee": "☕ (일자리를 잃은) 개발자에게 커피 사 주기",
|
|
13
|
+
"🎞 Export video": "🎞 동영상 내보내기",
|
|
14
|
+
"🎬 Clip to share (25 s)": "🎬 공유용 클립 (25초)",
|
|
15
|
+
"📋 Recap": "📋 요약",
|
|
16
|
+
"Agent voices": "에이전트 목소리",
|
|
17
|
+
"Agents": "에이전트",
|
|
18
|
+
"Agents list (left)": "에이전트 목록 (왼쪽)",
|
|
19
|
+
"All projects (most recent)": "모든 프로젝트 (최신순)",
|
|
20
|
+
"Ambience volume": "환경음 볼륨",
|
|
21
|
+
"An agent may be waiting for you ✋": "에이전트가 기다리고 있을 수 있어요 ✋",
|
|
22
|
+
"Animal Crossing (babble)": "Animal Crossing (옹알이)",
|
|
23
|
+
"Asking a question": "질문하는 중",
|
|
24
|
+
"brief": "지시",
|
|
25
|
+
"Busiest agents": "가장 바쁜 에이전트",
|
|
26
|
+
"Buy a coffee": "커피 사 주기",
|
|
27
|
+
"cache read": "캐시 읽기",
|
|
28
|
+
"cache write": "캐시 쓰기",
|
|
29
|
+
"Call": "통화",
|
|
30
|
+
"Cartoon voices (real voice)": "만화 목소리 (실제 음성)",
|
|
31
|
+
"Claude Code is not logged in: run “claude” once in a terminal to sign in.": "Claude Code에 로그인되어 있지 않습니다. 터미널에서 “claude”를 한 번 실행해 로그인하세요.",
|
|
32
|
+
"Claude Code not found: install it (npm i -g @anthropic-ai/claude-code) or set its path in the “agentStandup.claudePath” setting.": "Claude Code를 찾을 수 없습니다. 설치하거나(npm i -g @anthropic-ai/claude-code) “agentStandup.claudePath” 설정에 경로를 지정하세요.",
|
|
33
|
+
"Click to enable sound and follow your Claude Code session live.": "클릭하면 소리가 켜지고 Claude Code 세션을 실시간으로 따라갑니다.",
|
|
34
|
+
"Close": "닫기",
|
|
35
|
+
"commands": "명령",
|
|
36
|
+
"compaction": "압축",
|
|
37
|
+
"Context": "컨텍스트",
|
|
38
|
+
"context ": "컨텍스트 ",
|
|
39
|
+
"context compacted 🗜": "컨텍스트 압축됨 🗜",
|
|
40
|
+
"Copied": "복사했습니다",
|
|
41
|
+
"Copy as Markdown": "Markdown으로 복사",
|
|
42
|
+
"Copy failed": "복사 실패",
|
|
43
|
+
"Demo — a team fixes a slow login": "데모 — 팀이 느린 로그인을 고칩니다",
|
|
44
|
+
"done": "완료",
|
|
45
|
+
"Done": "완료",
|
|
46
|
+
"Done. ": "완료. ",
|
|
47
|
+
"duration": "소요 시간",
|
|
48
|
+
"Editing ": "편집 중: ",
|
|
49
|
+
"Editor": "편집기",
|
|
50
|
+
"EPISODE 1": "제1화",
|
|
51
|
+
"Episode recap": "에피소드 요약",
|
|
52
|
+
"errors": "오류",
|
|
53
|
+
"failing in a loop?": "실패를 반복하는 중?",
|
|
54
|
+
"Fetching ": "가져오는 중: ",
|
|
55
|
+
"files": "파일",
|
|
56
|
+
"Files": "파일",
|
|
57
|
+
"files changed": "변경된 파일",
|
|
58
|
+
"Files touched": "건드린 파일",
|
|
59
|
+
"Final report": "최종 보고",
|
|
60
|
+
"first error": "첫 오류",
|
|
61
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "프랑스어는 시스템 음성으로 읽습니다. “향상된” 또는 “프리미엄” 음성을 설치하면 훨씬 좋아집니다.",
|
|
62
|
+
"Hide code & file names in videos": "동영상에서 코드와 파일 이름 숨기기",
|
|
63
|
+
"Highlights": "하이라이트",
|
|
64
|
+
"Human": "사람",
|
|
65
|
+
"idle": "유휴",
|
|
66
|
+
"input": "입력",
|
|
67
|
+
"is sharing their screen": "님이 화면 공유 중",
|
|
68
|
+
"Join the call": "통화 참가",
|
|
69
|
+
"Keep my agents’ own names": "내 에이전트 이름 그대로 사용",
|
|
70
|
+
"Language": "언어",
|
|
71
|
+
"Lead": "메인 에이전트",
|
|
72
|
+
"Lead context": "메인 에이전트 컨텍스트",
|
|
73
|
+
"left the call": "님이 통화에서 나갔습니다",
|
|
74
|
+
"lines": "줄",
|
|
75
|
+
"Listing ": "목록 보는 중: ",
|
|
76
|
+
"Little lines & animations": "짧은 대사와 애니메이션",
|
|
77
|
+
"Live": "라이브",
|
|
78
|
+
"longest wait ": "최장 대기 ",
|
|
79
|
+
"Looking for a tool": "도구 찾는 중",
|
|
80
|
+
"looping?": "반복 중?",
|
|
81
|
+
"Music volume": "음악 볼륨",
|
|
82
|
+
"Natural voices": "자연스러운 목소리",
|
|
83
|
+
"need you": "응답 필요",
|
|
84
|
+
"needs you?": "호출 중?",
|
|
85
|
+
"needs you? ": "호출 중? ",
|
|
86
|
+
"No agents were harmed.": "이 과정에서 다친 에이전트는 없습니다.",
|
|
87
|
+
"No edits.": "편집 없음.",
|
|
88
|
+
"No French voice installed (Windows: Settings › Time & language › Speech).": "프랑스어 음성이 설치되어 있지 않습니다 (Windows: 설정 › 시간 및 언어 › 음성).",
|
|
89
|
+
"No recent session": "최근 세션 없음",
|
|
90
|
+
"No setting found": "일치하는 설정 없음",
|
|
91
|
+
"No system voice found: agents will mumble.": "시스템 음성을 찾을 수 없습니다. 에이전트가 웅얼거립니다.",
|
|
92
|
+
"Notify me when agents finish": "에이전트가 끝나면 알림",
|
|
93
|
+
"Office ambience (keys, coffee…)": "사무실 환경음 (키보드, 커피…)",
|
|
94
|
+
"Office music (FIP radio)": "사무실 음악 (FIP 라디오)",
|
|
95
|
+
"OFFLINE": "오프라인",
|
|
96
|
+
"Opening file…": "파일 여는 중…",
|
|
97
|
+
"output": "출력",
|
|
98
|
+
"Pause / Play (space)": "일시정지 / 재생 (스페이스)",
|
|
99
|
+
"PAUSED": "일시정지됨",
|
|
100
|
+
"Project to follow": "따라갈 프로젝트",
|
|
101
|
+
"Prompt from": "프롬프트 보낸 이:",
|
|
102
|
+
"Radio unreachable: check your connection or pick another station.": "라디오에 연결할 수 없습니다. 연결을 확인하거나 다른 방송국을 선택하세요.",
|
|
103
|
+
"Reactions": "리액션",
|
|
104
|
+
"Reactions & jokes": "리액션과 농담",
|
|
105
|
+
"Reactions bar (👍 👏 ☕)": "리액션 바 (👍 👏 ☕)",
|
|
106
|
+
"read-only": "읽기 전용",
|
|
107
|
+
"Reading ": "읽는 중: ",
|
|
108
|
+
"READY": "준비 완료",
|
|
109
|
+
"Recent activity": "최근 활동",
|
|
110
|
+
"Remove finished agents (2 min)": "끝난 에이전트 제거 (2분 후)",
|
|
111
|
+
"Repeated failures": "반복된 실패",
|
|
112
|
+
"Replay": "다시 보기",
|
|
113
|
+
"REPLAY": "다시 보기",
|
|
114
|
+
"report": "보고",
|
|
115
|
+
"report to": "보고 대상:",
|
|
116
|
+
"Role": "역할",
|
|
117
|
+
"running": "실행 중",
|
|
118
|
+
"Running ": "실행 중: ",
|
|
119
|
+
"running…": "실행 중…",
|
|
120
|
+
"Said": "발언",
|
|
121
|
+
"Same call repeated": "같은 호출 반복",
|
|
122
|
+
"Search the transcript": "대화 기록 검색",
|
|
123
|
+
"Search…": "검색…",
|
|
124
|
+
"Searching “": "검색 중: “",
|
|
125
|
+
"session": "세션",
|
|
126
|
+
"Sessions": "세션",
|
|
127
|
+
"Settings": "설정",
|
|
128
|
+
"Simlish (babble)": "Simlish (옹알이)",
|
|
129
|
+
"Sound": "소리",
|
|
130
|
+
"Sound (M key)": "소리 (M 키)",
|
|
131
|
+
"Sound is playing in another Agent Standup window. Click 🔈 to take it over here.": "다른 Agent Standup 창에서 소리가 재생 중입니다. 🔈 를 클릭하면 이 창으로 가져옵니다.",
|
|
132
|
+
"Sound is playing in another window — click to take it over here": "다른 창에서 소리가 재생 중입니다 — 클릭하면 이 창으로 가져옵니다",
|
|
133
|
+
"Source": "소스",
|
|
134
|
+
"Speed": "속도",
|
|
135
|
+
"stalled": "멈춤",
|
|
136
|
+
"stalled · ": "멈춤 · ",
|
|
137
|
+
"standing by": "대기 중",
|
|
138
|
+
"Start Claude Code in this folder — the call will start by itself.": "이 폴더에서 Claude Code를 시작하세요 — 통화가 자동으로 시작됩니다.",
|
|
139
|
+
"Start Claude Code in {0} — the call will start by itself.": "{0}에서 Claude Code를 시작하세요 — 통화가 자동으로 시작됩니다.",
|
|
140
|
+
"sub-agents active": "서브에이전트 활동 중",
|
|
141
|
+
"task": "작업",
|
|
142
|
+
"Tasks": "작업",
|
|
143
|
+
"THE END": "끝",
|
|
144
|
+
"This dev was replaced by his own agents. Spare a coffee for his career change? ☕": "이 개발자는 자기 에이전트들에게 일자리를 빼앗겼습니다. 새 출발을 응원하는 커피 한 잔 어떠세요? ☕",
|
|
145
|
+
"Time": "시간",
|
|
146
|
+
"to": "대상:",
|
|
147
|
+
"tokens": "토큰",
|
|
148
|
+
"Tokens": "토큰",
|
|
149
|
+
"Tokens recorded by Claude Code (counted once per message, not a bill)": "Claude Code가 기록한 토큰 (메시지당 한 번 집계, 청구 금액 아님)",
|
|
150
|
+
"tool calls": "도구 호출",
|
|
151
|
+
"tool errors": "도구 오류",
|
|
152
|
+
"tools": "도구",
|
|
153
|
+
"Tools": "도구",
|
|
154
|
+
"Translation unavailable right now: check that “claude” works in a terminal.": "지금은 번역을 사용할 수 없습니다. 터미널에서 “claude”가 동작하는지 확인하세요.",
|
|
155
|
+
"Tree": "트리",
|
|
156
|
+
"Untitled session": "제목 없는 세션",
|
|
157
|
+
"Updating todo list": "할 일 목록 업데이트 중",
|
|
158
|
+
"Using skill ": "스킬 사용 중: ",
|
|
159
|
+
"VS Code was started from inside a Claude Code session: restart VS Code normally (Dock, Spotlight) to enable translation.": "VS Code가 Claude Code 세션 안에서 실행되었습니다. 번역을 사용하려면 VS Code를 일반적인 방법(Dock, Spotlight)으로 다시 시작하세요.",
|
|
160
|
+
"waiting": "대기 중",
|
|
161
|
+
"Waiting for a session": "세션을 기다리는 중",
|
|
162
|
+
"waiting on ": "기다리는 중: ",
|
|
163
|
+
"waiting on sub-agents": "서브에이전트를 기다리는 중",
|
|
164
|
+
"Web search": "웹 검색",
|
|
165
|
+
"working": "작업 중",
|
|
166
|
+
"Writing ": "작성 중: ",
|
|
167
|
+
"You": "나",
|
|
168
|
+
"Your agents are done 🎉": "에이전트 작업이 끝났습니다 🎉",
|
|
169
|
+
"Your Claude Code agents as a live team call": "Claude Code 에이전트를 실시간 팀 통화로",
|
|
170
|
+
"your turn": "내 차례",
|
|
171
|
+
"Characters": "캐릭터",
|
|
172
|
+
"Reload": "새로고침",
|
|
173
|
+
"Open folder": "폴더 열기",
|
|
174
|
+
"custom character(s) loaded": "개의 커스텀 캐릭터를 불러왔습니다",
|
|
175
|
+
"Demo": "데모",
|
|
176
|
+
"Drop your pixel art (PNG) in this folder to replace the characters:": "이 폴더에 픽셀 아트(PNG)를 넣으면 캐릭터가 바뀝니다:",
|
|
177
|
+
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = 메인 에이전트 · you.png = 나 · 나머지 = 서브에이전트 · name-talk.png = 입을 벌린 모습",
|
|
178
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "다운로드 중입니다. 계속 사용하셔도 됩니다. 그동안은 시스템 음성을 사용합니다.",
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "품질 좋은 로컬 음성으로, 영어입니다 (프랑스어는 시스템 음성을 사용). 무료이며 아무것도 컴퓨터 밖으로 전송되지 않습니다.",
|
|
180
|
+
"Installing…": "설치 중…",
|
|
181
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "모델 불러오는 중 (처음 사용할 때 다운로드, 약 305 MB)…",
|
|
182
|
+
"npm not found: install Node.js to enable this option.": "npm을 찾을 수 없습니다. 이 옵션을 사용하려면 Node.js를 설치하세요.",
|
|
183
|
+
"The engine stopped: system voices are used instead.": "엔진이 멈췄습니다. 대신 시스템 음성을 사용합니다.",
|
|
184
|
+
"Very natural voices (installs on demand)": "매우 자연스러운 목소리 (필요할 때 설치)",
|
|
185
|
+
"Voices ready.": "음성이 준비되었습니다.",
|
|
186
|
+
"Voice style": "목소리 스타일",
|
|
187
|
+
"⌨ Keyboard shortcuts (?)": "⌨ 키보드 단축키 (?)",
|
|
188
|
+
"Keyboard shortcuts": "키보드 단축키",
|
|
189
|
+
"Mute / unmute": "음소거 / 해제",
|
|
190
|
+
"Pause / play": "일시정지 / 재생",
|
|
191
|
+
"Recap": "요약",
|
|
192
|
+
"This help": "이 도움말",
|
|
193
|
+
"Transcript": "대화 기록",
|
|
194
|
+
"Tree view ↔ call": "트리 보기 ↔ 통화",
|
|
195
|
+
"Tree view: T to go back, ? for help": "트리 보기: T로 돌아가기, ?로 도움말",
|
|
196
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "창 위쪽에서 통화, 트리, 세션을 전환할 수도 있습니다.",
|
|
197
|
+
"Display": "표시",
|
|
198
|
+
"Search settings…": "설정 검색…",
|
|
199
|
+
"may be waiting for your approval": "승인을 기다리고 있을 수 있습니다",
|
|
200
|
+
"Next / previous agent that needs you": "응답이 필요한 다음 / 이전 에이전트",
|
|
201
|
+
"Nobody needs you right now": "지금 응답을 기다리는 에이전트가 없습니다",
|
|
202
|
+
"stalled: a sub-agent has been silent for a long time": "멈춤: 서브에이전트가 오랫동안 조용합니다",
|
|
203
|
+
"waiting on another agent": "다른 에이전트를 기다리는 중",
|
|
204
|
+
"What the states mean": "상태의 의미",
|
|
205
|
+
"Find a session": "세션 찾기",
|
|
206
|
+
"Find a session…": "세션 찾기…",
|
|
207
|
+
"Go to a session": "세션으로 이동",
|
|
208
|
+
"No highlight in this session": "이 세션에는 하이라이트가 없습니다",
|
|
209
|
+
"Replay: next / previous highlight": "다시 보기: 다음 / 이전 하이라이트",
|
|
210
|
+
"Replay: previous / next event": "다시 보기: 이전 / 다음 이벤트",
|
|
211
|
+
"Resumed where you left off": "마지막으로 본 곳부터 이어서 재생합니다",
|
|
212
|
+
"Chatter between agents": "에이전트 간 잡담",
|
|
213
|
+
"Lively": "활발하게",
|
|
214
|
+
"Normal": "보통",
|
|
215
|
+
"nothing to report": "보고할 것 없음",
|
|
216
|
+
"Quiet": "조용하게",
|
|
217
|
+
"Laptop in front of your own characters": "내 캐릭터 앞에 노트북 놓기",
|
|
218
|
+
"Also ": "또한 ",
|
|
219
|
+
" on ": " → ",
|
|
220
|
+
"To sum up: ": "정리하면: ",
|
|
221
|
+
"Cat pack": "고양이 팩",
|
|
222
|
+
"characters from the pack": "개의 팩 캐릭터",
|
|
223
|
+
"Drawn characters": "기본 캐릭터",
|
|
224
|
+
"To tweak them, copy them into your folder and pick “My folder”.": "수정하려면 내 폴더에 복사한 뒤 “내 폴더”를 선택하세요.",
|
|
225
|
+
"My folder": "내 폴더",
|
|
226
|
+
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "앱이 그리는 캐릭터입니다. 직접 만든 이미지를 쓰려면 “내 폴더”를, 아니면 팩을 선택하세요."
|
|
227
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"Reload": "Recarregar",
|
|
9
9
|
"Open folder": "Abrir pasta",
|
|
10
10
|
"custom character(s) loaded": "personagem(ns) personalizado(s) carregado(s)",
|
|
11
|
+
"Demo": "Demo",
|
|
11
12
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Solte sua pixel art (PNG) nesta pasta para substituir os personagens:",
|
|
12
13
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = o agente principal · you.png = você · outros = os subagentes · name-talk.png = boca aberta",
|
|
13
14
|
" full — compaction soon": " cheio — compactação em breve",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"Files touched": "Arquivos modificados",
|
|
65
66
|
"Final report": "Relatório final",
|
|
66
67
|
"first error": "primeiro erro",
|
|
68
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "O francês é lido pelas vozes do seu sistema: instalar uma “aprimorada” ou “premium” faz muita diferença.",
|
|
67
69
|
"Hide code & file names in videos": "Ocultar código e arquivos nos vídeos",
|
|
68
70
|
"Highlights": "Destaques",
|
|
69
71
|
"Human": "Humano",
|
|
@@ -71,6 +73,7 @@
|
|
|
71
73
|
"input": "entrada",
|
|
72
74
|
"is sharing their screen": "está compartilhando a tela",
|
|
73
75
|
"Join the call": "Entrar na chamada",
|
|
76
|
+
"Keep my agents’ own names": "Manter os nomes dos meus agentes",
|
|
74
77
|
"Language": "Idioma",
|
|
75
78
|
"Lead": "Agente principal",
|
|
76
79
|
"Lead context": "Contexto do agente principal",
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
"lines": "linhas",
|
|
79
82
|
"Listing ": "Listando ",
|
|
80
83
|
"Little lines & animations": "Falas e animações",
|
|
84
|
+
"Live": "Ao vivo",
|
|
81
85
|
"longest wait ": "maior espera ",
|
|
82
86
|
"Looking for a tool": "Procurando uma ferramenta",
|
|
83
87
|
"looping?": "em loop?",
|
|
@@ -112,6 +116,8 @@
|
|
|
112
116
|
"Recent activity": "Atividade recente",
|
|
113
117
|
"Remove finished agents (2 min)": "Remover agentes concluídos (2 min)",
|
|
114
118
|
"Repeated failures": "Falhas repetidas",
|
|
119
|
+
"Replay": "Replay",
|
|
120
|
+
"REPLAY": "REPLAY",
|
|
115
121
|
"report": "relatório",
|
|
116
122
|
"report to": "relatório para",
|
|
117
123
|
"Role": "Função",
|
|
@@ -170,7 +176,7 @@
|
|
|
170
176
|
"Your Claude Code agents as a live team call": "Seus agentes do Claude Code em uma chamada de equipe ao vivo",
|
|
171
177
|
"your turn": "sua vez",
|
|
172
178
|
"Downloading, you can carry on: system voices are used meanwhile.": "Baixando, pode continuar: enquanto isso, as vozes do sistema são usadas.",
|
|
173
|
-
"Good-quality local voices,
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Vozes locais de boa qualidade, em inglês (o francês usa as vozes do seu sistema). Grátis, nada sai do seu computador.",
|
|
174
180
|
"Installing…": "Instalando…",
|
|
175
181
|
"Loading the model (downloaded on first use, about 305 MB)…": "Carregando o modelo (baixado no primeiro uso, cerca de 305 MB)…",
|
|
176
182
|
"npm not found: install Node.js to enable this option.": "npm não encontrado: instale o Node.js para ativar esta opção.",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"Files touched": "涉及的文件",
|
|
59
59
|
"Final report": "最终报告",
|
|
60
60
|
"first error": "首个错误",
|
|
61
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "法语由系统语音朗读:安装“增强”或“优质”语音会好很多。",
|
|
61
62
|
"Hide code & file names in videos": "在视频中隐藏代码和文件名",
|
|
62
63
|
"Highlights": "精彩时刻",
|
|
63
64
|
"Human": "真人",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"input": "输入",
|
|
66
67
|
"is sharing their screen": "正在共享屏幕",
|
|
67
68
|
"Join the call": "加入视频通话",
|
|
69
|
+
"Keep my agents’ own names": "保留我的智能体自己的名字",
|
|
68
70
|
"Language": "语言",
|
|
69
71
|
"Lead": "主代理",
|
|
70
72
|
"Lead context": "主代理上下文",
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"lines": "行",
|
|
73
75
|
"Listing ": "正在列出 ",
|
|
74
76
|
"Little lines & animations": "小台词和动画",
|
|
77
|
+
"Live": "实时",
|
|
75
78
|
"longest wait ": "最长等待 ",
|
|
76
79
|
"Looking for a tool": "正在找工具",
|
|
77
80
|
"looping?": "在循环?",
|
|
@@ -106,6 +109,8 @@
|
|
|
106
109
|
"Recent activity": "最近动态",
|
|
107
110
|
"Remove finished agents (2 min)": "移除已完成的代理(2 分钟后)",
|
|
108
111
|
"Repeated failures": "反复失败",
|
|
112
|
+
"Replay": "回放",
|
|
113
|
+
"REPLAY": "回放",
|
|
109
114
|
"report": "报告",
|
|
110
115
|
"report to": "汇报给",
|
|
111
116
|
"Role": "角色",
|
|
@@ -167,10 +172,11 @@
|
|
|
167
172
|
"Reload": "重新加载",
|
|
168
173
|
"Open folder": "打开文件夹",
|
|
169
174
|
"custom character(s) loaded": "个自定义角色已加载",
|
|
175
|
+
"Demo": "演示",
|
|
170
176
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "把你的像素画(PNG)放进此文件夹即可替换角色:",
|
|
171
177
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = 主代理 · you.png = 你 · 其他 = 子代理 · name-talk.png = 张嘴",
|
|
172
178
|
"Downloading, you can carry on: system voices are used meanwhile.": "正在下载,你可以继续使用:期间会使用系统语音。",
|
|
173
|
-
"Good-quality local voices,
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "高质量的本地语音,仅英语(法语使用系统语音)。免费,数据不会离开你的电脑。",
|
|
174
180
|
"Installing…": "正在安装…",
|
|
175
181
|
"Loading the model (downloaded on first use, about 305 MB)…": "正在加载模型(首次使用时下载,约 305 MB)…",
|
|
176
182
|
"npm not found: install Node.js to enable this option.": "未找到 npm:请安装 Node.js 以启用此选项。",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"Reload": "重新載入",
|
|
55
55
|
"Open folder": "開啟資料夾",
|
|
56
56
|
"custom character(s) loaded": "個自訂角色已載入",
|
|
57
|
+
"Demo": "示範",
|
|
57
58
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "把你的像素畫(PNG)放進此資料夾即可替換角色:",
|
|
58
59
|
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = 主代理 · you.png = 你 · 其他 = 子代理 · name-talk.png = 張嘴畫面",
|
|
59
60
|
"failing in a loop?": "卡在失敗迴圈?",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"Files touched": "異動的檔案",
|
|
65
66
|
"Final report": "最終報告",
|
|
66
67
|
"first error": "首次錯誤",
|
|
68
|
+
"French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "法文由系統語音朗讀:安裝“增強”或“高品質”語音會好很多。",
|
|
67
69
|
"Hide code & file names in videos": "在影片中隱藏程式碼與檔名",
|
|
68
70
|
"Highlights": "精華",
|
|
69
71
|
"Human": "真人",
|
|
@@ -71,6 +73,7 @@
|
|
|
71
73
|
"input": "輸入",
|
|
72
74
|
"is sharing their screen": "正在分享畫面",
|
|
73
75
|
"Join the call": "加入視訊通話",
|
|
76
|
+
"Keep my agents’ own names": "保留我的代理自己的名字",
|
|
74
77
|
"Language": "語言",
|
|
75
78
|
"Lead": "主代理",
|
|
76
79
|
"Lead context": "主代理上下文",
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
"lines": "行",
|
|
79
82
|
"Listing ": "列出 ",
|
|
80
83
|
"Little lines & animations": "小對白與動畫",
|
|
84
|
+
"Live": "即時",
|
|
81
85
|
"longest wait ": "最長等待 ",
|
|
82
86
|
"Looking for a tool": "正在找工具",
|
|
83
87
|
"looping?": "鬼打牆?",
|
|
@@ -112,6 +116,8 @@
|
|
|
112
116
|
"Recent activity": "最近活動",
|
|
113
117
|
"Remove finished agents (2 min)": "移除已完成的代理(2 分鐘)",
|
|
114
118
|
"Repeated failures": "重複失敗",
|
|
119
|
+
"Replay": "重播",
|
|
120
|
+
"REPLAY": "重播",
|
|
115
121
|
"report": "報告",
|
|
116
122
|
"report to": "回報給",
|
|
117
123
|
"Role": "角色",
|
|
@@ -170,7 +176,7 @@
|
|
|
170
176
|
"Your Claude Code agents as a live team call": "把你的 Claude Code 代理變成即時團隊通話",
|
|
171
177
|
"your turn": "換你了",
|
|
172
178
|
"Downloading, you can carry on: system voices are used meanwhile.": "下載中,你可以繼續使用:這段時間會先用系統語音。",
|
|
173
|
-
"Good-quality local voices,
|
|
179
|
+
"Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "品質不錯的本機語音,僅英文(法文使用系統語音)。免費,資料不會離開你的電腦。",
|
|
174
180
|
"Installing…": "安裝中…",
|
|
175
181
|
"Loading the model (downloaded on first use, about 305 MB)…": "正在載入模型(第一次使用時下載,約 305 MB)…",
|
|
176
182
|
"npm not found: install Node.js to enable this option.": "找不到 npm:請安裝 Node.js 才能啟用此選項。",
|
package/server.js
CHANGED
|
@@ -9,7 +9,7 @@ const crypto = require('crypto');
|
|
|
9
9
|
const codex = require('./codex.js'); // Codex sessions, read as the same call (see codex.js)
|
|
10
10
|
const kokoro = require('./kokoro.js'); // natural voices, installed on demand (see kokoro.js)
|
|
11
11
|
const zlib = require('zlib');
|
|
12
|
-
const { execFileSync } = require('child_process');
|
|
12
|
+
const { execFileSync, execFile } = require('child_process');
|
|
13
13
|
|
|
14
14
|
const PORT = process.env.PORT || 4747;
|
|
15
15
|
// Same place Claude Code writes to: honours CLAUDE_CONFIG_DIR (custom config dirs are common on Linux); AGENT_STANDUP_PROJECTS overrides it.
|
|
@@ -336,7 +336,7 @@ function parseRecord(st, r) {
|
|
|
336
336
|
else if (b.type === 'text' && typeof b.text === 'string' && b.text.trim()) { said = push({ ts, agent, kind: 'say', text: b.text.trim(), ...th() }); st.act = st.raw.length - 1; st.sayOf = m.id; st.sayAt = st.act; }
|
|
337
337
|
else if (b.type === 'tool_use') {
|
|
338
338
|
st.toolName[b.id] = b.name; if (b.input && b.input.run_in_background) (st.bg = st.bg || {})[b.id] = true;
|
|
339
|
-
if (b.name === 'Agent' || b.name === 'Task') push({ ts, agent, kind: 'agentcall', toolId: b.id, bg: !!(b.input && b.input.run_in_background), text: (b.input && b.input.prompt) || '', title: (b.input && b.input.description) || '' });
|
|
339
|
+
if (b.name === 'Agent' || b.name === 'Task') push({ ts, agent, kind: 'agentcall', toolId: b.id, bg: !!(b.input && b.input.run_in_background), text: (b.input && b.input.prompt) || '', title: (b.input && b.input.description) || '', ...(b.input && typeof b.input.name === 'string' && b.input.name.trim() ? { nm: b.input.name.trim() } : {}) });
|
|
340
340
|
else { push({ ts, agent, kind: 'tool', tool: toolLabel(b, ts), ...th() }); st.act = st.raw.length - 1; }
|
|
341
341
|
}
|
|
342
342
|
}
|
|
@@ -347,6 +347,15 @@ function parseRecord(st, r) {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
const codexRecord = codex.parser({ toolLabel, clip, resultMax: RESULT_MAX });
|
|
350
|
+
// A sub-agent the person named keeps that name on screen: the name given when it was launched, or else its type when that type is the person's own (.claude/agents/code-reviewer.md → "Code Reviewer").
|
|
351
|
+
// The types Claude Code ships are not names: those agents get a first name from the page, as before.
|
|
352
|
+
const STOCK_AGENT = /^(general-purpose|explore|plan|claude|agent|task|fork|main|default|worker|subagent|workflow-subagent|statusline-setup|output-style-setup|claude-code-guide|verification|magic-docs)$/i;
|
|
353
|
+
function ownName(s, type) {
|
|
354
|
+
const raw = String(s || '').split(':').pop().trim(); if (!raw || (type && STOCK_AGENT.test(raw))) return ''; // a plugin's agent is written plugin:agent
|
|
355
|
+
let n = raw.replace(/[_\-]+/g, ' ').replace(/[^\p{L}\p{N} .'’&+]/gu, ' ').replace(/\s+/g, ' ').trim(); if (!/[\p{L}\p{N}]/u.test(n)) return '';
|
|
356
|
+
if (n === n.toLowerCase()) n = n.replace(/(^|\s)\p{Ll}/gu, (m) => m.toUpperCase());
|
|
357
|
+
return n.length > 26 ? n.slice(0, 25).trimEnd() + '…' : n;
|
|
358
|
+
}
|
|
350
359
|
const metaCache = new Map(); // meta file path → { mtime, value }
|
|
351
360
|
function readMeta(f) {
|
|
352
361
|
try { const mt = fs.statSync(f).mtimeMs, c = metaCache.get(f); if (c && c.mtime === mt) return c.value; const v = JSON.parse(fs.readFileSync(f, 'utf8')); if (metaCache.size > 3000) metaCache.clear(); metaCache.set(f, { mtime: mt, value: v }); return v; } catch { return {}; }
|
|
@@ -367,6 +376,10 @@ function buildTimeline(proj, id, files = []) {
|
|
|
367
376
|
for (const j of subs.journals) { const st = readNew(j.file, 'wf:' + j.run, true), run = runs[j.run]; files.push(j.file); if (st && run) for (const e of st.raw) if (run.agents[e.child]) run.done.add(run.agents[e.child]); }
|
|
368
377
|
const childByTool = {}; // Agent tool_use id → the sub-agent it created, so we know who ordered whom
|
|
369
378
|
for (const a of Object.values(agents)) if (a.toolUseId) childByTool[a.toolUseId] = a.id;
|
|
379
|
+
const called = {}, born = {}; for (const st of states) if (st) { if (st.raw.length) born[st.agent] = st.raw[0].ts; for (const e of st.raw) if (e.kind === 'agentcall' && e.nm && childByTool[e.toolId]) called[childByTool[e.toolId]] = e.nm; }
|
|
380
|
+
const sameName = {}; for (const a of Object.values(agents).filter((x) => x.id !== 'main').sort((x, y) => (born[x.id] || Infinity) - (born[y.id] || Infinity) || (x.id < y.id ? -1 : 1))) { // in the order they started, so that a second agent of the same name is the one numbered 2 and stays so
|
|
381
|
+
const n = ownName(called[a.id]) || ownName(a.type, true); if (n) { const k = n.toLowerCase(); sameName[k] = (sameName[k] || 0) + 1; a.own = sameName[k] > 1 ? n + ' ' + sameName[k] : n; }
|
|
382
|
+
}
|
|
370
383
|
const runByTool = {}; for (const st of states) if (st) for (const e of st.raw) if (e.kind === 'wfrun' && runs[e.run] && !runs[e.run].by) { runs[e.run].by = e.agent; runs[e.run].toolId = e.toolId; runByTool[e.toolId] = runs[e.run]; } // Workflow tool_use id → the run it started
|
|
371
384
|
|
|
372
385
|
const finished = new Set(), now0 = Date.now(); for (const st of states) if (st) for (const e of st.raw) if (e.kind === 'agentdone') { if (childByTool[e.toolId]) finished.add(childByTool[e.toolId]); else if (runByTool[e.toolId]) for (const a of Object.values(runByTool[e.toolId].agents)) finished.add(a); } // a run that stopped: so did every agent in it
|
|
@@ -520,25 +533,25 @@ const quips = (texts, lang) => claudeMap('q' + lang, `Each string is a work repo
|
|
|
520
533
|
|
|
521
534
|
// ---- text-to-speech, generated on the machine and cached as wav:
|
|
522
535
|
// macOS → `say`, Windows → System.Speech (SAPI voices) through PowerShell, Linux → espeak-ng.
|
|
523
|
-
let sayVoices = null;
|
|
536
|
+
let sayVoices = null, sayVoicesJob = null;
|
|
524
537
|
const WIN = process.platform === 'win32', MAC = process.platform === 'darwin';
|
|
525
538
|
const PS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command'];
|
|
539
|
+
const VOICES_CMD = MAC ? ['say', ['-v', '?'], 5000] : WIN ? ['powershell.exe', [...PS, "Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name + '|' + $_.VoiceInfo.Culture.Name }"], 20000] : ['espeak-ng', ['--voices'], 5000];
|
|
540
|
+
function parseVoices(out) {
|
|
541
|
+
const voices = [];
|
|
542
|
+
if (MAC) { for (const line of out.split('\n')) { const m = line.match(/^(.+?)\s{2,}([a-z]{2}_[A-Z]{2})\s+#/); if (m) voices.push({ name: m[1].trim(), locale: m[2] }); } }
|
|
543
|
+
else if (WIN) { for (const line of out.split(/\r?\n/)) { const [name, loc] = line.trim().split('|'); if (name && loc) voices.push({ name, locale: loc.replace('-', '_') }); } }
|
|
544
|
+
else for (const line of out.split('\n').slice(1)) { const c = line.trim().split(/\s+/); if (c[1] && /^(fr|en)/.test(c[1])) voices.push({ name: c[1], locale: c[1].replace('-', '_').replace(/^(\w{2})$/, (m) => m + '_' + m.toUpperCase()) }); }
|
|
545
|
+
return { available: voices.length > 0, voices, platform: process.platform };
|
|
546
|
+
}
|
|
526
547
|
function listVoices() {
|
|
527
548
|
if (sayVoices) return sayVoices;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const out = execFileSync('powershell.exe', [...PS, "Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name + '|' + $_.VoiceInfo.Culture.Name }"], { encoding: 'utf8', timeout: 20000, windowsHide: true });
|
|
535
|
-
for (const line of out.split(/\r?\n/)) { const [name, loc] = line.trim().split('|'); if (name && loc) voices.push({ name, locale: loc.replace('-', '_') }); }
|
|
536
|
-
} else {
|
|
537
|
-
const out = execFileSync('espeak-ng', ['--voices'], { encoding: 'utf8', timeout: 5000 });
|
|
538
|
-
for (const line of out.split('\n').slice(1)) { const c = line.trim().split(/\s+/); if (c[1] && /^(fr|en)/.test(c[1])) voices.push({ name: c[1], locale: c[1].replace('-', '_').replace(/^(\w{2})$/, (m) => m + '_' + m.toUpperCase()) }); }
|
|
539
|
-
}
|
|
540
|
-
} catch {}
|
|
541
|
-
return (sayVoices = { available: voices.length > 0, voices, platform: process.platform });
|
|
549
|
+
let out = ''; try { out = execFileSync(VOICES_CMD[0], VOICES_CMD[1], { encoding: 'utf8', timeout: VOICES_CMD[2], windowsHide: true }); } catch {}
|
|
550
|
+
return (sayVoices = parseVoices(out));
|
|
551
|
+
}
|
|
552
|
+
function loadVoices() { // the same list for the page, asked without waiting: the first answer of the system takes a second or more, and the session the page asks for at the same moment must not wait behind it
|
|
553
|
+
if (sayVoices) return Promise.resolve(sayVoices);
|
|
554
|
+
return sayVoicesJob || (sayVoicesJob = new Promise((resolve) => { try { execFile(VOICES_CMD[0], VOICES_CMD[1], { encoding: 'utf8', timeout: VOICES_CMD[2], windowsHide: true }, (err, out) => resolve(sayVoices || (sayVoices = parseVoices(err ? '' : String(out))))); } catch { resolve(sayVoices || (sayVoices = parseVoices(''))); } }));
|
|
542
555
|
}
|
|
543
556
|
const ttsDir = path.join(os.tmpdir(), 'agent-standup-tts-' + (process.getuid ? process.getuid() : 'user'));
|
|
544
557
|
try { fs.mkdirSync(ttsDir, { recursive: true, mode: 0o700 }); } catch {}
|
|
@@ -556,7 +569,7 @@ function synthesize(voice, wpm, text, out) {
|
|
|
556
569
|
p.on('error', reject); p.on('close', (code) => (code === 0 && fs.existsSync(out) ? resolve() : reject(new Error('tts failed'))));
|
|
557
570
|
});
|
|
558
571
|
}
|
|
559
|
-
function tts(text, voice, rate, engine) {
|
|
572
|
+
function tts(text, voice, rate, engine, fb) {
|
|
560
573
|
const kk = engine === 'kokoro';
|
|
561
574
|
if (kk) { if (!kokoro.hasVoice(voice)) return Promise.reject(new Error('unknown voice')); }
|
|
562
575
|
else { const vs = listVoices(); if (!vs.available || !vs.voices.some((v) => v.name === voice)) return Promise.reject(new Error('unknown voice')); }
|
|
@@ -566,7 +579,7 @@ function tts(text, voice, rate, engine) {
|
|
|
566
579
|
if (ttsJobs.has(key)) return ttsJobs.get(key);
|
|
567
580
|
fs.mkdirSync(ttsDir, { recursive: true });
|
|
568
581
|
const tmp = file + '.part.wav';
|
|
569
|
-
const job = (kk ? kokoro.synth(text, voice, kokoro.speedFor(rate), tmp).catch((e) => { const vs = listVoices(), v = vs.voices.find((x) => x.
|
|
582
|
+
const job = (kk ? kokoro.synth(text, voice, kokoro.speedFor(rate), tmp).catch((e) => { const vs = listVoices(), v = vs.voices.find((x) => x.name === fb) || vs.voices.find((x) => x.locale.startsWith('en_')); if (!v) throw e; return synthesize(v.name, rate, text, tmp); }) : synthesize(voice, rate, text, tmp)) // a line Kokoro cannot make goes to a system voice instead of being dropped: the one the page names (fb, its best system voice for this character), or else the first English one
|
|
570
583
|
.then(() => { fs.renameSync(tmp, file); return file; }).finally(() => ttsJobs.delete(key));
|
|
571
584
|
ttsJobs.set(key, job); return job;
|
|
572
585
|
}
|
|
@@ -628,12 +641,12 @@ function start(port = PORT) {
|
|
|
628
641
|
if (url.pathname === '/api/warm') { trWorkers.forEach((w) => { if (!w.p) w.start(); }); return json({ ok: true }); }
|
|
629
642
|
if (url.pathname === '/api/voices') {
|
|
630
643
|
if (url.searchParams.get('engine') === 'kokoro') { const st = kokoro.status(); if (st.installed && !st.gaveUp) kokoro.warm(); return json({ available: st.installed && !st.gaveUp, ready: st.ready, voices: kokoro.voices(), platform: process.platform, engine: 'kokoro' }); }
|
|
631
|
-
return
|
|
644
|
+
return loadVoices().then((v) => json(v));
|
|
632
645
|
}
|
|
633
646
|
if (url.pathname === '/api/kokoro/install' && req.method === 'POST') return json(kokoro.install());
|
|
634
647
|
if (url.pathname === '/api/kokoro') return json(kokoro.status());
|
|
635
648
|
if (url.pathname === '/api/tts') {
|
|
636
|
-
return tts(url.searchParams.get('text') || '', url.searchParams.get('voice'), url.searchParams.get('rate'), url.searchParams.get('engine'))
|
|
649
|
+
return tts(url.searchParams.get('text') || '', url.searchParams.get('voice'), url.searchParams.get('rate'), url.searchParams.get('engine'), url.searchParams.get('fb'))
|
|
637
650
|
.then((f) => { res.writeHead(200, { 'content-type': 'audio/wav', 'cache-control': 'max-age=86400' }); sendFile(f); })
|
|
638
651
|
.catch((e) => json({ error: String(e.message) }, 400));
|
|
639
652
|
}
|