Something went wrong. Try again.
Something went wrong. Try again.
4.4 kB · 117 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118// Podcast transcripts — list + reader with click-to-seek audio.// Hash routing: #/ = episode list, #/ep/6 = transcript.// Branding (show name, link, accent color) comes from /api/meta.
const app = document.getElementById('app')
let META = { show_name: 'Podcast', show_url: null, accent: null }
async function loadMeta() { try { META = await (await fetch('/api/meta')).json() } catch { /* defaults are fine */ } document.title = `${META.show_name} Transcripts` if (META.accent) document.documentElement.style.setProperty('--a', META.accent)}
const esc = s => s.replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
const mmss = t => { t = Math.floor(t) return `${String(Math.floor(t / 60)).padStart(2, '0')}:${String(t % 60).padStart(2, '0')}`}
const STATUS_LABEL = { pending: 'queued', processing: 'transcribing…', error: 'failed', skipped: 'not transcribed',}
// ---------- list view ----------
async function renderList() { const { episodes } = await (await fetch('/api/episodes')).json() const totalWords = episodes.reduce((a, e) => a + (e.word_count || 0), 0)
app.innerHTML = ` <header> <h1>${esc(META.show_name)} — Transcripts</h1> <p class="tagline">every episode, transcribed & speaker-labeled · click any transcript to read along, click a timestamp to listen${ META.show_url ? ` · <a href="${esc(META.show_url)}">the actual podcast</a>` : ''}</p> </header> <ul class="eplist"> ${episodes.map(e => ` <li> <span class="num">${String(e.episode_num).padStart(2, '0')}</span> <span class="ep-title"> ${e.status === 'done' ? `<a href="#/ep/${e.episode_num}">${esc(e.guest || e.title)}</a>` : `<span class="dim">${esc(e.guest || e.title)}</span> <span class="status s-${e.status}">${STATUS_LABEL[e.status] || e.status}</span>`} </span> <span class="dur">${e.duration_sec ? mmss(e.duration_sec) : ''}</span> <span class="ep-desc">${esc(e.description || '')}</span> </li>`).join('')} </ul> <footer>${episodes.filter(e => e.status === 'done').length} transcripts · ${totalWords.toLocaleString()} words · rechecks the feed hourly</footer> `}
// ---------- transcript view ----------
async function renderEpisode(num) { const res = await fetch(`/api/episodes/${num}`) if (!res.ok) { location.hash = '#/'; return } const ep = await res.json() const turns = ep.transcript?.turns || [] const host = ep.transcript?.host
app.innerHTML = ` <header> <p class="crumb"><a href="#/">← all transcripts</a></p> <h1>Transcript: ${String(ep.episode_num).padStart(2, '0')} — ${esc(ep.guest || ep.title)}</h1> <p class="tagline">${esc(ep.description || '')}</p> ${ep.names_resolved ? '' : '<p class="warn">speaker names could not be auto-resolved for this episode — labels are generic</p>'} <audio id="player" controls preload="none" src="${esc(ep.audio_url)}"></audio> </header> <div class="transcript"> ${turns.map(t => ` <div class="turn ${t.speaker === host ? 'host' : 'guest'}"> <button class="ts" data-t="${t.t}" title="play from here">${mmss(t.t)}</button> <span class="spk">${esc(t.speaker)}</span> <span class="txt">${esc(t.text)}</span> </div>`).join('')} </div> <footer>${(ep.word_count || 0).toLocaleString()} words · transcribed by ElevenLabs Scribe v2, speakers named via Gemini${ ep.transcript?.review ? ` · copy-edited by ${esc(ep.transcript.review.model)} (${ep.transcript.review.edits_applied} fixes)` : ''}</footer> `
const player = document.getElementById('player') app.querySelectorAll('.ts').forEach(b => b.addEventListener('click', () => { player.currentTime = parseFloat(b.dataset.t) player.play() }))}
// ---------- router ----------
function route() { const m = location.hash.match(/^#\/ep\/(\d+)$/) const view = m ? renderEpisode(parseInt(m[1], 10)) : renderList() view.catch(err => { app.innerHTML = `<h1>podcast transcripts</h1> <p class="warn">backend unreachable: ${esc(err.message)}</p>` })}
window.addEventListener('hashchange', route)loadMeta().then(route)