Architecture
Internet (TLS, Let's Encrypt)
│
Caddy on BHS64 (MacLustr Tunnel gateway)
│ WireGuard
┌─────────────────▼──────────────────────────────┐
│ node M2M32c (macOS, PM2, placed by mld) │
│ │
visitor ─────▶ │ clip-web Next.js 16 (App Router) │
admin ─────▶ │ public cinema /, /feed, /film, /series … │
│ studio /studio/* (session + RBAC) │
│ API /api/* (public + studio) │
│ media /media/* (range, signed) │
│ │ enqueue ▲ read │
│ ▼ │ │
│ Redis 8 (BullMQ) PostgreSQL 17 (db clip) │
│ │ ▲ │
│ ▼ │ │
│ clip-worker Node (tsx) │
│ pipeline stages, scheduler, GC, backups │
│ FFmpeg / ffprobe │
│ │ │
│ ▼ │
│ asset store ~/data/clip-spboucher-ai │
│ content-addressed (SHA-256), outside the repo│
└────────┬───────────────────────────────────────┘
│ HTTPS (server side only)
▼
xAI API: grok-4.3 · Grok Imagine image/video · TTSProcesses
| Process | Role |
|---|---|
clip-web |
Next.js server: public pages (ISR-free, server-rendered from PostgreSQL), studio, API, media delivery with HTTP range |
clip-worker |
BullMQ workers (pipeline queue, concurrency from the budget policy), the programming scheduler, garbage collection, nightly backup |
They share one codebase (src/). Only server code imports src/lib/env.ts; the client bundle never sees a secret
(scripts/secrets-scan.ts greps the built client chunks in CI).
Pipeline
Idea ─▶ Pitches ─▶ [Research] ─▶ Script ─▶ Shot plan + bibles ─▶ Keyframes ─▶ Voice ─▶ Video ─▶ Assembly ─▶ QA ─▶ Approval ─▶ Publication
Creative Research Screen- Storyboard / Visual Visual Audio Motion Editor Critic policy Publisher
Director Editor writer Director Director Director DirectorStage order differs from the brief in one place: voice is synthesised before video. Narration length is the only reliable clock, so each shot's video duration is derived from its measured narration (cheap call first, expensive call sized correctly, no re-buying a clip because it was a second short).
Each stage is a pure function (project state) → (rows + assets), run by one BullMQ job:
- Typed contracts —
src/lib/contracts.ts(Zod):CreativeBrief,Pitch,Script,ShotPlan,Bible,QualityReport,FilmManifest. LLM output is parsed against the schema; a parse failure is a stage failure. - Idempotency — every paid call goes through
runProviderJob()(src/pipeline/provider-job.ts) with an idempotency key = SHA-256 of (kind, model, canonical input). Theprovider_jobsrow is unique on that key: a retry or a double click finds the finished row and reuses its asset; a crashed video job is resumed from its storedrequest_idrather than resubmitted. - Budget gate — before any paid call: kill switch, per-shot / per-film / daily / monthly ceilings, and the
estimate from
src/providers/pricing.ts. A refusal raisesBudgetErrorand parks the project inblocked. - Bounded retries — HTTP layer: exponential backoff + jitter, 3 attempts, circuit breaker per endpoint
(
src/providers/http.ts). Shot regeneration: capped bymax_regenerations_per_shot, then human approval. - Provenance — each
provider_jobsrow stores provider, model, settings, prompt, input hash, timestamps, cost and output asset. The publishedFilmManifestis a public projection without prompts.
Autonomy modes (per project): manual (each stage is started by an operator), assisted (free and cheap stages
run on their own, the pipeline pauses before paid video generation), autonomous (runs to QA inside the budget).
Publication always requires a human approval unless auto_publish is explicitly enabled in the policy, and never
happens on a failed QA without a recorded override.
Data model
db/migrations/001_init.sql. Groups:
- Identity:
users,sessions,audit_log,rate_limits - Editorial:
series,collections,collection_films,tags,film_tags - Production (private):
projects(brief),concept_candidates,script_versions,bibles,storyboards,shots,prompt_versions,provider_jobs(= generation attempts),assets,audio_tracks,subtitle_tracks,film_versions(manifest),quality_reviews,approvals - Public:
films(metadata only),publications,reactions,analytics_events,reports - Control:
budget_policy,cost_entries,schedules,settings
Public pages query films + delivery assets only; prompts live in shots / prompt_versions / provider_jobs
and are never selected by a public query (src/lib/films.ts is the single public read path).
Storage
AssetStorage (src/lib/storage.ts) with one driver today: a content-addressed filesystem store
(ASSET_ROOT/ab/cd/<sha256>.<ext>). Published delivery assets are served at /media/<sha>.<ext> with range
requests and immutable caching; everything else needs a studio session or an HMAC-signed, expiring URL
(getSignedUrl). An S3 driver is the documented migration path (same interface) — it is not implemented,
because the cluster has no object store and the brief asks to confirm real infrastructure first.
Search and recommendations
No xAI embedding model exists (live check), so CLIP ships an explainable lexical embedding: hashed TF-IDF over title, synopsis, thesis, tags, series and style (512 dimensions, cosine). Search = PostgreSQL full-text rank
- trigram similarity + cosine, and every result reports which signal matched. Related films = cosine + shared tags/series, with the reason shown ("same series", "shared tags: …"). No engagement signal feeds the ranking.
Migration path to distributed workers
Workers are stateless apart from the asset store. To scale out: run clip-worker on more nodes against the same
Redis/PostgreSQL and move AssetStorage to S3-compatible storage (MinIO on the LAN). docker-compose.yml
describes that topology for a non-macOS host.