Created
February 4, 2026 13:54
-
-
Save jacoblyles/f4818924a5be72b3dafb159e8cb050f3 to your computer and use it in GitHub Desktop.
Podscript.tv System Architecture
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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