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 runsuv 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 itsms-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.
- A container image. A
RUN decktalk installlayer 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.
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. Thenarrate 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.
How the price is worked out
One key sets the rate, and one command reports the price.decktalk.toml
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.
stateisestimatefor what a run would cost andchargedfor what one did cost.sectionsare the sections the price covers, in script order.charactersis what would be sent, andsecondsis how long the takes are expected to run.billingis how the provider declares it bills:per_character,per_second,free, orundeclaredfor a voice or a sound provider a host registered itself. A build’s total ismixedwhen it adds characters of speech to seconds of sound, and then carries both rates.dollarsis the characters or the seconds the bill counts, at the stated rate, rounded up to the cent.ceiling_dollarsis the most the run can reach, rounded up to the cent for each stage.dollars_per_1000_charactersordollars_per_minuteis the rate in US dollars, whichever the bill is counted in.price_keyis the key that states the rate, or null for a voice with no rate to state.averagedis true when the price is several rates together, such as a score’s music and effects, so the rate is their average andprice_keynames the one least surely stated.price_layernames the settings layer that set the rate, wheredefaultmeans nobody did.stagesholds one row per stage that buys, narration then score, each with the same fields, its ownstateand thestageit prices. A stage ischargedwhen 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.
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. Framek 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
- Install and build a first film: Quickstart
- Build in CI or with no network: Build in CI and offline
- Read every setting: Configuration