Sin Anestesia
Content automation for a daily news show
The problem
A journalist running a daily show had two manual bottlenecks. Each broadcast runs one to two hours: clipping the viral moments, reframing them vertically, subtitling them, writing copy for each network and publishing was half a day of work per episode. And to book guests, they read the day’s news and cross-referenced a contacts spreadsheet from memory.
Architecture
YouTube link
└─ yt-dlp ──────────► source video (mp4 ≤1080p)
└─ ffmpeg ───────► mono audio 16 kHz 16 kbps
└─ Whisper ───► transcript with word-level timestamps
└─ Claude Sonnet 5 ──► N clippable moments (start, end, hook)
└─ ffmpeg ────────► cut + 9:16 blurred bg + burned subtitles
└─ Claude Sonnet 5 ──► titles, hashtags, per-network caption
└─ YouTube / TikTok / Meta ──► publishedDecisions
A model roster per task, not one model for everything
Quality where the result is the product (highlights, copy, messages) and speed/cost where it is structured extraction (mapping columns, matching headlines against a list). Dropping news matching to Haiku degrades nothing perceptible and runs every 2 hours on a cron: that is where savings compound.
export const AI_MODELS = {
quality: 'claude-sonnet-5', // output the user sees
fast: 'claude-haiku-4-5-20251001', // cheap extraction
} as constNever trust the numbers the model returns
Claude's startSec/endSec are treated as proposals, not truth: each boundary snaps to the nearest real segment within 3 s, gets clamped to the maximum duration, and clips that are too short or overlap the previous one are dropped even though the prompt forbids it. This layer is what allows the prompt to stay simple: the model supplies editorial judgement, the code enforces hard constraints.
Defence against malformed output
The model almost always returns input already parsed, but sometimes serializes the whole object — or just an inner field — as a JSON string, occasionally wrapped in a markdown fence. Without normalizing, Mongoose receives a string where it expects an array and blows up with CastError in production. normalizeInput() covers the three observed cases. This is not defensive theory: each branch corresponds to a real pipeline failure.
Details you only learn by implementing
Sonnet 5 uses adaptive thinking by default, and that conflicts with forced tool_choice. The client detects this and disables thinking only for the models that need it. It is commented in the code so the next person does not "fix" it.
if (model.startsWith('claude-sonnet-5')) {
request.thinking = { type: 'disabled' } // conflicts with forced tool_choice
}Graceful degradation: AI as an upgrade, not a dependency
On contact import, Haiku proposes the column mapping. If the call fails — no API key, rate limit, timeout — it falls back to a regex heuristic table and the import keeps working, just with a worse mapping that the user reviews anyway before confirming.
The product
Figures
- 7,400
- lines of TypeScript (94 files)Sin Anestesia §1
- 14
- frontend screensSin Anestesia §1
- 7
- distinct AI uses in the systemSin Anestesia §3
- −20%
- prompt trimmed, measured with count_tokensSin Anestesia §4.5
- 4
- containers in productionSin Anestesia §1
- 72%
- of AI spend was transcriptionSin Anestesia §4.9
What it demonstrates
| Skill | Evidence |
|---|---|
| Reliable structured outputs | Forced tool-use + normalizeInput covering three real failure modes |
| Cost management | Two-model roster · prompt cut 20% with measurement · vendor benchmark with pre-set thresholds |
| Hallucination mitigation | Database resolution · sanitizeExternalNames · prompts that reward saying "no one fits" |
| Production robustness | Idempotent pipeline · heuristic fallback · retries with backoff · separate queues |
| Product judgement | Human confirmation before writing or publishing · user-configurable tone profile |
| Prompt engineering | Role + prioritised criteria + hard rules + format constraints in the schema |