I run a small Japanese skincare site as a side project. It needed articles — a lot of them — and I did not want to write a second content system.
What I already had was a video script pipeline: a generator that produces a structured script per topic, which then gets turned into narration, slides, and an uploaded short. It had been running daily for months.
The realisation was that the expensive part of that pipeline is not the video. It is the structure: a topic, an angle, an ordered set of sections, and a claim per section. A rendered video throws almost all of that away into pixels. An article can keep it.
So instead of writing an article generator, I wrote an adapter.
The shape that made it reusable
The script format was already close to an article outline:
{
"topic": "...",
"hook": "...",
"sections": [
{ "heading": "...", "points": ["...", "..."], "narration": "..." }
],
"cta": "..."
}
The video renderer consumes narration (for TTS) and points (for slide bullets). The article renderer consumes heading + points and uses narration as the prose seed for that section.
One input, two sinks. About 49 articles later, this is still the whole idea.
Three things that were not obvious
1. Narration prose is not article prose
Spoken narration is short, repetitive on purpose, and full of connective filler ("so, next…", "here's the thing"). Dropped verbatim into HTML it reads like a transcript, which readers bounce off.
The adapter rewrites each section with an explicit instruction to drop spoken connectives, merge the repeated setup, and keep concrete numbers. It is the only LLM call in the article path — everything else is deterministic.
2. The same claim must not exist twice under two URLs
If both the video description and the article carry the same paragraph, you are competing with yourself. The rule I settled on: the article is the canonical long form; the video description links to it and carries only the hook plus a one-line summary.
3. Cache invalidation, as always
The video pipeline caches rendered slides under a temp directory keyed by the script hash. When I started editing scripts to improve the article, the video side happily reused the old slides — same slug, stale cache. Symptom: "I fixed the script, why is the video unchanged?"
The fix is boring but worth stating: key the cache on the content hash, not the slug, and normalise out any timestamps before hashing. I got bitten by a related version of that in another project, where a JSON payload carried a generatedAt field — every diff looked dirty, so a "skip if unchanged" optimisation never once skipped anything. It quietly burned a daily API quota for weeks.
const stripVolatile = (o) => {
const { generatedAt, ...rest } = o;
return rest;
};
const contentHash = (o) => sha256(JSON.stringify(stripVolatile(o)));
If your JSON has a timestamp in it, hashing it raw means you have written if (true).
What the numbers look like
- 1 script format, 2 renderers (video, article)
- ~49 articles produced from scripts that were going to be written anyway
- 1 LLM call per section, everything else deterministic and re-runnable
- Publishing is a static build — no CMS, no database
The site itself is AI美容カルテ, a Japanese skincare-advice site; the articles are the long-form half of the same content the daily shorts cover.
The transferable bit
If you already generate content in any structured intermediate form — video scripts, slide decks, podcast outlines, release notes — you probably already own the expensive half of an article pipeline. Look at what your renderer throws away. That is your second output format.
Top comments (0)