Skip to content

Instantly share code, notes, and snippets.

@jacoblyles
Created February 4, 2026 13:54
Show Gist options
  • Select an option

  • Save jacoblyles/f4818924a5be72b3dafb159e8cb050f3 to your computer and use it in GitHub Desktop.

Select an option

Save jacoblyles/f4818924a5be72b3dafb159e8cb050f3 to your computer and use it in GitHub Desktop.
Podscript.tv System Architecture
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Podscript.tv System Architecture</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #c9d1d9;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-secondary: #7ee787;
--accent-tertiary: #d2a8ff;
--accent-orange: #ffa657;
--accent-red: #f85149;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--accent), var(--accent-tertiary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
color: var(--text-muted);
font-size: 1.1rem;
margin-bottom: 2rem;
}
h2 {
color: var(--accent);
font-size: 1.5rem;
margin: 2rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
h3 {
color: var(--accent-secondary);
font-size: 1.1rem;
margin: 1.5rem 0 0.75rem;
}
.diagram {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
margin: 1.5rem 0;
overflow-x: auto;
}
.diagram pre {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.4;
color: var(--text);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.25rem;
}
.card-title {
font-weight: 600;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.2rem;
}
.badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
.badge-blue { background: rgba(88, 166, 255, 0.15); color: var(--accent); }
.badge-green { background: rgba(126, 231, 135, 0.15); color: var(--accent-secondary); }
.badge-purple { background: rgba(210, 168, 255, 0.15); color: var(--accent-tertiary); }
.badge-orange { background: rgba(255, 166, 87, 0.15); color: var(--accent-orange); }
ul {
list-style: none;
padding-left: 0;
}
li {
padding: 0.25rem 0;
padding-left: 1.25rem;
position: relative;
}
li::before {
content: "→";
position: absolute;
left: 0;
color: var(--text-muted);
}
code {
background: rgba(110, 118, 129, 0.2);
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.85em;
}
.flow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
}
.flow-step {
background: var(--surface);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
}
.flow-arrow {
color: var(--text-muted);
font-size: 1.2rem;
}
.table-wrap {
overflow-x: auto;
margin: 1rem 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid var(--border);
}
th {
color: var(--text-muted);
font-weight: 500;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-pending { color: var(--accent-orange); }
.status-active { color: var(--accent-secondary); }
.status-complete { color: var(--accent); }
footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.9rem;
text-align: center;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<h1>Podscript.tv</h1>
<p class="subtitle">Podcast Transcription & Search Platform — System Architecture</p>
<h2>High-Level Architecture</h2>
<div class="diagram">
<pre>
┌─────────────────────────────────────────────────────────────────────────────────┐
│ CLIENTS │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Browser │ │ Admin UI │ │ RSS Reader │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
└───────────┼───────────────────┼─────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js 16) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Home Page │ │ Podcast │ │ Episode │ │ Admin Dashboard │ │
│ │ / │ │ Browser │ │ Player │ │ /admin/* │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ │
│ Components: AudioPlayerWithTranscript • PodcastSearch • Navbar │
└──────────────────────────────────┬──────────────────────────────────────────────┘
│ HTTP/JSON
┌─────────────────────────────────────────────────────────────────────────────────┐
│ BACKEND (FastAPI) │
│ ┌─────────────────────────────────────────────────────────────────────────────┐│
│ │ PUBLIC API │ ADMIN API (JWT Auth) ││
│ │ GET /api/podcasts │ POST /api/admin/login ││
│ │ GET /api/podcasts/{slug} │ GET /api/admin/podcasts ││
│ │ GET /api/podcasts/{slug}/episodes │ POST /api/admin/podcasts ││
│ │ GET /api/episodes/{id}/transcript │ GET /api/admin/jobs ││
│ │ GET /api/episodes/{id}/audio │ POST /api/admin/jobs/{id}/retry ││
│ │ GET /api/search?q=... │ POST /api/admin/search/reindex ││
│ │ POST /api/signup/request │ GET /api/admin/signup-requests ││
│ └─────────────────────────────────────────────────────────────────────────────┘│
│ │
│ Services: SearchService • TranscriptionService • EmailService • Security │
└────────┬──────────────────────┬──────────────────────┬──────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────────────┐
│ PostgreSQL │ │ Meilisearch │ │ Redis │
│ (Data Store) │ │ (Search) │ │ (Job Queue) │
│ │ │ │ │ │
│ • podcasts │ │ transcript_ │ │ ┌─────────────────────────────┐ │
│ • episodes │ │ segments index │ │ │ Celery Workers │ │
│ • transcripts │ │ │ │ │ • transcribe_episode │ │
│ • segments │ │ Filterable: │ │ │ • index_all_transcripts │ │
│ • signup_reqs │ │ episode_id │ │ │ • transcribe_batch │ │
│ • admin_users │ │ podcast_id │ │ └─────────────────────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ faster-whisper │
│ (Speech-to-Text Engine) │
│ │
│ • Word-level timestamps │
│ • VAD filtering │
│ • Language detection │
│ • Confidence scores │
└─────────────────────────────────────┘
</pre>
</div>
<h2>Technology Stack</h2>
<div class="grid">
<div class="card">
<div class="card-title"><span class="icon">⚛️</span> Frontend</div>
<p>
<span class="badge badge-blue">Next.js 16</span>
<span class="badge badge-blue">React 19</span>
<span class="badge badge-blue">TypeScript 5</span>
<span class="badge badge-purple">Tailwind CSS 4</span>
</p>
<ul>
<li>App Router with Server Components</li>
<li>Interactive audio player with transcript sync</li>
<li>Real-time search with highlighting</li>
<li>Admin dashboard for content management</li>
</ul>
</div>
<div class="card">
<div class="card-title"><span class="icon">🚀</span> Backend</div>
<p>
<span class="badge badge-green">FastAPI</span>
<span class="badge badge-green">Python 3.11+</span>
<span class="badge badge-green">Pydantic 2</span>
<span class="badge badge-green">SQLAlchemy 2</span>
</p>
<ul>
<li>Async REST API with type safety</li>
<li>JWT authentication for admin</li>
<li>RSS feed parsing & ingestion</li>
<li>Audio streaming with range requests</li>
</ul>
</div>
<div class="card">
<div class="card-title"><span class="icon">🗄️</span> Data Layer</div>
<p>
<span class="badge badge-orange">PostgreSQL 16</span>
<span class="badge badge-orange">asyncpg</span>
<span class="badge badge-orange">Alembic</span>
</p>
<ul>
<li>Normalized schema for podcasts/episodes</li>
<li>Transcript segments with timestamps</li>
<li>Signup request workflow</li>
<li>Admin user management</li>
</ul>
</div>
<div class="card">
<div class="card-title"><span class="icon">🔍</span> Search</div>
<p>
<span class="badge badge-purple">Meilisearch</span>
</p>
<ul>
<li>Full-text transcript search</li>
<li>Multi-word episode-level matching</li>
<li>Filter by podcast/episode</li>
<li>Typo tolerance & ranking</li>
</ul>
</div>
<div class="card">
<div class="card-title"><span class="icon">⚙️</span> Background Jobs</div>
<p>
<span class="badge badge-blue">Celery</span>
<span class="badge badge-blue">Redis</span>
</p>
<ul>
<li>Async transcription pipeline</li>
<li>Batch indexing tasks</li>
<li>Auto-retry with backoff</li>
<li>Job status tracking</li>
</ul>
</div>
<div class="card">
<div class="card-title"><span class="icon">🎙️</span> Transcription</div>
<p>
<span class="badge badge-green">faster-whisper</span>
</p>
<ul>
<li>OpenAI Whisper optimized</li>
<li>Word-level timestamps</li>
<li>Speaker segmentation</li>
<li>Multiple model sizes</li>
</ul>
</div>
</div>
<h2>Database Schema</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Table</th>
<th>Purpose</th>
<th>Key Fields</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>podcasts</code></td>
<td>Podcast metadata & RSS source</td>
<td>slug, title, rss_url, is_public, episode_count</td>
</tr>
<tr>
<td><code>episodes</code></td>
<td>Episode info & transcription status</td>
<td>slug, audio_url, transcription_status, published_at</td>
</tr>
<tr>
<td><code>transcripts</code></td>
<td>Full transcript text per episode</td>
<td>episode_id (1:1), full_text, word_count</td>
</tr>
<tr>
<td><code>transcript_segments</code></td>
<td>Timestamped transcript chunks</td>
<td>start_time_ms, end_time_ms, text, speaker_label</td>
</tr>
<tr>
<td><code>signup_requests</code></td>
<td>Podcast submission workflow</td>
<td>email, rss_url, status (pending/approved/rejected)</td>
</tr>
<tr>
<td><code>admin_users</code></td>
<td>Admin authentication</td>
<td>email, password_hash</td>
</tr>
</tbody>
</table>
</div>
<h2>Data Flows</h2>
<h3>Podcast Ingestion</h3>
<div class="flow">
<span class="flow-step">Admin submits RSS URL</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Parse RSS feed</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Create Podcast record</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Create Episode records</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Queue transcription jobs</span>
</div>
<h3>Transcription Pipeline</h3>
<div class="flow">
<span class="flow-step">Celery picks up job</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Download audio</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Run Whisper</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Store segments</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Index in Meilisearch</span>
</div>
<h3>Search Flow</h3>
<div class="flow">
<span class="flow-step">User enters query</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Frontend calls /api/search</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Multi-word matching</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Return highlighted segments</span>
<span class="flow-arrow">→</span>
<span class="flow-step">Link to episode + timestamp</span>
</div>
<h2>Transcription Job States</h2>
<div class="diagram">
<pre>
┌─────────────┐
│ PENDING │ ← Episode created, job queued
└──────┬──────┘
┌─────────────┐
│ DOWNLOADING │ ← Fetching audio from URL
└──────┬──────┘
┌──────────────┐
│ TRANSCRIBING │ ← Running faster-whisper
└──────┬───────┘
┌─────┴─────┐
▼ ▼
┌──────────┐ ┌────────┐
│COMPLETED │ │ FAILED │ → Retry available
└──────────┘ └────────┘
</pre>
</div>
<h2>Key Features</h2>
<div class="grid">
<div class="card">
<div class="card-title">🎧 Audio Player</div>
<ul>
<li>Play/pause/seek controls</li>
<li>Playback speed (0.5x - 2x)</li>
<li>Auto-scroll transcript sync</li>
<li>Click segment to jump</li>
</ul>
</div>
<div class="card">
<div class="card-title">🔎 Smart Search</div>
<ul>
<li>Multi-word episode matching</li>
<li>Podcast-scoped search</li>
<li>Highlighted results</li>
<li>Direct timestamp links</li>
</ul>
</div>
<div class="card">
<div class="card-title">📝 Signup Workflow</div>
<ul>
<li>Public submission form</li>
<li>Admin review queue</li>
<li>Email notifications</li>
<li>Auto podcast creation</li>
</ul>
</div>
<div class="card">
<div class="card-title">🛡️ Admin Dashboard</div>
<ul>
<li>JWT authentication</li>
<li>Podcast CRUD</li>
<li>Job monitoring</li>
<li>Search index management</li>
</ul>
</div>
</div>
<footer>
<p>
<strong>Podscript.tv</strong> — Open source podcast transcription platform<br>
<a href="https://github.com/Martian-Engineering/alexandria">github.com/Martian-Engineering/alexandria</a>
</p>
</footer>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment