This release is a pre-release and may not be stable for production use.
Write, preview, and render narration for a finished slide PDF — keeping the spoken words in a plain-text sidecar file next to the deck.
slideSonnet inverts the usual flow. Your PDF is the frozen artifact; the narration lives in a human-readable, git-diffable file keyed to slides by stable ids. Edit a line, hit re-render, and only that line is re-synthesized — no recompiling slides, no re-recording a microphone. A local GUI gives you subtitle-style editing synced to each slide; an LLM can write the first draft; the CLI scripts the whole pipeline.
v1 is a rewrite. Earlier 0.x releases compiled MARP/Beamer source into video with narration embedded inline (
<!-- say: -->,\say{}). v1 works from a PDF + narration sidecar instead. SeeCHANGELOG.md.
How it works
deck.pdf ──(invisible \ssid markers)──┐
├──► slidesonnet ──► deck.mp4 + deck.srt
deck.narration ──(@slide-id blocks)────┘ (preview in the GUI)
- Mark every slide in your Beamer source with a stable id (
\ssid{...}), compile to PDF. - Scaffold a sidecar from the PDF's ids (
slidesonnet init). - Write narration — by hand, by an LLM, or in the GUI (
slidesonnet edit). - Render a narrated (or silent) MP4 with subtitles (
slidesonnet export).
Installation
External dependencies
| Tool | Required? | What it does | Install |
|---|---|---|---|
| ffmpeg / ffprobe | Yes | Audio + video composition | sudo apt install ffmpeg |
| pdftoppm | Yes | Rasterize PDF pages to images | sudo apt install poppler-utils |
| latexmk + pdflatex | To compile your deck | Build the Beamer PDF (your job, not the tool's) | sudo apt install latexmk texlive-latex-base |
PyMuPDF (PDF id extraction) and the editor's web server (FastAPI + Uvicorn)
install as Python dependencies; the editor's browser interface comes built. After installing, run slidesonnet doctor.
slideSonnet runs on Linux with Python 3.13+ (macOS is untested). Native Windows is not supported; on Windows, use it inside WSL (the editor can still open in your Windows browser — see the WSL note below).
Install for use
Install slideSonnet as an isolated, global CLI — its own virtualenv, separate from your system Python and any source checkout:
uv tool install --prerelease=allow "slidesonnet[kokoro]"
# or: pipx install --pip-args=--pre "slidesonnet[kokoro]"
v1 is still a pre-release (1.0.0a…), so the pre-release flag is needed until
1.0 is final: without it, the installer picks the old 0.x tool, which works
differently (it compiled slide source, not PDFs). slidesonnet --version
should print 1.0.0a-something.
The [kokoro] extra adds Kokoro (82M,
Apache-2.0) for free, natural-sounding local speech (~2x real-time on CPU;
the model downloads on first use). Then run slidesonnet doctor to confirm the
external tools above are visible.
Upgrade with uv tool upgrade --prerelease=allow slidesonnet; remove with uv tool uninstall slidesonnet. To hack on slideSonnet itself instead, see
Development for the editable install.
Quick start
# 1. Drop the LaTeX macro next to your Beamer source and \usepackage it
slidesonnet sty # writes slidesonnet.sty
# 2. In your .tex: \usepackage{slidesonnet} and \ssid{...} on every frame,
# then compile. An ordinary compile is a *plain* build: page numbers and
# progress bars stay hidden while you iterate.
latexmk -pdf deck.tex
# 3. Scaffold the narration sidecar from the PDF's slide-ids
slidesonnet init deck.pdf # writes deck.narration
# 4. Write narration (edit deck.narration, or open the editor), then check it
slidesonnet edit deck.pdf
slidesonnet check deck.pdf
# 5. Render a draft from the plain build: narrated MP4 + subtitles
slidesonnet export deck.pdf -o deck.mp4 --draft # writes deck.draft.mp4
# 6. For the final video, compile a final build (page numbers shown), then
# export without --draft
latexmk -pdf -usepretex='\def\ssfinal{}' deck.tex
slidesonnet export deck.pdf -o deck.mp4 # writes deck.mp4
A final export refuses a plain build, a deck check reports errors for, a deck
with no narration yet, and one with review conversations still open, and says
which. --draft exports anyway and names the file <name>.draft.mp4, so a
draft never overwrites the real video.
For a quick look while you iterate, add --fast (the editor's Quick export
box): 720p, plain cuts instead of transitions, and the stills encoded in one
pass instead of frame by frame. The audio and subtitles are the full export's.
It writes <name>.fast.mp4, so it never replaces the full-quality video. On the
basel demo (6½ minutes, audio already generated) the full export takes about
two minutes and --fast about 15 seconds, then 2 seconds for a repeat after a
slide-only change.
Marking slides — the \ssid macro
slidesonnet sty writes slidesonnet.sty. In your Beamer preamble add
\usepackage{slidesonnet}, then give each emitted page an id:
\begin{frame}
\ssid<1>{euler-setup} % id for overlay step 1
\ssid<2>{euler-trick} % id for overlay step 2 (ranges work: \ssid<2-3>{...})
\only<1->{...}\onslide<2->{...}
\end{frame}
\begin{frame}
\ssid{intro-title} % non-overlay frame: one id for its single page
...
\end{frame}
The id is stamped as invisible text (PDF rendering mode 3 — like an OCR
layer): never shown, on any background, but reliably recovered from the text
layer. Any page you forget to name gets a positional auto-… default and a
warning, so it gets a real name.
The narration sidecar
An indented, line-oriented, git-diffable file (deck.narration). Each slide is
an @id block of one or more utterance: blocks and pause: lines:
# a comment
@intro-title
utterance:
text: Welcome to the course on the Basel problem.
pause: 1.5
utterance:
text: Today we'll see how Euler summed the reciprocals of the squares.
@intro-overview
pause: 3 # silent slide — held 3s while they read
@euler-trick
utterance: # voice and pace are optional
voice: bernoulli
pace: slow # slow | normal | fast
text: Watch the denominators carefully. This is the trick.
@<slide-id>starts a block; eachutterance:carries the spokentext:plus optionalvoice:/pace:/direct:(a director's note Inworld can follow). A slide can mix voices — each utterance is its own synthesis call.[Mengoli](/menˈɡoːli/)in a line fixes how a word is said (IPA or a respelling) while the subtitles still read "Mengoli".pause: Nis an explicit silence in seconds: between utterances, as an end-of-slide hold, or alone as a silent slide.- A slide can bracket itself with
transition-in:/transition-out:lines: a name and seconds, e.g.fade 0.5. Besidescut(the default) there are fades (fade,fadeblack,fadewhite,dissolve,crossfade), directionalwipe…/slide…/cover…/reveal…(e.g.wipeleft,slideup), andcircleopen/circleclose.
The full authoring guide — marking overlay steps, the complete sidecar
grammar, and the optional slidesonnet.toml config — is in
docs/authoring.md.
The editor
slidesonnet edit deck.pdf opens the editor in your browser (a local server;
with Kokoro nothing leaves your machine, while the paid Inworld engine sends the
narration text to Inworld): page through the deck, edit narration beside each
slide, set voice/pace, generate per-slide TTS, and play it back. Typing is saved
as you go. Play all plays slide by slide from where you are, starting at
once (the next slide is prepared while this one plays). Watch as video
plays one pre-rendered track with the pauses and transitions baked in, changing
the slide on the audio's own clock, so it matches the exported video exactly.
A diagnostics panel flags duplicate, missing, orphan, or auto-… ids.
Reviewing an agent's changes
When an agent (e.g. Claude Code with the bundled beamer-writer skill) revises
a deck, you review its work in the editor's Review tab. The first time the
editor opens a deck it records a base, and every slide that has changed since
shows its old version beside the new one, with a word-by-word diff of its narration.
Work is organised into conversations: each covers one or more slides, and
deck is the permanent deck-wide one. Choosing a conversation greys out the
other slides and shows its messages. From there you reply, ✓ accept, or reopen
it, and ✎ renames it. Send wakes an agent waiting for your notes. Clear
accepted makes accepted changes the new base. A slide that changes
with no conversation is filed and flagged, so nothing slips through unnoticed.
Conversation history lives in <deck>.review beside the deck.
The agent's side is slidesonnet review …:
slidesonnet review wait deck.pdf --since N --json # block until you press Send
slidesonnet review list deck.pdf --mine --json # conversations awaiting the agent
slidesonnet review comment deck.pdf @x @y -m "…" [--title "…"] # open a conversation
slidesonnet review reply deck.pdf c3 -m "…" [--add-slides @z] [--title "…"]
slidesonnet review title deck.pdf c3 "Shorter Euler proof" # rename, no message
slidesonnet review status deck.pdf # changed slides, unfiled changes
A conversation's id (c3) never changes. Its title is only a display name,
set by the latest --title or review title (or ✎ in the editor).
Many decks in one session
The editor opens on a library of the decks it finds, so a course of decks doesn't mean relaunching per deck:
slidesonnet edit # decks under the current folder
slidesonnet edit ~/courses/aicode # decks under a course folder
slidesonnet edit deck.pdf # that deck, plus its neighbours
slidesonnet edit deck.pdf --root ~/courses # ...while browsing a wider tree
A deck is any PDF with a matching .narration beside it; subfolders are searched
(week01/intro/intro.pdf and friends), and dot-folders, node_modules, and deck
caches are skipped. Each deck shows how much of it is narrated; type to filter
(press / or Ctrl+K to jump to the search box, Enter opens the
highlighted deck), and PDFs without a narration file are listed at the bottom
with the command that starts one. Once you're in a deck, Ctrl+K opens a type-to-filter deck
switcher and Alt+←/→ step to the previous/next deck. Switching saves the slide
you were editing first and cancels audio still generating for the deck you're
leaving — clips already finished stay cached, and coming back re-queues the rest.
WSL note: to open the editor in your Windows browser instead of a Linux one:
slidesonnet edit deck.pdf --app— a chromeless app window via Edge/Chrome (auto-detected on the Windows side; Firefox has no app-window mode).- install
wslview(sudo apt install wslu) — used automatically — for a normal tab in your default browser. --browser CMDfor full control (e.g."cmd.exe /c start", or a browser path; a{url}token is substituted). Also settable viaSLIDESONNET_BROWSER.
CLI
slidesonnet sty [-o PATH] write the LaTeX macro
slidesonnet init deck.pdf [--merge|--force] scaffold a blank sidecar
slidesonnet check deck.pdf reconcile ids (exit≠0 on errors)
slidesonnet tts deck.pdf [--engine ...] [--id ID ...] [--yes] synthesize into the cache
slidesonnet export deck.pdf -o OUT.mp4
[--draft] # not final yet: writes OUT.draft.mp4
[--fast] # quick look: 720p, cuts; writes OUT.fast.mp4
[--engine kokoro] [--silent]
[--timing tts|estimate|fixed:N] [--wpm N]
[--subtitles srt|vtt|both|none] [--sub-granularity segment|slide]
[--keep-scratch] # keep render intermediates (debugging)
[--yes] # generate paid clips without asking
slidesonnet subs deck.pdf -o OUT.srt|OUT.vtt [--engine ...] [--format srt|vtt]
[--timing ...] [--allow-estimates] # export already writes these
slidesonnet edit [deck.pdf|FOLDER] [--root DIR] launch the editor
slidesonnet review comment|reply|title|list|status|wait|... review conversations
slidesonnet clean deck.pdf [--keep nothing|api|current|exact] [--dry-run] # paid clips → trash
slidesonnet pool status [deck.pdf] which clip pool a deck uses, and why
slidesonnet pool migrate --root DIR [--apply] move old local caches into the pool
slidesonnet pool prune --root DIR [--apply] [--keep current|exact|api] [--empty-trash]
slidesonnet doctor
Errors print one line saying what went wrong and how to fix it; add -v
(slidesonnet check … -v, before or after the command) for the full traceback.
--audio-dir DIR before any command (or SLIDESONNET_AUDIO_DIR, or
[cache] audio_dir in slidesonnet.toml) points it at a shared speech-clip
pool, so every worktree of a course reuses the same synthesized audio instead
of re-buying it. See the config section of the authoring guide.
Every operation is also a typed Python function in slidesonnet.api
(init_sidecar, synthesize_deck, export, write_subs, …) so an LLM/CI loop
can drive the pipeline without the GUI.
Engines and keys
| Engine | Install | Runs | Cost |
|---|---|---|---|
kokoro (default) |
"slidesonnet[kokoro]" |
locally, CPU | free |
inworld |
"slidesonnet[inworld]" |
Inworld's cloud: narration text is sent to Inworld | paid per clip |
qwen3 |
"slidesonnet[qwen3]" |
locally, GPU (slow); can clone your voice | free |
Pick one per run with --engine, or per deck with [tts] backend in
slidesonnet.toml. Inworld needs an API key: put INWORLD_API_KEY=... in a
.env file beside the deck (a parent folder, or the folder you run from, also
works; a shell export wins over .env). Keep .env out of git. Before
generating paid clips, tts and export say how many new clips they would
make and ask; --yes answers for you, and without a terminal (scripts, CI)
they refuse unless --yes is given. Clips already generated are cached and
reused for free.
slidesonnet.toml (optional, beside the deck) also sets engine voices and
speeds ([tts.kokoro], [tts.inworld], [tts.qwen3]; Inworld also takes
temperature, delivery_mode, language, text_normalization and
send_direction), named voices
([voices.<name>]), video settings ([video]: resolution, fps, crf,
preset, pre_silence, tail_seconds, keep_scratch), the run log
([logging]: file, level, max_bytes, backup_count), a shared clip pool
([cache] audio_dir), and pronunciation files (a top-level key, so it goes
above the first [table]). Every key, with its default, is in the
authoring guide.
Timing & silent renders
--timing tts (default) uses real synthesized audio. --timing estimate
approximates from word count at --wpm for a fast rough cut with no TTS;
--timing fixed:N holds every page N seconds. --silent renders with no
narration (timing falls back to estimate) — pair it with subtitles for a
captioned silent cut.
Examples
examples/basel-problem/— a 22-page Euler proof with overlay steps and a second voice for the Bernoulli quote. ▶ Watch on YouTube (Inworld narration).examples/showcase/— a self-narrated tour that teaches the workflow as a two-voice dialog (written in this very format).
Build them from source with make basel / make showcase (Kokoro). Videos are
hosted as GitHub Release assets, not committed.
Development
make install # editable install with Kokoro + dev tools
make test-unit # fast unit tests (no external tools)
make test # full suite (needs ffmpeg, pdftoppm, kokoro)
make lint # ruff
make typecheck # mypy --strict
All source is fully typed (mypy --strict); paid cloud TTS is never exercised
in tests (it costs money) — Kokoro and mocks only.
License
slideSonnet's own code is MIT (see LICENSE).
It depends on PyMuPDF (for reading slide-ids), which is AGPL-3.0 (or a commercial license from Artifex). AGPL is copyleft: if you redistribute slideSonnet or run it as a network service, the AGPL terms apply to the combined work. For local use or a normal open-source install this is a non-issue — it only matters if you want to build a closed-source product on top of it.
Metadata
Release files for slidesonnet 1.0.0a3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| slidesonnet-1.0.0a3.tar.gz | 1.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| slidesonnet-1.0.0a3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.0 MB
Release files / slidesonnet-1.0.0a3.tar.gz
| Download URL | slidesonnet-1.0.0a3.tar.gz |
|---|---|
| Size | 1.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
26e319f86445aac9937960c5279ab3e15be463f7f37777fe1a8b51759afffc84
|
|
BLAKE2b-256 checksum How to use checksums |
d9613c73aa8a28af84c976adebc1beb1b66e24bb700d1ef0f1e23a0d46f2dfd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|
Release files / slidesonnet-1.0.0a3-py3-none-any.whl
| Download URL | slidesonnet-1.0.0a3-py3-none-any.whl |
|---|---|
| Size | 943.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d921765ebd9f76597c112ecc75c8ba376c96a92db916d9f012aae05d0e9be5a
|
|
BLAKE2b-256 checksum How to use checksums |
b9b567ddf1a3ab7603eccf10430d9baa0f1ac0412d244947aa1fb88ca2b203d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|