---
title: "We deleted the voice bill. Now the studio posts its own videos."
search_title: "Open-Source AI Video Studio on Claude Code: Free Voice Cloning, Auto-Posting"
description: "Our open-source Claude Code video studio now clones my voice offline in English and Hindi, renders on a CPU and posts to YouTube and X by itself: 40 videos in ten days, $0 for voice. Instagram is still by hand, and the post says why."
author: "Prajjwal Pathak"
published: 2026-10-17
canonical: https://cybiqon.in/lab/open-source-ai-video-studio-claude-code
tags: [AI Video, Claude Code, HyperFrames, Voice Cloning, Open Source, Automation]
---

# We deleted the voice bill. Now the studio posts its own videos.

*By Prajjwal Pathak · 2026-10-17 · [https://cybiqon.in/lab/open-source-ai-video-studio-claude-code](https://cybiqon.in/lab/open-source-ai-video-studio-claude-code)*

Six days ago I wrote up [the first version of our video studio](/lab/hyperframes-heygen-ai-video-pipeline): a Claude Code agent that researched, scripted, voiced and rendered a video every night while I slept. It had made twelve videos in four days. It used a paid [HeyGen](https://www.heygen.com/) clone of my voice, and every video still waited for me to post it by hand.

Since then the studio has made 28 more, 40 in ten days. It now clones my voice with open-source models that run on the same 4-core server that renders the frames, in English and in Hindi, so the voice line in our costs is $0. Three cron jobs run it every day: one for blog explainers, one for product Shorts and one for teaching videos. YouTube and X get their uploads from the scheduler at 18:30 IST, and the scheduler checks at 19:00 that they actually went public. Two new review gates make sure a topic is a problem real developers have, and that a second AI model from a different company has read the script before any voice is spent on it.

That is the headline. The fine print is that Instagram and Facebook are still posted by me, from my phone, with a caption the studio writes. The first X post made by cron goes out tonight. And the most useful thing we learned this week cost nothing to build: the owner (me) watched a Short, liked the visuals, and wrote "I learned nothing from it."

This post walks through the whole machine, stage by stage. It covers how a topic is chosen, how the script is reviewed, how the voice is cloned offline, how frames are built and rendered, and how a finished MP4 gets to YouTube and X. All of it is in [faceless-video-studio](https://github.com/cybiqon-ai/faceless-video-studio), MIT-licensed, which you can install into Claude Code with one command. The last sections show how, and where to plug in your own uploader.

## TL;DR

- **40 videos in ten days (8–17 October 2026), with no camera, no microphone session and no editor.** By the studio's own ledger, that is 18 teaching videos (three of them 6–7-minute long-forms), 12 blog explainers, 5 product and service promos, and 5 made in the first two days, before the tracks were split. The total runtime is 65 minutes.
- **The voice now costs $0.** HeyGen voice cloning was removed from the studio on 16 October. English is [Pocket TTS](https://github.com/kyutai-labs/pocket-tts) by Kyutai, at about real time on 4 CPU cores. Hindi is [IndicF5](https://github.com/AI4Bharat/IndicF5) by AI4Bharat, at about 30× real time. Both are cloned from my own recordings and run offline. The HeyGen voice had cost about $0.14 per minute of narration, and it stopped the pipeline the day its credit ran out.
- **The only required cost is Claude Code.** The agent's runs cost $11–26 each by Claude Code's own estimate. The optional extras cost cents: $0.03 for the second-opinion script review and $0.015 per X post.
- **Three tracks a day, on cron.** 12:30 blog explainer → YouTube, public, linked on the blog post. 15:15 product Short → YouTube scheduled for 18:30 plus X at 18:30, verified at 19:00. 23:00 teaching Short or long-form → Telegram for me to review.
- **40 pass/fail gates.** The newest two, A21 and A22, check the topic and the script before any voice is made. A21 means a teaching video must start from a problem developers are visibly asking about online, with at least two links as proof. A22 means a model from a different company reviews every script and writes its own version. A new mix check keeps the music at least 20 dB under the voice.
- **Posting is the part that is still half-manual.** YouTube and X are automatic. Instagram and Facebook are not yet. The studio delivers the MP4 and a ready caption to Telegram, and I post them.
- **You can run all of it.** `claude plugin marketplace add cybiqon-ai/faceless-video-studio` installs the studio. A 30-second recording of your voice makes it speak as you. Four small Python plugins connect it to your own calendar, chat and storage, and a `POST.json` per video hands its titles and captions to your own uploader.

## What changed since the first AI video pipeline

The first write-up was a snapshot from 11 October. This is the same studio six days later.

| | 11 October (post #1) | 17 October (now) |
|---|---|---|
| Videos made | 12 in 4 days | 40 in 10 days |
| Voice | HeyGen instant clone, paid per minute | Pocket TTS (English) + IndicF5 (Hindi), offline on CPU, $0 |
| Languages | English | English, Hindi (IndicF5 covers 11 Indian languages) |
| Quality gates | 37, then 38 | 40, plus a loudness check that blocks delivery |
| Topic choice | rotation + "what's new this week" | a problem scout that proves real developers have the problem |
| Script review | three Claude reviewer agents | three Claude reviewer agents + a second model from another company |
| Viewer | "a smart 12-year-old" | a developer who already codes |
| Daily runs | 1 (plus promo three days a week) | 3: blog, product, teaching |
| Posting | all by hand | YouTube and X by cron; Instagram and Facebook by hand |
| Installation | clone + `install.sh` | one-command Claude Code plugin, or clone + `install.sh` |

Four of those rows are changes of mind, not new features. The viewer, the topic rule, the voice and the posting all changed because something went wrong in front of real viewers. The sections below say what.

## The daily schedule: three tracks, one server

Everything runs on one Ubuntu server with 4 CPU cores and no GPU, in IST.

<figure>
<svg viewBox="0 0 720 300" role="img" aria-label="A timeline of one studio day in IST. 06:30: a researched video idea goes to Telegram. 12:30, retried at 14:00: the blog track renders an explainer of the day's blog post and uploads it public to YouTube, linked on the post. 15:15: the promo track renders a product Short, about 75 minutes, and uploads it to YouTube scheduled for 18:30. 18:30: the Short goes public on YouTube and is posted to X. 19:00: a verify job checks it is public and marks it posted, or sends an alert. 23:00: the teaching track renders tomorrow's Short or long-form and delivers it to Telegram for review, then it is posted by hand to Instagram and YouTube." style="width:100%;height:auto">
  <g fill="none" stroke="currentColor" stroke-width="2">
    <line x1="40" y1="150" x2="690" y2="150"/>
  </g>
  <g fill="currentColor" font-family="ui-monospace, monospace" font-size="13">
    <circle cx="70" cy="150" r="6"/>
    <text x="70" y="132" text-anchor="middle">06:30</text>
    <text x="70" y="180" text-anchor="middle">idea</text>
    <text x="70" y="196" text-anchor="middle" fill-opacity="0.6">→ Telegram</text>

    <circle cx="210" cy="150" r="6"/>
    <text x="210" y="132" text-anchor="middle">12:30 · 14:00</text>
    <text x="210" y="180" text-anchor="middle">blog explainer</text>
    <text x="210" y="196" text-anchor="middle" fill-opacity="0.6">16:9 → YouTube, public</text>
    <text x="210" y="212" text-anchor="middle" fill-opacity="0.6">→ linked on the post</text>

    <circle cx="350" cy="150" r="6"/>
    <text x="350" y="132" text-anchor="middle">15:15</text>
    <text x="350" y="180" text-anchor="middle">product Short</text>
    <text x="350" y="196" text-anchor="middle" fill-opacity="0.6">9:16 → YouTube,</text>
    <text x="350" y="212" text-anchor="middle" fill-opacity="0.6">scheduled 18:30</text>

    <circle cx="470" cy="150" r="8"/>
    <text x="470" y="112" text-anchor="middle">18:30</text>
    <text x="470" y="128" text-anchor="middle" fill-opacity="0.6">public on YouTube + X</text>

    <circle cx="560" cy="150" r="6"/>
    <text x="560" y="180" text-anchor="middle">19:00</text>
    <text x="560" y="196" text-anchor="middle" fill-opacity="0.6">verify, or alert</text>

    <circle cx="660" cy="150" r="6"/>
    <text x="660" y="132" text-anchor="middle">23:00</text>
    <text x="660" y="180" text-anchor="middle">teaching video</text>
    <text x="660" y="196" text-anchor="middle" fill-opacity="0.6">→ Telegram,</text>
    <text x="660" y="212" text-anchor="middle" fill-opacity="0.6">I review + post</text>
  </g>
  <g fill="currentColor" fill-opacity="0.08">
    <rect x="160" y="236" width="400" height="34" rx="6"/>
  </g>
  <g fill="currentColor" font-family="ui-monospace, monospace" font-size="12">
    <text x="360" y="258" text-anchor="middle">automatic end to end: render → upload → publish → verify</text>
  </g>
</svg>
<figcaption>One studio day. The blog and product tracks run without me; the teaching track still ends with my review.</figcaption>
</figure>

Each track is the same skill run in a different mode by the same runner: `bin/run.sh blog`, `bin/run.sh promo`, `bin/run.sh` (daily). The tracks share one lock, so they never render at the same time on a 4-core machine. Each has its own time and spend ceiling: 2 hours and $20 for the blog, 75 minutes and $25 for the product Short, 3 hours and $25 for a teaching Short, 7 hours and $60 for a long-form night.

The teaching track follows a weekly rotation, set in `config.json`:

| Day | Pillar | Format |
|---|---|---|
| Monday | AI term | Short, 35–60 s |
| Tuesday | Python | Short |
| Wednesday | AI deep dive | Long-form, 16:9, 4–8 min, plus a vertical companion |
| Thursday | Build a thing | Short |
| Friday | Tech news explained | Short |
| Saturday | AI term or Python | Short |
| Sunday | System design deep dive | Long-form, plus companion |

A long-form for Wednesday renders on Tuesday night with a 7-hour ceiling. A render of a 7-minute video is about 12,700 frames.

## Topic research: the problem scout (gate A21)

On 15 October I watched a Short the studio had made about stopping SQLite's `VACUUM INTO` on a read-only connection for an AI agent. It was true. The code had been run, and the footage was real. I wrote: "There's no point making a video on a problem which no one is facing."

The rotation had told the agent "AI term", and the agent had picked something that was new and interesting to *it*. Nobody had asked. A hook can't make a viewer recognise a problem they've never had. We shelved the video and added a step before anything else is chosen.

**The problem scout is a fresh sub-agent with one job.** It finds problems working developers are hitting this quarter in the day's subject, proves they have them, and ranks them. Its instructions:

- At least **12 web searches across at least 4 kinds of source**: Stack Overflow (the "frequent" and "votes" tabs), GitHub issues on popular tools, Hacker News "Ask HN" threads, Google's "People also ask", and official forums such as Python Discourse and the AI providers' developer forums. Reddit is on the list too.
- Every problem is quoted **in the asker's own words** and proven with **at least two links and their numbers**: views, votes, 👍 reactions, comment counts, dates. "Don't invent numbers: if a count isn't visible, write n/a and score lower."
- Each problem is scored 0–3 on five things: **Frequency, Recency, Everyday, Showable in 35–60 s, and We can prove it**. That makes a maximum of 15.
- A problem needs **9 or more**, and a 0 on Frequency or Everyday rules it out at any total. Interview questions are ruled out always. We don't make interview content.

The scout writes 8–15 ranked rows to `research/problems.md`, and every row goes into a persistent `problem-bank.json`. The next run reads the bank first: a high-scoring, unused problem from the last 30 days can be picked without searching again, as long as its evidence is re-checked.

**Gate A21 enforces it in code.** The plan must cite the chosen row, its words and its two links. Only then may the agent write `A21: PASS (real problem)`. `deliver.py` refuses to deliver a teaching video without that line and without `research/problems.md`, however good the video is.

The rule changed the hooks too. A hook now opens on the viewer's situation, using the asker's words, before it names any tool. The best hook is often the question title itself, tidied up.

Two practical notes from running it:

- **Reddit blocks data-centre IPs.** From our server, Reddit returns 403, and the free mirrors rate-limit agents. The scout copes because it needs four kinds of source, not Reddit in particular. If Reddit matters to your niche, use its official API with your own key.
- **A planned topic still gets scouted.** When I put a topic in the content calendar, the scout doesn't overrule me. It finds the real problems people have *with that topic*, and if none scores 9, it says so in the delivery message.

After the scout, the agent does 5–10 more searches for the facts. `ledger.py check` refuses any topic close to a video the studio has ever made. Everything that will appear on screen is saved in the video's `research/` folder first, including the output of the code it ran. "Nothing goes on screen that is not in this folder."

## Script review: a linter, three reviewer agents and a second AI model

A script has to get through Gate A before a second of voice is made. Voice and frames are where the time goes, so this is the cheapest place to reject a video.

**The viewer is fixed.** In the first week the viewer was "a smart 12-year-old who never coded". The scripts that came out of it were clear, and I called them childish. Four Instagram Reels got 120–150 views each, and **67% of viewers swiped away in the first three seconds**. Since 14 October the viewer is a developer or CS student who already codes. Known terms are never defined. Every Short carries at least one artifact worth saving — a command, a config, a number — and at least three named specifics every 30 seconds.

**The hook is written three times.** The agent drafts three hooks from a library of 20 templates taken from real tech Shorts, and scores each out of 12. Sentence one is at most 14 words and names the concrete subject plus a tension. The proof is on screen by 1.5 seconds, and something new happens every 10–15 seconds. A Short runs 35–60 seconds and ends on its payoff, with no recap.

**Then the checks, in order:**

1. **`script_lint.py` (A18)** checks the hook's length, banned openers ("In this video…"), sentences over 20 words, a jargon list, and the specifics-per-30-seconds count. It is 250 lines of plain Python, and it is the only reviewer that cannot be argued with.
2. **A second opinion from another model (A22).** `second_opinion.py` sends the script, with nothing else, to a model from a different company through [OpenRouter](https://openrouter.ai/). We use OpenAI's. It reviews the script and also writes its own version from the same facts. The Claude agent then decides point by point and records the outcome, e.g. `A22: second opinion adopted 6, rejected 5`. A model reviewing its own family's writing shares its blind spots. On its first run, the second opinion caught a Python `datetime` Short that said `default=encode` without ever showing what `encode` was. That had passed three Claude reviewers. It costs about $0.03 a script.
3. **Three fresh Claude sub-agents**, each seeing only the script and its brief:
   - **A16, new-to-me.** A mid-level developer must name one concrete thing they learned and who they would send the video to. A separate judge grades that answer, because a reviewer that grades itself passes everything.
   - **A19, the expert.** It flags anything that has become false by being simplified.
   - **A20, the cold viewer.** It plays a developer scrolling Reels and sees only the first three seconds. Would they stop?

A hard fail gets one rewrite. A second fail sends the run back to pick a new topic, not to polish the old one.

None of this makes a script teach. On 15 October I watched a Short that had passed A16 and said I liked the visuals but learned nothing. A16 passing is a lower bound, not proof. The fix was the "artifact plus three specifics" rule above. The rules keep moving.

## Cloning your voice offline, in English and Hindi

For the first week the studio used a HeyGen instant clone of my voice. It sounded good. It cost about $0.14 per minute of narration, allowed 30 requests a minute, and on 13 October its prepaid credit ran out mid-pipeline. I wanted the voice to be like the renderer: on our machine, with no meter.

We tested the open-source cloning models that run on a CPU and are licensed for commercial use:

| Model | Result |
|---|---|
| **XTTS-v2** | Clones well and speaks Hindi, but its licence (CPML) forbids commercial use, and its Hindi sounded flat to native listeners. Rejected. |
| **Chatterbox, NeuTTS** | 3–6× slower than Pocket TTS on our CPU, and they add an inaudible watermark. Rejected. |
| **[Pocket TTS](https://github.com/kyutai-labs/pocket-tts)** (Kyutai) | English. A 100M-parameter model built for CPUs. Kyutai reports about 6× real time on a MacBook Air M4 using 2 cores; on our server it voices at about real time. Clones from a plain WAV of 20–30 seconds of you talking. Code MIT, model CC-BY-4.0. **Chosen for English.** |
| **[IndicF5](https://github.com/AI4Bharat/IndicF5)** (AI4Bharat) | Hindi and 10 other Indian languages. About 30× real time on 4 cores, so a 60-second Short takes about 30 minutes, overnight. Needs a 6–10 second reference clip and its exact words. MIT. **Chosen for Hindi.** |

Both models are gated on Hugging Face: you accept their terms once with a free account. Both sets of terms say the same thing: clone only voices you have explicit permission to clone. Here, that's my own. `install_voice.sh pocket` or `install_voice.sh indicf5` puts each one in its own virtualenv with a CPU build of PyTorch, because their dependencies conflict. `tts_engine.py` is the one interface the rest of the studio sees: an engine class with `say(text) → samples`. Adding a third engine is a class and an install recipe.

**How a voice track is built** (`voice_build.py`, 292 lines):

1. The script is split into sentences, including on the Devanagari full stop `।` for Hindi.
2. The engine loads once and voices **each sentence as its own take**. Takes are cached by a hash of the text, engine, reference and speed. So when I ask for one line to change, only that line is re-voiced.
3. Spoken spellings come from `hf/say.json`. The engine says "timeout equals three point oh five, ten" while the caption keeps `timeout=(3.05, 10)`.
4. The speed is set after synthesis with FFmpeg's `atempo`, which keeps the pitch. My English clone at 1.0 was too fast for anyone learning. 0.64 sounded drawn out, and **0.82** stuck. IndicF5 speaks Hindi at a comfortable pace already, so it stays at 1.0.
5. [faster-whisper](https://github.com/SYSTRAN/faster-whisper) transcribes each take for word timings, which drive the captions. With `best_of: 3`, each sentence is voiced three times and the take Whisper hears closest to the script wins.
6. **Each take is trimmed on the real sound, never on timestamps.** The cut is at the first and last 10 ms window louder than 3% of the take's peak, with a little padding and 8 ms fades. Our first trim used word timestamps, which run about 0.2 seconds late, and it cut the start off a third of all sentences: "The" became "e". On one promo, 12 of 44 sentences were damaged.
7. The takes are joined with a configurable breath between sentences. I wanted about a second of silence after every sentence, so our studio uses 0.82 s.
8. `voice_build.json` records a hash of every voice file. Gate B17 refuses a video whose voice wasn't made by this script, or was changed after it, or has a take that starts or ends on speech.

**Recording the reference matters more than the model.** Some of what we learned:

- The clone copies the room. Pick a quiet room over a good microphone. A phone voice note 15 cm from your mouth is enough.
- Read expressively. A flat reading gives a flat clone.
- Record in the language the clone will speak. A Hindi voice cloned from English sounds like an English speaker reading Hindi.
- Background hiss is cloned too. FFmpeg's RNNoise filter (`arnndn`) removes it.
- **For IndicF5, keep the reference short.** F5 cuts a reference longer than about 15 seconds but keeps *all* of its transcript, so it thinks you speak faster than you do and chops the ends off words. Our first Hindi takes had exactly this bug. A 6.7-second clip fixed it, and the same lines came out 21.7 seconds long instead of 15.7. `tts_engine.py` now refuses a reference over 12 seconds.
- **Write Hindi the way people talk.** Hinglish — "तो हुआ ये कि…", "अब सोचो…" — sounds alive. Textbook Hindi, voiced correctly, still sounded flat.

A second voice is a variant in `config.json`, chosen per video with `voice_build.py --voice hi`:

```json
"voice": {
  "engine": "pocket",
  "reference": "voice/sample.wav",
  "language": "english_2026-04",
  "speed": 0.82,
  "lufs": -16,
  "variants": {
    "hi": {
      "engine": "indicf5",
      "reference": "voice/hindi.wav",
      "reference_text": "यार, आज एक मज़ेदार चीज़ हुई। …",
      "asr_language": "hi",
      "speed": 1.0
    }
  }
}
```

The Hindi voice is wired in and tested, but no Hindi video has been published yet. That is next on the list, not something we've proven with viewers.

## Loudness: voice at −16 LUFS, music 20 dB under, and a mix gate

On 16 October I wrote: "the voiceover is still low." Pocket's takes came out 2–3 dB quieter than HeyGen's had been, and one music bed at a volume of 0.16 was only 13 dB under the voice. Both were fixed with a rule — never raise the music to compensate, only bring the voice up — and with code:

- **Every voice line is levelled to −16 LUFS** with a gentle compressor, a static gain and a −1.5 dBFS limiter. We don't run FFmpeg's `loudnorm` on each line, because it pumps on a 5-second clip.
- **`mix_check.py`** computes each music bed's effective loudness: its file LUFS plus 20·log10 of its volume in the composition. The mean voice must be at least 20 dB louder than the loudest bed. `--fix` rewrites the bed volume, and `deliver.py` refuses a video that fails.
- **The final file** goes through one `loudnorm` pass to −14 LUFS integrated, −1.5 dB true peak. Spotify documents −14 LUFS as its normalisation target, and YouTube's is widely reported to be the same, so the platforms shouldn't turn us down or up much. (Broadcast, under EBU R 128, is −23.) The video stream is copied untouched, and the audio is AAC at 192 kb/s, 48 kHz.

## Frames: 22 looks, a signature beat, and real footage

Viewers judge the voice first and the look second. In the first week I told the agent two videos looked so alike that "it will soon be noticed as AI generated". Since then the look is a choice the agent has to justify, and a gate it has to pass.

**22 look families.** Blueprint, whiteboard, documentary, split-screen, isometric, editorial paper, collage-scrapbook, and seven hand-drawn styles: pixel-era, riso-poster, ink-machine, math-board, iso-hairline, cut-paper and sketchbook. The 40 videos so far have used 22 different looks between them. `ledger.py look-check` refuses a family used in the last six videos. Only the brand core stays fixed: the voice, one green accent and a two-second end mark.

**One signature beat per video.** Every video has one hero moment that the last five videos didn't have. It might be a registry block from HyperFrames, a Three.js or Lottie scene, a hand-drawn animate moment or a Manim transform. It is recorded in the ledger so it can't repeat.

**Three engines.** [HyperFrames](https://github.com/heygen-com/hyperframes) is the main one: every frame is HTML, CSS and a paused GSAP timeline, rendered deterministically by headless Chrome and FFmpeg. The seven hand-drawn styles come from [animate](https://github.com/cth9191/animate) by cth9191, vendored at v0.4.0, and may cover at most about 70% of a video. [Manim](https://www.manim.community/) is optional, for maths and diagrams, rendered to transparent WebM and laid over a HyperFrames scene.

**One sub-agent per frame, in parallel.** The plan breaks the script into beats. Each beat gets a fresh Claude sub-agent that builds that frame's HTML from the plan, the look's design file and the voice timings. The 85-second Short in the first post had 11; a long-form has 25–45 and is built in waves of 8. A script assembles them into one composition. This is where most of the agent's spend goes: on the first version's 85-second Short, about $10 of $18.35 was the 11 frame workers.

**Real footage beats anything drawn.** The videos I've liked most show the real thing:

- **Blog explainers** capture the source page itself at 2× in a headless browser and draw a highlighter on the exact sentences being discussed. The rectangles are read from the page's DOM, not guessed.
- **App and game Shorts** use the real app. For our Flutter games, a `flutter_test` harness drives the real level screen with the solver's own moves and saves 1080×1920 frames. Lumina's Short showed a mechanic, then a solver-proven number, and I loved it. The rule in the skill: "Never mock gameplay or UI."
- **Web footage** is captured with `page.screenshot` at device pixel ratio 3 in a loop. Chrome's screencast API on our server delivers 360×640 whatever size you ask for, so every screen recording before 10 October was a 3× upscale.

## Rendering video with HyperFrames on a CPU

`npx hyperframes render --quality high` turns the composition into an MP4 on the same 4-core server, in headless Chrome. Shorts are 1080×1920 and long-forms 1920×1080, both at 30 fps. Renders have taken from 2.5 minutes for a short promo to 19.5 minutes for a 7-minute long-form of 12,722 frames. There is no GPU and no cloud render.

Before rendering, `mix_check.py --fix` sets the music. After rendering, the final loudness pass runs. Then Gate B looks at what was actually made:

- `hyperframes check --strict` passes (B1).
- Safe zones, font sizes, one accent per frame, and no "AI slop" tells such as gradient text, neon, Inter/Roboto/Poppins, stock people or emoji (B2–B5).
- Footage fills at least 80% of the frame for at least half the runtime (B11). A fresh agent compares this video's contact sheet with the last three and fails it if they look like one template (B13).
- **A judge reviews at least six frames decoded from the final MP4, including frame 0** (B14). Browser snapshots once looked perfect while the rendered first frame showed a clip's untrimmed opening, with prices and an "Add to cart" button.
- `gap_check.py` caps the silence between lines (B16). `fill_check.py` fails a Short whose bottom 40% is empty in more than a quarter of sampled frames (B18).

A frame that fails goes back to its sub-agent. After two loops the run delivers with the failure noted, rather than burning the night.

## 40 quality gates, and the failures behind them

Twenty-two gates check the plan (A1–A22) and eighteen check the build (B1–B18). Eight are enforced in code — A16, A18, A19, A20, A21, A22, B17 and B18 — and `deliver.py` will not deliver a video without them. The rest are rubric rules that agents check and record. Version 1.1.0 of the repo, released with this post, documents all 40.

Almost none were in the first version. Each was added on the day a video failed that way:

| What went wrong | Gate it produced |
|---|---|
| The first Short: 97 words in 44 s, "too fast, too much content" | A8: 35–60 s, pauses kept |
| A script that "considers the viewer as an expert", then a rewrite called "childish" | A14–A16, A18: fixed developer viewer, linter, new-to-me review with a separate judge |
| Three simplifications that had become untrue | A19: expert review |
| "The hook doesn't make sense"; 67% swiping away in 3 s | A2, A20: subject + tension in ≤ 14 words, cold-viewer test |
| A true, well-made video about a problem nobody has | A21: problem scout, ≥ 2 links, ≥ 9/15 |
| A script that named a function it never showed | A22: second opinion from another model |
| Two videos so alike "it will soon be noticed as AI generated" | B11, B13, the look rotation |
| A rendered first frame that differed from every snapshot | B14 on the final file |
| 3–5 s silences from padded frames | B16 |
| A third of sentences losing their first syllable | B17 |
| The bottom 40% of every Short left empty | B18 |
| "The voiceover is still low" | Voice levelling + `mix_check.py` |

The lesson hasn't changed since the first post, but we've seen it more often: **rules written in prose get bent under pressure; rules checked in code don't.** Every gate that `deliver.py` checks has held. The ones only described in the skill's instructions are the ones an agent eventually talks itself past at 2 a.m.

## Posting to YouTube and X automatically, and why Instagram isn't yet

The open-source studio stops at the MP4. It delivers the video and a post kit and never posts by itself. That is on purpose: you should decide what goes out under your name, and how. Our own posting layer is about 700 lines of standard-library Python in our private ops repo. It reads exactly what the studio leaves behind, so here is how it works.

**What the studio hands over.** Every delivered video has a `POST.json` that `deliver.py` validates against a schema: three title options, a YouTube description, a caption with no URLs, hashtags, a pinned comment, a `thumbnail_at` timestamp, sources, notes, the cost, and the look used. Tracked links are built in code with `utm_source`, `utm_medium=video` and `utm_campaign=<date>-<slug>`. `deliver.py` sends the MP4 and a readable version of the kit through the `notify` plugin (Telegram by default) and prints one line a scheduler can grep:

```text
Rendered: orbitone-one-tap 51s 20.4MB msg 1769 parts=1
```

**YouTube** goes through the [YouTube Data API v3](https://developers.google.com/youtube/v3/docs/videos/insert) with a resumable upload:

- The **blog explainer** uploads public into its playlist. The description is `POST.json`'s plus a "Read the full post" link. The upload's video ID is then written onto the blog post, so the post and the app show the video. If the site hasn't rebuilt since the post went up, the script asks GitHub to redeploy it.
- The **product Short** uploads private with `status.publishAt` set to 18:30 IST, into a playlist per product. The playlist is created the first time a product appears. If the render finished after 18:30, it goes public at once.
- A **19:00 job** reads the video back, checks it is really public, marks it posted on our content calendar, and sends me an alert if not.
- An upload ledger keyed by slug makes every call idempotent. The 14:00 retry re-runs the whole blog job, and a video that already went up is never uploaded twice.

Two notes for anyone building the same thing. First, our scripts were written for a rule from 2020: uploads from an API project that hadn't passed Google's audit were locked to private. Google's current reference now says videos from unverified projects "are not restricted to private viewing mode", and our public uploads since 16 October have stayed public. Either way, the script reads back the privacy YouTube actually applied rather than trusting the one it asked for. Second, quota isn't a problem at this scale. Since June 2026 uploads have their own bucket of 100 `videos.insert` calls a day, and we use three.

**X** gets the same MP4 at 18:30 through the [X API v2](https://docs.x.com/x-api/media/quickstart/media-upload-chunked) chunked media upload, then a post. The script signs requests with OAuth 1.0a using only Python's standard library: `hmac`, `hashlib` and `urllib`, with no Twitter SDK. X's pay-per-use pricing made one design decision for us. A post costs **$0.015**, but **$0.20 if its text contains a URL**, and X turns a bare `cybiqon.in` into a link too. So the post text is the caption's first and last paragraphs plus two hashtags, at most 280 characters, and the script refuses to post if any link survives. The link lives in the bio. One post a day is about $0.45 a month. Tonight, 17 October, is the first post made by cron.

**Telegram** gets every video, from all three tracks, with the post kit. That is where I review the teaching videos. The bot is two-way: it is a Claude Code session in tmux with the Telegram plugin, so "make the hook shorter and re-render" from my phone becomes a `revise` run.

**Google Drive** gets every final render through an `archive` plugin, because the server deletes heavy media after delivery.

**Instagram and Facebook are still posted by hand.** Instagram does have a content-publishing API, and Reels are supported. To use it we'd need an Instagram professional account, a Meta app with the content-publishing permission at advanced access, and every video hosted at a public URL for Meta to fetch when we publish. Through Facebook Login the account must also be connected to a Facebook Page. The limit, 100 API-published posts in 24 hours, is no obstacle. None of that is hard, but it hasn't been done. Until it is, the studio writes the Instagram caption and I post from my phone. That takes about a minute, and it is honestly where the pipeline leaks. Of 40 videos made, only the YouTube and X uploads are posted without me.

## Running Claude Code unattended, safely enough

Cron calls `bin/run.sh`, which is 140 lines of bash. The important line:

```bash
flock -w "$WAIT" -E 75 "$LOCK" timeout "$LIMIT" claude -p "/video-studio $MODE" \
  --permission-mode acceptEdits \
  --allowedTools "Read,Write,Edit,Glob,Grep,WebSearch,WebFetch,Skill,Task,TodoWrite,Bash" \
  --disallowedTools "Bash(git push:*),Bash(rm -rf:*),Bash(curl:*),Bash(wget:*),Bash(sudo:*),Bash(crontab:*),Bash(ssh:*),Bash(scp:*)" \
  --strict-mcp-config --mcp-config '{"mcpServers":{}}' \
  --max-budget-usd "$BUDGET"
```

What each part is for:

- **`flock`** stops two tracks from rendering at once. A second run waits up to two hours for the lock.
- **`timeout`** and **`--max-budget-usd`** cap time and spend per run. A long-form night gets 7 hours and $60. The runner reads the day's format from the ledger before it starts. The budget is Claude Code's own estimate and a run can go a little past it, so set it below what you can afford.
- **Allow Bash, deny the dangerous commands by name.** The first unattended run stalled at a permission prompt for `node` that nobody was awake to answer. A headless run can't answer prompts, so a per-command allowlist only stalls it. The deny list is the safety: no pushing, no downloads with curl or wget, no sudo, ssh or crontab, no `rm -rf`. There is no `--dangerously-skip-permissions`.
- **No MCP servers** load in a studio run. The agent gets exactly the tools above.
- **Success is a count, not a status.** The run must print `deliver.py`'s `Rendered:` line. If it doesn't, the runner sends an alert saying why: killed by the time limit, lock held, Claude logged out, credits or usage limit reached, or "exited 0 but nothing was delivered". We learned this one the hard way on other jobs. An exit code of 0 had meant "did nothing" for 33 nights in a row.

## What an AI video studio costs to run

| Item | Cost |
|---|---|
| Voice (Pocket TTS, IndicF5) | **$0**, CPU time: ~1× real time English, ~30× Hindi |
| Rendering (HyperFrames) | **$0**, CPU time: 2.5–19.5 min a video on 4 cores |
| Captions (faster-whisper) | **$0** |
| Server | the 4-core Linux machine you already have |
| **Claude Code**, per run | **$11–26** by Claude Code's own estimate. Short runs logged $11.0–20.5; the first long-form plus companion was about $26 |
| *Optional:* second-opinion review (A22, OpenRouter) | about $0.03 a script |
| *Optional:* X posting | $0.015 a post with no link, $0.20 with one |
| *Optional:* HyperFrames music and sound-effect search | uses a HeyGen API key if you set one; the voice never does |

Claude Code is the only cost the studio needs. The figures above are what Claude Code reports at API prices. On a Claude subscription, the runs use your plan's usage instead of adding a bill.

We still don't know the full cost per video. Only 6 of the 40 ledger entries have `cost_usd` filled in. In the first post I said that was the first thing I'd do differently. It's still the first thing. Fill it in from day one.

## What didn't work

1. **The "smart 12-year-old" viewer.** It fixed scripts that assumed an expert, then overcorrected into scripts I called childish, and 67% of Instagram viewers left in three seconds. **Fix:** the viewer is a developer who codes, with an artifact and three specifics every 30 seconds.
2. **Interesting topics nobody needed.** A correct video about a SQLite edge case that no one asks about. **Fix:** the problem scout and A21.
3. **Gates that pass a script that doesn't teach.** A16 passed a Short I learned nothing from. **Fix, partial:** a model from another company reviews and rewrites every script (A22). The honest version is that review agents raise the floor, and a viewer still has to tell you if it teaches.
4. **A voice on a meter.** HeyGen credit ran out on 13 October, in the middle of a run. **Fix:** open-source voices on our own CPU.
5. **A long Hindi reference chopped every word.** F5 cut the reference at 15 seconds but kept its whole transcript. **Fix:** a 6–10 second clip, and code that refuses anything over 12.
6. **Quoted phrases disappeared.** Pocket dropped "tool query" from `sees "Error executing tool query"` in 5 of 5 takes. **Fix:** no quotes in spoken text.
7. **The voice got quieter when we switched engines.** **Fix:** levelling to −16 LUFS and a mix gate, never louder music.
8. **Reddit blocks the server.** **Fix:** the scout needs four kinds of source, not one site.
9. **Tracking.** Most ledger entries have no cost and only two are marked posted. Posting by hand meant the "posted" count was always behind. **Fix, partial:** automated posts mark themselves.

## What worked

**Real footage, again.** The blog explainers built from the source page, and the game Shorts built from the real game, are still the videos I like best. The studio's rule now is to show the real thing whenever one exists.

**Moving the voice onto our own machine.** It removed a bill, a rate limit and an outage in one change. Revisions became free: re-voicing one sentence costs a few seconds of CPU.

**A second opinion from a different model.** For three cents it caught a gap that three reviewers from the same family missed. Not all its advice is good — on one script the agent adopted 6 points and rejected 5 — but having the decision recorded is the useful part.

**Letting a calendar choose some topics.** The `planner` plugin lets me put a topic on our content calendar and have the studio make it, mark it made, and still prove a real problem behind it.

**Posting product Shorts with no human step.** Since 16 October a product Short is made at 15:15, scheduled, published at 18:30 and verified at 19:00 every day, without my input. It's the first track where "made" and "posted" are the same number.

## Install it in one command and plug it into your pipeline

You need Linux (macOS works with GNU `timeout` and `flock` from Homebrew), Node.js 22+, Python 3.10+, FFmpeg, [Claude Code](https://code.claude.com), a free Hugging Face account and about 5 GB of disk. No GPU.

**1. Install HyperFrames and the studio as Claude Code plugins:**

```bash
claude plugin marketplace add heygen-com/hyperframes
claude plugin install hyperframes@hyperframes

claude plugin marketplace add cybiqon-ai/faceless-video-studio
claude plugin install faceless-video-studio@faceless-video-studio
```

As a plugin, the command is `/faceless-video-studio:video-studio`, and it updates itself. If you'd rather own the files, clone the repo and run `./install.sh ~/my-studio`. That checks your tools, links the skill into `~/.claude/skills/video-studio` and creates a studio folder.

**2. Install a voice and record yourself.** Accept the model terms on Hugging Face, then:

```bash
pip install -U huggingface_hub && hf auth login
STUDIO_ROOT=~/my-studio skill/video-studio/scripts/install_voice.sh pocket    # English
STUDIO_ROOT=~/my-studio skill/video-studio/scripts/install_voice.sh indicf5   # Hindi, optional
```

Record 20–30 seconds of yourself speaking naturally to `~/my-studio/voice/sample.wav`. The repo's [voice guide](https://github.com/cybiqon-ai/faceless-video-studio/blob/main/docs/voice.md) has an English and a Hindi script to read.

**3. Set up and make a video:**

```bash
cd ~/my-studio && claude
> /video-studio setup                          # checks tools, voices two test lines for you to approve
> /video-studio plan "why my requests call hangs forever"   # research + plan + Gate A, no voice spend
> /video-studio daily                          # the full run: today's pillar from your rotation
```

Edit `brand/brand-core.md` (your accent colour and handle), `brand/pillars.md` (your audience and products) and `brand/products.json` (where promos link) to make it yours. The repo's `examples/cybiqon/` is our real, filled-in brand.

**4. Put it on a schedule** once a few manual runs look right:

```cron
0 23 * * *  STUDIO_ROOT=/home/you/my-studio /home/you/faceless-video-studio/bin/run.sh
30 12 * * * STUDIO_ROOT=/home/you/my-studio STUDIO_LIMIT=2h STUDIO_BUDGET=20 /home/you/faceless-video-studio/bin/run.sh blog
```

Cron has a short `PATH`, so set it to wherever `claude`, `node` and `npx` live.

**5. Plug it into your own systems.** Four optional plugins, each a small Python file named in `config.json`, connect the studio to private systems without forking it:

| Plugin | What it does | Our use |
|---|---|---|
| `planner` | `topic(day, track, pillar, slot)` lets a content calendar choose topics; `mark_made()` marks them done | our content calendar |
| `notify` | `send()` and `send_video()` deliver the MP4 and post kit | Telegram (built in); Slack, email or your own bot work the same way |
| `links` | builds tracked links for each product | UTM links to our apps and sites |
| `archive` | `store(video, post, revision)` keeps the final render elsewhere | Google Drive |

**For automatic posting**, run your own uploader after the run, the way ours does. Wait for the `Rendered:` line, read the video's `POST.json` for the title, description and caption, and upload `hf/renders/video.mp4`. Make it idempotent with a ledger keyed by slug, print a count line, and verify the result separately.

If you'd rather have this set up for your business than build it yourself, [this is the kind of thing we build for clients](/services/ai-agents).

## Verdict

Ten days in, the studio makes three videos a day on a machine with no GPU. The only bill it needs is Claude Code. The voice is mine, made offline, in two languages. Half of the output reaches viewers without me touching it.

Three things I'd tell anyone building the same thing:

**Own the slow, metered parts.** The voice was the one stage that could fail for reasons outside our machine, and it did. Moving it to open-source models removed the cost, the rate limit and the outage. It also made revisions free, which changes how picky you can afford to be.

**Make the agent prove the topic, not just the facts.** We had built gates for true facts, clear hooks and good frames, and still made a correct video nobody needed. The cheapest gate in the studio is the one that runs first: show me two people asking about this.

**Automate the last step, or count it honestly.** For a week the studio made videos faster than I posted them, and the ledger's "posted" column said so. Whatever you automate, measure the number that reaches viewers, not the number rendered.

---

*All figures are first-party unless marked, as of 17 October 2026. Video counts, durations, look families and per-run costs come from the studio's ledger (`cost_usd`, filled for 6 of 40 entries) and run logs. Run costs are Claude Code's own estimates, not a bill. YouTube uploads come from the upload ledger; X prices from our X API account and X's pricing page; the second-opinion cost from OpenRouter usage. Voice speeds, reference limits and the rejected models come from the studio's voice tests between 13 and 16 October. Render times come from HyperFrames' render logs. Gates come from the skill's rubric. Owner feedback is quoted from notes made the day it was given. Instagram view figures are from four Reels, as reported on 14 October.*

## FAQ

### Is the Faceless Video Studio free?

Yes. The studio is open source under the MIT licence at github.com/cybiqon-ai/faceless-video-studio. Its voice (Pocket TTS and IndicF5), captions (faster-whisper) and renderer (HyperFrames) are free and run on your own CPU. The only required cost is Claude Code, which runs the agent. Our runs cost $11–26 each at API prices, or come out of a Claude subscription's usage.

### Can I clone my voice for free, offline?

Yes. Pocket TTS by Kyutai clones an English voice from 20–30 seconds of recording and runs at about real time on 4 CPU cores. IndicF5 by AI4Bharat clones Hindi and 10 other Indian languages from a 6–10 second clip and its transcript, at about 30 times real time. Both are free to download from Hugging Face after accepting their terms, and nothing is uploaded.

### Do I need a GPU to make AI videos with Claude Code?

No. Our studio runs on a 4-core Linux server with no GPU. Voice cloning, captioning and HyperFrames rendering all run on the CPU. A Short renders in a few minutes, and a 7-minute long-form took 19.5 minutes. The Hindi voice is the slowest step, at about 30 minutes for a 60-second Short, which a nightly schedule hides.

### Does the studio post to YouTube, Instagram and X automatically?

The open-source studio delivers an MP4 and a post kit, and does not post by itself. Our own uploader posts the product Shorts to YouTube and X and the blog explainers to YouTube automatically, using the YouTube Data API and the X API. Instagram and Facebook are still posted by hand: Meta's publishing API needs an Instagram professional account, a Meta app with the publishing permission, and each video at a public URL, and we haven't set that up yet.

### Does it still need HeyGen?

No. HeyGen voice cloning was removed on 16 October 2026 and replaced with Pocket TTS and IndicF5. HyperFrames, the renderer, is an open-source project from HeyGen under Apache 2.0, and needs no HeyGen account. An optional HeyGen API key only enables HyperFrames' own music and sound-effect search.

### Which languages can the studio speak?

English through Pocket TTS, which also supports French, German, Spanish, Italian, Portuguese and Dutch. Through IndicF5 it speaks Hindi, Bengali, Gujarati, Marathi, Punjabi, Odia, Assamese, Tamil, Telugu, Kannada and Malayalam. Each language is a voice variant in the studio's config, chosen per video.

### How does the studio choose video topics?

For teaching videos, a problem-scout agent searches Stack Overflow, GitHub issues, Hacker News, Google's "People also ask" and developer forums for problems working developers are asking about. Each problem needs at least two links with their numbers and a score of 9 or more out of 15. The studio refuses to deliver a teaching video without that proof.

### How long does one video take to make?

A product Short takes about 75 minutes from start to delivery, most of it research, review agents and parallel frame building. The render itself takes 2.5–19.5 minutes. The runner caps a Short at 3 hours and a long-form at 7.

## Sources

- [faceless-video-studio](https://github.com/cybiqon-ai/faceless-video-studio) — the studio described here, MIT
- [faceless-video-studio: cloning your voice](https://github.com/cybiqon-ai/faceless-video-studio/blob/main/docs/voice.md) — the voice method and measurements
- [faceless-video-studio: all gates](https://github.com/cybiqon-ai/faceless-video-studio/blob/main/docs/gates.md) — every gate and the failure behind it
- [HyperFrames on GitHub](https://github.com/heygen-com/hyperframes) — HeyGen, Apache 2.0
- [Pocket TTS on GitHub](https://github.com/kyutai-labs/pocket-tts) — Kyutai
- [Pocket TTS on Hugging Face](https://huggingface.co/kyutai/pocket-tts) — model card and licence
- [IndicF5 on GitHub](https://github.com/AI4Bharat/IndicF5) — AI4Bharat
- [IndicF5 on Hugging Face](https://huggingface.co/ai4bharat/IndicF5) — model card
- [faster-whisper](https://github.com/SYSTRAN/faster-whisper) — SYSTRAN, MIT
- [animate](https://github.com/cth9191/animate) — cth9191, MIT
- [Manim Community](https://www.manim.community/) — MIT
- [HyperFrames documentation](https://hyperframes.heygen.com/introduction) — HeyGen
- [YouTube Data API: Videos: insert](https://developers.google.com/youtube/v3/docs/videos/insert) — Google; upload quota and the `status.privacyStatus` note
- [YouTube Data API: Videos resource](https://developers.google.com/youtube/v3/docs/videos) — `status.publishAt` can be set only on private videos
- [YouTube Data API: revision history](https://developers.google.com/youtube/v3/revision_history) — the 2020 private-lock rule and the 2026 quota buckets
- [X API pricing](https://docs.x.com/x-api/getting-started/pricing) — $0.015 per post, $0.200 per post with a URL
- [X API: chunked media upload](https://docs.x.com/x-api/media/quickstart/media-upload-chunked) — X developer documentation
- [Instagram Platform: content publishing](https://developers.facebook.com/docs/instagram-platform/content-publishing) — Meta for Developers
- [Claude Code: run programmatically](https://code.claude.com/docs/en/headless) — Anthropic, `claude -p`
- [Claude Code CLI reference](https://code.claude.com/docs/en/cli-reference) — Anthropic
- [Claude Code plugin commands](https://code.claude.com/docs/en/plugins/cli-reference) — Anthropic
- [FFmpeg loudnorm filter](https://ffmpeg.org/ffmpeg-filters.html#loudnorm) — FFmpeg documentation
- [Spotify: loudness normalisation](https://support.spotify.com/us/artists/article/loudness-normalization/) — the −14 LUFS target
- [EBU R 128: Loudness normalisation](https://tech.ebu.ch/publications/r128) — European Broadcasting Union
- [Twelve videos in four days. Nobody picked up a camera.](/lab/hyperframes-heygen-ai-video-pipeline) — the first version of this studio
- [We built the game in six days. The store review took fifteen.](/lab/shipped-the-game-missed-the-deadline) — one of the games whose real footage appears in the product Shorts
- [Eight bots played all 500 levels](/lab/eight-bots-played-all-500-levels) — the solver behind the "solver-proven" numbers in the game Shorts
