Skip to main content

slidecast

Turn a list of HTML slides and recorded clips, plus narration, into a narrated MP4.

You bring the slide design — any HTML you like — and the words. slidecast screenshots each slide with a headless browser, narrates it with a pluggable text-to-speech provider, and stitches the frames into one MP4 with ffmpeg. It has no opinion about how your slides look and no hard dependency on a specific voice or browser: every piece is swappable.

Install

pip install slidecast              # core (requests only)
pip install slidecast[playwright]  # default renderer (headless Chromium)
pip install slidecast[gtts]        # Google Translate TTS
pip install slidecast[ffmpeg]      # bundled ffmpeg binary (no system install)
pip install 'slidecast[mlx]'       # Kokoro in-process on Apple Silicon (no TTS server)
pip install 'slidecast[compose]'   # synthesize a music bed and stings (numpy)

After installing the Playwright extra, fetch the browser once:

playwright install chromium

You also need ffmpeg — on PATH, via $SLIDECAST_FFMPEG, or the [ffmpeg] extra's bundled binary.

Library

from slidecast import Reel, KokoroTTS

reel = Reel(width=1280, height=720, tts=KokoroTTS(voice="af_heart"))
reel.add("<!doctype html><h1>Hello</h1>", "Hello, and welcome.")
reel.add("<!doctype html><h1>Goodbye</h1>", "Thanks for watching.", tail_pad=0.8)
reel.render("out.mp4", make_poster=True)

A slide with empty narration becomes a silent hold (min_duration seconds). When the TTS provider reports a clip duration, the segment is padded to fit the speech exactly; when it can't (e.g. MP3), the audio drives the length.

Recorded clips

Mix screen recordings in with the slides. A clip plays through; if its narration runs longer, the last frame holds until the voice finishes. It's letterboxed to the reel's size, never stretched, and its own audio is replaced by the narration.

reel.add("<!doctype html><h1>The demo</h1>", "Here's how it works.")
reel.add_clip("scan.webm", "Click Scan, and every question gets a cited draft.")
reel.add_clip("fill.webm", "Then fill the form.", tail_pad=0.6)

Record the clips any way you like: Playwright's record_video_dir, a screenshot sequence encoded with ffmpeg, or a screen recorder.

Music and stings

Lay a music bed under the narration, and bookend the reel with short stings:

from slidecast import Music, Reel, Sting

reel = Reel(tts=..., music=Music("bed.mp3", volume=0.22, duck=True),
            intro="intro.wav", outro=Sting("outro.wav", volume=0.7), loudness=-16)

The bed loops to the reel's length and fades in and out. volume is its level in the pauses. While someone speaks or a sting plays it dips duck_db lower (12 dB by default): the reel knows when each line starts and ends, so the bed eases down just before the line and swells back in the pause after it, with no pumping between words. The intro plays from the start, and the first narration waits for it (lead_in, which defaults to the intro's length). The outro is timed to end on the last frame. The narration keeps its level. loudness (LUFS) normalizes the final audio, and is off by default. Only the audio is re-encoded; the video stream is copied, unless render(fade_in=..., end_hold=...) asks for a re-encoded master anyway.

Composing a bed and stings

No music to hand? slidecast can write its own, synthesized from scratch with numpy, so there is nothing to license. The bed is a soft lo-fi loop (Fmaj7 – G6 – Em7 – Am7 at 84 BPM: pad, electric-piano arpeggio, bass, a soft kick and shaker). The intro is a rising swoosh that lands on a bell chord at 1.0 s; the outro is a run of falling bells that resolves on Cmaj7.

slidecast compose -o audio/ --length 95 --bpm 84 --seed 7

That writes music_bed.wav, intro_sting.wav (5.1 s) and outro_sting.wav (5.0 s), 16-bit stereo. The same seed gives the same audio. The bed fades out over its last 3 s, so make it a few seconds longer than the reel, or the fade dips each time it loops.

Or let the reel compose them as it renders. It writes them into its work directory and makes the bed outlast the reel:

reel = Reel(tts=..., music="compose", intro="compose", outro="compose", lead_in=2.0)
reel = Reel(tts=..., music=Music("compose", volume=0.18))   # a composed bed, your settings

In Python, music_bed(length, bpm, seed), intro_sting(seed) and outro_sting() return the audio as numpy arrays, and write_wav(path, audio) saves one. The intro's ring-out runs to 5.1 s; a lead_in of about 2 s lets the voice start over its tail instead of waiting for silence.

CLI

slidecast render reel.yaml -o out.mp4 --poster
width: 1280
height: 720
fps: 25
tts:
  provider: kokoro        # kokoro | mlx | gtts | say | silent
  url: http://127.0.0.1:8021/v1/audio/speech
  voice: af_heart
  response_format: wav
music: bed.mp3            # or compose, or {file, volume: 0.22, fade_in: 1.0, fade_out: 2.0, duck: true, duck_db: 12}
intro: intro.wav          # or compose, or {file, volume: 0.75}; the first narration waits for it
outro: {file: outro.wav, volume: 0.7}   # ends with the reel
lead_in: 1.8              # optional: silence before the first narration (default: intro length)
loudness: -16             # optional: LUFS target for the final audio
slides:
  - video: demo.mp4       # a recorded clip, relative to the spec
    narration: "Watch it run."
  - html_file: intro.html
    narration: "Before any of this, here's why it matters."
    tail_pad: 0.8
  - html: "<!doctype html><h1>Step one</h1>"
    narration: ""          # silent slide
    min_duration: 3

The swappable pieces

Text-to-speech — anything with synthesize(text, path) -> seconds | None:

  • KokoroTTS — any OpenAI-compatible /v1/audio/speech endpoint (Kokoro, OpenAI, LocalAI, …). Defaults to WAV so the clip length is measurable.
  • MLXKokoroTTS — Kokoro running in-process on Apple Silicon via mlx-audio, so no server is needed: MLXKokoroTTS(voice="af_heart", speed=1.0, lang_code="a"), or tts: {provider: mlx, voice: af_heart} in YAML. The model (mlx-community/Kokoro-82M-bf16) downloads on first use and loads once per reel. If phonemizing fails with an espeak path under /Users/runner/... (a path baked into the espeakng-loader wheel), run brew install espeak-ng. slidecast then points ESPEAK_DATA_PATH and PHONEMIZER_ESPEAK_LIBRARY at Homebrew's copy itself, unless you've set them.
  • GTTSTTS — Google Translate TTS (gtts).
  • MacSayTTS — macOS's built-in say: offline, no dependencies, WAV. Premium voices (e.g. "Ava (Premium)") are a free download under System Settings › Accessibility › Spoken Content.
  • SilentTTS — a silent track of a fixed length. No dependencies; the default, so a reel renders end to end with nothing configured.

Pass phonetic={r"\bSOC\b": "sock"} to rewrite how tricky tokens are spoken without changing the on-screen text.

Renderer — a context manager exposing screenshot(html, path, *, width, height):

  • PlaywrightRenderer — headless Chromium, launched once per reel (default).
  • ChromeBinaryRenderer — drive an existing Chrome/Chromium binary by path.

Music — Music(file, volume, fade_in, fade_out, duck, duck_db) for the bed, and Sting(file, volume) for the intro and outro. A plain path works for either and uses the defaults; "compose" synthesizes one.

ffmpeg steps are exposed directly (build_segment, build_clip_segment, concat, master, mix_music, poster) and take an injectable runner, so you can compose your own pipeline or test command construction without invoking ffmpeg.

License

MIT

Metadata

Release files for slidecast 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for slidecast 0.4.0
File Size Uploaded Provenance Actions
slidecast-0.4.0.tar.gz 42.8 kB With provenance Details for slidecast-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for slidecast 0.4.0
File Interpreter ABI Platform Actions
slidecast-0.4.0-py3-none-any.whl Python 3 none any Details for slidecast-0.4.0-py3-none-any.whl

Total release size: 76.8 kB

Release files / slidecast-0.4.0.tar.gz

Download URL slidecast-0.4.0.tar.gz
Size 42.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e1118fd6eff60f1d14ef7984e00b4793b9fc33d12374c71e2b76f36e320e5411
BLAKE2b-256 checksum
How to use checksums
497a915f4a811b9eb8edf8d8dfc78b1d4f565b384c299a28676246870e65ec8d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / slidecast-0.4.0-py3-none-any.whl

Download URL slidecast-0.4.0-py3-none-any.whl
Size 34.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bc5f11de8c163b1da748a4e3938c4ceb3ff443abbb044ee37733b0cca994a4a5
BLAKE2b-256 checksum
How to use checksums
6d9f6833d5e4118e28807fdfab11a79b5149b0bd1fe606077bedd753db1df12c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Security for slidecast 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for slidecast-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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