The presentation framework for people who think in code.
Auditorium lets you build live technical presentations as Python scripts. Each slide is an async def function. Animate algorithms step by step, render live-computed plots, run numerical demos โ anything Python can do, your slides can do. No PowerPoint. No Markdown. Just code.
from auditorium import Deck
deck = Deck(title="My Talk")
@deck.slide
async def sorting_demo(ctx):
"""Explain how the algorithm builds the sorted prefix."""
await ctx.md("## Bubble Sort, Step by Step")
data = [5, 3, 8, 1, 2]
for i in range(len(data)):
for j in range(len(data) - 1 - i):
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
await ctx.md(f"`{data}`")
await ctx.sleep(0.5)
await ctx.step()
await ctx.md("**Sorted!**")pip install auditorium
auditorium run talk.pyMost presentation tools treat slides as static documents. Auditorium treats them as programs.
- ๐ Run algorithms live โ sort arrays, traverse graphs, train models, all animated on stage
- ๐ Compute content โ generate plots, tables, or LaTeX from data, not screenshots
- ๐ฆ Use any Python library โ numpy, matplotlib, pandas, whatever you import works
- ๐ Share with students worldwide โ
--publicgives every connected browser your live deck - ๐ Go public โ
--publicgives you an instant shareable URL, no deployment needed - ๐ค Render and share โ render to mp4 frame by frame, or ship a self-contained HTML bundle that replays the whole timeline
If you've ever wished you could await inside a PowerPoint slide, this is for you.
pip install auditorium # or: uv add auditoriumCreate talk.py:
from auditorium import Deck
deck = Deck(title="My Talk")
@deck.slide
async def intro(ctx):
"""Notes for the presenter โ only visible in presenter view."""
await ctx.md("# Welcome!")
await ctx.md("*Press right arrow to continue*")
@deck.slide
async def demo(ctx):
"""Show progressive reveals and timed content."""
await ctx.md("## Key Points")
await ctx.step()
await ctx.md("- First point")
await ctx.step()
await ctx.md("- Second point")
await ctx.sleep(1)
await ctx.md("*(that one appeared automatically)*")Run it:
auditorium run talk.py| Feature | Description | |
|---|---|---|
| ๐ | Imperative Python slides | Each slide is an async def โ loops, conditionals, imports, anything |
| ๐งช | Jupyter display protocol | ctx.show(obj) renders any _repr_html_ / _repr_svg_ / _repr_png_ object โ matplotlib, pandas, altair, tesserax, โฆ |
| ๐๏ธ | Progressive reveals | await ctx.step() pauses for keypress, await ctx.sleep(n) auto-advances |
| ๐งฎ | LaTeX math | KaTeX bundled โ $inline$ and $$display$$ in any markdown |
| ๐ป | Syntax highlighting | Fenced code blocks with highlight.js (bundled) |
| ๐ | Flexible layouts | columns, rows with "auto" sizing, arbitrarily nested |
| ๐ค | Presenter mode | --presenter โ notes, timer, stage mirror, next-scene preview |
| ๐ | Shared navigation | Presenter drives all audience tabs; audience keyboards are inert |
| ๐ | Public sharing | --public bridges to a relay โ instant shareable URL, no deployment |
| ๐ | Late-join sync | New viewers see the full slide state immediately |
| ๐ | HTML / PNG export | Self-contained interactive HTML, or PNG stills. No PDF โ see below |
| ๐ฌ | Deterministic video | auditorium render steps frames against a paused timeline โ two renders are byte-identical |
| ๐ | Frame ranges | --from/--to make parallel rendering a shell-level fan-out |
| ๐๏ธ | Preview client | auditorium preview โ scrubber, frame stepping, loop-a-range |
| ๐ | Geometry layer | Line, Arrow, Path, Circle with anchors that track their boxes |
| โป๏ธ | Hot reload | Edit your .py and the browser updates instantly, holding your position |
| ๐ก | Offline | All assets bundled โ zero CDN, zero internet required |
| ๐ | Auto-reconnect | Survives server restarts without losing your place |
Present from your laptop, share with the world:
auditorium run talk.py --publicโญโโโโโโโโโโโโโโโโโโโโโโโโโ Auditorium โโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Deck: My Talk โ
โ Slides: 15 โ
โ URL: http://127.0.0.1:8000 โ
โ Mode: independent (per-tab) โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Public URL: http://vps.apiad.net:4243/r/my-talk/
Anyone with the link sees your presentation in real time. No deployment, no hosting โ your laptop runs the deck, a lightweight relay forwards it.
# Choose your own URL slug
auditorium run talk.py --public --name my-talk
# Use your own relay server
auditorium run talk.py --public --relay myserver.com:4243Self-host a relay (it's one command):
auditorium relay # run directly
make relay-install # install as systemd service
make relay-update # pull + sync + restartauditorium preview talk.py opens the authoring surface: the stage plus a
transport bar.
auditorium preview talk.py- ๐๏ธ Scrubber with a tick per beat, so you can see the structure of the timeline
- ๐๏ธ Frame stepping with
.and,โ one output frame at a time - ๐ Loop a range with
i,oandl, to tune one animation without replaying the deck - โป๏ธ Hot reload holds your position โ edit the
.pyand you stay at the same instant
The frame counter reports rendered frames, including the dwell a render
spends on each beat โ so the number beside the scrubber is the number you can
pass to --from / --to. The stage is the render target scaled down, not a
reflowed copy of it, so what you are looking at is what the mp4 will contain.
Start with --presenter to sync all audience tabs to your navigation:
auditorium run talk.py --presenterTwo tabs open: your presenter view (notes + timer + slide mirror + next-slide preview) and the audience view. Navigate from the presenter tab โ every connected browser follows in real time.
- ๐ Docstrings become speaker notes (never shown to the audience)
- โก Late-joining tabs catch up instantly (full slide state replayed)
- ๐ Audience keyboards are locked โ only the presenter navigates, enforced by the server rather than by the audience's good manners
The presenter broadcasts intent โ "seek to t", "play from here to there" โ not positions. Every surface runs the same deterministic engine over the same timeline, so a command is enough and there is nothing to drift.
Without --presenter, each tab navigates independently.
@deck.slide
async def layout_demo(ctx):
"""Layouts nest freely."""
await ctx.md("## Two Columns")
left, right = await ctx.columns([2, 1])
async with left:
await ctx.md("Main content (2/3 width)")
async with right:
await ctx.md("Sidebar (1/3)")Use "auto" for natural-size regions:
header, body, footer = await ctx.rows(["auto", 1, "auto"])Lines, arrows, paths and circles live in an SVG overlay sharing the stage's coordinate space โ what CSS cannot express: stroke draw-on, geometric motion, and edges that connect boxes.
from auditorium.nodes import Arrow, Circle, Line, Path
@deck.scene
async def wiring(s):
a = await s.show("<div class='aud-block aud-block-info'>compile</div>")
b = await s.show("<div class='aud-block aud-block-success'>seek(t)</div>")
wire = await s.draw(Arrow(from_=a.bottom, to=b.top, stroke="#2563eb", width=3))
await s.play(wire.animate.draw_on(), run_time=0.6, ease="out-cubic")
await s.play(a.animate.move_by(160, 0), run_time=0.8) # the arrow followsAnchors are symbolic. a.bottom is not a coordinate โ it is a promise the
browser keeps on every frame. Python never computes layout, so an arrow tracks
its box through motion and through flex reflow. Every handle exposes left,
right, top, bottom and center.
draw_on() strokes a shape into existence along its own length. Every
geometric node is created with pathLength="1", so the animation runs in
normalized units and needs no measurement of a geometry the anchors may be
about to change. The same normalization applies to dash: write
dash="0.05 0.02" for a 5% dash and a 2% gap, not a pattern in pixels โ
which would render as a solid line.
What this layer does not do. Path morphing between two d strings is not
in v1. Interpolating paths with differing command counts needs normalisation
that has not been built, and a cross-fade was not worth shipping as a
substitute. Path is static geometry you can draw on and move; it does not
tween into another path.
ctx.show(...) speaks the Jupyter display protocol. Pass any object that implements _repr_html_, _repr_svg_, _repr_png_, or _repr_jpeg_ and it just renders โ no adapters, no bundling, no glue code.
import pandas as pd
import matplotlib.pyplot as plt
from tesserax import Canvas, Circle, Square
from tesserax.layout import RowLayout
@deck.slide
async def live_data(ctx):
# A pandas DataFrame โ _repr_html_
df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
await ctx.show(df)
# A matplotlib figure โ _repr_png_ (or _repr_svg_ with the svg backend)
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [4, 5, 6])
await ctx.show(fig)
# A tesserax Canvas โ _repr_svg_
with Canvas() as canvas:
with RowLayout():
Square(30, fill="green")
Circle(20, fill="red")
await ctx.show(canvas.fit(padding=10))This works with matplotlib figures, pandas DataFrames, altair charts, plotly figures, tesserax canvases, sympy expressions, IPython rich objects, and anything else in the Jupyter ecosystem. Plain strings are still passed through as HTML. Runnable example: examples/tesserax_demo.py.
Requires pip install auditorium[record] and playwright install chromium.
# Export a self-contained HTML bundle, or PNG stills
auditorium export talk.py -f html -o talk.html
auditorium export talk.py -f png -o slides/# Render to video, frame by frame
auditorium render talk.py -o talk.mp4
auditorium render talk.py -o clip.mp4 --size vertical --fps 60
auditorium render talk.py -o frames/ --format png-sequence
# Render a frame range -- fan several out in parallel, then concat
auditorium render talk.py -o part1.mp4 --from 0 --to 300render replaces the old record. record screen-captured a live browser,
so a loaded machine produced a different video; render steps frames against
a paused timeline, and two renders of the same deck are byte-identical.
Auditorium 4.0 removed PDF export, deliberately. It is not coming back.
A deck is no longer a list of slides โ it is a timeline. A scene is a continuous function of time, so there is no canonical instant to print. Every answer to "which moment becomes a page?" is invented rather than derived: the end of each scene loses every build stage, one page per pause produces a run of near-identical cumulative pages, and asking the author to name capture points is a knob nobody wants to turn.
PNG and HTML export survive because both are total functions of the timeline. A PNG is "the frame at time t", well-defined for any scene. The HTML bundle carries the whole timeline and replays it. Neither has to guess.
If you want a PDF, that is a real thing to want โ but you are the one who knows which instants matter. Export PNGs, pick your frames, and assemble them:
auditorium export talk.py -f png -o slides/
img2pdf slides/*.png -o talk.pdfIf the document was always meant to be printed, do not start here. Author
it in a document engine โ scriptorium
ships a deck theme for 16:9 slides and is built for pagination, which is a
genuinely hard problem and not this project's problem. Auditorium is for
animation. Print is a different craft, and pretending otherwise produced the
worst code in this repository.
Time is a coordinate in 4.0, so navigation moves between beats โ the pause
points await ctx.step() and await s.beat() record โ rather than between
slide indices.
Present and presenter views:
| Key | Action |
|---|---|
| โ / Space / Page Down | Play to the next beat (skip to it if already playing) |
| โ / Page Up | Back to the previous beat |
r |
Restart from the beginning |
| End | Jump to the end |
Preview client (auditorium preview) adds:
| Key | Action |
|---|---|
| Space | Play / pause |
. / , |
Step one frame forward / back |
i / o |
Set the loop in / out point to the current time |
l / x |
Toggle looping / clear the loop |
| Home / End | Jump to the start / end |
Backward navigation resets the timeline and replays forward. That is deliberate: seeking is path-dependent, so a rewound state and a freshly-seeked one would otherwise differ โ and the renderer only ever travels forward. Replaying is what keeps what you see equal to what you get.
See examples/demo.py โ six scenes that animate, including a bubble sort whose every swap on screen is a swap the algorithm actually made.
auditorium run examples/demo.pyMIT