@double_geste/agent-standup 1.22.0 → 1.23.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/bin/agent-standup.js +6 -3
- package/kokoro-worker.js +25 -0
- package/kokoro.js +82 -0
- package/package.json +3 -1
- package/public/index.html +94 -30
- package/public/locales/de.json +12 -1
- package/public/locales/en.json +11 -0
- package/public/locales/es.json +12 -1
- package/public/locales/ja.json +12 -1
- package/public/locales/pt-BR.json +17 -2
- package/public/locales/zh-CN.json +12 -1
- package/public/locales/zh-TW.json +17 -6
- package/public/meme.css +60 -0
- package/public/meme.js +235 -0
- package/public/sfx/oh-my-god.mp3 +0 -0
- package/public/sfx/scratch-1.mp3 +0 -0
- package/public/sfx/scratch-2.mp3 +0 -0
- package/public/sfx/scratch-3.mp3 +0 -0
- package/server.js +61 -33
package/bin/agent-standup.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// agent-standup [serve] [--port 4747] [--open] start the local server and (optionally) open the page
|
|
4
4
|
// agent-standup status [--here] [--json] counts of working / needs you / your turn
|
|
5
5
|
// agent-standup statusline same, one short line; reads Claude Code's statusLine JSON on stdin
|
|
6
|
-
// Everything reads the local session files Claude Code already writes. Nothing is sent anywhere.
|
|
6
|
+
// Everything reads the local session files Claude Code already writes. Nothing is sent anywhere unless you turn on the optional French translation or jokes (they use your own Claude login or ANTHROPIC_API_KEY).
|
|
7
7
|
const path = require('path'), { spawn } = require('child_process');
|
|
8
8
|
const pkg = require('../package.json');
|
|
9
9
|
const argv = process.argv.slice(2), flags = {}, rest = [];
|
|
@@ -26,7 +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
|
--characters DIR folder with your own pixel-art characters (default ~/.agent-standup/characters)
|
|
29
|
-
--here status: only sessions of the current folder
|
|
29
|
+
--here status: only sessions of the current folder (matched on its real path, as Claude Code names it)
|
|
30
30
|
--json status: machine-readable output (add --titles to include session titles)
|
|
31
31
|
`;
|
|
32
32
|
if (flags.help || cmd === 'help') { process.stdout.write(HELP); process.exit(0); }
|
|
@@ -34,7 +34,10 @@ if (flags.version || cmd === 'version') { console.log(pkg.version); process.exit
|
|
|
34
34
|
if (flags.projects) process.env.AGENT_STANDUP_PROJECTS = path.resolve(flags.projects);
|
|
35
35
|
if (flags.config) process.env.CLAUDE_CONFIG_DIR = path.resolve(flags.config);
|
|
36
36
|
if (flags.characters) process.env.AGENT_STANDUP_CHARACTERS = path.resolve(flags.characters);
|
|
37
|
-
if (flags.port)
|
|
37
|
+
if (flags.port !== undefined) { // a real port number or nothing: no silent fallback to another port
|
|
38
|
+
if (!/^\d+$/.test(String(flags.port)) || +flags.port < 1 || +flags.port > 65535) { console.error(`Invalid --port "${flags.port}": use a number between 1 and 65535.`); process.exit(2); }
|
|
39
|
+
process.env.PORT = String(+flags.port);
|
|
40
|
+
}
|
|
38
41
|
const server = require('../server.js');
|
|
39
42
|
const projKey = (dir) => String(dir || '').replace(/[^a-zA-Z0-9]/g, '-'); // Claude Code's project folder naming
|
|
40
43
|
|
package/kokoro-worker.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Kokoro worker: loads the Kokoro-82M model once, then reads one JSON request per stdin line ({ id, text, voice, speed, out }),
|
|
3
|
+
// writes a 16-bit WAV to `out` and answers { id, ok, seconds } or { id, ok: false, error }. Started by kokoro.js.
|
|
4
|
+
const fs = require('fs'), path = require('path'), readline = require('readline'), { createRequire } = require('module');
|
|
5
|
+
const MODEL = 'onnx-community/Kokoro-82M-v1.0-ONNX';
|
|
6
|
+
const send = (o) => fs.writeSync(1, JSON.stringify(o) + '\n');
|
|
7
|
+
function wav(samples, rate) { // Float32 in [-1, 1] → 16-bit mono WAV
|
|
8
|
+
const n = samples.length, buf = Buffer.alloc(44 + n * 2);
|
|
9
|
+
buf.write('RIFF', 0); buf.writeUInt32LE(36 + n * 2, 4); buf.write('WAVEfmt ', 8); buf.writeUInt32LE(16, 16); buf.writeUInt16LE(1, 20); buf.writeUInt16LE(1, 22); buf.writeUInt32LE(rate, 24); buf.writeUInt32LE(rate * 2, 28); buf.writeUInt16LE(2, 32); buf.writeUInt16LE(16, 34); buf.write('data', 36); buf.writeUInt32LE(n * 2, 40);
|
|
10
|
+
for (let i = 0; i < n; i++) { const s = Math.max(-1, Math.min(1, samples[i])); buf.writeInt16LE(Math.round(s < 0 ? s * 32768 : s * 32767), 44 + i * 2); }
|
|
11
|
+
return buf;
|
|
12
|
+
}
|
|
13
|
+
async function main() {
|
|
14
|
+
const dir = process.env.AGENT_STANDUP_KOKORO_DIR, dtype = process.env.AGENT_STANDUP_KOKORO_DTYPE || 'q4f16';
|
|
15
|
+
const req = createRequire(path.join(dir, 'node_modules', 'x.js')); // resolve from the install folder, not from the extension
|
|
16
|
+
const tf = req('@huggingface/transformers'); tf.env.cacheDir = path.join(dir, 'models');
|
|
17
|
+
const { KokoroTTS } = req('kokoro-js'); const tts = await KokoroTTS.from_pretrained(MODEL, { dtype, device: 'cpu' });
|
|
18
|
+
send({ ready: true });
|
|
19
|
+
for await (const line of readline.createInterface({ input: process.stdin })) {
|
|
20
|
+
let m; try { m = JSON.parse(line); } catch { continue; }
|
|
21
|
+
try { const a = await tts.generate(String(m.text), { voice: m.voice, speed: +m.speed || 1 }); fs.writeFileSync(m.out, wav(a.audio, a.sampling_rate)); send({ id: m.id, ok: true, seconds: a.audio.length / a.sampling_rate }); }
|
|
22
|
+
catch (e) { send({ id: m.id, ok: false, error: String((e && e.message) || e).slice(0, 200) }); }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
main().catch((e) => { console.error(String((e && e.stack) || e)); process.exit(1); });
|
package/kokoro.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Natural voices with Kokoro-82M (kokoro-js on ONNX Runtime), installed on demand outside the extension.
|
|
2
|
+
// Nothing here runs until the person clicks Install in ⚙ → Sound: the packages (about 430 MB) go to one user folder, the model (about 155 MB) is fetched on first use.
|
|
3
|
+
const fs = require('fs'), os = require('os'), path = require('path'), { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const WIN = process.platform === 'win32';
|
|
6
|
+
const DIR = process.env.AGENT_STANDUP_KOKORO_DIR || (WIN ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'agent-standup', 'kokoro') : path.join(os.homedir(), '.cache', 'agent-standup', 'kokoro'));
|
|
7
|
+
const PKG = 'kokoro-js@1.2.1', DTYPE = 'q4f16', SIZE_MB = 580, MAX_RESTARTS = 3;
|
|
8
|
+
const WORKER = process.env.AGENT_STANDUP_KOKORO_WORKER || path.join(__dirname, 'kokoro-worker.js'), LOG = path.join(DIR, 'install.log');
|
|
9
|
+
|
|
10
|
+
// In the order agents get them (the lead takes the first), so successive agents sound distinct. French has a single voice in Kokoro.
|
|
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_nicole', 'Nicole', 'en_US'], ['am_puck', 'Puck', 'en_US'], ['ff_siwis', 'Siwis', 'fr_FR']].map(([name, label, locale]) => ({ name, label, locale }));
|
|
12
|
+
const voices = () => VOICES, hasVoice = (n) => VOICES.some((v) => v.name === n);
|
|
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
|
+
const exists = (f) => { try { fs.statSync(f); return true; } catch { return false; } };
|
|
15
|
+
const installed = () => exists(path.join(DIR, 'node_modules', 'kokoro-js', 'package.json'));
|
|
16
|
+
|
|
17
|
+
function findNpm() {
|
|
18
|
+
const home = os.homedir(), names = WIN ? ['npm.cmd', 'npm.bat', 'npm'] : ['npm'], cands = [];
|
|
19
|
+
for (const d of (process.env.PATH || '').split(path.delimiter)) for (const n of names) cands.push(path.join(d, n));
|
|
20
|
+
const dirs = WIN ? [path.join(process.env.APPDATA || '', 'npm'), path.join(process.env.ProgramFiles || 'C:\\Program Files', 'nodejs')] : ['/opt/homebrew/bin', '/usr/local/bin', path.join(home, '.local/bin'), path.join(home, '.npm-global/bin'), path.join(home, '.volta/bin')];
|
|
21
|
+
try { const nvm = path.join(home, '.nvm', 'versions', 'node'); for (const v of fs.readdirSync(nvm)) dirs.push(path.join(nvm, v, 'bin')); } catch {}
|
|
22
|
+
for (const d of dirs) for (const n of names) cands.push(path.join(d, n));
|
|
23
|
+
return cands.find((c) => c && exists(c)) || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---- install: `npm install --prefix DIR kokoro-js@x`, output in DIR/install.log, one job at a time
|
|
27
|
+
let job = null, lastError = '';
|
|
28
|
+
function install() {
|
|
29
|
+
if (job || installed()) return status();
|
|
30
|
+
const npm = findNpm(); if (!npm) { lastError = 'npm not found'; return status(); }
|
|
31
|
+
fs.mkdirSync(DIR, { recursive: true }); lastError = '';
|
|
32
|
+
const out = fs.openSync(LOG, 'w'), args = ['install', '--prefix', DIR, '--no-audit', '--no-fund', PKG];
|
|
33
|
+
job = WIN && /\.(cmd|bat)$/i.test(npm) ? spawn('"' + npm + '"', args.map((a) => (a === DIR ? '"' + a + '"' : a)), { shell: true, stdio: ['ignore', out, out], windowsHide: true }) : spawn(npm, args, { stdio: ['ignore', out, out], windowsHide: true });
|
|
34
|
+
const done = (why) => { job = null; try { fs.closeSync(out); } catch {} if (why) lastError = why; else warm(); };
|
|
35
|
+
job.on('error', (e) => done('npm: ' + e.message));
|
|
36
|
+
job.on('close', (code) => done(code === 0 && installed() ? '' : 'npm install failed (exit ' + code + ')'));
|
|
37
|
+
return status();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---- worker: one child process that holds the model, started on first use, restarted after a crash up to MAX_RESTARTS times; one line at a time
|
|
41
|
+
let worker = null, restarts = 0, seq = 0; const queue = []; let busy = false;
|
|
42
|
+
function startWorker() {
|
|
43
|
+
if (worker) return worker;
|
|
44
|
+
if (restarts >= MAX_RESTARTS) throw new Error('kokoro worker gave up');
|
|
45
|
+
const p = spawn(process.execPath, [WORKER], { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', AGENT_STANDUP_KOKORO_DIR: DIR, AGENT_STANDUP_KOKORO_DTYPE: DTYPE }, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
46
|
+
const w = (worker = { p, ready: false, pending: new Map(), err: '' }); let buf = '';
|
|
47
|
+
p.stdout.on('data', (d) => {
|
|
48
|
+
buf += d; let i;
|
|
49
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
50
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1); let m; try { m = JSON.parse(line); } catch { continue; } // library chatter is ignored
|
|
51
|
+
if (m.ready) { w.ready = true; lastError = ''; continue; }
|
|
52
|
+
const r = w.pending.get(m.id); if (!r) continue; w.pending.delete(m.id); m.ok ? r.resolve(m) : r.reject(new Error(m.error || 'tts failed'));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
p.stderr.on('data', (d) => { w.err = (w.err + d).slice(-600); }); p.stdin.on('error', () => {});
|
|
56
|
+
const gone = (why) => { if (worker !== w) return; worker = null; restarts++; lastError = why; const jobs = [...w.pending.values()]; w.pending.clear(); busy = false; for (const r of jobs) r.reject(new Error(why)); };
|
|
57
|
+
p.on('error', (e) => gone('kokoro worker: ' + e.message)); p.on('close', (code) => gone('kokoro worker exited (' + code + ') ' + (w.err.trim().split('\n').pop() || '')));
|
|
58
|
+
return w;
|
|
59
|
+
}
|
|
60
|
+
function warm() { if (installed() && !worker && restarts < MAX_RESTARTS) try { startWorker(); } catch {} }
|
|
61
|
+
function pump() {
|
|
62
|
+
if (busy || !queue.length) return;
|
|
63
|
+
const j = queue.shift(); let w; try { w = startWorker(); } catch (e) { j.reject(e); return pump(); }
|
|
64
|
+
const id = ++seq, next = (fn) => (v) => { busy = false; fn(v); pump(); }; busy = true;
|
|
65
|
+
w.pending.set(id, { resolve: next(j.resolve), reject: next(j.reject) });
|
|
66
|
+
w.p.stdin.write(JSON.stringify({ id, text: j.text, voice: j.voice, speed: j.speed, out: j.out }) + '\n');
|
|
67
|
+
}
|
|
68
|
+
function synth(text, voice, speed, out) {
|
|
69
|
+
if (!installed()) return Promise.reject(new Error('kokoro not installed'));
|
|
70
|
+
if (!hasVoice(voice)) return Promise.reject(new Error('unknown voice'));
|
|
71
|
+
return new Promise((resolve, reject) => { queue.push({ text: String(text).slice(0, 2500), voice, speed, out, resolve, reject }); pump(); });
|
|
72
|
+
}
|
|
73
|
+
function status() {
|
|
74
|
+
let log = ''; try { log = fs.readFileSync(LOG, 'utf8').slice(-400); } catch {}
|
|
75
|
+
return { dir: DIR, sizeMb: SIZE_MB, installed: installed(), installing: !!job, ready: !!(worker && worker.ready), gaveUp: restarts >= MAX_RESTARTS, error: lastError || null, npm: !!findNpm(), log };
|
|
76
|
+
}
|
|
77
|
+
function shutdown() {
|
|
78
|
+
for (const j of queue.splice(0)) j.reject(new Error('kokoro shut down'));
|
|
79
|
+
if (worker) { const w = worker; worker = null; try { w.p.kill(); } catch {} for (const r of w.pending.values()) r.reject(new Error('kokoro shut down')); w.pending.clear(); }
|
|
80
|
+
busy = false; if (job) { try { job.kill(); } catch {} job = null; }
|
|
81
|
+
}
|
|
82
|
+
module.exports = { DIR, voices, hasVoice, speedFor, status, install, warm, synth, shutdown };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Your Claude Code 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"
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"server.js",
|
|
12
|
+
"kokoro.js",
|
|
13
|
+
"kokoro-worker.js",
|
|
12
14
|
"demo.js",
|
|
13
15
|
"public",
|
|
14
16
|
"media",
|
package/public/index.html
CHANGED
|
@@ -237,11 +237,14 @@
|
|
|
237
237
|
<h4 data-fr="Son" data-en="Sound">Son</h4>
|
|
238
238
|
<button class="mi" data-t="voice"><span data-fr="Voix des agents" data-en="Agent voices">Voix des agents</span><i class="sw"></i></button>
|
|
239
239
|
<button class="mi" data-t="office"><span data-fr="Ambiance de bureau (clavier, café…)" data-en="Office ambience (keys, coffee…)">Ambiance de bureau (clavier, café…)</span><i class="sw"></i></button>
|
|
240
|
+
<button class="mi" data-t="meme"><span data-fr="🔥 Mode mème (n’importe quoi)" data-en="🔥 Meme mode (unhinged)">🔥 Mode mème (n’importe quoi)</span><i class="sw"></i></button>
|
|
241
|
+
<div class="box" id="memeBox" style="display:none"><div class="lbl"><span data-fr="Intensité" data-en="Intensity">Intensité</span> <span id="memeLvlVal"></span></div><input id="memeLvl" type="range" min="1" max="11" step="1" style="width:100%" aria-label="Meme intensity"><div class="lbl"><span data-fr="Volume des effets" data-en="Effects volume">Volume des effets</span></div><input id="memeVol" type="range" min="0.1" max="1" step="0.05" style="width:100%" aria-label="Meme volume"></div>
|
|
240
242
|
<div class="box" id="officeBox" style="display:none"><input id="officeVol" type="range" min="0.1" max="1" step="0.05" style="width:100%" aria-label="Ambience volume"></div>
|
|
241
243
|
<button class="mi" data-t="notify"><span data-fr="Me prévenir quand les agents ont fini" data-en="Notify me when agents finish">Me prévenir quand les agents ont fini</span><i class="sw"></i></button>
|
|
242
244
|
<button class="mi" data-t="music"><span data-fr="Musique d’ambiance (radio FIP)" data-en="Office music (FIP radio)">Musique d’ambiance (radio FIP)</span><i class="sw"></i></button>
|
|
243
245
|
<div class="box" id="musicBox" style="display:none"><select id="station"></select><input id="musicVol" type="range" min="0" max="1" step="0.05" style="width:100%" aria-label="Music volume"></div>
|
|
244
246
|
<div class="box" id="vstyleBox"><select id="vstyle"></select></div>
|
|
247
|
+
<div class="box" id="kokoroBox" style="display:none"><button class="btn" id="kokoroInstall" style="width:100%"></button><div class="hint" id="kokoroHint" style="padding:2px 0 6px"></div></div>
|
|
245
248
|
<button class="mi" data-t="roster"><span data-fr="Liste des agents (à gauche)" data-en="Agents list (left)">Liste des agents (à gauche)</span><i class="sw"></i></button>
|
|
246
249
|
<button class="mi" data-t="reactions"><span data-fr="Barre de réactions (👍 👏 ☕)" data-en="Reactions bar (👍 👏 ☕)">Barre de réactions (👍 👏 ☕)</span><i class="sw"></i></button>
|
|
247
250
|
<button class="mi" data-t="banter"><span data-fr="Petites répliques et animations" data-en="Little lines & animations">Petites répliques et animations</span><i class="sw"></i></button>
|
|
@@ -282,6 +285,9 @@
|
|
|
282
285
|
</footer>
|
|
283
286
|
<div id="card"></div><div id="recap"><div class="box"></div></div>
|
|
284
287
|
|
|
288
|
+
<link rel="stylesheet" href="/meme.css">
|
|
289
|
+
<script src="/meme.js"></script>
|
|
290
|
+
<script src="/api/prefs.js"></script>
|
|
285
291
|
<script>
|
|
286
292
|
// =====================================================================
|
|
287
293
|
// helpers, prefs, i18n
|
|
@@ -297,10 +303,12 @@ const EMBED = Q.get('embed') === '1' || window.parent !== window;
|
|
|
297
303
|
if (Q.get('pin')) S.pin = Q.get('pin');
|
|
298
304
|
|
|
299
305
|
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' };
|
|
300
|
-
try {
|
|
306
|
+
{ 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
|
|
307
|
+
const sv = window.SAVED_PREFS && typeof window.SAVED_PREFS === 'object' ? window.SAVED_PREFS : {}; Object.assign(prefs, (sv._t || 0) > (loc._t || 0) ? sv : loc); }
|
|
301
308
|
if (prefs.vstyle === 'babble') prefs.vstyle = 'animal', prefs.vstyleChosen = true;
|
|
302
309
|
if (!prefs.vstyleChosen && (prefs.vstyle === 'animal' || prefs.vstyle === 'sims')) prefs.vstyle = 'cartoon'; // the gibberish styles are opt-in: understandable voices by default
|
|
303
|
-
|
|
310
|
+
let prefsTimer = null;
|
|
311
|
+
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); };
|
|
304
312
|
// 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.
|
|
305
313
|
// A missing string falls back to English, so a half-translated language still works. See public/locales/README.md to add one.
|
|
306
314
|
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'], ['pt-BR', 'Português (Brasil)', 'pt-BR'], ['ja', '日本語', 'ja']];
|
|
@@ -339,7 +347,7 @@ async function loadChars() {
|
|
|
339
347
|
} catch {}
|
|
340
348
|
CHARS = next;
|
|
341
349
|
}
|
|
342
|
-
const customFor = (id) => (id === 'main' ? CHARS.lead : id === 'user' ? CHARS.you : CHARS.pool.length ? CHARS.pool
|
|
350
|
+
const customFor = (id) => (id === 'main' ? CHARS.lead : id === 'user' ? CHARS.you : CHARS.pool.length ? CHARS.pool.reduce((b, c) => (hash(id + '|' + (c.idle || '')) > hash(id + '|' + (b.idle || '')) ? c : b)) : null);
|
|
343
351
|
function renderCharInfo() {
|
|
344
352
|
const el = $('#charInfo'); if (!el) return;
|
|
345
353
|
el.innerHTML = CHARS.n ? `✓ ${CHARS.n} ${esc(L('personnage(s) personnalisé(s) chargé(s)', 'custom character(s) loaded'))}` : `${esc(L('Déposez vos images (PNG) dans ce dossier pour remplacer les personnages :', 'Drop your pixel art (PNG) in this folder to replace the characters:'))}<br><code style="word-break:break-all">${esc(CHARS.dir)}</code><br>${esc(L('lead.png = le chef · you.png = vous · les autres = les sous-agents · nom-talk.png = bouche ouverte', 'lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth'))}`;
|
|
@@ -439,7 +447,7 @@ function avatarSVG(p, o, uid = '') { // uid keeps SVG ids unique per instance:
|
|
|
439
447
|
const sparks = star(22, 28, 3) + star(78, 22, 2.6, '#ff9bb8') + star(70, 40, 1.9, '#9be7ff') + star(30, 46, 1.6, '#ffe58a');
|
|
440
448
|
const custFig = (c, talking) => { const base = 'x="20" y="6" width="60" height="58" preserveAspectRatio="xMidYMax meet"', px = 'image-rendering:pixelated;';
|
|
441
449
|
return `<image class="cimg" href="${c.idle}" ${base} style="${px}${talking && c.talk ? 'display:none' : ''}"/>` + (c.talk ? `<image class="cimg cimg-talk" href="${c.talk}" ${base} style="${px}${talking ? '' : 'display:none'}"/>` : ''); };
|
|
442
|
-
const fig = p.custom ? custFig(p.custom, st && o.m
|
|
450
|
+
const fig = p.custom ? custFig(p.custom, st && o.m === 2) : `${arm('l')}${bodyShape(p.shape, `url(#${gid})`, c2)}${arm('r')}${face}${acc}`;
|
|
443
451
|
return `<svg viewBox="0 0 100 75" preserveAspectRatio="xMidYMid slice"${st ? ' xmlns="http://www.w3.org/2000/svg" width="400" height="300"' : ''}>
|
|
444
452
|
<defs><linearGradient id="${gid}" gradientUnits="userSpaceOnUse" x1="0" y1="12" x2="0" y2="72"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c}"/></linearGradient>
|
|
445
453
|
<linearGradient id="${uid}dk${p.scene}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${sc.desk[0]}"/><stop offset="1" stop-color="${sc.desk[1]}"/></linearGradient>
|
|
@@ -534,7 +542,7 @@ const AUD = {
|
|
|
534
542
|
const src = c.createBufferSource(); src.buffer = buf; src.loop = true; const lp = c.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 500;
|
|
535
543
|
const g = c.createGain(); g.gain.value = 0.3; src.connect(lp).connect(g).connect(this.sfx); src.start(); this.apply(); OFFICE.start();
|
|
536
544
|
},
|
|
537
|
-
apply() { if (!this.ctx) return; this.sfx.gain.value = prefs.office && !this.off() ? 1 : 0; this.voice.gain.value = prefs.voice && !this.off() ? 1 : 0; this.applyMusic(); RADIO.sync(); },
|
|
545
|
+
apply() { if (!this.ctx) return; this.sfx.gain.value = prefs.office && !this.off() ? 1 : 0; this.voice.gain.value = prefs.voice && !this.off() ? 1 : 0; this.applyMusic(); RADIO.sync(); if (typeof MEME !== 'undefined') MEME.apply(); },
|
|
538
546
|
// Music fades down quickly when an agent starts talking and comes back slowly once the room is quiet.
|
|
539
547
|
applyMusic() { if (!this.ctx) return; this.amb.gain.setTargetAtTime((this.ducked ? 0.35 : 1) * (prefs.officeVol ?? 0.8), this.ctx.currentTime, 0.25); const v = prefs.music && !this.off() ? (prefs.musicVol ?? 0.35) * (this.ducked ? 0.16 : 1) : 0; this.music.gain.setTargetAtTime(v, this.ctx.currentTime, this.ducked ? 0.12 : 0.7); },
|
|
540
548
|
blip(f) { if (!this.ctx || this.off()) return; const c = this.ctx, t = c.currentTime, o = c.createOscillator(), g = c.createGain(); o.type = 'sine'; o.frequency.setValueAtTime(f, t); o.frequency.exponentialRampToValueAtTime(f * 1.5, t + 0.1); g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.12, t + 0.015); g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22); o.connect(g).connect(c.destination); o.start(t); o.stop(t + 0.25); },
|
|
@@ -652,17 +660,23 @@ function rankVoices(list) { // list of names → best distinct voices first
|
|
|
652
660
|
return out.slice(0, 5);
|
|
653
661
|
}
|
|
654
662
|
const TTS = {
|
|
655
|
-
server: false, platform: '', pool: { fr: [], en: [] }, cache: new Map(),
|
|
663
|
+
server: false, engine: 'system', platform: '', pool: { fr: [], en: [] }, cache: new Map(),
|
|
656
664
|
async init() {
|
|
657
|
-
|
|
665
|
+
this.engine = 'system'; this.server = false; this.pool = { fr: [], en: [] }; this.cache.clear();
|
|
666
|
+
try {
|
|
667
|
+
if (prefs.vstyle === 'kokoro') { // natural voices installed on demand; until they are, the system voices stay in use
|
|
668
|
+
const k = await (await fetch('/api/voices?engine=kokoro')).json();
|
|
669
|
+
if (k.available && k.ready) { this.server = true; this.engine = 'kokoro'; this.platform = k.platform || ''; for (const l of ['fr', 'en']) this.pool[l] = k.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name); return; }
|
|
670
|
+
}
|
|
671
|
+
const d = await (await fetch('/api/voices')).json(); this.platform = d.platform || ''; if (!d.available) return; this.server = true;
|
|
658
672
|
for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
|
|
659
673
|
},
|
|
660
674
|
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; },
|
|
661
675
|
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 : +$('#speed').value))) / 10) * 10; },
|
|
662
676
|
buf(text, voice, rate) {
|
|
663
|
-
const key = voice + '|' + rate + '|' + text;
|
|
677
|
+
const key = this.engine + '|' + voice + '|' + rate + '|' + text;
|
|
664
678
|
if (!this.cache.has(key)) {
|
|
665
|
-
this.cache.set(key, fetch(`/api/tts?voice=${encodeURIComponent(voice)}&rate=${rate}&text=${encodeURIComponent(text)}`).then((r) => { if (!r.ok) throw new Error('tts'); return r.arrayBuffer(); }).then((b) => AUD.ctx.decodeAudioData(b)));
|
|
679
|
+
this.cache.set(key, fetch(`/api/tts?engine=${this.engine}&voice=${encodeURIComponent(voice)}&rate=${rate}&text=${encodeURIComponent(text)}`).then((r) => { if (!r.ok) throw new Error('tts'); return r.arrayBuffer(); }).then((b) => AUD.ctx.decodeAudioData(b)));
|
|
666
680
|
if (this.cache.size > 60) this.cache.delete(this.cache.keys().next().value);
|
|
667
681
|
this.cache.get(key).catch(() => this.cache.delete(key));
|
|
668
682
|
}
|
|
@@ -745,12 +759,12 @@ async function speakServer(text, p, gain, token, fr = FR()) {
|
|
|
745
759
|
const buf = await TTS.buf(speakable(text), voice, TTS.rate()); if (token !== S.token) return;
|
|
746
760
|
const c = AUD.ctx, src = c.createBufferSource(); src.buffer = buf;
|
|
747
761
|
const an = c.createAnalyser(); an.fftSize = 256; const g = c.createGain(); g.gain.value = gain; let stops = [];
|
|
748
|
-
if (prefs.vstyle === 'natural') src.connect(an); else { const ch = fxChain(c, src, fxFor(p)); stops = ch.stops; ch.node.connect(an); }
|
|
762
|
+
if (prefs.vstyle === 'natural' || prefs.vstyle === 'kokoro') src.connect(an); else { const ch = fxChain(c, src, fxFor(p)); stops = ch.stops; ch.node.connect(an); }
|
|
749
763
|
an.connect(g); g.connect(AUD.voice);
|
|
750
764
|
const data = new Uint8Array(an.fftSize);
|
|
751
765
|
S.level[p.id] = () => { an.getByteTimeDomainData(data); let s = 0; for (const v of data) { const x = (v - 128) / 128; s += x * x; } return Math.sqrt(s / data.length); }; // drives the mouth
|
|
752
766
|
AUD.live.add(src);
|
|
753
|
-
await new Promise((res) => { src.onended = res; setTimeout(res, buf.duration * 1000 + 800); src.start(); });
|
|
767
|
+
captionRun(p.id, text, buf.duration * 1000); await new Promise((res) => { src.onended = res; setTimeout(res, buf.duration * 1000 + 800); src.start(); });
|
|
754
768
|
AUD.live.delete(src); delete S.level[p.id]; stops.forEach((o) => { try { o.stop(); } catch {} });
|
|
755
769
|
}
|
|
756
770
|
function speakBrowser(text, p, token, fr = FR()) { // fallback where no server voice exists (Windows/Linux)
|
|
@@ -770,7 +784,32 @@ function speakBrowser(text, p, token, fr = FR()) { // fallback where no server
|
|
|
770
784
|
next();
|
|
771
785
|
});
|
|
772
786
|
}
|
|
787
|
+
// Subtitles: a long line is shown one piece (sentence, or clause of a long sentence) at a time, the piece being said. The split is by characters, spread over the audio's real length when known.
|
|
788
|
+
function pieces(text) {
|
|
789
|
+
const out = [];
|
|
790
|
+
for (const sen of String(text).split(/(?<=[.!?…]["')\]]*)\s+/)) {
|
|
791
|
+
const t = sen.trim(); if (!t) continue;
|
|
792
|
+
if (t.length <= 110) { out.push(t); continue; }
|
|
793
|
+
let cur = '';
|
|
794
|
+
for (const part of t.split(/(?<=[,;:—])\s+/)) { if (cur && cur.length + part.length > 90) { out.push(cur); cur = part; } else cur = cur ? cur + ' ' + part : part; }
|
|
795
|
+
if (cur) out.push(cur);
|
|
796
|
+
}
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
const capTimers = new Map(); // agent id → timers that advance its subtitle
|
|
800
|
+
function captionClear(id) { (capTimers.get(id) || []).forEach(clearTimeout); capTimers.delete(id); }
|
|
801
|
+
function setCaption(id, text) {
|
|
802
|
+
const t = $('#t-' + id), b = t && t.querySelector('.bubble'), say = S.say[id]; if (!b || !say) return;
|
|
803
|
+
b.innerHTML = (say.label ? `<small>${esc(say.label)}</small>` : '') + esc(text); if (S.present === id) $('#pcap').textContent = text;
|
|
804
|
+
}
|
|
805
|
+
function captionRun(id, text, totalMs) {
|
|
806
|
+
captionClear(id); const ps = pieces(text); if (ps.length < 2 || !(totalMs > 0)) return;
|
|
807
|
+
const n = ps.reduce((a, x) => a + x.length, 0), timers = []; let at = 0; setCaption(id, ps[0]); at += totalMs * ps[0].length / n;
|
|
808
|
+
for (let i = 1; i < ps.length; i++) { const x = ps[i], when = at; timers.push(setTimeout(() => setCaption(id, x), when)); at += totalMs * x.length / n; }
|
|
809
|
+
capTimers.set(id, timers);
|
|
810
|
+
}
|
|
773
811
|
async function speakLine(text, p, primary, token, fr = FR()) {
|
|
812
|
+
captionRun(p.id, text, estMs(text));
|
|
774
813
|
if (AUD.off() || !prefs.voice) { if (!AUD.off() && prefs.office) AUD.murmur(0.5 + (p.vi % 5) / 10, estMs(text) / 1000); return sleep(estMs(text)); }
|
|
775
814
|
if (S.hurry) return sleep(estMs(text));
|
|
776
815
|
if ((prefs.vstyle === 'animal' || prefs.vstyle === 'sims') && AUD.ctx) { try { await speakBabble(text, p, primary ? 1 : 0.5, token); return; } catch {} }
|
|
@@ -834,7 +873,7 @@ function paintStatus() {
|
|
|
834
873
|
$('#seek').value = S.idx; $('#pos').textContent = `${Math.min(S.idx + 1, S.events.length)} / ${S.events.length}`;
|
|
835
874
|
}
|
|
836
875
|
const applyStatus = (ev) => {
|
|
837
|
-
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); const t = $('#t-' + ev.to); if (t) t.classList.remove('vanishing'); }
|
|
876
|
+
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'); }
|
|
838
877
|
if (ev.kind === 'finish') { if (S.waitOn[ev.to]) S.waitOn[ev.to].delete(ev.agent); if (S.status[ev.agent] !== 'done') { S.status[ev.agent] = 'done'; S.doneAt[ev.agent] = S.clock; } delete S.run[ev.agent]; const lr = S.run[ev.to]; if (lr && /^(Task|Agent)$/.test(lr.name) && !(S.waitOn[ev.to] && S.waitOn[ev.to].size)) delete S.run[ev.to]; }
|
|
839
878
|
if (ev.kind === 'report') { if (S.waitOn[ev.to]) S.waitOn[ev.to].delete(ev.agent); S.status[ev.agent] = 'done'; S.doneAt[ev.agent] = S.clock; delete S.run[ev.agent]; }
|
|
840
879
|
};
|
|
@@ -877,7 +916,7 @@ const WIN1M = /(opus-4-[78]|opus-5|sonnet-5|fable-5)/;
|
|
|
877
916
|
function ctxWin(model, ctx) { const base = WIN1M.test(model || '') ? 1e6 : 2e5; return ctx > base ? 1e6 : base; } // the transcript does not say which window is in use: infer it, a bigger observed context proves the 1M window
|
|
878
917
|
function tally(ev) { // numbers that pile up over a session: token usage, errors, last sign of life
|
|
879
918
|
if (ev.kind === 'compact') { S.compacts[ev.agent] = (S.compacts[ev.agent] || 0) + 1; delete S.ctxWarned[ev.agent]; }
|
|
880
|
-
const det = loopStep(S.rep, ev); if (det) { S.loops[ev.agent] = det; later('badges', renderBadges, 300); } else if (S.loops[ev.agent] && (ev.kind === 'results' ? ev.items.every((it) => !it.err) : (ev.kind === 'tools' && S.rep[ev.agent].n < 2))) { delete S.loops[ev.agent]; later('badges', renderBadges, 300); }
|
|
919
|
+
const det = loopStep(S.rep, ev); if (det) { S.loops[ev.agent] = det; later('badges', renderBadges, 300); } else if (S.loops[ev.agent] && (ev.kind === 'results' ? (S.loops[ev.agent].kind === 'errors' && ev.items.every((it) => !it.err)) : (ev.kind === 'tools' && S.rep[ev.agent].n < 2))) { delete S.loops[ev.agent]; later('badges', renderBadges, 300); }
|
|
881
920
|
if (ev.kind === 'usage') { const c = ev.u.i + ev.u.r + ev.u.w; if (c > 0 && ev.model !== '<synthetic>') { S.ctx[ev.agent] = c; S.model[ev.agent] = ev.model; later('tok', renderCtx, 300); } const u = (S.usage[ev.agent] = S.usage[ev.agent] || { i: 0, o: 0, r: 0, w: 0 }); u.i += ev.u.i; u.o += ev.u.o; u.r += ev.u.r; u.w += ev.u.w; later('tok', renderTokens, 300); }
|
|
882
921
|
else if (ev.kind === 'results') ev.items.forEach((it) => { if (it.err) { S.errs[ev.agent] = (S.errs[ev.agent] || 0) + 1; later('badges', renderBadges, 300); oops(ev.agent); } });
|
|
883
922
|
else if (ev.kind === 'tools' || ev.kind === 'say') S.lastAct[ev.agent] = ev.ts;
|
|
@@ -1127,7 +1166,7 @@ function vanish(id) {
|
|
|
1127
1166
|
S.gone.add(id); const t = $('#t-' + id), p = S.agents[id]; if (!t || !p) return;
|
|
1128
1167
|
toast(`${p.name} ${L('a quitté l’appel', 'left the call')} 👋`);
|
|
1129
1168
|
if (t.style.display === 'none' || document.body.classList.contains('tree')) return;
|
|
1130
|
-
S.leaving.add(id); const say = prefs.banter && !S.hurry && !S.speakers.has(id), go = () => { t.classList.remove('bye'); if (!S.leaving.has(id)) return; t.classList.add('vanishing'); poof(t, p.shirt); AUD.tone(660, .35, 'sine', .07); AUD.tone(330, .5, 'sine', .06, .12);
|
|
1169
|
+
S.leaving.add(id); const say = prefs.banter && !S.hurry && !S.speakers.has(id), go = () => { t.classList.remove('bye'); if (!S.leaving.has(id)) return; t.classList.add('vanishing'); poof(t, p.shirt); MEME.fire('leave', { id }); AUD.tone(660, .35, 'sine', .07); AUD.tone(330, .5, 'sine', .06, .12);
|
|
1131
1170
|
setTimeout(() => { S.leaving.delete(id); t.classList.remove('vanishing'); if (S.gone.has(id)) t.style.display = 'none'; }, 1250); };
|
|
1132
1171
|
if (say) { const b = t.querySelector('.bubble'), line = byeLine(id); b.innerHTML = `<small>👋</small>${esc(line)}`; t.classList.add('bye'); t._byeT = setTimeout(go, 1500); } else go(); // a short goodbye before the poof
|
|
1133
1172
|
}
|
|
@@ -1142,12 +1181,12 @@ const mouths = new Map();
|
|
|
1142
1181
|
function startTalk(id, text, label) {
|
|
1143
1182
|
const t = $('#t-' + id); if (!t) return;
|
|
1144
1183
|
if (S.present && S.present !== id) hidePresent();
|
|
1145
|
-
if (!S.speakers.size && !(S.focusLock > Date.now())) setFocus(id); S.gone.delete(id); if (S.doneAt[id] != null) S.doneAt[id] = S.clock; t.style.display = ''; t.classList.add('speaking'); S.speakers.add(id); S.say[id] = { text, label }; AUD.duck(true);
|
|
1146
|
-
const b = t.querySelector('.bubble'); b.innerHTML = (label ? `<small>${esc(label)}</small>` : '') + esc(
|
|
1147
|
-
if (S.present === id) { $('#pcap').textContent =
|
|
1184
|
+
if (!S.speakers.size && !(S.focusLock > Date.now())) setFocus(id); S.gone.delete(id); if (S.doneAt[id] != null) S.doneAt[id] = S.clock; t.style.display = ''; t.classList.add('speaking'); S.speakers.add(id); S.say[id] = { text, label }; AUD.duck(true); if (id === 'user' && [...S.speakers].some((x) => x !== 'user')) MEME.fire('interrupt'); MEME.fire('talk', { id, text });
|
|
1185
|
+
const shown0 = pieces(text), first = shown0.length > 1 ? shown0[0] : text; const b = t.querySelector('.bubble'); b.innerHTML = (label ? `<small>${esc(label)}</small>` : '') + esc(first);
|
|
1186
|
+
if (S.present === id) { $('#pcap').textContent = first; $('#pcam').classList.add('speaking'); }
|
|
1148
1187
|
const m = t.querySelector('.mouthE'); let open = false; clearInterval(mouths.get(id));
|
|
1149
1188
|
mouths.set(id, setInterval(() => {
|
|
1150
|
-
if (!m) { const ct = t.querySelector('.cimg-talk'); if (ct) { const lv0 = S.level[id], op = lv0 ? lv0() > 0.05 : (open = !open); ct.style.display = op ? '' : 'none'; t.querySelector('.cimg').style.display = op ? 'none' : ''; } return; } // custom character: swap between its two frames
|
|
1189
|
+
if (!m) { const ct = t.querySelector('.cimg-talk'); if (ct) { const lv0 = S.level[id], op = lv0 ? lv0() > 0.05 : (open = !open); ct.style.display = op ? '' : 'none'; t.querySelector('.cimg').style.display = op ? 'none' : ''; if (S.present === id) { const pc = $('#pcam .cimg-talk'), pi = $('#pcam .cimg'); if (pc && pi) { pc.style.display = op ? '' : 'none'; pi.style.display = op ? 'none' : ''; } } } return; } // custom character: swap between its two frames
|
|
1151
1190
|
const pm = S.present === id ? $('#pcam .mouthE') : null, lv = S.level[id]; if (lv) { const v = lv(); const ry = (.9 + Math.min(4.2, v * 28)).toFixed(1), rx = (4.8 - Math.min(1.1, v * 6)).toFixed(1); m.setAttribute('ry', ry); m.setAttribute('rx', rx); if (pm) { pm.setAttribute('ry', ry); pm.setAttribute('rx', rx); } }
|
|
1152
1191
|
else { open = !open; m.setAttribute('ry', open ? 4.2 : 1.1); m.setAttribute('rx', open ? 4 : 4.7); if (pm) { pm.setAttribute('ry', open ? 4.2 : 1.1); pm.setAttribute('rx', open ? 4 : 4.7); } }
|
|
1153
1192
|
}, 70));
|
|
@@ -1159,17 +1198,19 @@ setInterval(() => { // now and then a busy agent has a little thought; never w
|
|
|
1159
1198
|
if (!prefs.banter || !S.playing || S.speakers.size || document.hidden) return; const busy = Object.keys(S.agents).filter((id) => id !== 'user' && agentState(id, Date.now()).k === 'working' && $('#t-' + id) && $('#t-' + id).style.display !== 'none');
|
|
1160
1199
|
if (busy.length && Math.random() < 0.5) { const id = busy[Math.floor(Math.random() * busy.length)], tl = S.lastTool[id] || ''; thought(id, /^Bash/.test(tl) ? '⌨️' : /^(Grep|Glob|Read)/.test(tl) ? '🔍' : ['💭', '🎧', '🧠', '☕', '🎵'][Math.floor(Math.random() * 5)]); }
|
|
1161
1200
|
}, 7000);
|
|
1162
|
-
function oops(id) { if (!prefs.banter || !S.playing || S.recomputing) return; const t = $('#t-' + id); if (!t || t.style.display === 'none' || (t._oops && Date.now() - t._oops < 4000)) return; t._oops = Date.now(); t.classList.remove('oops'); void t.offsetWidth; t.classList.add('oops'); thought(id, '😬'); }
|
|
1201
|
+
function oops(id) { MEME.fire('fail', { id }); if (!prefs.banter || !S.playing || S.recomputing) return; const t = $('#t-' + id); if (!t || t.style.display === 'none' || (t._oops && Date.now() - t._oops < 4000)) return; t._oops = Date.now(); t.classList.remove('oops'); void t.offsetWidth; t.classList.add('oops'); thought(id, '😬'); }
|
|
1163
1202
|
function confetti() {
|
|
1164
|
-
if (!prefs.banter || document.hidden) return; const box = $('#center'), cols = ['#f5c04a', '#7c9cff', '#ff7a6b', '#5ad19a', '#c58bff'];
|
|
1203
|
+
MEME.fire('win'); if (!prefs.banter || document.hidden) return; const box = $('#center'), cols = ['#f5c04a', '#7c9cff', '#ff7a6b', '#5ad19a', '#c58bff'];
|
|
1165
1204
|
for (let i = 0; i < 26; i++) { const c = document.createElement('i'); c.className = 'confetti'; c.style.left = Math.random() * 100 + '%'; c.style.background = cols[i % cols.length]; c.style.animationDelay = Math.random() * 0.6 + 's'; c.style.animationDuration = 1.8 + Math.random() * 1.4 + 's'; box.appendChild(c); setTimeout(() => c.remove(), 4200); }
|
|
1166
1205
|
}
|
|
1167
1206
|
function stopTalk(id) {
|
|
1207
|
+
captionClear(id);
|
|
1168
1208
|
clearInterval(mouths.get(id)); mouths.delete(id); S.speakers.delete(id); if (!S.speakers.size) AUD.duck(false); if (S.focus === id && S.speakers.size) setFocus([...S.speakers][0]);
|
|
1169
1209
|
if (S.present === id) { $('#pcap').textContent = ''; $('#pcam').classList.remove('speaking'); const pm = $('#pcam .mouthE'); if (pm) { pm.setAttribute('ry', .9); pm.setAttribute('rx', 4.6); } }
|
|
1170
1210
|
const t = $('#t-' + id); if (!t) return;
|
|
1171
1211
|
t.classList.remove('speaking'); const m = t.querySelector('.mouthE'); if (m) { m.setAttribute('ry', .9); m.setAttribute('rx', 4.6); }
|
|
1172
1212
|
const ct = t.querySelector('.cimg-talk'); if (ct) { ct.style.display = 'none'; t.querySelector('.cimg').style.display = ''; }
|
|
1213
|
+
const pc = $('#pcam .cimg-talk'), pi = $('#pcam .cimg'); if (pc && pi && S.present === id) { pc.style.display = 'none'; pi.style.display = ''; }
|
|
1173
1214
|
}
|
|
1174
1215
|
const clearSpeaking = () => [...S.speakers].forEach(stopTalk);
|
|
1175
1216
|
|
|
@@ -1341,6 +1382,7 @@ async function react(ev, token) {
|
|
|
1341
1382
|
if (!S.agents[who]) return; startTalk(who, line, ev.kind === 'order' && prefs.banter ? '💬' : '🎬'); await speakLine(line, S.agents[who], true, token); stopTalk(who);
|
|
1342
1383
|
}
|
|
1343
1384
|
async function card(text, sub, ms = 2500) {
|
|
1385
|
+
MEME.fire('card', { text });
|
|
1344
1386
|
const c = $('#card'); c.innerHTML = `<div><h1>${esc(text)}</h1><p>${esc(sub || '')}</p></div>`; c.classList.add('on'); AUD.tone(392, .5, 'triangle', .12); AUD.tone(523, .6, 'triangle', .12, .25);
|
|
1345
1387
|
await sleep(ms / Math.min(2, spd())); c.classList.remove('on');
|
|
1346
1388
|
}
|
|
@@ -1372,7 +1414,7 @@ async function run(i) {
|
|
|
1372
1414
|
if (ev.kind === 'usage' || ev.kind === 'compact') { if (ev.kind === 'compact' && S.live && prefs.banter) toast(`${nameOf(ev.agent)}: ${L('contexte compacté 🗜', 'context compacted 🗜')}`); i++; continue; }
|
|
1373
1415
|
if (ev.kind === 'finish') { applyStatus(ev); paintStatus(); i++; continue; }
|
|
1374
1416
|
if (ev.kind === 'results') { applyResults(ev, S.boost > 1); await sleep((S.demo ? 900 : S.live ? 30 : 80) / spd()); i++; continue; }
|
|
1375
|
-
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name); paintStatus(); await sleep((S.demo ? 1100 : S.live ? 110 : 250) / spd()); i++; continue; }
|
|
1417
|
+
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name); MEME.fire('tool', { id: ev.agent }); paintStatus(); await sleep((S.demo ? 1100 : S.live ? 110 : 250) / spd()); i++; continue; }
|
|
1376
1418
|
const group = [ev]; let j = i + 1; // sub-agents talking back-to-back were running in parallel: they talk over each other
|
|
1377
1419
|
if (isSub(ev.agent) && ev.kind !== 'order') {
|
|
1378
1420
|
while (j < Math.min(S.events.length, i + 28) && group.length < 3) {
|
|
@@ -1385,13 +1427,14 @@ async function run(i) {
|
|
|
1385
1427
|
}
|
|
1386
1428
|
}
|
|
1387
1429
|
if (FR() && !S.hurry) { prefetchTr(i); await Promise.race([ensureTr(group.map((g) => baseText(g))), sleep(S.live ? 7000 : 14000)]); if (token !== S.token) return; }
|
|
1430
|
+
if (group.length >= 3) MEME.fire('combo', { ids: group.map((g) => g.agent) });
|
|
1388
1431
|
prefetchQuips(i); warmTTS(i);
|
|
1389
1432
|
const playOne = (g) => {
|
|
1390
1433
|
applyStatus(g);
|
|
1391
1434
|
const label = g.kind === 'order' ? `${L('pour', 'to')} ${nameOf(g.to)}` : g.kind === 'report' ? `${L('rapport à', 'report to')} ${nameOf(g.to)}` : '', line = spoken(g);
|
|
1392
1435
|
startTalk(g.agent, line, label);
|
|
1393
|
-
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1394
|
-
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
1436
|
+
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); MEME.fire('order', { from: g.agent, to: g.to }); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1437
|
+
if (g.kind === 'report') { AUD.ding(); MEME.fire('report', { from: g.agent, to: g.to }); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
1395
1438
|
return speakLine(clipSpeech(line, S.pace), S.agents[g.agent], true, token, FR() && trMem.has(baseText(g))).then(() => stopTalk(g.agent));
|
|
1396
1439
|
};
|
|
1397
1440
|
let plays; if (S.live) { plays = []; let chain = Promise.resolve(); for (const g of group) { chain = chain.then(() => (token === S.token ? playOne(g) : null)); plays.push(chain); } } else plays = group.map(playOne); // live: one voice at a time so each can be heard; replay keeps the overlapping chatter
|
|
@@ -1447,9 +1490,9 @@ async function fillProjects() { // live view: follow the latest activity anywh
|
|
|
1447
1490
|
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('');
|
|
1448
1491
|
sel.value = prefs.proj && rows.some((r) => r.proj === prefs.proj) ? prefs.proj : ''; sel.style.display = rows.length > 1 ? '' : 'none';
|
|
1449
1492
|
}
|
|
1450
|
-
$('#liveProj').onchange = () => { prefs.proj = $('#liveProj').value; S.pin = ''; savePrefs(); S.cur = null; followCurrent(); };
|
|
1493
|
+
$('#liveProj').onchange = () => { Q.delete('proj'); prefs.proj = $('#liveProj').value; S.pin = ''; savePrefs(); S.cur = null; followCurrent(); };
|
|
1451
1494
|
async function followCurrent() {
|
|
1452
|
-
const cur = await (await fetch('/api/current?proj=' + encodeURIComponent(Q.get('proj') || prefs.proj || '') + (S.pin ? '&id=' + encodeURIComponent(S.pin) : ''))).json();
|
|
1495
|
+
const cur = await (await fetch('/api/current?proj=' + encodeURIComponent(Q.get('proj') || prefs.proj || '') + (S.pin ? '&id=' + encodeURIComponent(S.pin) : '') + (S.cur && !S.pin ? '&sproj=' + encodeURIComponent(S.cur.proj) + '&sid=' + encodeURIComponent(S.cur.id) : ''))).json();
|
|
1453
1496
|
if (!cur.id) { $('#empty').classList.add('on'); return false; } $('#empty').classList.remove('on');
|
|
1454
1497
|
if (S.pin && cur.id !== S.pin) S.pin = ''; // the pinned session is gone: follow the latest one instead of staying silently pinned
|
|
1455
1498
|
if (S.cur && S.cur.id === cur.id && S.cur.proj === cur.proj) return false;
|
|
@@ -1629,15 +1672,35 @@ function setView(v) { prefs.view = v; savePrefs(); document.body.classList.toggl
|
|
|
1629
1672
|
|
|
1630
1673
|
$('#viewSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setView(b.dataset.v); };
|
|
1631
1674
|
$('#langSel').onchange = () => setLang($('#langSel').value, true);
|
|
1632
|
-
$('#charReload').onclick = async () => { await loadChars(); for (const id in S.agents) S.agents[id].custom = customFor(id); buildStage(); paintStatus(); renderCharInfo(); };
|
|
1675
|
+
$('#charReload').onclick = async () => { await loadChars(); for (const k in imgCache) delete imgCache[k]; for (const id in S.agents) S.agents[id].custom = customFor(id); buildStage(); paintStatus(); renderCharInfo(); };
|
|
1633
1676
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
1634
1677
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
|
1635
|
-
$('#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 === '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.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
|
|
1678
|
+
$('#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', 'meme'].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 === 'meme') { $('#memeBox').style.display = prefs.meme ? '' : 'none'; if (prefs.meme) { claimAudio(); AUD.init(); } MEME.apply(); } 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.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
|
|
1636
1679
|
if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
|
|
1637
1680
|
$('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
|
|
1638
1681
|
$('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
|
|
1639
1682
|
$('#station').onchange = (e) => { prefs.station = e.target.value; savePrefs(); AUD.init(); RADIO.sync(); };
|
|
1640
|
-
$('#vstyle').innerHTML = [['animal', 'Animal Crossing (babillage)', 'Animal Crossing (babble)'], ['sims', 'Simlish (babillage)', 'Simlish (babble)'], ['cartoon', 'Voix cartoon (voix réelle)', 'Cartoon voices (real voice)'], ['natural', 'Voix naturelles', 'Natural voices']].map(([v, fr, en]) => `<option value="${v}" data-fr="${fr}" data-en="${en}">${L(fr, en)}</option>`).join(''); $('#vstyle').value = prefs.vstyle; $('#vstyle').onchange = () => { prefs.vstyle = $('#vstyle').value; prefs.vstyleChosen = true; savePrefs(); };
|
|
1683
|
+
$('#vstyle').innerHTML = [['animal', 'Animal Crossing (babillage)', 'Animal Crossing (babble)'], ['sims', 'Simlish (babillage)', 'Simlish (babble)'], ['cartoon', 'Voix cartoon (voix réelle)', 'Cartoon voices (real voice)'], ['natural', 'Voix naturelles', 'Natural voices'], ['kokoro', 'Voix très naturelles (à installer)', 'Very natural voices (installs on demand)']].map(([v, fr, en]) => `<option value="${v}" data-fr="${fr}" data-en="${en}">${L(fr, en)}</option>`).join(''); $('#vstyle').value = prefs.vstyle; $('#vstyle').onchange = () => { prefs.vstyle = $('#vstyle').value; prefs.vstyleChosen = true; savePrefs(); TTS.init().then(kokoroUI); kokoroUI(); };
|
|
1684
|
+
// ---- ⚙ → very natural voices (Kokoro): install box and status, polled while installing
|
|
1685
|
+
let kokoroTimer = null;
|
|
1686
|
+
async function kokoroUI() {
|
|
1687
|
+
const box = $('#kokoroBox'), on = prefs.vstyle === 'kokoro'; box.style.display = on ? '' : 'none'; if (!on) { clearInterval(kokoroTimer); kokoroTimer = null; return; }
|
|
1688
|
+
let st; try { st = await (await fetch('/api/kokoro')).json(); } catch { return; }
|
|
1689
|
+
const b = $('#kokoroInstall'), h = $('#kokoroHint'); b.style.display = st.installed ? 'none' : '';
|
|
1690
|
+
b.disabled = st.installing || !st.npm; b.textContent = st.installing ? L('Installation…', 'Installing…') : L(`Installer (${st.sizeMb} Mo)`, `Install (${st.sizeMb} MB)`);
|
|
1691
|
+
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.')
|
|
1692
|
+
: 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.')
|
|
1693
|
+
: 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 155 Mo)…', 'Loading the model (downloaded on first use, about 155 MB)…'))
|
|
1694
|
+
: L('Voix locales de bonne qualité, en anglais surtout (une seule voix française). Gratuit, rien n’est envoyé nulle part.', 'Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.');
|
|
1695
|
+
if ((st.installing || (st.installed && !st.ready && !st.gaveUp)) && !kokoroTimer) kokoroTimer = setInterval(async () => { await kokoroUI(); }, 2500);
|
|
1696
|
+
if (!st.installing && (st.ready || st.gaveUp || !st.installed) && kokoroTimer) { clearInterval(kokoroTimer); kokoroTimer = null; if (st.ready && TTS.engine !== 'kokoro') TTS.init(); }
|
|
1697
|
+
}
|
|
1698
|
+
$('#kokoroInstall').onclick = async () => { try { await fetch('/api/kokoro/install', { method: 'POST' }); } catch {} kokoroUI(); };
|
|
1699
|
+
kokoroUI();
|
|
1700
|
+
const showMemeLvl = () => { const l = prefs.memeLevel ?? 11; $('#memeLvl').value = l; $('#memeLvlVal').textContent = l + ' / 11' + (l === 11 ? ' 🔥' : ''); }; showMemeLvl();
|
|
1701
|
+
$('#memeLvl').oninput = (e) => { prefs.memeLevel = +e.target.value; savePrefs(); showMemeLvl(); };
|
|
1702
|
+
$('#memeVol').value = prefs.memeVol ?? 0.8; $('#memeBox').style.display = prefs.meme ? '' : 'none';
|
|
1703
|
+
$('#memeVol').oninput = (e) => { prefs.memeVol = +e.target.value; savePrefs(); MEME.apply(); };
|
|
1641
1704
|
$('#officeVol').value = prefs.officeVol; $('#officeBox').style.display = prefs.office ? '' : 'none';
|
|
1642
1705
|
$('#officeVol').oninput = (e) => { prefs.officeVol = +e.target.value; savePrefs(); AUD.applyMusic(); };
|
|
1643
1706
|
$('#musicVol').oninput = (e) => { prefs.musicVol = +e.target.value; savePrefs(); AUD.applyMusic(); };
|
|
@@ -1665,13 +1728,14 @@ if (EMBED) $('#mBrowser').style.display = '';
|
|
|
1665
1728
|
|
|
1666
1729
|
(async function init() {
|
|
1667
1730
|
if (!LANGS.some((l) => l[0] === prefs.lang)) prefs.lang = 'en'; await loadLocale(prefs.lang); await loadChars();
|
|
1668
|
-
document.body.classList.toggle('rost', prefs.roster !== false); document.body.classList.toggle('react', !!prefs.reactions); setView(prefs.view); applyLang(); applyMute(); TTS.init().then(updateHint);
|
|
1731
|
+
document.body.classList.toggle('rost', prefs.roster !== false); document.body.classList.toggle('react', !!prefs.reactions); setView(prefs.view); applyLang(); MEME.apply(); applyMute(); TTS.init().then(updateHint);
|
|
1669
1732
|
loadHealth().then(() => { // French by default only if translation can actually work; otherwise stay in English without a warning
|
|
1670
1733
|
const h = S.health || {};
|
|
1671
1734
|
if (!prefs.langChosen && FR() && h.claude === null && !h.apiKey) { prefs.lang = 'en'; applyLang(); }
|
|
1672
1735
|
if (FR()) fetch('/api/warm').catch(() => {});
|
|
1673
1736
|
});
|
|
1674
|
-
if (Q.get('lang')) { await setLang(Q.get('lang'), false); }
|
|
1737
|
+
if (Q.get('lang')) { await setLang(Q.get('lang'), false); }
|
|
1738
|
+
if (Q.get('meme') === '1') { prefs.meme = true; $('#memeBox').style.display = ''; MEME.apply(); } // a link can force meme mode (demo links) // not saved: lets a link force a language
|
|
1675
1739
|
if (Q.get('demo')) {
|
|
1676
1740
|
if (Q.get('view') === 'tree') setView('tree');
|
|
1677
1741
|
await setMode('demo'); if (Q.get('speed')) $('#speed').value = Q.get('speed');
|
package/public/locales/de.json
CHANGED
|
@@ -165,5 +165,16 @@
|
|
|
165
165
|
"Open folder": "Ordner öffnen",
|
|
166
166
|
"custom character(s) loaded": "eigene Figur(en) geladen",
|
|
167
167
|
"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:",
|
|
168
|
-
"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"
|
|
168
|
+
"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",
|
|
169
|
+
"🔥 Meme mode (unhinged)": "🔥 Meme-Modus (völlig überdreht)",
|
|
170
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "Wird heruntergeladen, du kannst weitermachen: bis dahin kommen die Systemstimmen zum Einsatz.",
|
|
171
|
+
"Effects volume": "Lautstärke der Effekte",
|
|
172
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Lokale Stimmen in guter Qualität, vor allem Englisch (nur eine französische Stimme). Kostenlos, nichts verlässt deinen Rechner.",
|
|
173
|
+
"Installing…": "Installation läuft…",
|
|
174
|
+
"Intensity": "Intensität",
|
|
175
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "Modell wird geladen (beim ersten Mal heruntergeladen, etwa 155 MB)…",
|
|
176
|
+
"npm not found: install Node.js to enable this option.": "npm nicht gefunden: Installiere Node.js, um diese Option zu nutzen.",
|
|
177
|
+
"The engine stopped: system voices are used instead.": "Die Engine wurde beendet: stattdessen kommen die Systemstimmen zum Einsatz.",
|
|
178
|
+
"Very natural voices (installs on demand)": "Sehr natürliche Stimmen (werden bei Bedarf installiert)",
|
|
179
|
+
"Voices ready.": "Stimmen bereit."
|
|
169
180
|
}
|
package/public/locales/en.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ This dev was replaced by his agents — buy him a coffee",
|
|
9
9
|
"🎞 Export video": "🎞 Export video",
|
|
10
10
|
"📋 Recap": "📋 Recap",
|
|
11
|
+
"🔥 Meme mode (unhinged)": "🔥 Meme mode (unhinged)",
|
|
11
12
|
"Agent voices": "Agent voices",
|
|
12
13
|
"Agents": "Agents",
|
|
13
14
|
"Agents list (left)": "Agents list (left)",
|
|
@@ -41,10 +42,12 @@
|
|
|
41
42
|
"done": "done",
|
|
42
43
|
"Done": "Done",
|
|
43
44
|
"Done. ": "Done. ",
|
|
45
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "Downloading, you can carry on: system voices are used meanwhile.",
|
|
44
46
|
"Drop your pixel art (PNG) in this folder to replace the characters:": "Drop your pixel art (PNG) in this folder to replace the characters:",
|
|
45
47
|
"duration": "duration",
|
|
46
48
|
"Editing ": "Editing ",
|
|
47
49
|
"Editor": "Editor",
|
|
50
|
+
"Effects volume": "Effects volume",
|
|
48
51
|
"EPISODE 1": "EPISODE 1",
|
|
49
52
|
"Episode recap": "Episode recap",
|
|
50
53
|
"errors": "errors",
|
|
@@ -56,10 +59,13 @@
|
|
|
56
59
|
"Files touched": "Files touched",
|
|
57
60
|
"Final report": "Final report",
|
|
58
61
|
"first error": "first error",
|
|
62
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.",
|
|
59
63
|
"Highlights": "Highlights",
|
|
60
64
|
"Human": "Human",
|
|
61
65
|
"idle": "idle",
|
|
62
66
|
"input": "input",
|
|
67
|
+
"Installing…": "Installing…",
|
|
68
|
+
"Intensity": "Intensity",
|
|
63
69
|
"is sharing their screen": "is sharing their screen",
|
|
64
70
|
"Join the call": "Join the call",
|
|
65
71
|
"Language": "Language",
|
|
@@ -70,6 +76,7 @@
|
|
|
70
76
|
"lines": "lines",
|
|
71
77
|
"Listing ": "Listing ",
|
|
72
78
|
"Little lines & animations": "Little lines & animations",
|
|
79
|
+
"Loading the model (downloaded on first use, about 155 MB)…": "Loading the model (downloaded on first use, about 155 MB)…",
|
|
73
80
|
"longest wait ": "longest wait ",
|
|
74
81
|
"Looking for a tool": "Looking for a tool",
|
|
75
82
|
"looping?": "looping?",
|
|
@@ -84,6 +91,7 @@
|
|
|
84
91
|
"No recent session": "No recent session",
|
|
85
92
|
"No system voice found: agents will mumble.": "No system voice found: agents will mumble.",
|
|
86
93
|
"Notify me when agents finish": "Notify me when agents finish",
|
|
94
|
+
"npm not found: install Node.js to enable this option.": "npm not found: install Node.js to enable this option.",
|
|
87
95
|
"Office ambience (keys, coffee…)": "Office ambience (keys, coffee…)",
|
|
88
96
|
"Office music (FIP radio)": "Office music (FIP radio)",
|
|
89
97
|
"OFFLINE": "OFFLINE",
|
|
@@ -134,6 +142,7 @@
|
|
|
134
142
|
"task": "task",
|
|
135
143
|
"Tasks": "Tasks",
|
|
136
144
|
"THE END": "THE END",
|
|
145
|
+
"The engine stopped: system voices are used instead.": "The engine stopped: system voices are used instead.",
|
|
137
146
|
"This dev was replaced by his own agents. Spare a coffee for his career change? ☕": "This dev was replaced by his own agents. Spare a coffee for his career change? ☕",
|
|
138
147
|
"Time": "Time",
|
|
139
148
|
"to": "to",
|
|
@@ -149,6 +158,8 @@
|
|
|
149
158
|
"Untitled session": "Untitled session",
|
|
150
159
|
"Updating todo list": "Updating todo list",
|
|
151
160
|
"Using skill ": "Using skill ",
|
|
161
|
+
"Very natural voices (installs on demand)": "Very natural voices (installs on demand)",
|
|
162
|
+
"Voices ready.": "Voices ready.",
|
|
152
163
|
"VS Code was started from inside a Claude Code session: restart VS Code normally (Dock, Spotlight) to enable translation.": "VS Code was started from inside a Claude Code session: restart VS Code normally (Dock, Spotlight) to enable translation.",
|
|
153
164
|
"waiting": "waiting",
|
|
154
165
|
"WAITING": "WAITING",
|