Skip to main content
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 decktalk logger, 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.
Those lists are the one authority. Check a name against them before you write it.

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, takes cancel, 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.
Two class facts sit beside the fields rather than in them. 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.
A fix is one of two shapes, discriminated by 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.
A project’s view reads its run set when a line is delivered rather than when a renderer attaches, so a caller may subscribe before it makes the call it wants to watch. A 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.
Every line carries four fields the library mints, so an emitter states only what it measured. 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 a DeckTalkError, 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

This is the only reading of the environment there is. 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 with asyncio.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 to Machine.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

A cost.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.
A project opened on that machine with 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.
  • 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