decktalk is a library first and a command line second. The command line is the first client of
exactly the surface this page documents, so anything decktalk build does, project.build() does
the same way and returns the same object the command prints.
Three rules hold over the whole package.
- Nothing in the library prints. A run reports through its event stream, and a renderer
subscribes to it. Its log records go to the standard
decktalklogger, and three readers of one account says where they reach. Machine.from_environment()is the only function that reads the environment, the per-machine settings file or the working directory. Everything below it takes what it needs as an argument, so two projects held in one process cannot reach each other.- Every path a result carries is relative to the project root, spelled with forward slashes on
all three platforms, so
result.model_dump_json()needs no root to be correct.
The whole public surface
The supported API is the root,decktalk.__all__, and six public modules, each with its own
__all__. Nothing else is public, and any name outside them may move without notice.
The root holds the entry points, the errors and the few types every caller names:
Every other public type lives in the module that defines its kind:
A type any public field, parameter or return value names is importable from one of them, which
tests/contract/test_api.py holds by walking every public signature.
Opening a project
A script that opens two projects should make one
Machine itself and hand it to both, because the
machine is what carries the shared toolchain and the shared stream.
Project carries these, each read from the files on disk.
To write a project rather than open one:
init is a machine call rather than a project one, because there is no project until it returns.
example names a packaged project to write instead of the starter, and skills writes the six
packaged skills into the new directory.
decktalk.section_numbers("3,5-7") turns the one spelling a command line and a query string use
into (3, 5, 6, 7). The library itself always takes a run of numbers and never a string.
The twelve calls
Every one of these opens a run, takescancel, and returns the frozen result named after it.
only is a sequence of section numbers on every call that takes it. Stage is the enum the six
stages are members of, in run order: NARRATE, CUE, RECORD, SCORE, ASSEMBLE, VERIFY.
Every call in the table also appends its own events file,
build/events/<run>.jsonl.
The parameters that spend or change what a call does
A project’s
Threshold has two fields. stop_on is the least severe finding that fails a call:
Severity.ERROR, the default, fails on an error, Severity.WARNING on any finding, and None on
nothing. allow is a set of codes that never fail a call, whatever their severity, and they are
still reported. A call’s ok is false when a finding reaches it, and a build stops after the first
stage whose findings reach it, so verify measures a film only when every earlier stage passed.
A build that stops is not an error. It returns its BuildResult with ok false, every finding the
stages that ran made, the cost so far, the stages it did not reach as skipped, and stopped_at
naming the stage it stopped after. stopped_at is None on a build that ran through.
What a call raises
Errors says what each one carries.
A call that writes holds a lock on the build directory for its whole length, so two writing runs
cannot overwrite each other’s work. A second writer meets
ProjectLocked. status, words and
check(pages=False) take nothing and run beside a build.
check without a browser
pages=False judges the written files with no browser at all and says which judgements it could not
reach, so a new deck gets its first rows without a download and a hook with no browser still runs.
frames=False keeps the browser and drops the freeze comparison. A check that opens pages freezes
frames into the build directory and draws the storyboard over them, so it holds the build lock for
as long as a stage that produces would.
storyboard selectors
slides names the slides, after and before name the state just after and just before one cue,
which are the pair an author compares to see what a reveal changed, and times names seconds of the
section’s own clock. Each one takes a sequence, and one that matches nothing draws nothing.
clip
spend, max_cost and ApprovalRequired
No library call buys anything without spend=True, whatever the price. One switch decides it, so
there is nothing two flags could contradict. The command line adds one rule of its own: with neither
--spend nor --no-spend, a voice whose adapter declares it free, such as dtsp, is bought from
without asking, and a build still asks about any score it would buy beside it.
The voice is built only when a take must be bought, so a call that buys nothing reads no key.
A call with spend=False, the default, plays every take on disk, voiced or placeholder. For narrate
each turn whose take is missing plays a placeholder with estimated words and is reported as
TAKE_MISSING, which is a warning, so the default threshold
builds the film past it. score reports its plan without buying it. A repository that commits
its takes and its [voices.narrator] id therefore builds its real film on a machine that holds no key.
max_cost is the --max-cost ceiling in US dollars on the whole run, compared against
cost.ceiling_dollars. The ceiling says when it is checked and why.
ApprovalRequired is raised at the spend gate rather than buying, in four cases.
It is also raised before any browser starts when a run that may spend opens a page under
record.page_policy = "untrusted", as
Voice and render in separate processes explains.
Cost is the priced object every result that can buy carries in cost, and every cost.priced event line reports.
Set the rate under the key
price_key names, [elevenlabs] dollars_per_1000_characters for
ElevenLabs, so that a ceiling guards a real number. A provider that declares itself free is never asked
about and passes any ceiling.
Cancel
Every mutating call takes cancel. Cancel is the token a host holds to stop a run between
sections, and it is thread safe, because the caller that stops a run, such as a renderer’s own
thread, is never the thread doing the work. A stage checks the token between sections, so a
cancelled run leaves whole artifacts behind rather than half of one, and raises Cancelled.
Nothing in the CLI sets the token. A Ctrl-C reaches the run as KeyboardInterrupt: the first starts
no new section and waits for the ones in flight, keeping every take already made, and a second
leaves at once. A Ctrl-C during narrate’s takes, score’s requests or anywhere in a build raises Cancelled from
the interrupt instead, so it can carry what the run made.
A refusal raised after a run paid for something carries the result its command answers with: a
narrate stopped or refused while it makes takes carries a NarrateResult of every take finished so
far, a score stopped or refused after a request was paid for carries a ScoreResult, and a build
stopped or refused after it paid for something carries a BuildResult. Each has ok false, the
refusal as its error and what the run spent. Every other refusal’s result is None.
Cancel has cancel() to ask the run to stop, is_set() to ask whether it has been asked, and
check(), which raises Cancelled and is what a stage calls.
What every result carries
Result reserves four keys and every result has them. A result whose command opens a run also
declares run, and one whose command writes files also declares written.
written is how an artifact is found without knowing a path.
Result.reports_findings is true when
the command can report a judgement, and Result.spends is true when it can buy something. The
command line derives its own flags from those two, which is why no list of commands lives beside the
models.
Every result is a frozen pydantic model, so result.model_dump_json() gives exactly what the
command line prints under --json, and uv run decktalk schema NAME prints the schema of the
result named after that command.
Findings and fixes
A finding is a judgement about the film or about the files it is made from. It is never an error, because an error means DeckTalk could not run at all.Location carries a required where plus file, line, section and cue, each of which is
None when nothing knows it.
A Code member carries more than its name. code.sentence is what the condition is,
code.severity is whether it fails the default run, code.raised_by is RaisedBy.RUNTIME when the page reports it
from the browser and RaisedBy.PYTHON when DeckTalk judges it itself, from the project’s files or from what a run produced, code.raised_in names the commands
whose run can report it, and code.url is its page. The
settings keys that move a code are declared on the keys themselves, and decktalk.explain(KEY)
lists the codes one key decides.
kind. Each carries a title, which is one sentence
saying what applying it does, and an applicability.
Applicability.SAFE cannot lose the author’s work. Applicability.UNSAFE can, so it is applied only
when the caller asked for that. Applicability.DISPLAY is a change only a person can make, so it is
reported and never applied.
apply takes one finding or any iterable of them, and takes the fix from its finding rather than on
its own, because what a fix resolves is the finding’s own code. Each FixOutcome carries code,
title, applied, why for one that was left alone, and files for the ones it changed.
Machine.apply is the same call for a fix that changes a machine setting or runs a command.
Events
Machine.events is the stream every run of every project writes to, and Project.events is its
view filtered to the runs that project opened. The stream lives on the machine because install and
doctor hold no project and would otherwise leave a renderer silent through the two commands that
download a few hundred megabytes.
Subscription detaches by
close() or by leaving its with. A renderer that raises never stops a run: the failure becomes a
run.log line at Level.ERROR.
Each name has one model. Skip and fail are not names, because
stage.done and
section.done carry an outcome instead.
The event stream gives the fields each line carries beyond the four.
Outcome, Unit and Level are the enums behind the outcome, unit and level fields, with the
same members in capitals.
JsonlSink is the subscriber that appends one JSON line per event to a file. Every run of a project
gets one, at build/events/<run>.jsonl, and events.keep_runs prunes the oldest. A sink given
max_bytes, as every project run’s is from events.max_bytes, leaves progress, fetch, debug
and info lines out once the file reaches it, keeps every line about the run’s shape, its findings
and its costs, and reports how many it left out as dropped on run.done.
decktalk.events.AnyEvent is the discriminated union of these models, so a reader parses a stored
line by its event and never by trying each shape.
serve and Origin
serve hands back the origin rather than a result, because the caller holds it open. The origin
answers only for the deck directory and the files the document declares, each by the spelling the
document gives it, so a page the recorder drives and a preview an author leaves running reach the
same short list and neither can read the credential beside it. It also answers two aliases, which are where a previewed page reads the cue times
the last run resolved and the canvas the project lays its page out on.
It is also a context manager, so the
with form closes it.
Watching a project
Project.reload() reads the project again from disk and returns a new Project on the same
machine, with the same overrides. Project.sections_touching(path) says which sections a change to
one file would change: a page belongs to the sections that play it, a clip to the section that plays
it, and the script, the cue file and decktalk.toml each belong to every section at once.
Those two are the whole of what a watch loop needs.
build hands each stage the touched sections it can act on, so a saved clip skips record and is
cut again.
Errors
Every refusal DeckTalk makes on purpose is aDeckTalkError, so except DeckTalkError catches an
environment DeckTalk cannot work in and never catches a bug in DeckTalk itself. An interrupt that lands in
narrate’s takes, score’s requests or anywhere in a build arrives as Cancelled, which is a DeckTalkError.
One that lands anywhere else, including cue, record, assemble or verify called alone, arrives as
KeyboardInterrupt. A loop that should end on Ctrl-C catches Cancelled and KeyboardInterrupt before it catches
DeckTalkError.
A host that misuses the library itself gets a ValueError rather than a DeckTalkError, because the
fault is in the host’s code and not in the environment. Registering its own voice or sound provider
under a name DeckTalk ships is one, which Speech providers describes.
Each one carries hint, which is a whole command with its flags rather than a topic, and
location, which is the place the reader should open. Either is None when nothing knows it.
ProviderError adds retryable, which is true when the same request may succeed later, and is what
decides whether a caller waits.
The count of classes is the count of distinct recoveries and not the count of codes. ErrorCode has
nine members, and USAGE and INTERNAL are the two with no class: a command line the parser
refused is the command line’s own refusal, and anything DeckTalk did not mean to raise reaches the
caller as it is.
A member carries value, which is the code as JSON writes it, sentence, exit_code and url.
Error codes lists each code with its exit code.
ErrorInfo is the same refusal as data, which is what a result’s error holds. It carries code,
message, hint, location and docs, and ErrorInfo.of(error) is the one place an exception
becomes one.
The machine
overrides is an iterable of key=value
strings, spelled as --set spells them.
Running DeckTalk inside a service
A service that renders other people’s projects holds three things apart that an author’s own machine holds together: the API key, the pages, and the processes that touch each. This section is the contract a host builds on.A machine the host builds
Machine.from_environment() reads the process. Machine.of reads nothing, so every value a job may
see is one the host chose.
The page policy and every base URL are machine-scoped, so the host sets them on its machine, in the
file at
machine_file or in overrides, and a project file that names one is refused. A voice
therefore sends its key and its script only to the [elevenlabs] base_url or [dtsp] base_url the
host named.
One call on one worker thread
Every call is synchronous and blocks until its run is over. Chromium is driven through Playwright’s synchronous API, which refuses to run on a thread whose asyncio loop is running, so an async host runs each call on a worker thread, for example withasyncio.to_thread. Each call opens and closes
its own run, and the machine’s toolchain and .env rule are bound for that run alone, so two calls on
two threads do not share them. The machine’s voices travel on the run itself, so every provider the
run builds comes from the table the machine was given, on whichever thread builds it.
A run checks its Cancel between sections, and tools.timeout_seconds stops any one ffmpeg call that
runs longer than it allows.
Three readers of one account
A run’s account reaches a host three ways, and all three carry the same sentences. A subscriber toMachine.events receives every line as it is minted. The run’s file under build/events/ holds the
same lines. And a module below the stages writes standard logging records under the decktalk.*
loggers, which one handler DeckTalk puts on the decktalk logger turns into run.log lines of the run
bound to the current thread, and which then propagate to whatever logging the host configured,
stamped with decktalk_run, decktalk_stage and decktalk_section for its formatter. Nothing is
printed, a record written while no run is bound reaches the host’s logging alone, and every value of
a credential the machine holds is replaced by its name before any of the three sees it. The
decktalk logger is set to DEBUG only when the host chose no level before importing DeckTalk, so a
host that set one keeps it.
Voice and render in separate processes
The API key must never share a process with a page, because a page is someone else’s script and runs in a Chromium the process started. So a host runs the stages in two kinds of process, and the build directory is the only thing that moves between them.cue reads only files and opens no page, so it runs in either. assemble draws the poster from the
page, which is why it belongs to the render side.
The two parts are read off the pipeline rather than kept in a list of the host’s own:
decktalk.Stage.keyed_stages() is every stage whose row holds_api_key, and decktalk.Stage.keyless_stages()
is every other. No row both holds the key and opens_pages.
A run that may spend refuses to open a page under record.page_policy = "untrusted", with
APPROVAL, before any browser starts. So a host that sets that policy on both machines cannot put
the key beside a page: a build(spend=True) whose stages open a page is refused before its first stage
buys anything. A
render job may call build() once the voice job has bought every take, because a build that does not
spend plays them and never builds the voice, so no key is read in the process that opens the pages.
A ledger of what was bought
Acost.priced line prices a whole run, before or after it. A take.charged line is written the moment
the provider is paid for one take, once per take, after the provider answered and before anything
that could fail writes the take. So a host that keeps its own ledger records every take it paid for,
including one whose file never reached the disk, and a run that fails on its fourth take still leaves
three lines. A provider that declares it bills nothing, such as dtsp, is paid nothing, so its takes
write no take.charged line.
The digest is what makes a retry safe to meter. A take already on disk is read from the cache and never
bought again, so it earns no line, and a ledger keyed on
digest sees at once when two runs bought
the same words.
score writes a sound.charged line the same way, once for each request the sound provider is
paid for: one per ambience bed or effect, and one per part of the music. name is the name the item
has in decktalk.toml, kind is music, ambience or effect, digest is the request’s digest
in the sound ledger, seconds is how much audio was asked for, and dollars is those seconds at the
rate per minute its kind’s table states. A request whose reply broke after it was sent, or that a
Ctrl-C abandoned, writes its line
with possibly_charged true, as a take’s does.
Speech providers
The voices a project can name in[voices.NAME] provider are a closed set of adapters in the engine:
elevenlabs, the cloud voice, and dtsp, which speaks the DeckTalk speech protocol to
decktalk-voice, a separate local server that is not part of this repository and is not yet public. Nothing is loaded
from entry points, and a project file names an adapter and never code.
A host can still hand Machine.of its own table, which is code it wrote and imported itself, and is
how a test or a service runs the real stages against a voice that spends nothing. The types it
builds against are public in decktalk.speech.
provider = "silent" voices through it, and can name no voice
the table leaves out. The bytes a provider answers with must be a take ffprobe can decode, such as the
short silent MP3 this host keeps beside it, because DeckTalk measures every take it is handed. A host’s provider declares no bill, so a run that buys from it is priced at
nothing anybody stated, and max_cost refuses to guard it.
A voice is billed by the name it is registered under, so a name DeckTalk ships holds only the adapter
DeckTalk ships. A table that puts a host’s own factory under elevenlabs or dtsp, or under any
spelling of one in another case, width or with space around it, raises ValueError when the machine
is built, naming the shipped voice and saying to register the factory under another name. Without
that, a host’s code under dtsp would be billed as the free local voice and bought by a run that may
not spend. The sound table a host hands Machine.of follows the same rule, because a sound is priced
at the rate its provider’s name declares: a host’s own sound factory under elevenlabs raises
ValueError naming [score] provider. Each table is copied when the machine is built, so changing the
mapping afterwards changes no provider.
Settings
Project.settings is a decktalk.settings.Settings, with one frozen config object per table.
Project.layers says which layer set each key.
explain reads one setting whole: its type, its unit, its safe range, the value in force, every layer
that stated it, the variable that sets it, the findings it decides, the published numbers it feeds, and
what a candidate value would clamp in this project. It is the result config explain prints. Without
project the answer is about the defaults and the machine alone, which is what an agent reading the
instruction set before it has a project needs. machine is the machine whose file and environment are
the layers under and over the project, this process’s own when none is given. value is a candidate
spelled the way a command line spells it, held to the same safe range as a value that is written.
decktalk.results.Layer is the five layers, lowest first: DEFAULT, MACHINE, PROJECT,
ENVIRONMENT, OVERRIDE. decktalk.results.Scope says which file a settings write lands in,
PROJECT or MACHINE.
Related
- Run the same calls from a shell: CLI
- Look up a settings key, its range and its hazard: Configuration
- Look up the files a run writes: Build artifacts
- Look up what a page may declare: Runtime