Skip to content

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