Back to home

Sin Anestesia

Content automation for a daily news show

Role
AI Engineer & Full-Stack Developer
Status
In production

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

pipelinetext
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 ──► published
Every stage is idempotent: if Bull retries, it never pays for transcription twice.

Decisions

01

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 const
02

Never 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.

03

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.

04

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
}
05

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

Human confirmation made visible: the AI drafts the title, description and a different caption for each network, but nothing ships until the journalist hits "Approve and publish".
Each broadcast goes in as a link and comes out as N clips ready for review.
The tone profile is user-configurable and is the single field with the most influence on the quality of everything the app writes.
The show’s schedule: booked interviews and planned publications.
Private access: the app was built for one specific newsroom. The palette comes from the show’s logo, not from a template.

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

SkillEvidence
Reliable structured outputsForced tool-use + normalizeInput covering three real failure modes
Cost managementTwo-model roster · prompt cut 20% with measurement · vendor benchmark with pre-set thresholds
Hallucination mitigationDatabase resolution · sanitizeExternalNames · prompts that reward saying "no one fits"
Production robustnessIdempotent pipeline · heuristic fallback · retries with backoff · separate queues
Product judgementHuman confirmation before writing or publishing · user-configurable tone profile
Prompt engineeringRole + prioritised criteria + hard rules + format constraints in the schema

Stack

  • NestJS
  • TypeScript
  • MongoDB
  • Redis + Bull
  • ffmpeg (libass)
  • yt-dlp
  • Next.js
  • React
  • Tailwind
  • Claude Sonnet 5
  • Claude Haiku 4.5
  • Whisper
  • YouTube Data API v3
  • TikTok Content Posting API
  • Meta Graph
  • Docker Compose