Meefik's Blog

Freedom and Open Source

talkdeck: AI-made narrated decks

06 Nov 2026 | javascript

I wanted to hand a topic to an AI agent and get back a video presentation — narrated, with a talking head in the corner — not a PDF I then have to record and present myself. The static slide is the well-solved, commodity half: Marp turns Markdown into a clean HTML deck and a PDF. What’s missing is the narration and the face, and the part that has to be scriptable if an agent is going to produce it at all.

So I wrote talkdeck: a small Node CLI on top of Marp Core that turns a Markdown deck into a 1080p video with a voice-over and a lip-synced talking head. The point is that the whole thing is drivable from the command line — no UI, no manual steps — because the goal is that an agent writes the file and runs the command.

The demo above was made that way: an agent wrote the slides.md and ran one command. The voice is a clone of a fifteen-second sample, and the face in the corner was lip-synced to the same audio.

Why no UI

For an agent to produce a deck, the pipeline has to be a function, not a click-through. So everything is a command: talkdeck make my-talk --video builds the video, and the standalone HTML player (with a presenter mode) and the PDF are the Marp-native byproducts you get for free.

The voice and the avatar are small, well-defined HTTP services — the voice is an OpenAI-compatible /audio/speech endpoint, the avatar is a simple async task API — so the same agent that writes the slides can also drive the models. There’s a SKILL.md in the repo with the instructions, and no bespoke API to learn.

The notes are the script

The speaker notes are plain HTML comments at the bottom of a slide, one sentence per line:

## Slide title

visible content

<!-- First sentence of the narration. -->
<!-- Second sentence. -->

They show in the presenter view (press P in the HTML player: current and next slide, the notes, a timer, zoom). And they are the script the text-to-speech engine reads for --video. One source of truth — the narration lives where the notes live, so an agent writing the deck writes the script in the same file. A slide without notes is silent: it’s shown for a few seconds and gets no avatar.

The voice

The default engine is Qwen3-TTS, running locally. It has no fixed voice list — a voice is a sample that gets cloned: a 10–15 second recording of one clean speaker plus the transcript. The template ships two, maria and leon; you can make your own from a description, once:

talkdeck voice anna ./my-talk --describe "young, upbeat voice"

Every slide is then spoken by a clone of the same sample, so the voice stays identical across the deck. The voice is a per-deck setting in the slides.md header — one voice and one face for the whole deck, so a deck builds the same way for everyone.

The face

A portrait in avatars/ plus the slide’s voice-over goes to a talking-head service (Ditto, also local), and the lip-synced head comes back composited as a circle with a border in a corner of that slide. Position, size and border are header settings; dark slides get the inverse border color automatically.

The honest numbers, measured on a Radeon RX 7600 XT (16 GB): the voice-over is the cheap step, about 2× realtime; the talking head is the bottleneck, about 5× realtime and one job at a time. So a 60-second slide takes roughly 2 minutes to narrate and 5–6 minutes for the head. Both models fit on the 16 GB card together (the voice ~9 GB, the head ~4 GB at peak), and the first request after an idle gap cold-starts the model. A full deck takes a while — this is a batch pipeline, not a real-time thing. While you iterate on the narration, build single slides (--slides 3) and it’s fast enough.

It runs on your machine

Both services are small containers: one docker-compose.yml per GPU vendor (CUDA and ROCm), models download into named volumes on first start. The deck file carries the content and the voice/avatar names; the addresses live in the environment (TALKDECK_TTS_URL, TALKDECK_AVATAR_URL), because those depend on the machine. Your slides and your voice samples never leave the box.

Caching

Generated files are kept in the output directory with a manifest (.cache.json) of what they were made from. Edit one slide’s notes and only that slide gets re-synthesized — the voice-over, the head and the encoded video segment for the other slides are reused as-is. Each slide encodes into its own cached segment and the video is joined by concat, without re-encoding.

Wrap up

The goal was a video presentation an AI agent can make: a scriptable pipeline, one file as the source of truth, and local models for the voice and the face. If you want a narrated, talking-head deck that doesn’t send your content to a cloud:

Stack: Node, Marp Core, Qwen3-TTS, Ditto, ffmpeg

Comments