Skip to content

How to Make YouTube Shorts with Claude Code (Free Guide)

Updated 2026-11-02

Claude Code can write and render a Short for you with no camera, no editor and no cost per video. You need a paid Claude plan, Node.js, FFmpeg and Python with uv (for the free voice). Below is the exact recipe we used for the Short on this page: script, free local voice, word timings, then a render. It suits text-led Shorts; it won't film anything or post for you.

The result of this tutorial: 21.7 seconds, 1080x1920, with sound. The voice came from a free model on our computer. Rendered in about 65 seconds on 4 CPU cores.

Glidezo isn’t connected to Anthropic, YouTube or HeyGen. Tested on: Linux, 4 CPU cores, 15 GB of memory, no GPU, Node.js 24, on 2026-11-02. Claude Code ran these steps in a clean folder, and the video above is the result. The times below are what we measured, not counting first-time downloads.

What you will need to download once: Chrome for HyperFrames (261 MB), the voice model (338 MB) and the speech model (640 MB), all on your computer.

The short way: ask Claude Code

Make an empty folder, open Claude Code in it, and paste this:

Follow glidezo.com/learn/make-youtube-shorts-with-claude-code to make a 20-second YouTube Short about [your topic]. Write the script first and show it to me. Use HyperFrames 0.8.223 and the local voice. Show me snapshots before the full render.

We didn’t test that one prompt from start to finish: we ran the steps below. If something fails, the steps are what to compare against.

1. Write the script

Save this as script.txt. Ours is 58 words, which came out at 21 seconds:

This Short has no camera, no timeline and no editor. Claude Code wrote it as a web page. A free voice model read this script on my computer. Another model timed every word, so the captions land on the beat. Then HyperFrames turned the page into a video, one frame at a time. Change a word, render again.

2. Make the voice

The voice model runs on your computer and needs a small Python environment. We used uv for that.

uv venv .venv
uv pip install --python .venv/bin/python kokoro-onnx soundfile

# Workaround we needed on Linux: without it the voice stops with "phontab: No such file"
mkdir -p espeak
ln -sf "$PWD"/.venv/lib/python*/site-packages/espeakng_loader/espeak-ng-data/* espeak/

export HYPERFRAMES_PYTHON="$PWD/.venv/bin/python" ESPEAK_DATA_PATH="$PWD/espeak"
npx --yes hyperframes@0.8.223 tts script.txt --voice am_michael --output voice.wav

9 seconds. You now have voice.wav. It is clear, but you can hear that it’s synthetic. A recording of your own voice works in the same place.

3. Time every word

npx --yes hyperframes@0.8.223 models install parakeet
npx --yes hyperframes@0.8.223 transcribe voice.wav --dir .

15 seconds. This writes transcript.json, with a start and end time per word.

Read it. Ours heard “HyperFrames turn”, where the script says “turned”, and a lowercase “short”. We fixed those by hand in the file. Claude Code can compare it with script.txt for you.

4. Build the page

A Short here is one web page. Save this as build.mjs and run node build.mjs. It makes one heading per sentence, a progress bar, and captions that light up word by word. Change HEADINGS to match your sentences: it stops with an error if the counts differ.

build.mjs (94 lines)
// Turns voice.wav + transcript.json into index.html: a 1080x1920 HyperFrames page.
// One heading per sentence, plus captions that light up word by word.
import { readFileSync, writeFileSync } from "node:fs";

const WORDS_PER_LINE = 3;
const ACCENT = "#facc15";
const COLOURS = ["#1e1b4b", "#0c4a6e", "#134e4a", "#4a044e", "#7c2d12", "#1e1b4b"];
const HEADINGS = [
  "No camera.<br>No timeline.<br>No editor.",
  "It's a web page.",
  "The voice runs on my computer.",
  "Every word is timed.",
  "One frame at a time.",
  "Change a word.<br>Render again.",
];

const escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const words = JSON.parse(readFileSync("transcript.json", "utf8"));
const duration = Math.ceil((words.at(-1).end + 0.6) * 10) / 10;

// A new heading starts on the word after each sentence ends.
const starts = [0];
words.forEach((word, i) => {
  if (/[.?!]$/.test(word.text) && i + 1 < words.length) starts.push(words[i + 1].start);
});
if (starts.length !== HEADINGS.length) throw new Error(`${starts.length} sentences, ${HEADINGS.length} headings`);

// A caption line ends at punctuation or WORDS_PER_LINE words.
const lines = [[]];
for (const word of words) {
  const line = lines.at(-1);
  line.push(word);
  if (line.length === WORDS_PER_LINE || /[.,?!]$/.test(word.text)) lines.push([]);
}
if (lines.at(-1).length === 0) lines.pop();

const headingHtml = HEADINGS.map((text, i) => `<h1 id="h${i}">${text}</h1>`).join("\n");
const lineHtml = lines
  .map((line, l) => `<p id="l${l}">${line.map((w, i) => `<span id="l${l}w${i}">${escapeHtml(w.text.replace(/[.,?!]$/, ""))}</span>`).join(" ")}</p>`)
  .join("\n");

const script = [`tl.fromTo("#bar", { width: 0 }, { width: 1080, duration: ${duration}, ease: "none" }, 0);`];
HEADINGS.forEach((_, i) => {
  const end = starts[i + 1] ?? duration;
  script.push(`tl.to("#root", { backgroundColor: "${COLOURS[i % COLOURS.length]}", duration: 0.4 }, ${starts[i]});`);
  script.push(`tl.fromTo("#h${i}", { opacity: 0, y: 70 }, { opacity: 1, y: 0, duration: 0.45, ease: "power3.out" }, ${starts[i]});`);
  script.push(`tl.set("#h${i}", { opacity: 0 }, ${end});`);
});
lines.forEach((line, l) => {
  const next = lines[l + 1]?.[0].start ?? duration;
  script.push(`tl.set("#l${l}", { opacity: 1 }, ${line[0].start});`);
  script.push(`tl.set("#l${l}", { opacity: 0 }, ${next});`);
  line.forEach((word, i) => {
    script.push(`tl.fromTo("#l${l}w${i}", { color: "#ffffff", scale: 1 }, { color: "${ACCENT}", scale: 1.12, duration: 0.08 }, ${word.start});`);
    script.push(`tl.to("#l${l}w${i}", { color: "#ffffff", scale: 1, duration: 0.08 }, ${word.end});`);
  });
});

writeFileSync("index.html", `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1080, height=1920">
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 1080px; height: 1920px; overflow: hidden; }
#root { position: relative; width: 100%; height: 100%; background: ${COLOURS[0]}; font-family: Arial, Helvetica, sans-serif; color: #fff; }
#bar { position: absolute; left: 0; top: 0; height: 18px; width: 0; background: ${ACCENT}; }
h1 { position: absolute; left: 90px; right: 90px; top: 400px; font-size: 118px; line-height: 1.08; opacity: 0; }
p { position: absolute; left: 60px; right: 60px; top: 1160px; text-align: center; font-size: 72px; font-weight: 800; opacity: 0; }
span { display: inline-block; margin: 0 10px; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-duration="${duration}" data-width="1080" data-height="1920" data-fps="30">
<audio id="voice" src="voice.wav" data-start="0" data-duration="${duration}" data-track-index="1" data-volume="1"></audio>
<div id="all" class="clip" data-start="0" data-duration="${duration}" data-track-index="0">
<div id="bar"></div>
${headingHtml}
${lineHtml}
</div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
${script.join("\n")}
window.__timelines = window.__timelines || {};
window.__timelines["main"] = tl;
tl.seek(0);
</script>
</body>
</html>
`);
console.log(`index.html: ${duration} s, ${lines.length} caption lines`);

5. Look before you render

npx --yes hyperframes@0.8.223 check
npx --yes hyperframes@0.8.223 snapshot --at 1,8,15,20

15 seconds for check. Ours passed with warnings that some caption lines are on screen briefly (line_unread). For captions that follow speech, that is simply the pace of the voice. snapshot saves still frames, and it prints where.

6. Render

npx --yes hyperframes@0.8.223 render . --crf 23 --output short.mp4

65 to 67 seconds, for 21.7 seconds of video. The file came out at 1.5 MB. For this page we also re-packed the audio to 96 kbps with FFmpeg, which made it 1.3 MB. You don’t need to for YouTube.

7. Upload it yourself

Upload short.mp4 at YouTube Studio. YouTube treats a vertical video of up to three minutes as a Short.

What this can’t do

  • It won’t film anything. There’s no footage or real-looking people here, only what you can draw with a web page. To add a real clip, put it behind the text as a <video> layer, or use a video model as covered in can Claude make videos?
  • There is no music. We didn’t add any. If you do, use a track you have the rights to.
  • It won’t post or schedule.
  • The look is simple. It’s a starting point, not a style. Ask Claude Code to change it, then look at the snapshots.
  • It isn’t one click. Python, Node.js and FFmpeg all have to work on your computer, and we hit one bug on the way (step 2).
  • Nothing here runs on Glidezo online. You run it all on your own computer.

Other ways to do it

Questions

Is this free?
The tools are. HyperFrames, the voice model and the speech model are free and run on your computer, so there is no cost per video. Claude Code itself needs a paid Claude plan: Pro is $20 a month, or $17 a month billed yearly, read on 2026-11-02.
Can I do this without Claude Code?
Yes. Every step above is a command or a file you can run yourself. Claude Code saves you the typing and can change the look when you ask.
Can Claude post the Short for me?
This recipe doesn't. You upload the MP4 yourself. Tools for posting from Claude exist, but we haven't tried them, and we'd read each one's terms and YouTube's rules first.
How long can a Short be?
YouTube treats a video as a Short if it has a square or vertical aspect ratio and is up to three minutes long, for uploads on or after 15 October 2024.
Can I use my own voice?
Yes, and it will sound better. Save your recording as voice.wav and skip step 2. The transcribe step works on your recording in the same way.
Does it work on Windows or Mac?
We only ran this on Linux. HyperFrames lists Node.js 22+ and FFmpeg as its needs, so it should carry over, but the commands for the Python environment and the voice workaround will differ, and we haven't tested them.

Sources