app
Adom TTS: Text to Speech
Public Made by Adomby adom
Adom's shared voice service. Reads your AI's answers back to you while you drive, and renders perfectly-timed voiceover tracks for your demo videos, so you never record your own voice.
← Commit history
v1.9.37 — clip freshness: every clip shows WHEN ITS AUDIO was synthesized + how long ago (teal fresh, red when days old); removed the wall clock (the age of the CLIP is what matters, not the current time)
4 files changed
+30−53
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.36"+version = "1.9.37" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1@@ -1 +1 @@-1.9.36+1.9.37
page.json−21@@ -12,7 +12,6 @@ "audio", "demo", "accessibility",- "cli", "open-source", "mit" ],@@ -65,25 +64,5 @@ ], "install": { "binary_name": "adom-tts"- },- "description": "Adom's shared text-to-speech CLI + Playground. Reads your AI answers back to you while driving, and renders perfectly-timed voiceover tracks for demo videos.",- "dependencies": {},- "scripts": {- "install": "./install.sh",- "uninstall": "./uninstall.sh"- },- "needs_sudo": false,- "files": [- "package.json",- "install.sh",- "uninstall.sh",- "bin/adom-tts",- "skills/**",- "completions/**"- ],- "org": "adom",- "author": {- "name": "John Lauer",- "email": "[email protected]" } }
src/player.html+28−30@@ -87,20 +87,16 @@ /* the clip DESCRIPTION (human sentence) — always visible, ellipsized when tight */ .subject-meta { font-size:12.5px; color: var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex: 1 1 auto; } .subject-meta .sep { color: var(--border); }- /* clip AGE badge — surfaces when the clip's AUDIO is old (a stale mp3- re-queued today). Neutral when fresh, amber when hours old, red when- days old, so an ancient clip is impossible to miss. */- .clip-age { font-size:11px; font-family: ui-monospace,Menlo,monospace; padding:2px 7px; border-radius:999px; white-space:nowrap; flex:none;- color: var(--muted); border:1px solid var(--border); background: transparent; }- .clip-age.warn { color: var(--amber); border-color: rgba(212,160,23,.5); background: rgba(212,160,23,.12); }- .clip-age.stale { color: var(--red); border-color: rgba(224,82,77,.6); background: rgba(224,82,77,.15); font-weight:600; }+ /* clip AGE badge — ALWAYS shown for the current clip: when its AUDIO was+ synthesized + how long ago, so you can always tell if you're about to hear+ old news. Teal when fresh (minutes), amber when hours old, red when days+ old. This is the "is this ancient?" answer, always on screen. */+ .clip-age { font-size:11.5px; font-family: ui-monospace,Menlo,monospace; padding:2px 8px; border-radius:999px; white-space:nowrap; flex:none;+ color: var(--teal); border:1px solid rgba(0,184,177,.45); background: var(--teal-soft); }+ .clip-age.warn { color: var(--amber); border-color: rgba(212,160,23,.5); background: rgba(212,160,23,.14); }+ .clip-age.stale { color: var(--red); border-color: rgba(224,82,77,.6); background: rgba(224,82,77,.16); font-weight:600; } /* Zone 3: actions */ .actions { display:flex; align-items:center; gap:8px; margin-left:auto; flex:none; }- /* live wall clock — the player's current date + time, always visible, so you- can sanity-check freshness at a glance ("it's 4:12 PM; that clip says it- was made in June — something's wrong"). */- .clock { font-size:11.5px; font-family: ui-monospace,Menlo,monospace; color: var(--muted); white-space:nowrap; flex:none;- padding:3px 9px; border:1px solid var(--border); border-radius:999px; } .queue-chip { font-size:11.5px; color: var(--blue); border:1px solid rgba(0,97,239,0.5); background: rgba(0,97,239,0.12); padding:3px 9px; border-radius:999px; white-space:nowrap; } .queue-chip.hidden { display:none; } .thread-chip { font-size:11.5px; color: var(--purple); border:1px solid rgba(140,107,247,0.5); background: rgba(140,107,247,0.12); padding:3px 10px; border-radius:999px; white-space:nowrap; }@@ -282,7 +278,6 @@ <!-- Zone 3: ACTIONS / status + live-link LED (always visible, even skinny) --> <div class="actions">- <span id="clock" class="clock" title="the player's current date and time"></span> <span id="queueChip" class="queue-chip hidden"></span> <span id="threadChip" class="thread-chip" style="display:none"></span> <span id="status" class="pill">idle</span>@@ -402,36 +397,39 @@ // obvious). created_ms = when the AUDIO was synthesized (mp3 mtime), which // catches a weeks-old clip re-queued today that the enqueue-time TTL misses. let lastServerNow = Date.now();+ let curCreatedMs = 0; // synth time of the CURRENT clip, so the age can re-tick function fmtAgo(ms){ const s = Math.max(0, Math.floor(ms/1000));- if (s < 90) return s + 's ago';+ if (s < 45) return 'just now';+ if (s < 90) return '1m ago'; const m = Math.floor(s/60); if (m < 90) return m + 'm ago'; const h = Math.floor(m/60); if (h < 36) return h + 'h ago'; const d = Math.floor(h/24); return d + ' day' + (d===1?'':'s') + ' ago'; }+ // ALWAYS show, for the current clip, WHEN its audio was made + how long ago,+ // color-coded so old news is unmissable. This is the whole point: you should+ // never have to wonder whether a clip is fresh. function setClipAge(created, now){ const el = document.getElementById('clipAge'); const sp = document.getElementById('spCreated');- if (!created || !now){ el.style.display='none'; sp.textContent = '—'; return; }- const age = Math.max(0, now - created);+ curCreatedMs = created || 0;+ if (!created){ el.style.display='none'; sp.textContent = '—'; return; }+ const age = Math.max(0, (now || lastServerNow) - created); const abs = new Date(created).toLocaleString(undefined, {month:'short', day:'numeric', hour:'numeric', minute:'2-digit'});- sp.textContent = abs + ' · ' + fmtAgo(age);+ const ago = fmtAgo(age);+ sp.textContent = abs + ' · ' + ago;+ el.style.display = ''; el.className = 'clip-age'; const min = age / 60000;- if (min < 10){ el.style.display='none'; return; } // fresh: no badge, card still shows it- el.style.display = '';- if (min >= 1440){ el.classList.add('stale'); el.textContent = '⚠ ' + fmtAgo(age); }- else if (min >= 60){ el.classList.add('warn'); el.textContent = fmtAgo(age); }- else { el.textContent = fmtAgo(age); }- }- // live wall clock — the player's current date + time, always on screen- function tickClock(){- const el = document.getElementById('clock');- if (el) el.textContent = new Date().toLocaleString(undefined,- {weekday:'short', month:'short', day:'numeric', hour:'numeric', minute:'2-digit'});+ if (min >= 1440){ el.classList.add('stale'); el.textContent = '⚠ audio from ' + abs + ' · ' + ago; }+ else if (min >= 60){ el.classList.add('warn'); el.textContent = 'audio from ' + abs + ' · ' + ago; }+ else { el.textContent = 'audio ' + ago; } // fresh: teal, compact+ el.title = 'this clip’s audio was synthesized ' + abs + ' (' + ago + ')'; }- tickClock(); setInterval(tickClock, 15000);+ // keep the relative age honest without a new clip event (polls refresh+ // lastServerNow every ~700ms, so this just re-renders the text)+ setInterval(function(){ if (curCreatedMs) setClipAge(curCreatedMs, lastServerNow); }, 10000); function applyClip(c){ curClipId = c.id; // Subject = WHO is speaking (AI thread) + WHAT the clip says (--desc).@@ -595,7 +593,7 @@ renderHistory(j.history || [], j.now_ms || Date.now()); // NEW clip? swap without reloading the page. if (j.id && j.id !== curClipId) applyClip(j);- else if (!j.id && curClipId && a.ended) { subjectName.textContent = 'waiting for a clip…'; }+ else if (!j.id && curClipId && a.ended) { subjectName.textContent = 'waiting for a clip…'; setClipAge(0); } // control commands (replay/pause/play/toggle) if (j.seq > lastCmdSeq) { lastCmdSeq = j.seq;