Skip to content
AI systems & agentsInternal2026

Repurposer

A standalone content-repurposing microservice that turns one source transcript into voice-compliant, publish-ready drafts across eight channel formats, gated by a deterministic…

149
passing tests, zero AI calls in the suite
8
output formats generated per source transcript
13
voice/style lint rules enforced pre-delivery
ArchitectureGated system — no screenshot
Application
  • Python 3.12
  • FastAPI
  • SQLAlchemy 2 (async)
  • arq
  • Anthropic API
Data
  • PostgreSQL
  • Redis
Supporting
  • Alembic
  • structlog

Repurposer holds live data, so this shows the verified technology stack by layer rather than a screenshot. Hosts, ports and topology are deliberately absent.

Problem

Turning one piece of long-form content (a call, a video transcript) into channel-ready drafts for every platform an operator publishes to was manual, repetitive work: rewriting the same idea eight different ways, then separately checking each draft against house voice rules before anything could go out. There was no service that took a transcript once and produced all eight formats as a single traceable job, with the brand-voice check enforced automatically rather than left to a human proofreader. The spec it was built from came from a separate internal handoff document, not a blank page, which meant part of the problem was reconciling a written spec against the actual shape of the systems it had to plug into once those systems were real instead of assumed.

What was built

Repurposer is a FastAPI microservice with one job type: submit a source transcript, request a set of output formats, and get back voice-compliant drafts for each one. A generation pipeline calls Claude per format against a format-specific prompt template, then every output passes through a deterministic lint gate (13 voice rules, block or warn severity, regex- or phrase-based) before it's considered passed. Passed drafts are handed to a delivery step that posts them into the operator's internal content-queue system, tagged so the queue can tell repurposed drafts apart from manually written ones. An internal operator UI shows job status, and a webhook exists to accept material from an external trigger source, currently running in shadow mode (received but not acted on) until an operator deliberately turns it on. Regeneration is a first-class operation, not a full re-submit: a single format can be regenerated with an optional steering prompt appended to the next attempt, the endpoint refuses a second regenerate while one is already in flight for that format, and nothing is written to the job state until the new attempt is successfully enqueued. Delivery failures are treated as non-fatal — a paid generation result is committed to the database before the delivery call happens, so a delivery-step failure or mid-request cancellation can never lose already-paid-for content, and a regenerated format that gets re-delivered creates a new entry in the queue rather than silently overwriting the old one.

Technical approach

The service follows a house microservice pattern: it owns a dedicated PostgreSQL schema (not shared tables) with a strict role split enforced at the database level — a migrator role holds DDL privileges and an app role is DML-only, verified by a test that asserts the app role gets a permissions error on CREATE TABLE. All schema changes are hand-written Alembic migrations; ORM-driven autogenerate is explicitly prohibited so the schema is always reviewable diff-by-diff. The core tables are sources (one per transcript, with a 100-120,000 character length constraint), jobs (fans out to multiple formats per request), outputs (one row per format, per job), and format_configs/voice_rules as a seeded, editable-without-code registry. Jobs run through arq on a shared Redis instance but on a dedicated DB index and queue name to avoid colliding with sibling services on the same host. Only one module in the codebase is allowed to import the Anthropic SDK, and a test enforces that no literal Claude model string appears anywhere outside the config module — a guardrail against silently drifting model versions across the codebase. The test suite makes zero live AI calls (149 passing tests); a separate test-live path exists behind a pytest marker for the roughly eight real Claude calls needed to validate all formats end to end, gated behind topping up API credits. A delivery decision reversed the original spec: rather than posting drafts directly to a third-party scheduling tool, drafts route through the operator's own internal FastAPI queue app, because that queue owns a multi-stage approval gate that a direct integration would have bypassed.

Creative approach

Craft

There is no marketing surface here, but the internal operator UI is a real, deliberately small design surface rather than a placeholder: a single self-contained static HTML page served directly by the API, no build step and no CDN assets, styled to a specific dark-obsidian-plus-violet palette called for in the spec rather than left to whatever the browser defaults to. It walks the operator through the actual job lifecycle — a format picker populated live from the formats endpoint, job submission, status polling every two seconds, lint violations surfaced verbatim rather than summarized, a per-format regenerate control that accepts an optional steering prompt, a copy-JSON action, and a direct link into the downstream queue once a draft exists. The page ships unauthenticated as a shell (it contains no data), and every data call it makes carries the operator's bearer token from local storage, so the one page that does exist stays consistent with the service's bearer-auth-on-every-route posture instead of carving out a UI-only exception.

Reframe

The deterministic lint gate is the load-bearing idea: rather than trusting an LLM to consistently follow voice rules, or asking a human to catch drift after the fact, every generated output is checked against explicit machine-checkable rules (regex and phrase matches with block/warn severity) before it's allowed to reach a queue. That turns 'does this sound right' from a subjective judgment call into a pass/fail gate the pipeline itself enforces, which is also what let the team ship a fully tested pipeline with zero live AI spend in CI. The same instinct shows up in how placeholder substitution is implemented in the prompt renderer: substituted content is never rescanned for further placeholders, so an operator's own context text that happens to contain something that looks like a template variable is left alone as literal text instead of being silently re-interpreted — a small decision, but one that prevents a whole class of injection-shaped bugs from ever reaching the model.

Process and what failed

The original handoff spec assumed drafts would post directly into a separate scheduling tool; that assumption was overturned once the actual delivery target's architecture became clear — it runs its own multi-stage gated approval queue, and a direct integration would have let repurposed content skip those gates. The build otherwise landed cleanly across four phases, but two of the four phases remain formally unaccepted: the generation pipeline's live-call acceptance test is blocked on the Anthropic account behind it having zero API credits, and the delivery phase is blocked on provisioning an API key for the downstream queue service. Both are described in the handoff as 'passed, not delivered' by design, not as bugs.

Outcome

All four planned build phases are committed and merged (149 tests passing, zero paid tokens spent building or testing any of it) and the service runs as an always-on local process with worker, monitoring hooks, and nightly database backups in place, including a restore drill that has already passed once. Live-call acceptance of the generation pipeline and actual delivery into the downstream content queue are both still blocked on operator-only steps (API credit top-up, API key provisioning) as of the last handoff, meaning the code path is built and tested but has not yet processed a real, delivered job end to end. The activation runbook for both remaining gates is fully specified — top up the account behind the generation client, run the live-call test once to close the first gate, then issue a scoped API key for the downstream queue and restart the worker to close the second — so what remains is two operator actions, not further engineering.