Skip to main content
Read this before you set up a machine, a container or a CI runner, and before your first voiced build. It answers two questions: what has to be on the machine, and what a run costs.

Platforms

DeckTalk runs on Linux, macOS and Windows, and records with headless Chromium on all three. On a musl system such as Alpine the installer stops before it installs anything and says why, rather than leaving a page of resolver output to read. Build in a container from a Debian-based image, such as debian, ubuntu or python:3.13-slim, and never from python:3.13-alpine. Debian is also where Chromium’s system libraries can be installed, because Playwright installs them with apt-get.

Python

DeckTalk needs Python 3.12 or later. The one-line installer never asks you to supply one. It installs uv if the machine has none and then runs uv tool install decktalk, and uv fetches a Python of its own. uv tool install decktalk does the same thing without the script. pipx install decktalk uses a Python that is already on the machine, so that one is where the floor bites. Seven packages come with DeckTalk: blake3, filelock, Playwright, Pydantic, Rich, tomlkit and Typer. Playwright ships its own browser driver, so Node is not needed. KaTeX is not a dependency at all: the pinned release ships inside the wheel, and DeckTalk serves it at /__decktalk/katex/ on the page’s own origin, so a project typesets its equations with no network and no content delivery network.

What DeckTalk downloads

Two tools, one time per machine, fetched by DeckTalk itself: headless Chromium to record the slides, and ffmpeg with ffprobe to cut, mix and encode them. The first command that opens a page fetches Chromium, and the first command that encodes fetches ffmpeg. Each download is announced before it starts and reports its bytes as they arrive, and under --events it is one tool.fetch line per step on stderr. decktalk doctor reports on both and fetches neither unless it is on a terminal, where it offers once to fetch what is absent, or --fix is passed, which runs decktalk install to fetch it under any flag. DeckTalk pins no ffmpeg build for every platform. Where it pins none, it uses the ffmpeg and ffprobe on PATH, and it refuses with a TOOL error naming both when there is no pinned build and nothing on PATH either.

Roughly how large

The browser is the large one. Playwright 1.63 fetches it in three parts, the browser, its headless shell and a small ffmpeg of its own, and its installer states each part’s size on the first tenth it reports, which is the size DeckTalk’s progress lines carry. On macOS on Apple silicon the parts are 190.9, 98.9 and 1.0 MB, 291 MB in all. On Linux x86-64 and on Windows they come to about 318 and 327 MB, which are the sizes of the same three archives on Playwright’s host rather than a measured install. The pinned ffmpeg arrives as two archives of 28.2 and 28.1 MB on macOS on Apple silicon, and as one archive of 125.8 MB on Linux x86-64 and 168.3 MB on Windows, so a first run fetches about 347 MB on macOS on Apple silicon, about 445 MB on Linux x86-64 and about 495 MB on Windows. In a pipe each download prints a line as it starts, a line as it ends, and lines between as it arrives. For ffmpeg those come every ten seconds whatever the speed of the link, while bytes keep arriving. Chromium’s come from Playwright’s installer, which owns that download and reports each part at every tenth, so on a slow link they can be further apart. An ffmpeg archive’s start line carries its size when the host states one. A part of Chromium starts by name alone, and its size arrives on its first progress line. An ffmpeg download that stops partway, because the link dropped or the host sent nothing for a minute, keeps what arrived beside the archive’s name in the cache, and the next run asks the host for the rest, starting again only when the host cannot send part of a file. The pinned SHA-256 is checked on the whole archive either way. Budget a gigabyte of cache for a machine that records, and a few minutes on a home connection for the first run.

Where they land

Both land in one directory, Chromium in its ms-playwright folder and ffmpeg in its ffmpeg folder. It is per user, so a second project on the same machine downloads nothing. tools.cache_dir moves the whole directory, browser and encoder together, and decktalk doctor --json names it as cache. Anyone may empty it. The take store, a second copy of every take this machine buys, is kept apart from it in the per-user data folder, which a cleaner leaves alone and a backup includes. narration.store_dir moves it. Both keys take an absolute path or one that starts with ~, and a relative one is refused. DeckTalk tells Playwright where the browser folder is on the download and on every launch, so a PLAYWRIGHT_BROWSERS_PATH set in the shell is not used: set tools.cache_dir instead.

What decktalk install is for

decktalk install makes both downloads up front and does nothing else. A second run names what was already there and fetches nothing.
No command needs it to have been run. It is worth running when you want the download to happen at a time you choose:
  • A container image. A RUN decktalk install layer is cached, so the download is not in every build.
  • A CI job. One step to cache, and one place to read when a download fails.
  • A machine that will be offline. Fetch everything while there is a network. After that, a build that does not spend needs no network at all.
  • Linux. It is the one command that installs Chromium’s system libraries, and therefore the one command that may ask for a password.

The sudo step on Linux

On Linux, decktalk install passes --with-deps to Playwright, and Playwright installs Chromium’s system libraries with apt-get. It runs apt-get through sudo, or through su root -c when sudo is missing, or directly when install is already running as root. If the distribution has no apt-get, that step fails and install stops before it fetches ffmpeg. Nothing else passes --with-deps. A build fetches Chromium without it, and therefore without sudo, because a build runs unattended in a script or in CI, where a password prompt is a hang rather than a question. On a Linux machine whose libraries are missing, the browser downloads and then will not start, and the error says to run decktalk install.

A machine that supplies its own tools

A machine with no admin rights, an index mirror or an intercepting proxy can still record. Each case is one setting, and none of them changes the project. The [tools] keys are machine keys. They describe this machine, so they go in its own settings file through decktalk config set KEY VALUE --scope machine, and a decktalk.toml that sets one is refused by name. Every setting has an environment variable of its own, spelled DECKTALK_<TABLE>_<KEY>, so [tools] ffmpeg is DECKTALK_TOOLS_FFMPEG and [tools] cache_dir is DECKTALK_TOOLS_CACHE_DIR. decktalk config explain tools.ffmpeg prints the value that wins and the layer that set it, and its --json lists every layer that sets the key. If the machine already has an approved Chrome or Chromium, record with that one instead.
The pinned Playwright build is the reference build, and DeckTalk’s timing limits are measured against it. A browser of your own is supported and unverified, so measure a first film with decktalk verify before you trust those limits on it. decktalk doctor prints the Chromium revision and the ffmpeg build a run would use, so a machine’s owner can read what a recording will launch before it launches. What leaves your machine is the whole list of hosts DeckTalk contacts, and Size the machine lists the machine keys that say how much work a machine takes on at once and how long it waits.

Pages and programs

A deck is a web page and a render section is a program, and both run with your rights when you build. Pages and programs you did not write says how far each is trusted, and how to build a project someone else wrote.

What a voiced run needs

A build that does not spend needs no account, no API key and no network once the tools are on the machine. A build voiced by ElevenLabs, the cloud voice, needs two values from ElevenLabs. The local voice, dtsp, needs only the voice’s [voices.NAME] id and decktalk-voice, a separate speech server running on this machine, and no key. That server is in development and not yet public, so today every voiced build uses ElevenLabs. The same server times the words of a take whose voice sent none. The [dtsp] keys say where it listens. Put the key in .env beside decktalk.toml, or export it. A variable already in the environment wins over the file, and a value that still starts with <, such as <your-elevenlabs-key>, counts as unset, so .env.example as it ships approves nothing. A voice’s id is [voices.NAME] id in decktalk.toml, and DECKTALK_VOICES_<NAME>_ID, such as DECKTALK_VOICES_NARRATOR_ID, in the environment overrides it for anyone who keeps it out of the file. A voice’s id is committed with the project because it grants nothing: a voice cloned on an ElevenLabs account is usable only with that account’s key. A film may mix voices and providers, so decktalk check prices each provider’s turns on a row of its own. DeckTalk never prints the key. No value read from .env or from the environment reaches a --json payload, a log line or an error message, and an error that quotes a reply from the service has every credential in it replaced before the quote is cut to length. decktalk doctor reports whether the key is set and never what it is.

What spends and what does not

Two things call ElevenLabs. The narrate stage sends each turn’s text and gets speech back, and decktalk score generates the music, the ambience bed and the effects. No library or command call spends without approval. On the command line the flags are --no-spend, which plays every take on disk, writes placeholder narration where a take is missing and buys nothing, --spend, which approves the spend, and --max-cost N, which puts a ceiling on a spend something else approved and approves nothing itself. A free provider such as dtsp makes its takes under either flag once a voice is named, because it costs nothing. With neither flag, a build with something to buy prices the run and asks on a terminal, once, about its takes and its sounds added together. Without a terminal it refuses and names the two flags that answer, so a script or a CI job cannot spend by accident. A build whose takes are all on disk buys nothing, so it asks nothing and reads no key, and a provider that declares itself free, such as dtsp, is never asked about, because it costs nothing. Everything else spends nothing: init, install, doctor, status, check, words, storyboard, serve, config, schema, cue, record, assemble, verify, clip, and any build with --no-spend. A voiced turn costs the characters of its whole text, so an edit costs every changed turn in full rather than the changed sentence. A section read by one voice is one turn. The starter’s three sections are 476 characters and make 50 seconds of film, which is the order of magnitude to expect: a script is cheap and a long film is a few thousand characters. decktalk check --json gives the exact figure for your own script. A take is named by the digest of its text and its voice settings, so an unchanged turn is read from the cache and a new voice id voices every turn that voice reads again.
--replace-voiced with --spend voices a section again and plays the new take in place of the one you already have, and --replace-score with --spend buys every bought sound again. They are the two flags on build that buy what you already own.

How the price is worked out

One key sets the rate, and one command reports the price.
decktalk.toml
ElevenLabs declares that it bills per character, so elevenlabs.dollars_per_1000_characters is what your plan charges per thousand characters. Its default is 0.0, because DeckTalk does not know your plan and will not invent it. Each voice declares how it bills, per character, per second of audio, or free, and the key its rate is set under, and the price is worked out the way it declares. decktalk check prices the narration of a voiced run without calling anything, and its result carries a Cost. It leaves the score out, which decktalk score --no-spend prices. A build asks about both at once: the price it shows before it buys is its takes and its sounds added together, the same total its result reports.
  • state is estimate for what a run would cost and charged for what one did cost.
  • sections are the sections the price covers, in script order.
  • characters is what would be sent, and seconds is how long the takes are expected to run.
  • billing is how the provider declares it bills: per_character, per_second, free, or undeclared for a voice or a sound provider a host registered itself. A build’s total is mixed when it adds characters of speech to seconds of sound, and then carries both rates.
  • dollars is the characters or the seconds the bill counts, at the stated rate, rounded up to the cent.
  • ceiling_dollars is the most the run can reach, rounded up to the cent for each stage.
  • dollars_per_1000_characters or dollars_per_minute is the rate in US dollars, whichever the bill is counted in.
  • price_key is the key that states the rate, or null for a voice with no rate to state.
  • averaged is true when the price is several rates together, such as a score’s music and effects, so the rate is their average and price_key names the one least surely stated.
  • price_layer names the settings layer that set the rate, where default means nobody did.
  • stages holds one row per stage that buys, narration then score, each with the same fields, its own state and the stage it prices. A stage is charged when the run had leave to buy from it, and a charged cost counts only its charged rows, so a stage that only priced what it would buy is never shown as spent. A total adds only the amounts each row is billed on.
dollars and ceiling_dollars differ when a section’s take cannot be matched. A take is named by its text and its voice, so with no voice id named a voiced take of a turn’s current text cannot be matched to the voice that made it: each such turn’s characters go into the ceiling and none of them into the price. A section whose text changed, or whose take is gone, needs a take whatever the voice is, so its characters go into the price. The key is not needed to price a run, because the price never asks the voice anything. Every surface that states a price states one sentence, which keeps the certain part and the ceiling apart. A narrate stopped or refused once its takes are under way, and a score or build stopped or refused after it paid for something, still report their cost: the refusal carries the run’s result, with what it spent. The take.charged and sound.charged lines it wrote are the record of each payment.

The ceiling

--max-cost N is compared against ceiling_dollars and never against dollars, because a provider charges one request at a time and a cap that claimed to stop a run halfway would be a lie. It is checked before the first paid call, against the whole run: a build adds its takes and its sounds together first, so a run it refuses has bought nothing. Each request that buys is checked again as it is made, against what the run already approved, so a stage that asks for more than the build was priced at is still refused, and the refusal says that what the run bought before it is kept.
--max-cost also refuses while the price is still the default, before it compares anything.
A default is an assumption rather than what your plan charges, and a cap worked out from an assumption would let a run through at a price nobody stated. State the rate and the cap means something. Error codes covers every refusal of this kind.

ElevenLabs plans

Every ElevenLabs plan can call the API, including the free one, so DeckTalk works on any of them. What differs is what you may do with the audio: ElevenLabs’ free tier carries no commercial license and attaches attribution and voice-library conditions to anything you publish. Those are their terms rather than DeckTalk’s, so read the plan page before you publish a film, and start on a paid plan if you intend to.

Time

Recording steps each section one frame at a time, and each frame the page changed on costs a capture and an encode, so a section takes as long to record as its changing frames take to draw and a heavy page records slower than it plays. A frame nothing changed on is written again without a capture, which is most frames of a narrated slide, and a stretch of them is written again without stepping the page to each. Frame k is the page at second k / fps however the page reached it, so record splits each long section into chunks and runs as many recorders at once as record.concurrency allows, each recording one chunk after another, longest first. Each chunk is a lossless file of its own, and the chunks of a section are joined into its recording without an encode, so a film records in about its frames divided by the recorders rather than in the time of its longest section, and most of a build is the recording. How finely a section is split weighs what a chunk costs to start, a page loaded in a recorder’s Chromium and stepped to its first frame, against what one step costs, which are this machine’s record.chunk_startup_seconds and record.step_seconds. A section’s work is the steps its last recording took, or one a frame when it has none, and it is split into about sqrt(step_seconds * steps * recorders / chunk_startup_seconds) chunks, never more than the recorders, and never into chunks shorter than record.chunk_frames_min frames, so a short section is recorded whole. Build times gives the starter’s build cold, kept and after one changed word, measured on a named machine. Rebuild one section shows how to record only what changed.

The ffmpeg license

The ffmpeg builds DeckTalk downloads are GPL builds. DeckTalk runs ffmpeg as a separate process and does not link against it. THIRD_PARTY_NOTICES.md names each build and its license. To use a build of your own, set [tools] ffmpeg and [tools] ffprobe to its two executables.

Next