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
Publish 1.9.18
6 files changed
+51−11
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.17"+version = "1.9.18" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1@@ -1 +1 @@-1.9.17+1.9.18
bin/adom-tts⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-tts",- "version": "1.9.17",+ "version": "1.9.18", "type": "app", "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.", "license": "MIT",
page.json+14−1@@ -4,7 +4,7 @@ "slug": "adom-tts", "title": "Adom TTS — Text to Speech", "brief": "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.",- "version": "1.9.17",+ "version": "1.9.18", "license": "MIT", "tags": [ "tts",@@ -12,6 +12,7 @@ "audio", "demo", "accessibility",+ "cli", "open-source", "mit" ],@@ -64,5 +65,17 @@ ], "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,+ "org": "adom",+ "author": {+ "name": "John Lauer",+ "email": "[email protected]" } }
src/player.html+34−7@@ -68,9 +68,22 @@ .app-subtitle { font-family:'Familjen Grotesk',system-ui,sans-serif; font-weight:500; font-size:13px; color: var(--muted); } .sep { color: var(--border); margin: 0 1px; } .vsep { width:1px; height:20px; background: var(--border); flex: none; }- /* Zone 2: subject */- .subject { display:flex; align-items:baseline; gap:8px; min-width:0; flex:1; overflow:hidden; }- .subject-name { font-size:14px; font-weight:600; color: var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }+ /* Zone 2: subject — truncates when tight; hover/tap reveals the full story */+ .subject { display:flex; align-items:baseline; gap:8px; min-width:0; flex:1; overflow:visible; position:relative; cursor:default; outline:none; }+ .subject-name { font-size:14px; font-weight:600; color: var(--fg); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:0 1 auto; min-width:4.5em; }+ /* rich tooltip: everything the truncated header hides, in one card */+ .subject-pop {+ position:absolute; top: calc(100% + 10px); left: 0; width: min(340px, 86vw); z-index: 30;+ background: var(--elevated); border:1px solid var(--border); border-radius:12px;+ padding:13px 15px; box-shadow: 0 14px 40px rgba(0,0,0,.5);+ opacity:0; transform: translateY(-4px); pointer-events:none; transition: opacity .14s, transform .14s;+ }+ .subject:hover .subject-pop, .subject:focus-within .subject-pop, .subject.open .subject-pop { opacity:1; transform:none; pointer-events:auto; }+ .sp-desc { font-size:13px; color: var(--fg); line-height:1.5; margin-bottom:10px; }+ .sp-row { display:flex; gap:10px; font-size:12px; margin:4px 0; }+ .sp-k { color: var(--muted); width:52px; flex:none; text-transform:uppercase; letter-spacing:.5px; font-size:10px; padding-top:1px; }+ .sp-v { color: var(--fg); min-width:0; overflow-wrap:anywhere; }+ .sp-v.mono { font-family: ui-monospace,Menlo,monospace; font-size:11.5px; } /* 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); }@@ -241,10 +254,16 @@ <div class="vsep" aria-hidden="true"></div> - <!-- Zone 2: SUBJECT (what's playing right now) -->- <div class="subject">+ <!-- Zone 2: SUBJECT (what's playing right now); hover/tap for the full card -->+ <div class="subject" id="subject" tabindex="0"> <span class="subject-name" id="subjectName">waiting for a clip…</span> <span class="subject-meta" id="subjectMeta"></span>+ <div class="subject-pop" id="subjectPop" role="tooltip">+ <div class="sp-desc" id="spDesc">No clip yet — the next `say --play` from any thread lands here.</div>+ <div class="sp-row"><span class="sp-k">thread</span><span class="sp-v" id="spThread">—</span></div>+ <div class="sp-row"><span class="sp-k">file</span><span class="sp-v mono" id="spFile">—</span></div>+ <div class="sp-row"><span class="sp-k">voice</span><span class="sp-v mono" id="spVoice">—</span></div>+ </div> </div> <!-- Zone 3: ACTIONS / status + live-link LED (always visible, even skinny) -->@@ -371,8 +390,11 @@ subjectName.textContent = c.label || c.title || 'audio clip'; metaBase = c.desc ? c.desc : (c.meta || ''); meta.textContent = metaBase;- var subj = document.querySelector('.subject');- if (subj) subj.title = (c.title || '') + (c.meta ? ' · ' + c.meta : '');+ // rich tooltip card: the untruncated story of the current clip+ document.getElementById('spDesc').textContent = c.desc || '(no description sent)';+ document.getElementById('spThread').textContent = c.label || '—';+ document.getElementById('spFile').textContent = c.title || '—';+ document.getElementById('spVoice').textContent = c.meta || '—'; threadChip.style.display = 'none'; // thread now leads the subject zone buildTranscript(c.words || []); // reset transport UI for the fresh clip@@ -410,6 +432,11 @@ function showPlayError(err){ lastErr=(err&&err.name||'PlayError')+(err&&err.message?': '+err.message:''); if(err&&err.name==='NotAllowedError'){setStatus('tap play ▶','paused');playBtn.classList.add('attention');setIcon('play');} else setStatus('play failed','error'); updateDiag(); ev('playreject','&msg='+encodeURIComponent(((err&&err.name)||'')+' '+((err&&err.message)||'').slice(0,60))); } playBtn.addEventListener('click', () => { if (a.ended) { a.currentTime=0; tryPlay(); } else if (a.paused) tryPlay(); else a.pause(); }); + // tap toggles the subject card (hover covers mouse; this covers touch)+ var subjEl = document.getElementById('subject');+ subjEl.addEventListener('click', function(e){ if (e.target.closest('.subject-pop')) return; subjEl.classList.toggle('open'); });+ document.addEventListener('click', function(e){ if (!subjEl.contains(e.target)) subjEl.classList.remove('open'); });+ seek.addEventListener('input', () => { seeking = true; if (isFinite(a.duration)) { a.currentTime = (seek.value/1000)*a.duration; curEl.textContent = fmt(a.currentTime); } }); seek.addEventListener('change', () => { seeking = false; });