Fetch the toolchain up front
- Chromium, ffmpeg and ffprobe all go into DeckTalk’s cache, so a job caches one directory.
Requirements lists where it is on all three platforms, and
tools.cache_dirmoves it. - On Linux,
installis also what installs Chromium’s system libraries, which needs root. A build never installs them. On a terminal it asks before it does, and without a terminal it goes ahead, because a job that raninstallasked for exactly this. decktalk doctorreports what this machine holds and what a run would use. Without a terminal, or under--jsonor--no-input, it fetches nothing unless--fixis passed, and on a terminal otherwise it offers once to fetch what is missing. Itscachefield is the directory to cache, read from the machine rather than guessed.- Each download prints a line as it starts, progress lines as it arrives, with its size once the
host states it, and a
Fetchedline with what arrived, so a job log shows a fetch is still moving. Roughly how large has the sizes.
install, DeckTalk itself reaches the network only to buy speech or sound, and a build with
--no-spend never does. A free provider such as dtsp is still called under --no-spend once a voice is named, on the
address the machine names, a loopback address by default, because spend gates money and nothing else. A page that loads a font or a library from another host still reaches for
it, as What leaves your machine explains.
Never prompt
--no-input is on every command. Under it a run never asks a person anything. Each prompt either
takes the safe default or refuses and names the flag that would have answered, because a prompt an
agent cannot answer and a flag that does not exist are the same failure.
The refusal is the whole of what a caller needs.
--json also turns every prompt off, so a caller that reads the output never blocks on a question it
cannot see.
Read the run as data
Two flags make a run machine-readable, and they answer different questions.--json is the answer. The command prints exactly one flat object on stdout, prints nothing else
there, turns colour off and sends every log line to stderr. Four keys are on every result, which are
schema, ok, findings and error, and each command adds its own fields beside them. There is no
wrapper object and no nesting. The JSON contract is the whole of it.
--events is the account of the run while it happens. One JSON object per line goes to stderr as
each event occurs, so --json and --events together are the answer on stdout and the progress on
stderr with neither in the other’s way.
event is the discriminator. A stage.done and a section.done each carry an outcome of ran,
kept, skipped, stopped or failed, so a job can tell work that was done from work that was
kept, and a failed run’s run.done carries its error. The event stream
lists every event and its fields.
Every run appends the same lines to build/events/<run>.jsonl whether --events was passed or not,
and the first line names that file. So a job that backgrounds a build can follow the file, and
decktalk status --json lists any run whose file is still open under runs.
[events] keep_runs prunes the oldest, and keeps twenty runs by default.
The exit codes a job gates on
Every command shares one exit rule, so a pipeline asserts on the code rather than on the output. Exit codes is the table, and the rule a job follows is one line: a non-zero exit witherror null is a finding the job
may fix, and a non-null error means DeckTalk could not run or did not finish. When the object beside
it is the command’s own result, its cost is what the stopped run spent.
A build that stops on a finding follows the same rule. It exits 1 with error null, and its result
carries stopped_at, the stage it stopped after, beside its findings and the spend so far. A job
reads stopped_at to learn that the build stopped short. film is null when it stopped before
assemble, and names the film when assemble itself stopped it, because that film was made and then
never measured.
Choose what fails the pipeline
--fail-on names the least severe finding that fails a run, and --allow CODE carries on past one
code, which is still reported and still in the JSON.
Which findings fail a run is the table of both.
decktalk schema finding prints every code, and each one has a page under
the finding codes saying what moves it.
A run that uses --fail-on never still exits 3 on an error, because an error is not a finding and no
threshold covers it.
Judge the files with no browser
check judges script.md, cues.json and the pages, writes nothing a stage reads, and prices the
narration a voiced build would buy. --no-pages drops the browser entirely and says which judgements it could not
reach, so a docs-only job, a pre-commit hook and a container with no Chromium can all still run it.
--no-pages cannot be mistaken for
the full check. --no-frames is the middle setting: it keeps the browser and the page’s own catalog
and drops the freeze comparison alone.
check --json carries cost, so one call is also how an agent prices the narration before
approving it. The score is priced apart, by decktalk score --no-spend --json.
Say where the project is
Every command takes-p DIR, and DECKTALK_PROJECT sets the default, so a job can run from the
repository root without repeating the path.
DECKTALK_<TABLE>_<KEY>, such
as DECKTALK_VIDEO_PRESET. It sits above decktalk.toml and below --set.
decktalk config explain KEY prints the value in force with the layer that set it, beside the key’s
default, its safe range and the hazard of leaving that range, which is what to run when a job and a
laptop disagree.
Size the machine
The keys below say how much work this machine takes on at once and how long it waits, which are the keys to change on a runner with fewer CPUs or less memory than a laptop. They describe the machine rather than the film, so they are machine keys.
A recording waits for every frame it steps, so a frame drawn slowly is late on the clock of the
run and never on the clock of the film.
record.concurrency set above this machine’s share therefore
costs speed and never correctness: a starved machine records slower, and every reveal still lands on
its word.
A page that stops answering is held to its deadlines instead. A call into it that outlives
record.page_deadline_seconds, such as a step of a page that spins its main thread, closes the page
and stops the run with a TOOL error naming the section and the frame, and a chunk or a run that takes
record.deadline_slowdown times its expected time is stopped the same way. check, storyboard,
cue and assemble hold every call into the pages they open to the same deadline, so no page can
hold a build open.
decktalk.toml that sets any of them is refused by name, because a project travels and a
limit that suits one machine is wrong on the next:
DECKTALK_RECORD_CONCURRENCY, which is how a job
sets one for a container it does not keep.
A GitHub Actions job
.github/workflows/video.yml
main and on every pull request, and its token can read the
repository and nothing more. Each action is pinned to a commit, with its version in the comment
beside it.
DECKTALK_VERSION pins DeckTalk, and the tool cache is keyed on it as well as on the runner. Each
DeckTalk version pins its own ffmpeg build and installs with the Playwright release that chooses its
Chromium, so a new version starts from an empty cache and fetches the builds it wants, rather than
restoring the ones the last version used. decktalk install fetches anything the restored cache
lacks.
DECKTALK_PROJECT names the project’s directory, so every command finds it from the repository
root and the upload reads the film from the project’s own build/final/. The reports are written to
reports/ at the root, outside the project. The second upload is the film alone with archive
set to false, so it is a single file that plays in the browser, and it takes its name from that file.
decktalk build runs all six stages, and verify is the last of them, so the film is measured inside
the same run that made it and its findings already decide the build’s exit code. A job does not need
a second command to gate on the film.
--no-spend plays every take the repository holds and a placeholder for each turn whose take a
provider that bills would sell, so the measurement covers the recording, the encode and the mix and not only the
cue times, on a job that spends nothing and holds no key. A job whose voice is the free dtsp and
that runs decktalk-voice beside it gets every missing take voiced under the same flag.
A project with a render section names a program, and the runner refuses it as any machine does by
default: record.programs is refuse, and ask refuses too without a terminal, so the build is
refused before its first stage with INPUT. A job whose project’s programs you have read
allows them with DECKTALK_RECORD_PROGRAMS: run in the build step’s env. Never set it on a job that
builds a fork’s pull request, because the program runs with the job’s rights.
Programs a project names says why.
Build with the takes you committed
A take is found by its content, so a repository that commits its takes builds its real film on a runner with no API key. Rundecktalk narrate --spend once on your own machine and commit takes/.
It holds the takes and their words files, and in takes/aligned/ the words an aligner measured in a
take whose voice sent none, so a job with no aligner plays them as they were timed. A placeholder,
the take index and the joined narration stay under build/, so a job that plays them changes no
tracked file. A project
with a score does the same with decktalk score --spend and score/, which holds the bought sounds
and their ledger, while the music joined from its parts stays under build/score/.
mp3_44100_128 is about 1 MB, and its words file about 11 KB, so a
10 minute film’s takes come to about 10 MB, and each change of voice adds that again because the old
takes stay until you remove them. A repository reaches GitHub’s advised 1 GB only after about a hundred
such revoicings. If you would rather keep the audio out of the repository’s history, Git LFS takes it
with one line in .gitattributes, and the checkout step then needs lfs: true:
.gitattributes
DECKTALK_VOICES_NARRATOR_ID instead, and still leaves the key out.
A run with --no-spend plays every take on disk and never builds a voice that bills, so it reads no
key. Each turn whose take is missing plays a placeholder and is reported as
TAKE_MISSING, once per turn, which is once per section when one
voice reads it, with the command that buys its take. A free provider is the exception: once a voice
is named it makes the missing takes, and when its server is not running each turn plays a placeholder whose finding says to start it rather than to buy anything. That finding is a warning, because the film is still a whole film with estimated timing in that
section, so the default --fail-on error builds it and exits 0 and the finding sits in
build.json for the job summary. A job that must fail on a missing take passes --fail-on warning, and
--allow carries on past any other warning it accepts.
A pull request from a fork runs the fork’s page on your runner. Under the default
record.page_policy that page reaches the network as it would in a browser.
A page someone else wrote explains the policy that refuses it.
Cue timing on a hosted runner
A hosted runner has no GPU that Chromium can use, so every frame is drawn in software and a recording takes longer. It does not land a reveal any later. The recorder steps the section clock one frame at a time and draws each frame at its own second, so a reveal shows on the same frame on a hosted runner as on your own machine, andCUE_OFF fails the job above exactly as
it fails a build on your desk. DeckTalk’s own end-to-end suite holds every reveal to its frame on
every runner it runs on.
A slower runner does have limits. One call into a page that takes longer than
record.page_deadline_seconds, 15 seconds by default, stops the run with a
TOOL error naming the section and the frame. A recording more than
record.deadline_slowdown times slower, 30 by default, than this machine’s
record.chunk_startup_seconds and record.step_seconds stops the same way. A chunk whose renderer
crashed is recorded again in a fresh page up to record.renderer_retries times, once by default.
On a runner much slower than the machine those step times were set for, raise the limits with
DECKTALK_RECORD_PAGE_DEADLINE_SECONDS and DECKTALK_RECORD_DEADLINE_SLOWDOWN.
Build offline
A build that does not spend runs with no network at all after these three steps.1
Fetch the tools while there is a network
2
Write the project after the tools are there
3
Build without buying
PAGE_KATEX_MISSING.
deck/index.html
PAGE_CDN_ASSET, which is the one finding that names a file an
offline build would not have. Put the file beside the page and the deck is self-contained.
Related
- Read the JSON an agent parses: The JSON contract
- Read every command and every flag: CLI
- Read what a machine has to have: Requirements
- Turn one key for one run: Configuration