@double_geste/agent-standup 1.28.0 → 1.30.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 +3 -3
- package/bin/agent-standup.js +4 -2
- package/codex.js +2 -2
- package/kokoro.js +2 -2
- package/package.json +4 -3
- package/public/index.html +48 -30
- 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 +7 -1
- 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 +63 -45
- package/vibe.js +322 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Standup (command line)
|
|
2
2
|
|
|
3
|
-
Your Claude Code and
|
|
3
|
+
Your Claude Code, Codex and Mistral Vibe sessions as a live team call, without any editor. Free, MIT, and it never calls Claude: it only reads the session files Claude Code, Codex and Mistral Vibe already write.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @double_geste/agent-standup # then open http://localhost:4747 (add --open to open it for you)
|
|
@@ -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
|
-
Unofficial: not affiliated with Anthropic or
|
|
18
|
+
Unofficial: not affiliated with Anthropic, OpenAI or Mistral AI.
|
package/bin/agent-standup.js
CHANGED
|
@@ -9,12 +9,12 @@ const pkg = require('../package.json');
|
|
|
9
9
|
const argv = process.argv.slice(2), flags = {}, rest = [];
|
|
10
10
|
for (let i = 0; i < argv.length; i++) {
|
|
11
11
|
const a = argv[i], val = () => argv[++i];
|
|
12
|
-
if (a === '--port') flags.port = val(); else if (a === '--projects') flags.projects = val(); else if (a === '--codex') flags.codex = val(); else if (a === '--characters') flags.characters = val(); else if (a === '--config-dir') flags.config = val();
|
|
12
|
+
if (a === '--port') flags.port = val(); else if (a === '--projects') flags.projects = val(); else if (a === '--codex') flags.codex = val(); else if (a === '--vibe') flags.vibe = val(); else if (a === '--characters') flags.characters = val(); else if (a === '--config-dir') flags.config = val();
|
|
13
13
|
else if (a === '--open') flags.open = true; else if (a === '--here') flags.here = true; else if (a === '--json') flags.json = true; else if (a === '--titles') flags.titles = true;
|
|
14
14
|
else if (a === '-h' || a === '--help') flags.help = true; else if (a === '-v' || a === '--version') flags.version = true; else rest.push(a);
|
|
15
15
|
}
|
|
16
16
|
const cmd = rest[0] || 'serve';
|
|
17
|
-
const HELP = `Agent Standup ${pkg.version} — your Claude Code and
|
|
17
|
+
const HELP = `Agent Standup ${pkg.version} — your Claude Code, Codex and Mistral Vibe agents as a live video call
|
|
18
18
|
|
|
19
19
|
Usage
|
|
20
20
|
agent-standup [serve] [--port 4747] [--open] start the local server (http://localhost:4747)
|
|
@@ -26,6 +26,7 @@ Options
|
|
|
26
26
|
--open open the page in your browser
|
|
27
27
|
--projects DIR where the session files are (default ~/.claude/projects, or $CLAUDE_CONFIG_DIR/projects)
|
|
28
28
|
--codex DIR where Codex keeps its files (default ~/.codex, or $CODEX_HOME); "off" to leave Codex sessions out
|
|
29
|
+
--vibe DIR where Mistral Vibe keeps its files (default ~/.vibe, or $VIBE_HOME); "off" to leave Vibe sessions out
|
|
29
30
|
--characters DIR folder with your own pixel-art characters (default ~/.agent-standup/characters)
|
|
30
31
|
--here status: only sessions of the current folder (matched on its real path, as Claude Code names it)
|
|
31
32
|
--json status: machine-readable output (add --titles to include session titles)
|
|
@@ -34,6 +35,7 @@ if (flags.help || cmd === 'help') { process.stdout.write(HELP); process.exit(0);
|
|
|
34
35
|
if (flags.version || cmd === 'version') { console.log(pkg.version); process.exit(0); }
|
|
35
36
|
if (flags.projects) process.env.AGENT_STANDUP_PROJECTS = path.resolve(flags.projects);
|
|
36
37
|
if (flags.codex !== undefined) process.env.AGENT_STANDUP_CODEX = /^(0|off|no|false)?$/i.test(String(flags.codex)) ? 'off' : path.resolve(String(flags.codex));
|
|
38
|
+
if (flags.vibe !== undefined) process.env.AGENT_STANDUP_VIBE = /^(0|off|no|false)?$/i.test(String(flags.vibe)) ? 'off' : path.resolve(String(flags.vibe));
|
|
37
39
|
if (flags.config) process.env.CLAUDE_CONFIG_DIR = path.resolve(flags.config);
|
|
38
40
|
if (flags.characters) process.env.AGENT_STANDUP_CHARACTERS = path.resolve(flags.characters);
|
|
39
41
|
if (flags.port !== undefined) { // a real port number or nothing: no silent fallback to another port
|
package/codex.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// events a Claude Code record gives (say, user, tool, results, usage, compact), so everything after that is shared: the timeline, the live feed, the page.
|
|
5
5
|
// All Codex sessions are listed under one made-up project, PROJ. No Claude Code project folder can have that name: theirs are paths with every other character turned into "-".
|
|
6
6
|
const fs = require('fs'), os = require('os'), path = require('path');
|
|
7
|
-
const PROJ = '_codex';
|
|
7
|
+
const PROJ = '_codex', SOURCE = 'codex';
|
|
8
8
|
// Where Codex keeps its files: AGENT_STANDUP_CODEX (a folder; "0" or "off" turns Codex sessions off), else $CODEX_HOME, else ~/.codex.
|
|
9
9
|
// When AGENT_STANDUP_PROJECTS points the app at one folder of Claude Code sessions (--projects, the tests), Codex is only read if it is asked for too.
|
|
10
10
|
function home() {
|
|
@@ -206,4 +206,4 @@ function parser({ toolLabel, clip, resultMax = {} }) {
|
|
|
206
206
|
if (p.type === 'web_search_call' && !cx.items && id) call(id, 'WebSearch', { query: (p.action && p.action.query) || '' });
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
|
-
module.exports = { PROJ, home, dirKey, sessions, row, fileOf, inProject, projectLabel, title, boardRow, parser };
|
|
209
|
+
module.exports = { PROJ, SOURCE, home, dirKey, sessions, row, fileOf, inProject, projectLabel, title, boardRow, parser };
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Your Claude Code and
|
|
3
|
+
"version": "1.30.0",
|
|
4
|
+
"description": "Your Claude Code, Codex and Mistral Vibe 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"
|
|
7
7
|
},
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"server.js",
|
|
12
12
|
"codex.js",
|
|
13
|
+
"vibe.js",
|
|
13
14
|
"kokoro.js",
|
|
14
15
|
"kokoro-worker.js",
|
|
15
16
|
"demo.js",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"type": "git",
|
|
28
29
|
"url": "git+https://github.com/Atoll92/agent-standup.git"
|
|
29
30
|
},
|
|
30
|
-
"homepage": "https://
|
|
31
|
+
"homepage": "https://agent-standup.com",
|
|
31
32
|
"bugs": {
|
|
32
33
|
"url": "https://github.com/Atoll92/agent-standup/issues"
|
|
33
34
|
},
|
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;
|
|
@@ -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 };
|
|
416
|
-
if (a.id === 'main') return { ...base, name: a.name || '
|
|
421
|
+
const h = hash(a.id), base = { ...a, given: 'given' in a ? a.given : a.name };
|
|
422
|
+
if (a.id === 'main') return { ...base, name: a.name || 'Margot', 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;
|
|
@@ -1912,7 +1930,7 @@ async function loadSession(proj, id) {
|
|
|
1912
1930
|
if (proj === '_demo') S.events.forEach((e) => { if (e.fr) trMem.set(baseText(e), e.fr); }); // the demo ships its own French
|
|
1913
1931
|
$('#subtitle').textContent = S.title; recomputeStatus(0); buildStage(); renderHighlights(); setFocus('main'); renderLog(true); computeGantt(); layout(); paintStatus(); prefetchTr(0); return true;
|
|
1914
1932
|
}
|
|
1915
|
-
const niceProj = (p) => (p === '_demo' ? '' : p === '_codex' ? 'Codex' : String(p).replace(/^-+/, '').replace(/-/g, '/').split('/').slice(-2).join('/'));
|
|
1933
|
+
const niceProj = (p) => (p === '_demo' ? '' : p === '_codex' ? 'Codex' : p === '_vibe' ? 'Mistral Vibe' : String(p).replace(/^-+/, '').replace(/-/g, '/').split('/').slice(-2).join('/'));
|
|
1916
1934
|
async function fillProjects() { // live view: follow the latest activity anywhere, or pin one project
|
|
1917
1935
|
const sel = $('#liveProj'); let rows = []; try { rows = await (await fetch('/api/projects')).json(); } catch {}
|
|
1918
1936
|
sel.innerHTML = `<option value="">${L('Tous les projets (le plus récent)', 'All projects (most recent)')}</option>` + rows.map((r) => `<option value="${esc(r.proj)}">${esc(niceProj(r.proj))}</option>`).join('');
|
|
@@ -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');
|
|
@@ -2265,7 +2283,7 @@ function paintPill() {
|
|
|
2265
2283
|
const what = (id) => `${nameOf(id)} — ${st[id].txt.replace(/^\S+\s/, '')}`, more = (a) => (a.length > 1 ? ` +${a.length - 1}` : ''), lead = st.main || { k: 'idle' };
|
|
2266
2284
|
if (by.blocked.length) { cls = 'blocked'; to = by.blocked[0]; txt = '✋ ' + what(to) + more(by.blocked); }
|
|
2267
2285
|
else if (by.stalled.length) { cls = 'stalled'; to = by.stalled[0]; txt = '⚠ ' + what(to) + more(by.stalled); }
|
|
2268
|
-
else if (by.working.length || by.waiting.length) { // "2 working ·
|
|
2286
|
+
else if (by.working.length || by.waiting.length) { // "2 working · Margot — waiting on Ken, Mira", or what the only one at work is doing
|
|
2269
2287
|
const w = by.working, wt = lead.k === 'waiting' ? 'main' : w.length ? '' : by.waiting[0]; cls = 'working'; to = wt || (w.length === 1 ? w[0] : 'main');
|
|
2270
2288
|
txt = (w.length === 1 && !wt ? '⚙ ' + what(w[0]) : w.length ? `⚙ ${w.length} ${L('au travail', 'working')}` : '') + (wt ? (w.length ? ' · ' : '🔗 ') + what(wt) : '');
|
|
2271
2289
|
}
|
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 をインストールしてください。",
|