Architecture¶
The authoritative, detailed description lives in PLAN.md at the repository root (sections 2–8). This page is the short map agents read first.
Layers¶
src/thumbforge/
cli/ Typer apps only; Rich rendering (cli/_render.py); error→exit mapping (cli/_errors.py)
core/ Pydantic domain models, error hierarchy, id/hash helpers, adapter Protocols (sources.py, providers.py), layout spec model (layout.py), services (fetch, hero, iterate, batch)
providers/ ImageProvider protocol, capabilities, registry + entry points, fake.py, antigravity.py
sources/ MetadataSource implementations; ytdlp.py (default); youtube_api.py (optional extra)
storage/ SQLAlchemy 2.0 models, repositories, Alembic migrations, content-addressed AssetStore
templates/ TOML loading into core.layout.LayoutSpec, Jinja2 prompt rendering, builtin templates, versioning + builtin sync (loader.py)
imaging/ Pillow fit/crop, text overlay (HarfBuzz-shaped fallback font for Devanagari, ADR 0019), YouTube compliance check, final render (finalize.py), bundled fonts
settings.py pydantic-settings (TOML + THUMBFORGE_* env), platformdirs paths
logging.py structlog configuration
Dependency rule¶
cli → core, storage, providers, sources, templates, imaging. core imports no other internal package. Every other package may import core only. Nothing imports cli.
core is a single layer, so its own modules may import each other: core.models imports core.enums, and the Phase 6 services import core.models. The constraint that matters is acyclicity, so core.enums, core.errors and core.ids are leaves that import nothing from core. Enforced by import-linter contracts (Phase 1), including core leaf modules import nothing from core.
Request flow (batch)¶
flowchart LR
CLI[cli/batch.py] --> BS[BatchService]
BS --> MS[MetadataSource]
BS --> TR[TemplateRenderer]
BS --> SEM{{Semaphore}}
SEM --> IP[ImageProvider]
IP --> OV[Overlay]
OV --> CC[Compliance]
CC --> AS[AssetStore]
AS --> RR[RunRepository]
RR --> DB[(SQLite)]
Data model¶
Nine tables: channel, playlist, video, playlist_item, template, provider_profile, run, iteration, asset. Primary keys are 26-character Crockford base32 ULIDs (core.ids.new_id()), timestamps are ISO-8601 UTC strings. The SQLite engine enforces PRAGMA journal_mode=WAL;, PRAGMA foreign_keys=ON;, PRAGMA busy_timeout=5000; on every connection. Deterministic constraint naming conventions ensure safe Alembic batch migrations (render_as_batch=True). A batch run references its hero through run.parent_run_id and run.reference_asset_id (ON DELETE RESTRICT). Full column list and ERD: PLAN.md §3.
Key decisions¶
| Topic | ADR |
|---|---|
| Python 3.14 + uv | 0001 |
| Typer + Rich | 0002 |
| Settings (TOML, platformdirs) | 0003 |
| SQLite + SQLAlchemy + Alembic | 0004 |
| yt-dlp metadata | 0005 |
| Jinja2 prompts, TOML layouts | 0006 |
| Pillow | 0007 |
| Deterministic text overlay | 0008 |
| ruff / pyright / pytest / prettier | 0009 |
| Provider plugins | 0010 |
| Content-addressed assets | 0011 |
| Idempotency + resumable runs | 0012 |
| Antigravity adapter | 0013 |
| Secrets | 0014 |
| structlog | 0015 |
| zensical docs | 0016 |
Shared models live in core |
0018 |
| Complex-script titles | 0019 |