@double_geste/agent-standup 1.23.0 → 1.24.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/kokoro-worker.js +6 -1
- package/kokoro.js +2 -2
- package/package.json +1 -1
- package/public/index.html +105 -58
- package/public/locales/de.json +4 -4
- package/public/locales/en.json +4 -4
- package/public/locales/es.json +4 -4
- package/public/locales/ja.json +4 -4
- package/public/locales/pt-BR.json +4 -4
- package/public/locales/zh-CN.json +4 -4
- package/public/locales/zh-TW.json +4 -4
- package/server.js +42 -5
- package/public/meme.css +0 -60
- package/public/meme.js +0 -235
- 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/kokoro-worker.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs'), path = require('path'), readline = require('readline'), { createRequire } = require('module');
|
|
5
5
|
const MODEL = 'onnx-community/Kokoro-82M-v1.0-ONNX';
|
|
6
6
|
const send = (o) => fs.writeSync(1, JSON.stringify(o) + '\n');
|
|
7
|
+
const finite = (samples) => { for (let i = 0; i < samples.length; i++) if (samples[i] !== samples[i]) return false; return true; }; // a quantised model can return NaN for a whole line, which would be written as silence
|
|
7
8
|
function wav(samples, rate) { // Float32 in [-1, 1] → 16-bit mono WAV
|
|
8
9
|
const n = samples.length, buf = Buffer.alloc(44 + n * 2);
|
|
9
10
|
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);
|
|
@@ -18,7 +19,11 @@ async function main() {
|
|
|
18
19
|
send({ ready: true });
|
|
19
20
|
for await (const line of readline.createInterface({ input: process.stdin })) {
|
|
20
21
|
let m; try { m = JSON.parse(line); } catch { continue; }
|
|
21
|
-
try {
|
|
22
|
+
try {
|
|
23
|
+
let a = await tts.generate(String(m.text), { voice: m.voice, speed: +m.speed || 1 });
|
|
24
|
+
if (!finite(a.audio)) { a = await tts.generate(String(m.text), { voice: m.voice, speed: +m.speed || 1 }); if (!finite(a.audio)) throw new Error('the model returned NaN'); } // made once more; if it fails again the server hands the line to a system voice
|
|
25
|
+
fs.writeFileSync(m.out, wav(a.audio, a.sampling_rate)); send({ id: m.id, ok: true, seconds: a.audio.length / a.sampling_rate });
|
|
26
|
+
}
|
|
22
27
|
catch (e) { send({ id: m.id, ok: false, error: String((e && e.message) || e).slice(0, 200) }); }
|
|
23
28
|
}
|
|
24
29
|
}
|
package/kokoro.js
CHANGED
|
@@ -4,11 +4,11 @@ const fs = require('fs'), os = require('os'), path = require('path'), { spawn }
|
|
|
4
4
|
|
|
5
5
|
const WIN = process.platform === 'win32';
|
|
6
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 = '
|
|
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
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'], ['
|
|
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'], ['ff_siwis', 'Siwis', 'fr_FR']].map(([name, label, locale]) => ({ name, label, locale }));
|
|
12
12
|
const voices = () => VOICES, hasVoice = (n) => VOICES.some((v) => v.name === n);
|
|
13
13
|
const speedFor = (wpm) => Math.max(0.5, Math.min(2, Math.round((wpm / 178) * 20) / 20)); // 178 words a minute is about Kokoro's natural pace
|
|
14
14
|
const exists = (f) => { try { fs.statSync(f); return true; } catch { return false; } };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.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"
|
package/public/index.html
CHANGED
|
@@ -85,11 +85,12 @@
|
|
|
85
85
|
.prop.cheer { animation:cheer 1.1s ease-out both } @keyframes cheer { 0% { transform:scale(.3) translateY(10px); opacity:0 } 40% { transform:scale(1.25); opacity:1 } 100% { transform:translateY(-14px); opacity:0 } }
|
|
86
86
|
/* ---------- screen sharing (presentation mode) ---------- */
|
|
87
87
|
#present { display:none; flex:none; position:relative; height:clamp(190px,46vh,430px); background:radial-gradient(ellipse at 50% 0,#1b2033,#0b0d14); border-bottom:1px solid var(--line); padding:12px 16px; justify-content:center }
|
|
88
|
-
#present.on { display:flex; animation:fadein .3s }
|
|
88
|
+
#present.on { display:flex; animation:fadein .3s } #present.pic { height:clamp(220px,60vh,640px) }
|
|
89
89
|
.pwin { flex:0 1 900px; display:flex; flex-direction:column; border-radius:10px; overflow:hidden; background:#0d1117; box-shadow:0 18px 50px rgba(0,0,0,.6), 0 0 0 2px rgba(240,160,32,.55) }
|
|
90
90
|
.pbar { display:flex; align-items:center; gap:6px; padding:7px 10px; background:#1c1f2b; font-size:11.5px; color:#9aa0b4 } .pbar i { width:10px; height:10px; border-radius:50%; display:inline-block }
|
|
91
91
|
.pbar .r { background:#ff5f57 } .pbar .y { background:#febc2e } .pbar .g { background:#28c840 } #ptitle { margin:0 0 0 14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0 }
|
|
92
92
|
#pbody { flex:1; overflow:hidden; padding:10px 14px; font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; color:#d6dae6 } .pwin.editor #pbody { background:#1a1b26; padding:0 }
|
|
93
|
+
.pwin.image #pbody { display:flex; gap:8px; align-items:center; justify-content:center; background:#0b0c11; padding:8px } .pwin.image #pbody img { flex:0 1 auto; min-width:0; max-width:100%; max-height:100%; object-fit:contain; border-radius:4px } .pmore { color:#9aa0b4; font-size:12px } .dimg { display:block; max-width:100%; border-radius:6px; margin:4px 0 }
|
|
93
94
|
.ptab { display:flex; align-items:center; gap:10px; background:#16161e; padding:5px 12px; border-bottom:1px solid #2a2b3d; font:11.5px -apple-system,system-ui,sans-serif } .ptab span { background:#1a1b26; padding:3px 10px; border-radius:6px 6px 0 0; color:#e9eaf0 } .ptab em { color:#6b7089; font-style:normal }
|
|
94
95
|
.ln { display:flex; gap:10px; padding:0 12px; white-space:pre-wrap; word-break:break-all } .ln b { width:12px; flex:none; color:#7a7f98; font-weight:400 } .ln.add { background:rgba(63,185,80,.16); color:#9be9a8 } .ln.del { background:rgba(248,81,73,.16); color:#ffa198 }
|
|
95
96
|
.tl { white-space:pre-wrap; word-break:break-word } .tl .ps { color:#7ee787 } .tl.cmd { color:#e6edf3 } .tl.tool { color:#8b95b0 } .dotg { color:#58a6ff }
|
|
@@ -237,8 +238,6 @@
|
|
|
237
238
|
<h4 data-fr="Son" data-en="Sound">Son</h4>
|
|
238
239
|
<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
240
|
<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>
|
|
242
241
|
<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>
|
|
243
242
|
<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>
|
|
244
243
|
<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>
|
|
@@ -260,6 +259,8 @@
|
|
|
260
259
|
<button class="mi" id="mCoffee" style="display:none"><span id="mCoffeeT"></span></button>
|
|
261
260
|
<button class="mi" id="mRecap"><span data-fr="📋 Récapitulatif" data-en="📋 Recap">📋 Récapitulatif</span></button>
|
|
262
261
|
<button class="mi" id="mExport"><span data-fr="🎞 Exporter la vidéo" data-en="🎞 Export video">🎞 Exporter la vidéo</span></button>
|
|
262
|
+
<button class="mi" id="mClip"><span data-fr="🎬 Clip à partager (25 s)" data-en="🎬 Clip to share (25 s)">🎬 Clip à partager (25 s)</span></button>
|
|
263
|
+
<button class="mi" data-t="hideCode"><span data-fr="Masquer code et fichiers dans les vidéos" data-en="Hide code & file names in videos">Masquer code et fichiers dans les vidéos</span><i class="sw"></i></button>
|
|
263
264
|
<div class="keys">Space ⏯ · M 🔇 · T tree · R recap · L log · Esc ✕</div>
|
|
264
265
|
<button class="mi" id="mBrowser" style="display:none"><span data-fr="↗ Ouvrir dans le navigateur" data-en="↗ Open in browser">↗ Ouvrir dans le navigateur</span></button>
|
|
265
266
|
</div>
|
|
@@ -285,8 +286,6 @@
|
|
|
285
286
|
</footer>
|
|
286
287
|
<div id="card"></div><div id="recap"><div class="box"></div></div>
|
|
287
288
|
|
|
288
|
-
<link rel="stylesheet" href="/meme.css">
|
|
289
|
-
<script src="/meme.js"></script>
|
|
290
289
|
<script src="/api/prefs.js"></script>
|
|
291
290
|
<script>
|
|
292
291
|
// =====================================================================
|
|
@@ -302,7 +301,7 @@ const Q = new URLSearchParams(location.search);
|
|
|
302
301
|
const EMBED = Q.get('embed') === '1' || window.parent !== window;
|
|
303
302
|
if (Q.get('pin')) S.pin = Q.get('pin');
|
|
304
303
|
|
|
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' };
|
|
304
|
+
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 };
|
|
306
305
|
{ 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
306
|
const sv = window.SAVED_PREFS && typeof window.SAVED_PREFS === 'object' ? window.SAVED_PREFS : {}; Object.assign(prefs, (sv._t || 0) > (loc._t || 0) ? sv : loc); }
|
|
308
307
|
if (prefs.vstyle === 'babble') prefs.vstyle = 'animal', prefs.vstyleChosen = true;
|
|
@@ -542,7 +541,7 @@ const AUD = {
|
|
|
542
541
|
const src = c.createBufferSource(); src.buffer = buf; src.loop = true; const lp = c.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 500;
|
|
543
542
|
const g = c.createGain(); g.gain.value = 0.3; src.connect(lp).connect(g).connect(this.sfx); src.start(); this.apply(); OFFICE.start();
|
|
544
543
|
},
|
|
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();
|
|
544
|
+
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(); },
|
|
546
545
|
// Music fades down quickly when an agent starts talking and comes back slowly once the room is quiet.
|
|
547
546
|
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); },
|
|
548
547
|
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); },
|
|
@@ -1052,7 +1051,7 @@ function pushTerm(id, tool) {
|
|
|
1052
1051
|
}
|
|
1053
1052
|
function applyResults(ev, silent) { // the output of a tool call arrives
|
|
1054
1053
|
const buf = S.term[ev.agent] || []; let last = null;
|
|
1055
|
-
for (const it of ev.items) for (let i = buf.length - 1; i >= 0; i--) if (buf[i].id === it.id) { Object.assign(buf[i], { out: it.out, err: it.err, end: ev.ts }); last = buf[i]; break; }
|
|
1054
|
+
for (const it of ev.items) for (let i = buf.length - 1; i >= 0; i--) if (buf[i].id === it.id) { Object.assign(buf[i], { out: it.out, err: it.err, end: ev.ts, img: it.img || 0, picAt: it.img && !silent ? performance.now() : 0 }); last = buf[i]; break; }
|
|
1056
1055
|
if (S.run[ev.agent] && ev.items.some((it) => it.id === S.run[ev.agent].id)) delete S.run[ev.agent];
|
|
1057
1056
|
if (silent || !last) return;
|
|
1058
1057
|
setTicker(ev.agent, last); if (S.present === ev.agent) { renderPresent(); armPresent(); }
|
|
@@ -1081,8 +1080,11 @@ function statOf(e, id) {
|
|
|
1081
1080
|
if (e.end == null) { const r = S.run[id], el = r ? performance.now() - r.start : 0; return `⠋ ${e.tool.name} · ${L('en cours', 'running')}${el > 1500 ? ' ' + fmtDur(el) : ''}`; }
|
|
1082
1081
|
return `${e.err ? '✗' : '✓'} ${e.tool.name} · ${fmtDur(e.end - e.ts)}`;
|
|
1083
1082
|
}
|
|
1083
|
+
const mediaUrl = (agent, tool, n) => `/api/media?proj=${encodeURIComponent(S.cur.proj)}&id=${encodeURIComponent(S.cur.id)}&agent=${encodeURIComponent(agent)}&tool=${encodeURIComponent(tool)}&n=${n}`;
|
|
1084
1084
|
function termModel(id) {
|
|
1085
1085
|
const buf = S.term[id] || [], last = buf[buf.length - 1], lines = [], more = (n) => ({ c: 'dim', t: `… +${n} ${L('lignes', 'lines')}` });
|
|
1086
|
+
const pic = S.cur && buf.slice(-12).reverse().find((e) => e.img), fresh = pic && performance.now() - pic.picAt < (S.live ? 8000 : 2500 / spd()); // a picture a tool returned (screenshot, image file) stays up for a few seconds whatever comes next, then for the next few calls: a screenshot is usually followed by a click or two
|
|
1087
|
+
if (pic && (fresh || (buf.length - buf.indexOf(pic) <= 4 && (pic === last || !/^(edit|write|read)$/.test(last.k))))) return { view: 'image', title: pic.k === 'read' ? base(pic.tool.d.file) : pic.tool.name.split('__').pop(), imgs: Array.from({ length: Math.min(pic.img, 3) }, (_, n) => mediaUrl(id, pic.id, n)), more: pic.img - 3, lines, stat: statOf(last, id) };
|
|
1086
1088
|
if (last && (last.k === 'edit' || last.k === 'write' || last.k === 'read')) {
|
|
1087
1089
|
const d = last.tool.d, rows = (txt, c) => String(txt || '').split('\n').slice(0, 14).map((t) => ({ c, t }));
|
|
1088
1090
|
if (last.k === 'read') {
|
|
@@ -1108,20 +1110,21 @@ const lineHtml = (l, view) => view === 'editor'
|
|
|
1108
1110
|
// ---------- screen sharing: a real video-call presentation (window + presenter camera) ----------
|
|
1109
1111
|
function renderPresent() {
|
|
1110
1112
|
const id = S.present, p = S.agents[id]; if (!p) return; const m = termModel(id);
|
|
1111
|
-
$('#present .pwin').className = 'pwin ' + (m.view === '
|
|
1112
|
-
$('#ptitle').textContent = m.view === '
|
|
1113
|
-
const pb = $('#pbody');
|
|
1114
|
-
|
|
1115
|
-
pb.
|
|
1113
|
+
$('#present .pwin').className = 'pwin ' + (m.view === 'term' ? 'term' : m.view); if ($('#present').classList.toggle('pic', m.view === 'image') !== S.picUp) { S.picUp = m.view === 'image'; layout(); } // a picture gets a taller window
|
|
1114
|
+
$('#ptitle').textContent = m.view === 'term' ? `${p.name} — zsh — 96×24` : m.title;
|
|
1115
|
+
const pb = $('#pbody'), shown = m.view === 'image' ? m.imgs.join(' ') : '';
|
|
1116
|
+
if (shown) { if (pb.dataset.img !== shown) pb.innerHTML = m.imgs.map((u) => `<img src="${esc(u)}" alt="">`).join('') + (m.more > 0 ? `<span class="pmore">+${m.more}</span>` : ''); } // the same pictures are left in place: redrawing them would make them blink
|
|
1117
|
+
else pb.innerHTML = (m.view === 'editor' ? `<div class="ptab"><span>${esc(m.tab.file)}</span><em>${esc(m.tab.path)}</em></div>` : '') + m.lines.map((l) => lineHtml(l, m.view)).join('') + (m.view === 'term' ? '<div class="tl"><span class="ps">❯</span> <span class="cursor"></span></div>' : '');
|
|
1118
|
+
pb.dataset.img = shown; pb.scrollTop = m.view === 'term' ? pb.scrollHeight : 0; $('#pstat').textContent = m.stat;
|
|
1116
1119
|
const td = S.todos[id], el = $('#ptodo'); el.style.display = td && td.length ? '' : 'none'; $('#present').classList.toggle('has-todo', !!(td && td.length));
|
|
1117
1120
|
if (td) el.innerHTML = `<h5>${L('Tâches', 'Tasks')} · ${td.filter((x) => x.s === 'completed').length}/${td.length}</h5>` + td.map((x) => `<div class="td ${x.s}"><i>${x.s === 'completed' ? '☑' : x.s === 'in_progress' ? '◐' : '☐'}</i>${esc(x.c)}</div>`).join('');
|
|
1118
1121
|
$('#pbadge').textContent = `🖥 ${p.name} ${L('partage son écran', 'is sharing their screen')}`;
|
|
1119
1122
|
if (S.camFor !== id) { $('#pcam').innerHTML = avatarSVG(p, undefined, 'c' + p.gid + '_') + `<div class="tag"><b>${esc(p.name)}</b></div>`; S.camFor = id; }
|
|
1120
1123
|
$('#pcam').classList.toggle('speaking', S.speakers.has(id));
|
|
1121
1124
|
}
|
|
1122
|
-
function armPresent() { // stays up while a command is still running, then ~4 s after the last action
|
|
1125
|
+
function armPresent() { // stays up while a command is still running, then ~4 s after the last action (~9 s when it shows a picture)
|
|
1123
1126
|
clearTimeout(presentTimer); if (S.hold) return;
|
|
1124
|
-
presentTimer = setTimeout(() => { if (S.present && S.run[S.present] && S.playing) armPresent(); else hidePresent(); }, 4200 / Math.max(1, spd() / 1.2));
|
|
1127
|
+
presentTimer = setTimeout(() => { if (S.present && S.run[S.present] && S.playing) armPresent(); else hidePresent(); }, ($('#present').classList.contains('pic') ? 9000 : 4200) / Math.max(1, spd() / 1.2));
|
|
1125
1128
|
}
|
|
1126
1129
|
function showPresent(id) {
|
|
1127
1130
|
if (S.boost > 1 || (S.speakers.size && !S.speakers.has(id))) return; // catching up, or someone else has the floor
|
|
@@ -1166,7 +1169,7 @@ function vanish(id) {
|
|
|
1166
1169
|
S.gone.add(id); const t = $('#t-' + id), p = S.agents[id]; if (!t || !p) return;
|
|
1167
1170
|
toast(`${p.name} ${L('a quitté l’appel', 'left the call')} 👋`);
|
|
1168
1171
|
if (t.style.display === 'none' || document.body.classList.contains('tree')) return;
|
|
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);
|
|
1172
|
+
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);
|
|
1170
1173
|
setTimeout(() => { S.leaving.delete(id); t.classList.remove('vanishing'); if (S.gone.has(id)) t.style.display = 'none'; }, 1250); };
|
|
1171
1174
|
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
|
|
1172
1175
|
}
|
|
@@ -1181,7 +1184,8 @@ const mouths = new Map();
|
|
|
1181
1184
|
function startTalk(id, text, label) {
|
|
1182
1185
|
const t = $('#t-' + id); if (!t) return;
|
|
1183
1186
|
if (S.present && S.present !== id) hidePresent();
|
|
1184
|
-
|
|
1187
|
+
S.spokeAt = performance.now(); S.fill = 0;
|
|
1188
|
+
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);
|
|
1185
1189
|
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
1190
|
if (S.present === id) { $('#pcap').textContent = first; $('#pcam').classList.add('speaking'); }
|
|
1187
1191
|
const m = t.querySelector('.mouthE'); let open = false; clearInterval(mouths.get(id));
|
|
@@ -1198,13 +1202,13 @@ setInterval(() => { // now and then a busy agent has a little thought; never w
|
|
|
1198
1202
|
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');
|
|
1199
1203
|
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)]); }
|
|
1200
1204
|
}, 7000);
|
|
1201
|
-
function oops(id) {
|
|
1205
|
+
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, '😬'); }
|
|
1202
1206
|
function confetti() {
|
|
1203
|
-
|
|
1207
|
+
if (!prefs.banter || document.hidden) return; const box = $('#center'), cols = ['#f5c04a', '#7c9cff', '#ff7a6b', '#5ad19a', '#c58bff'];
|
|
1204
1208
|
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); }
|
|
1205
1209
|
}
|
|
1206
1210
|
function stopTalk(id) {
|
|
1207
|
-
captionClear(id);
|
|
1211
|
+
captionClear(id); S.spokeAt = performance.now();
|
|
1208
1212
|
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]);
|
|
1209
1213
|
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); } }
|
|
1210
1214
|
const t = $('#t-' + id); if (!t) return;
|
|
@@ -1246,7 +1250,7 @@ function renderDetail() {
|
|
|
1246
1250
|
const u = S.usage[id], tokHtml = u && tokTotal(u) ? `<h3>${L('Jetons', 'Tokens')} · ⚡ ${fmtTok(tokTotal(u))}</h3><div class="dim">${L('entrée', 'input')} ${fmtTok(u.i)} · ${L('sortie', 'output')} ${fmtTok(u.o)} · ${L('écriture cache', 'cache write')} ${fmtTok(u.w)} · ${L('lecture cache', 'cache read')} ${fmtTok(u.r)}</div>` : '';
|
|
1247
1251
|
$('#dbody').innerHTML = tokHtml + (order ? `<h3>${L('Consigne de', 'Prompt from')} ${esc(nameOf(order.agent))}</h3><pre>${esc(order.text)}</pre>` : '') +
|
|
1248
1252
|
`<h3>${L('Outils', 'Tools')} (${tools.length})</h3><div>${Object.entries(counts).map(([n, c]) => `${esc(n)} ×${c}`).join(' · ') || '—'}</div>` +
|
|
1249
|
-
`<h3>${L('Activité récente', 'Recent activity')}</h3>` + (S.term[id] || []).slice(-30).reverse().map((e) => `<details><summary>${e.end == null ? '⠋' : e.err ? '✗' : '✓'} ${esc(e.tool.name)} <span class="dim">${e.end != null ? fmtDur(e.end - e.ts) : L('en cours', 'running')}</span> — ${esc((e.tool.arg || '').slice(-60))}</summary><pre>${esc((e.tool.d && e.tool.d.kind === 'cmd' ? '$ ' + e.tool.d.text + '\n' : '') + (e.out || ''))}</pre
|
|
1253
|
+
`<h3>${L('Activité récente', 'Recent activity')}</h3>` + (S.term[id] || []).slice(-30).reverse().map((e) => `<details><summary>${e.end == null ? '⠋' : e.err ? '✗' : '✓'} ${esc(e.tool.name)} <span class="dim">${e.end != null ? fmtDur(e.end - e.ts) : L('en cours', 'running')}</span> — ${esc((e.tool.arg || '').slice(-60))}</summary><pre>${esc((e.tool.d && e.tool.d.kind === 'cmd' ? '$ ' + e.tool.d.text + '\n' : '') + (e.out || ''))}</pre>${e.img && S.cur ? `<img class="dimg" src="${esc(mediaUrl(id, e.id, 0))}" alt="" loading="lazy">` : ''}</details>`).join('') +
|
|
1250
1254
|
evs.filter((e) => e.agent === id && (e.kind === 'say' || e.kind === 'report')).map((e) => `<h3>${e.kind === 'report' ? L('Rapport final', 'Final report') : L('A dit', 'Said')}</h3><pre>${esc(e.text)}</pre>`).join('');
|
|
1251
1255
|
}
|
|
1252
1256
|
$('#dclose').onclick = () => { S.detail = null; $('#detail').classList.remove('on'); };
|
|
@@ -1306,12 +1310,18 @@ function computeHighlights() {
|
|
|
1306
1310
|
if (e.kind !== 'compact') prevT = e.t;
|
|
1307
1311
|
const d = loopStep(R, e); if (d && loops.length < 3 && !loops.some((l) => l.agent === e.agent)) loops.push({ i, agent: e.agent, d });
|
|
1308
1312
|
});
|
|
1309
|
-
if (firstErr >= 0) out.push({ i: firstErr, t: '✗ ' + L('première erreur', 'first error') });
|
|
1310
|
-
if (gap.ms >= 30000) out.push({ i: gap.i, t: '⏱ ' + L('plus longue attente ', 'longest wait ') + fmtDur(gap.ms) });
|
|
1311
|
-
if (comp.length) out.push({ i: comp[0], t: `🗜 ${L('compactage', 'compaction')}${comp.length > 1 ? ' ×' + comp.length : ''}` });
|
|
1312
|
-
loops.forEach((l) => out.push({ i: l.i, t: `🔁 ${nameOf(l.agent)} ${l.d.kind === 'errors' ? L('échoue en boucle ?', 'failing in a loop?') : L('boucle ?', 'looping?')}` }));
|
|
1313
|
+
if (firstErr >= 0) out.push({ i: firstErr, k: 'err', t: '✗ ' + L('première erreur', 'first error') });
|
|
1314
|
+
if (gap.ms >= 30000) out.push({ i: gap.i, k: 'wait', t: '⏱ ' + L('plus longue attente ', 'longest wait ') + fmtDur(gap.ms) });
|
|
1315
|
+
if (comp.length) out.push({ i: comp[0], k: 'compact', t: `🗜 ${L('compactage', 'compaction')}${comp.length > 1 ? ' ×' + comp.length : ''}` });
|
|
1316
|
+
loops.forEach((l) => out.push({ i: l.i, k: 'loop', t: `🔁 ${nameOf(l.agent)} ${l.d.kind === 'errors' ? L('échoue en boucle ?', 'failing in a loop?') : L('boucle ?', 'looping?')}` }));
|
|
1313
1317
|
out.sort((a, b) => a.i - b.i); return out;
|
|
1314
1318
|
}
|
|
1319
|
+
// Where a short clip starts: one spoken line before the first error or loop when there is one (so it is seen coming), otherwise before the lead first hands out work.
|
|
1320
|
+
function clipStart() {
|
|
1321
|
+
const ev = S.events, h = computeHighlights().find((x) => x.k === 'err' || x.k === 'loop'), at = h ? h.i : ev.findIndex((e) => e.kind === 'order'); if (at < 0) return 0;
|
|
1322
|
+
let i = at; while (i > 0 && at - i < 12 && !SPK.has(ev[--i].kind));
|
|
1323
|
+
return i;
|
|
1324
|
+
}
|
|
1315
1325
|
function renderHighlights() {
|
|
1316
1326
|
const box = $('#hl'); if (!box) return; const hs = S.live ? [] : computeHighlights();
|
|
1317
1327
|
box.classList.toggle('has', hs.length > 0); box.innerHTML = hs.length ? `<span class="dim" style="font-size:11.5px">${L('Moments clés', 'Highlights')}</span>` + hs.map((h) => `<button class="hlc" data-i="${h.i}">${esc(h.t)}</button>`).join('') : '';
|
|
@@ -1381,8 +1391,25 @@ async function react(ev, token) {
|
|
|
1381
1391
|
else return;
|
|
1382
1392
|
if (!S.agents[who]) return; startTalk(who, line, ev.kind === 'order' && prefs.banter ? '💬' : '🎬'); await speakLine(line, S.agents[who], true, token); stopTalk(who);
|
|
1383
1393
|
}
|
|
1394
|
+
// Small talk: when nobody has said anything for a while, someone fills the silence the way people do in video calls. At most three per silence, further and further apart; any other line starts the count again.
|
|
1395
|
+
const FILL = {
|
|
1396
|
+
en: { share: ['Can everyone see my screen?', 'Bear with me, it’s loading.', 'Sorry, my machine is slow today.', 'Is this big enough for everyone?', 'One second… almost there.', 'Tell me if I’m going too fast.'],
|
|
1397
|
+
busy: ['Still here, just heads-down.', 'Sorry, I was on mute.', 'Quick update: still on it.', 'No blockers on my side.', 'Can you hear me?', 'Is it just me, or is it quiet?', 'I’ll have an update in a minute.'],
|
|
1398
|
+
wait: ['Can you hear me?', 'You guys still here?', 'Hello? I think you’re on mute.', 'Did we lose someone?', 'Is it my connection?', 'Shall we take this offline?', 'Let’s circle back when you’re ready.', 'I’ll give everyone a minute back.', 'Any other business?', 'Sorry, go ahead. No, you go.'] },
|
|
1399
|
+
fr: { share: ['Vous voyez bien mon écran ?', 'Deux secondes, ça charge.', 'Désolé, ma machine rame aujourd’hui.', 'C’est assez gros pour tout le monde ?', 'Une seconde… j’y suis presque.', 'Dites-moi si je vais trop vite.'],
|
|
1400
|
+
busy: ['Toujours là, je suis dessus.', 'Pardon, j’étais en muet.', 'Petit point : j’avance.', 'Pas de point bloquant de mon côté.', 'Vous m’entendez ?', 'C’est moi, ou c’est calme ?', 'Je vous fais un retour dans une minute.'],
|
|
1401
|
+
wait: ['Vous m’entendez ?', 'Vous êtes toujours là ?', 'Allô ? Je crois que ton micro est coupé.', 'On a perdu quelqu’un ?', 'C’est ma connexion ?', 'On en reparle en off ?', 'On refait un point quand tu veux.', 'Je vous rends une minute.', 'D’autres sujets ?', 'Pardon, vas-y. Non, toi d’abord.'] },
|
|
1402
|
+
};
|
|
1403
|
+
async function smallTalk(token) {
|
|
1404
|
+
const n = S.fill || 0, last = S.events[S.events.length - 1]; if (!prefs.banter || n > 2 || !last || S.speakers.size || S.hurry || S.boost > 1 || (document.hidden && !S.exporting)) return;
|
|
1405
|
+
if (performance.now() - S.spokeAt < (S.live ? 45000 : 12000 / spd()) * [1, 2.5, 6][n] || (S.live && Date.now() - last.ts > 6e5)) return; // live: a session untouched for ten minutes is a call that is over
|
|
1406
|
+
const here = Object.keys(S.agents).filter((id) => id !== 'user' && $('#t-' + id) && $('#t-' + id).style.display !== 'none'), busy = here.filter((id) => agentState(id, Date.now()).k === 'working'), pick = (a) => a[Math.floor(Math.random() * a.length)];
|
|
1407
|
+
const who = S.present && here.includes(S.present) ? S.present : pick(busy.length ? busy : here), P = FILL[FR() ? 'fr' : 'en'][who === S.present ? 'share' : busy.length ? 'busy' : 'wait']; if (!who) return; // whoever shares their screen speaks, so the screen stays up
|
|
1408
|
+
let line = pick(P); if (line === S.lastFill) line = P[(P.indexOf(line) + 1) % P.length]; S.lastFill = line;
|
|
1409
|
+
startTalk(who, line, '💬'); S.fill = n + 1; await speakLine(line, S.agents[who], true, token); stopTalk(who);
|
|
1410
|
+
}
|
|
1384
1411
|
async function card(text, sub, ms = 2500) {
|
|
1385
|
-
|
|
1412
|
+
|
|
1386
1413
|
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);
|
|
1387
1414
|
await sleep(ms / Math.min(2, spd())); c.classList.remove('on');
|
|
1388
1415
|
}
|
|
@@ -1394,11 +1421,11 @@ const SPK = new Set(['say', 'report', 'order', 'user']);
|
|
|
1394
1421
|
function aheadSpeakable(i, limit) { let n = 0; for (let k = i, m = Math.min(S.events.length, i + 800); k < m; k++) if (SPK.has(S.events[k].kind) && ++n > limit) break; return n; }
|
|
1395
1422
|
const isSub = (id) => id !== 'main' && id !== 'user';
|
|
1396
1423
|
async function run(i) {
|
|
1397
|
-
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; syncUI(); AUD.init();
|
|
1424
|
+
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; S.spokeAt = performance.now(); S.fill = 0; syncUI(); AUD.init();
|
|
1398
1425
|
if (i === 0 && prefs.epi && !S.live) { await card(L('ÉPISODE 1', 'EPISODE 1'), S.title); if (token !== S.token) return; }
|
|
1399
1426
|
while (token === S.token) {
|
|
1400
1427
|
const lim = S.stopAt || S.events.length;
|
|
1401
|
-
if (i >= lim) { if (S.live && !S.stopAt) { if (!S.idle) { S.idle = true; syncUI(); } await sleep(350); continue; } break; }
|
|
1428
|
+
if (i >= lim) { if (S.live && !S.stopAt) { if (!S.idle) { S.idle = true; syncUI(); } await smallTalk(token); await sleep(350); continue; } break; }
|
|
1402
1429
|
if (S.idle) { S.idle = false; syncUI(); }
|
|
1403
1430
|
if (S.live) { // way behind: skip ahead to stay current, but only count what is actually said: sub-agents produce thousands of silent tool events
|
|
1404
1431
|
const rem = S.events.length - i;
|
|
@@ -1413,8 +1440,8 @@ async function run(i) {
|
|
|
1413
1440
|
const ev = S.events[i]; S.idx = i; tally(ev);
|
|
1414
1441
|
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; }
|
|
1415
1442
|
if (ev.kind === 'finish') { applyStatus(ev); paintStatus(); i++; continue; }
|
|
1416
|
-
if (ev.kind === 'results') { applyResults(ev, S.boost > 1); await sleep((S.demo ? 900 : S.live ? 30 : 80) / spd()); i++; continue; }
|
|
1417
|
-
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name);
|
|
1443
|
+
if (ev.kind === 'results') { applyResults(ev, S.boost > 1); await sleep((S.boost === 1 && S.present === ev.agent && ev.items.some((it) => it.img) ? (S.live ? 900 : 1800) : S.demo ? 900 : S.live ? 30 : 80) / spd()); i++; continue; } // a picture gets a moment on screen
|
|
1444
|
+
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name); paintStatus(); await smallTalk(token); await sleep((S.demo ? 1100 : S.live ? 110 : 250) / spd()); i++; continue; }
|
|
1418
1445
|
const group = [ev]; let j = i + 1; // sub-agents talking back-to-back were running in parallel: they talk over each other
|
|
1419
1446
|
if (isSub(ev.agent) && ev.kind !== 'order') {
|
|
1420
1447
|
while (j < Math.min(S.events.length, i + 28) && group.length < 3) {
|
|
@@ -1427,14 +1454,13 @@ async function run(i) {
|
|
|
1427
1454
|
}
|
|
1428
1455
|
}
|
|
1429
1456
|
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) });
|
|
1431
1457
|
prefetchQuips(i); warmTTS(i);
|
|
1432
1458
|
const playOne = (g) => {
|
|
1433
1459
|
applyStatus(g);
|
|
1434
1460
|
const label = g.kind === 'order' ? `${L('pour', 'to')} ${nameOf(g.to)}` : g.kind === 'report' ? `${L('rapport à', 'report to')} ${nameOf(g.to)}` : '', line = spoken(g);
|
|
1435
1461
|
startTalk(g.agent, line, label);
|
|
1436
|
-
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join();
|
|
1437
|
-
if (g.kind === 'report') { AUD.ding();
|
|
1462
|
+
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1463
|
+
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
1438
1464
|
return speakLine(clipSpeech(line, S.pace), S.agents[g.agent], true, token, FR() && trMem.has(baseText(g))).then(() => stopTalk(g.agent));
|
|
1439
1465
|
};
|
|
1440
1466
|
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
|
|
@@ -1589,6 +1615,8 @@ function avatarImg(p, m, st) { // m: 0 silent, 1 speaking (closed), 2 speaking
|
|
|
1589
1615
|
const k = p.id + m + (st || ''); if (imgCache[k]) return imgCache[k];
|
|
1590
1616
|
const im = new Image(); im.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(avatarSVG(p, { m, working: st === 'working', done: st === 'done' })); return (imgCache[k] = im);
|
|
1591
1617
|
}
|
|
1618
|
+
const mediaMem = new Map();
|
|
1619
|
+
function mediaImg(url) { let im = mediaMem.get(url); if (!im) { im = new Image(); im.src = url; mediaMem.set(url, im); if (mediaMem.size > 24) mediaMem.delete(mediaMem.keys().next().value); } return im; }
|
|
1592
1620
|
function wrap(c, text, maxW, maxLines) {
|
|
1593
1621
|
const words = String(text).split(/\s+/), lines = []; let cur = '';
|
|
1594
1622
|
for (const w of words) { const t = cur ? cur + ' ' + w : w; if (c.measureText(t).width > maxW && cur) { lines.push(cur); cur = w; if (lines.length === maxLines) break; } else cur = t; }
|
|
@@ -1600,21 +1628,35 @@ function tileOnCanvas(c, id, x, y, w, h, big) {
|
|
|
1600
1628
|
if (im.complete && im.naturalWidth) c.drawImage(im, x, y, w, h);
|
|
1601
1629
|
c.lineWidth = sp ? 4 : 2; c.strokeStyle = sp ? (p.lead ? '#f5c04a' : '#7c9cff') : st === 'working' ? '#f0a020' : 'rgba(255,255,255,.14)'; c.strokeRect(x, y, w, h);
|
|
1602
1630
|
c.font = `bold ${big ? 16 : 12}px sans-serif`; const nm = p.name + (st === 'done' ? ' ✓' : ''); c.fillStyle = 'rgba(0,0,0,.7)'; c.fillRect(x + 6, y + h - (big ? 30 : 22), c.measureText(nm).width + 12, big ? 24 : 18); c.fillStyle = '#fff'; c.fillText(nm, x + 12, y + h - (big ? 12 : 8));
|
|
1603
|
-
if (!big && S.lastTool[id] && st === 'working') { c.font = '10px monospace'; c.fillStyle = 'rgba(15,20,35,.85)'; c.fillRect(x, y, w, 15); c.fillStyle = '#ffd24a'; c.fillText(S.lastTool[id].slice(0, Math.floor(w / 6)), x + 4, y + 11); }
|
|
1631
|
+
if (!big && S.lastTool[id] && st === 'working') { c.font = '10px monospace'; c.fillStyle = 'rgba(15,20,35,.85)'; c.fillRect(x, y, w, 15); c.fillStyle = '#ffd24a'; c.fillText((prefs.hideCode ? S.lastTool[id].split(' ')[0] : S.lastTool[id]).slice(0, Math.floor(w / 6)), x + 4, y + 11); }
|
|
1604
1632
|
if (big && sp && S.say[id]) { c.font = '15px sans-serif'; const Ls = wrap(c, S.say[id].text, w - 40, 4), bh = Ls.length * 19 + 14; c.fillStyle = '#fff'; c.beginPath(); c.roundRect(x + 10, y + 10, w - 20, bh, 10); c.fill(); c.fillStyle = '#1b1d27'; Ls.forEach((l, i) => c.fillText(l, x + 20, y + 30 + i * 19)); }
|
|
1605
1633
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
c
|
|
1634
|
+
const SITE = 'atoll92.github.io/agent-standup';
|
|
1635
|
+
function drawFrame(cv, mark = 0) { // mark: 1 = a clip to share (site address in the corner), 2 = its end card
|
|
1636
|
+
const c = cv.getContext('2d'), W = cv.width, H = cv.height, hide = prefs.hideCode; c.fillStyle = '#12131a'; c.fillRect(0, 0, W, H); // hide: code, outputs, file names and the session title are masked; what agents say is kept
|
|
1637
|
+
if (mark === 2) { c.textAlign = 'center'; c.font = '900 76px "Comic Sans MS",sans-serif'; c.fillStyle = '#e9eaf0'; c.fillText('Agent Standup', W / 2, H / 2 - 24); c.font = '28px sans-serif'; c.fillStyle = '#cdd6f4'; c.fillText(L('Vos agents Claude Code en appel d’équipe', 'Your Claude Code agents as a live team call'), W / 2, H / 2 + 36); c.font = 'bold 26px sans-serif'; c.fillStyle = '#f5c04a'; c.fillText(SITE, W / 2, H / 2 + 96); c.textAlign = 'start'; return; }
|
|
1638
|
+
c.font = 'bold 20px "Comic Sans MS",sans-serif'; c.fillStyle = '#e9eaf0'; c.fillText('🎥 ' + ((!hide && S.title) || 'Agent Standup'), 20, 32);
|
|
1639
|
+
if (mark) { c.font = '13px sans-serif'; c.textAlign = 'right'; const w = c.measureText(SITE).width; c.fillStyle = '#12131a'; c.fillRect(W - 32 - w, 8, w + 32, 34); c.fillStyle = '#8b90a8'; c.fillText(SITE, W - 20, 30); c.textAlign = 'start'; }
|
|
1609
1640
|
const ids = Object.keys(S.agents).filter((id) => id === 'main' || id === 'user' || (S.status[id] && !S.gone.has(id))), sp = [...S.speakers].filter((id) => S.agents[id]).slice(0, 3), big = sp.length ? sp : ['main', 'user'];
|
|
1610
|
-
const pres = S.present && S.agents[S.present], bw = Math.min(480, (W - 40) / big.length - 12), bh = pres ? 330 : bw * 0.75;
|
|
1641
|
+
const pres = S.present && S.agents[S.present], m = pres && termModel(S.present), bw = Math.min(480, (W - 40) / big.length - 12), bh = pres ? (m.view === 'image' ? 420 : 330) : bw * 0.75; // a picture gets a taller window
|
|
1611
1642
|
if (pres) {
|
|
1612
|
-
const
|
|
1643
|
+
const x = 20, y = 50, w = W - 40, ed = m.view === 'editor', col = { cmd: '#e6edf3', out: '#aab2c5', err: '#ff7b72', tool: '#8b95b0', dim: '#6b7089', run: '#ffd24a', add: '#9be9a8', del: '#ffa198', ctx: '#d6dae6' };
|
|
1613
1644
|
c.fillStyle = ed ? '#1a1b26' : '#0d1117'; c.beginPath(); c.roundRect(x, y, w, bh, 12); c.fill(); c.fillStyle = '#1c1f2b'; c.fillRect(x, y, w, 28);
|
|
1614
1645
|
['#ff5f57', '#febc2e', '#28c840'].forEach((cl, i) => { c.fillStyle = cl; c.beginPath(); c.arc(x + 16 + i * 18, y + 14, 5, 0, 7); c.fill(); });
|
|
1615
|
-
c.font = '13px sans-serif'; c.fillStyle = '#9aa0b4'; c.fillText(ed ? m.title : S.agents[S.present].name + ' — zsh', x + 90, y + 19); c.font = '14px monospace';
|
|
1616
|
-
|
|
1617
|
-
|
|
1646
|
+
c.font = '13px sans-serif'; c.fillStyle = '#9aa0b4'; c.fillText(ed ? (hide ? m.title.split(' — ').pop() : m.title) : m.view === 'image' ? (hide ? '' : m.title) : S.agents[S.present].name + ' — zsh', x + 90, y + 19); c.font = '14px monospace';
|
|
1647
|
+
if (m.view === 'image') { // masked like the code: a picture from the session is replaced by an empty frame
|
|
1648
|
+
const im = hide ? null : mediaImg(m.imgs[0]), ax = x + 14, ay = y + 38, aw = w - 238, ah = bh - 82, k = im && im.complete && im.naturalWidth ? Math.min(aw / im.naturalWidth, ah / im.naturalHeight, 1.5) : 0;
|
|
1649
|
+
if (k) c.drawImage(im, ax + (aw - im.naturalWidth * k) / 2, ay + (ah - im.naturalHeight * k) / 2, im.naturalWidth * k, im.naturalHeight * k);
|
|
1650
|
+
else { c.fillStyle = 'rgba(255,255,255,.07)'; c.beginPath(); c.roundRect(ax + aw / 2 - 170, ay + 10, 340, ah - 20, 10); c.fill(); c.font = '64px sans-serif'; c.textAlign = 'center'; c.fillStyle = '#6b7089'; c.fillText('🖼', ax + aw / 2, ay + ah / 2 + 22); c.textAlign = 'start'; }
|
|
1651
|
+
c.font = '14px monospace';
|
|
1652
|
+
}
|
|
1653
|
+
const rows = ed ? m.lines.slice(0, 13) : m.lines.slice(-13), cw = c.measureText('M').width;
|
|
1654
|
+
rows.forEach((l, i) => {
|
|
1655
|
+
const yy = y + 52 + i * 21, pre = l.c === 'cmd' ? '❯ ' : l.c === 'tool' ? '● ' : ed && l.n ? String(l.n).padStart(4) + ' ' : l.c === 'add' ? '+ ' : l.c === 'del' ? '− ' : '', txt = String(l.t).split('\n')[0].slice(0, 112 - pre.length);
|
|
1656
|
+
const keep = !hide || l.c === 'run' ? txt.length : l.c === 'tool' ? txt.indexOf('(') + 1 : 0; // masked: only our own words stay ("running…", the tool's name), the rest becomes bars of the same length
|
|
1657
|
+
if (l.c === 'add' || l.c === 'del') { c.fillStyle = l.c === 'add' ? 'rgba(63,185,80,.16)' : 'rgba(248,81,73,.16)'; c.fillRect(x, yy - 15, w, 21); } c.fillStyle = col[l.c] || '#d6dae6'; c.fillText(pre + txt.slice(0, keep), x + 14, yy);
|
|
1658
|
+
if (keep < txt.length) { c.globalAlpha = 0.4; for (const b of txt.slice(keep).matchAll(/\S+/g)) c.fillRect(x + 14 + (pre.length + keep + b.index) * cw, yy - 10, b[0].length * cw - 3, 10); c.globalAlpha = 1; }
|
|
1659
|
+
});
|
|
1618
1660
|
c.font = '12px monospace'; c.fillStyle = '#8b95b0'; c.fillText(m.stat, x + 14, y + bh - 8);
|
|
1619
1661
|
c.fillStyle = 'rgba(240,160,32,.94)'; c.beginPath(); c.roundRect(x + 12, y + bh - 34, 300, 24, 12); c.fill(); c.fillStyle = '#201400'; c.font = 'bold 13px sans-serif'; c.fillText('🖥 ' + S.agents[S.present].name + ' ' + L('partage son écran', 'is sharing their screen'), x + 24, y + bh - 17);
|
|
1620
1662
|
tileOnCanvas(c, S.present, x + w - 210, y + bh - 160, 196, 147, false);
|
|
@@ -1623,20 +1665,29 @@ function drawFrame(cv) {
|
|
|
1623
1665
|
others.forEach((id, i) => tileOnCanvas(c, id, 20 + (i % cols) * (tw + 8), ty + Math.floor(i / cols) * (th + 8), tw, th, false));
|
|
1624
1666
|
const lead = sp[0] && S.say[sp[0]];
|
|
1625
1667
|
if (lead) { c.font = '22px sans-serif'; const Ls = wrap(c, `${nameOf(sp[0])} : ${lead.text}`, W - 80, 2); c.fillStyle = 'rgba(0,0,0,.75)'; c.fillRect(0, H - 22 - Ls.length * 28, W, Ls.length * 28 + 22); c.fillStyle = '#fff'; Ls.forEach((l, i) => c.fillText(l, 40, H - 12 - (Ls.length - 1 - i) * 28 - 6)); }
|
|
1626
|
-
const cd = $('#card'); if (cd.classList.contains('on')) { c.fillStyle = '#05060c'; c.fillRect(0, 0, W, H); c.textAlign = 'center'; c.font = '900 90px "Comic Sans MS",sans-serif'; c.fillStyle = '#d8372b'; c.fillText(cd.querySelector('h1').textContent, W / 2 + 5, H / 2 + 5); c.fillStyle = '#fff'; c.fillText(cd.querySelector('h1').textContent, W / 2, H / 2); c.font = '26px sans-serif'; c.fillStyle = '#cdd6f4'; c.fillText(cd.querySelector('p').textContent.slice(0, 80), W / 2, H / 2 + 60); c.textAlign = 'start'; }
|
|
1668
|
+
const cd = $('#card'); if (cd.classList.contains('on')) { c.fillStyle = '#05060c'; c.fillRect(0, 0, W, H); c.textAlign = 'center'; c.font = '900 90px "Comic Sans MS",sans-serif'; c.fillStyle = '#d8372b'; c.fillText(cd.querySelector('h1').textContent, W / 2 + 5, H / 2 + 5); c.fillStyle = '#fff'; c.fillText(cd.querySelector('h1').textContent, W / 2, H / 2); c.font = '26px sans-serif'; c.fillStyle = '#cdd6f4'; if (!hide) c.fillText(cd.querySelector('p').textContent.slice(0, 80), W / 2, H / 2 + 60); c.textAlign = 'start'; }
|
|
1627
1669
|
}
|
|
1628
1670
|
let recorder = null;
|
|
1629
|
-
|
|
1671
|
+
const REC_TYPES = ['video/mp4;codecs=avc1.42E01F,mp4a.40.2', 'video/webm;codecs=vp9,opus', 'video/webm']; // MP4 (H.264 + AAC) where the browser can record it: X, phones and most chat apps refuse .webm
|
|
1672
|
+
async function exportVideo(from = 0, to = 0, clip = 0) { // clip: length in ms of a short cut to share; it starts where the replay is, or at clipStart() when the replay is at rest or nearly over
|
|
1630
1673
|
if (recorder) { recorder.stop(); return; }
|
|
1631
|
-
const wasLive = S.live; if (wasLive) await setMode('replay'); pause(); claimAudio(); AUD.init(); await AUD.ctx.resume(); AUD.muted = false; AUD.apply();
|
|
1674
|
+
const wasLive = S.live, here = !wasLive && S.idx > 0 && aheadSpeakable(S.idx, 3) >= 3 ? S.idx : -1; if (wasLive) await setMode('replay'); pause(); claimAudio(); AUD.init(); await AUD.ctx.resume(); AUD.muted = false; AUD.apply();
|
|
1675
|
+
const back = S.idx; if (clip) from = here >= 0 ? here : clipStart();
|
|
1632
1676
|
const cv = document.createElement('canvas'); cv.width = 1280; cv.height = 720, stream = new MediaStream([...cv.captureStream(30).getTracks(), ...AUD.rec.stream.getTracks()]);
|
|
1633
|
-
const mime =
|
|
1677
|
+
const mime = REC_TYPES.find((m) => MediaRecorder.isTypeSupported(m)) || '', ext = mime.startsWith('video/mp4') ? 'mp4' : 'webm', chunks = [], t0 = performance.now(); let outro = false;
|
|
1634
1678
|
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 4e6 }); recorder.ondataavailable = (e) => e.data.size && chunks.push(e.data);
|
|
1635
|
-
const timer = setInterval(() => {
|
|
1679
|
+
const timer = setInterval(() => {
|
|
1680
|
+
const el = performance.now() - t0; drawFrame(cv, clip ? (outro ? 2 : 1) : 0);
|
|
1681
|
+
if (clip && S.playing && !S.stopAt && el > clip - 4000) S.stopAt = S.idx + 1; // the line being said is finished rather than cut…
|
|
1682
|
+
if (clip && S.playing && el > clip + 8000) pause(); // …unless it is a very long one
|
|
1683
|
+
$('#pill').textContent = `⏹ ${Math.max(0, Math.min(100, Math.round((clip ? el / clip : (S.idx - from) / ((to || S.events.length) - from)) * 100)))}%`;
|
|
1684
|
+
}, 33), done = new Promise((res) => (recorder.onstop = res));
|
|
1636
1685
|
S.exporting = true; S.stopAt = to || 0; S.idx = from; recomputeStatus(from); recorder.start(500); run(from);
|
|
1637
|
-
const poll = setInterval(() => { if (!S.playing && recorder) { setTimeout(() => recorder &&
|
|
1638
|
-
await done; clearInterval(timer); clearInterval(poll); S.exporting = false; S.stopAt = 0; recorder = null;
|
|
1639
|
-
|
|
1686
|
+
const poll = setInterval(() => { if (!S.playing && recorder) { const rec = recorder; outro = !!clip; setTimeout(() => recorder === rec && rec.stop(), clip ? 2600 : 900); clearInterval(poll); } }, 300);
|
|
1687
|
+
await done; clearInterval(timer); clearInterval(poll); S.exporting = false; S.stopAt = 0; recorder = null;
|
|
1688
|
+
if (clip) { pause(); S.idx = back; recomputeStatus(back); paintStatus(); } // back where the replay was, so the same clip can be taken again
|
|
1689
|
+
syncUI();
|
|
1690
|
+
const blob = new Blob(chunks, { type: mime.split(';')[0] || 'video/webm' }); window.__lastExport = blob.size; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `agent-standup${clip ? '-clip' : ''}.${ext}`; a.click();
|
|
1640
1691
|
}
|
|
1641
1692
|
|
|
1642
1693
|
// =====================================================================
|
|
@@ -1675,7 +1726,7 @@ $('#langSel').onchange = () => setLang($('#langSel').value, true);
|
|
|
1675
1726
|
$('#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(); };
|
|
1676
1727
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
1677
1728
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
|
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'
|
|
1729
|
+
$('#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(); }
|
|
1679
1730
|
if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
|
|
1680
1731
|
$('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
|
|
1681
1732
|
$('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
|
|
@@ -1690,17 +1741,13 @@ async function kokoroUI() {
|
|
|
1690
1741
|
b.disabled = st.installing || !st.npm; b.textContent = st.installing ? L('Installation…', 'Installing…') : L(`Installer (${st.sizeMb} Mo)`, `Install (${st.sizeMb} MB)`);
|
|
1691
1742
|
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
1743
|
: 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
|
|
1744
|
+
: 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)…'))
|
|
1694
1745
|
: 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
1746
|
if ((st.installing || (st.installed && !st.ready && !st.gaveUp)) && !kokoroTimer) kokoroTimer = setInterval(async () => { await kokoroUI(); }, 2500);
|
|
1696
1747
|
if (!st.installing && (st.ready || st.gaveUp || !st.installed) && kokoroTimer) { clearInterval(kokoroTimer); kokoroTimer = null; if (st.ready && TTS.engine !== 'kokoro') TTS.init(); }
|
|
1697
1748
|
}
|
|
1698
1749
|
$('#kokoroInstall').onclick = async () => { try { await fetch('/api/kokoro/install', { method: 'POST' }); } catch {} kokoroUI(); };
|
|
1699
1750
|
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(); };
|
|
1704
1751
|
$('#officeVol').value = prefs.officeVol; $('#officeBox').style.display = prefs.office ? '' : 'none';
|
|
1705
1752
|
$('#officeVol').oninput = (e) => { prefs.officeVol = +e.target.value; savePrefs(); AUD.applyMusic(); };
|
|
1706
1753
|
$('#musicVol').oninput = (e) => { prefs.musicVol = +e.target.value; savePrefs(); AUD.applyMusic(); };
|
|
@@ -1712,6 +1759,7 @@ $('#bMute').onclick = toggleMute;
|
|
|
1712
1759
|
$('#bPlay').onclick = togglePlay;
|
|
1713
1760
|
$('#mRecap').onclick = () => { $('#menu').classList.remove('on'); openRecap(); };
|
|
1714
1761
|
$('#mExport').onclick = () => { $('#menu').classList.remove('on'); exportVideo(); };
|
|
1762
|
+
$('#mClip').onclick = () => { $('#menu').classList.remove('on'); exportVideo(0, 0, 25000); };
|
|
1715
1763
|
$('#mBrowser').onclick = () => { $('#menu').classList.remove('on'); window.parent.postMessage({ agentStandup: 'openBrowser' }, '*'); };
|
|
1716
1764
|
$('#seek').oninput = (e) => seekTo(+e.target.value);
|
|
1717
1765
|
$('#eDemo').onclick = () => setMode('demo');
|
|
@@ -1728,14 +1776,13 @@ if (EMBED) $('#mBrowser').style.display = '';
|
|
|
1728
1776
|
|
|
1729
1777
|
(async function init() {
|
|
1730
1778
|
if (!LANGS.some((l) => l[0] === prefs.lang)) prefs.lang = 'en'; await loadLocale(prefs.lang); await loadChars();
|
|
1731
|
-
document.body.classList.toggle('rost', prefs.roster !== false); document.body.classList.toggle('react', !!prefs.reactions); setView(prefs.view); applyLang();
|
|
1779
|
+
document.body.classList.toggle('rost', prefs.roster !== false); document.body.classList.toggle('react', !!prefs.reactions); setView(prefs.view); applyLang(); applyMute(); TTS.init().then(updateHint);
|
|
1732
1780
|
loadHealth().then(() => { // French by default only if translation can actually work; otherwise stay in English without a warning
|
|
1733
1781
|
const h = S.health || {};
|
|
1734
1782
|
if (!prefs.langChosen && FR() && h.claude === null && !h.apiKey) { prefs.lang = 'en'; applyLang(); }
|
|
1735
1783
|
if (FR()) fetch('/api/warm').catch(() => {});
|
|
1736
1784
|
});
|
|
1737
1785
|
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
|
|
1739
1786
|
if (Q.get('demo')) {
|
|
1740
1787
|
if (Q.get('view') === 'tree') setView('tree');
|
|
1741
1788
|
await setMode('demo'); if (Q.get('speed')) $('#speed').value = Q.get('speed');
|
package/public/locales/de.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"☕ Buy the (replaced) dev a coffee": "☕ Spendier dem (ersetzten) Dev einen Kaffee",
|
|
13
13
|
"☕ This dev was replaced by his agents — buy him a coffee": "☕ Dieser Dev wurde von seinen Agenten ersetzt — spendier ihm einen Kaffee",
|
|
14
14
|
"🎞 Export video": "🎞 Video exportieren",
|
|
15
|
+
"🎬 Clip to share (25 s)": "🎬 Clip zum Teilen (25 s)",
|
|
15
16
|
"📋 Recap": "📋 Zusammenfassung",
|
|
16
17
|
"Agent voices": "Agenten-Stimmen",
|
|
17
18
|
"Agents": "Agenten",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"Files touched": "Bearbeitete Dateien",
|
|
59
60
|
"Final report": "Abschlussbericht",
|
|
60
61
|
"first error": "erster Fehler",
|
|
62
|
+
"Hide code & file names in videos": "Code & Dateinamen in Videos ausblenden",
|
|
61
63
|
"Highlights": "Highlights",
|
|
62
64
|
"Human": "Mensch",
|
|
63
65
|
"idle": "untätig",
|
|
@@ -159,6 +161,7 @@
|
|
|
159
161
|
"Writing ": "Schreibe ",
|
|
160
162
|
"You": "Du",
|
|
161
163
|
"Your agents are done 🎉": "Deine Agenten sind fertig 🎉",
|
|
164
|
+
"Your Claude Code agents as a live team call": "Deine Claude Code-Agenten live im Team-Videoanruf",
|
|
162
165
|
"your turn": "du bist dran",
|
|
163
166
|
"Characters": "Figuren",
|
|
164
167
|
"Reload": "Neu laden",
|
|
@@ -166,13 +169,10 @@
|
|
|
166
169
|
"custom character(s) loaded": "eigene Figur(en) geladen",
|
|
167
170
|
"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
171
|
"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
172
|
"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
173
|
"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
174
|
"Installing…": "Installation läuft…",
|
|
174
|
-
"
|
|
175
|
-
"Loading the model (downloaded on first use, about 155 MB)…": "Modell wird geladen (beim ersten Mal heruntergeladen, etwa 155 MB)…",
|
|
175
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "Modell wird geladen (beim ersten Mal heruntergeladen, etwa 305 MB)…",
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "npm nicht gefunden: Installiere Node.js, um diese Option zu nutzen.",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "Die Engine wurde beendet: stattdessen kommen die Systemstimmen zum Einsatz.",
|
|
178
178
|
"Very natural voices (installs on demand)": "Sehr natürliche Stimmen (werden bei Bedarf installiert)",
|
package/public/locales/en.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"☕ Buy the (replaced) dev a coffee": "☕ Buy the (replaced) dev a coffee",
|
|
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
|
+
"🎬 Clip to share (25 s)": "🎬 Clip to share (25 s)",
|
|
10
11
|
"📋 Recap": "📋 Recap",
|
|
11
|
-
"🔥 Meme mode (unhinged)": "🔥 Meme mode (unhinged)",
|
|
12
12
|
"Agent voices": "Agent voices",
|
|
13
13
|
"Agents": "Agents",
|
|
14
14
|
"Agents list (left)": "Agents list (left)",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"duration": "duration",
|
|
48
48
|
"Editing ": "Editing ",
|
|
49
49
|
"Editor": "Editor",
|
|
50
|
-
"Effects volume": "Effects volume",
|
|
51
50
|
"EPISODE 1": "EPISODE 1",
|
|
52
51
|
"Episode recap": "Episode recap",
|
|
53
52
|
"errors": "errors",
|
|
@@ -60,12 +59,12 @@
|
|
|
60
59
|
"Final report": "Final report",
|
|
61
60
|
"first error": "first error",
|
|
62
61
|
"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.",
|
|
62
|
+
"Hide code & file names in videos": "Hide code & file names in videos",
|
|
63
63
|
"Highlights": "Highlights",
|
|
64
64
|
"Human": "Human",
|
|
65
65
|
"idle": "idle",
|
|
66
66
|
"input": "input",
|
|
67
67
|
"Installing…": "Installing…",
|
|
68
|
-
"Intensity": "Intensity",
|
|
69
68
|
"is sharing their screen": "is sharing their screen",
|
|
70
69
|
"Join the call": "Join the call",
|
|
71
70
|
"Language": "Language",
|
|
@@ -76,7 +75,7 @@
|
|
|
76
75
|
"lines": "lines",
|
|
77
76
|
"Listing ": "Listing ",
|
|
78
77
|
"Little lines & animations": "Little lines & animations",
|
|
79
|
-
"Loading the model (downloaded on first use, about
|
|
78
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "Loading the model (downloaded on first use, about 305 MB)…",
|
|
80
79
|
"longest wait ": "longest wait ",
|
|
81
80
|
"Looking for a tool": "Looking for a tool",
|
|
82
81
|
"looping?": "looping?",
|
|
@@ -171,5 +170,6 @@
|
|
|
171
170
|
"Writing ": "Writing ",
|
|
172
171
|
"You": "You",
|
|
173
172
|
"Your agents are done 🎉": "Your agents are done 🎉",
|
|
173
|
+
"Your Claude Code agents as a live team call": "Your Claude Code agents as a live team call",
|
|
174
174
|
"your turn": "your turn"
|
|
175
175
|
}
|