Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

slideSonnet

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. See CHANGELOG.md.

How it works

deck.pdf  ──(invisible \ssid markers)──┐
                                        ├──►  slidesonnet  ──►  deck.mp4 + deck.srt
deck.narration  ──(@slide-id blocks)────┘                       (preview in the GUI)
  1. Mark every slide in your Beamer source with a stable id (\ssid{...}), compile to PDF.
  2. Scaffold a sidecar from the PDF's ids (slidesonnet init).
  3. Write narration — by hand, by an LLM, or in the GUI (slidesonnet edit).
  4. 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; each utterance: carries the spoken text: plus optional voice: / 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: N is 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. Besides cut (the default) there are fades (fade, fadeblack, fadewhite, dissolve, crossfade), directional wipe…/slide…/cover…/reveal… (e.g. wipeleft, slideup), and circleopen/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 CMD for full control (e.g. "cmd.exe /c start", or a browser path; a {url} token is substituted). Also settable via SLIDESONNET_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

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)

Source distribution for slidesonnet 1.0.0a3
File Size Uploaded
slidesonnet-1.0.0a3.tar.gz 1.0 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for slidesonnet 1.0.0a3
File Interpreter ABI Platform
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
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page