Skip to content

Latest commit

ย 

History

472 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽ“ Auditorium

PyPI - License PyPI - Python Version PyPI

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.py

โœจ Why Auditorium?

Most 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 โ€” --public gives every connected browser your live deck
  • ๐Ÿ”— Go public โ€” --public gives 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.


๐Ÿš€ Quick Start

pip install auditorium    # or: uv add auditorium

Create 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

๐Ÿ“‹ Features

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

๐ŸŒ Share Publicly

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:4243

Self-host a relay (it's one command):

auditorium relay                    # run directly
make relay-install                  # install as systemd service
make relay-update                   # pull + sync + restart

๐ŸŽ›๏ธ Preview

auditorium 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, o and l, to tune one animation without replaying the deck
  • โ™ป๏ธ Hot reload holds your position โ€” edit the .py and 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.


๐ŸŽค Presenter Mode

Start with --presenter to sync all audience tabs to your navigation:

auditorium run talk.py --presenter

Two 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.


๐Ÿ“ Layouts

@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"])

๐Ÿ“ Geometry

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 follows

Anchors 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.


๐Ÿงช Show Any Jupyter Object

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.


๐ŸŽฌ Rendering & Export

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 300

render 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.

Why there is no PDF export

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.pdf

If 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.


โŒจ๏ธ Navigation

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.


๐Ÿ“š Example

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.py

๐Ÿ“œ License

MIT

About

An HTML+CSS+JS generator from pure Python code

Topics

Resources

Stars

87 stars

Watchers

6 watching

Forks

Releases

Used by

Contributors

Languages