Metadata-Version: 2.4
Name: snippet-cast
Version: 0.1.56
Summary: Turn annotated Python snippets into narrated screencast videos.
Author-email: Kasper Munch <kaspermunch@birc.au.dk>
License: MIT
Project-URL: Homepage, https://github.com/munch-group/snippet-cast
Project-URL: Repository, https://github.com/munch-group/snippet-cast
Project-URL: Documentation, https://munch-group.org/snippet-cast
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: <3.15,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pillow
Requires-Dist: pygments
Provides-Extra: piper
Requires-Dist: piper-tts<2,>=1.5.0; extra == "piper"
Provides-Extra: jupyter
Requires-Dist: ipython; extra == "jupyter"
Dynamic: license-file

# snippet-cast

Turn an annotated Python snippet into a narrated screencast video.

Narration is written as trailing `#:` comments on the snippet's own lines, so
the input stays valid, runnable Python:

```python
def fib(n):             #: We define fib, taking one argument, n.
    a, b = 0, 1          #: Start from the first two Fibonacci numbers.
    for _ in range(n):   #: Loop n times.
        a, b = b, a + b  #: Advance the pair; b becomes the running sum.
    return a             #: Return a — the nth Fibonacci number.
```

Each `#:` line becomes one "beat": the code is revealed up to that line, the
line is highlighted, and its narration is spoken. `snippet-cast` renders
syntax-highlighted code frames with a progressive reveal, a Python-Tutor-style
live variable panel, optional burned-in captions and a typing-in animation,
synthesises speech per line, and stitches everything into an MP4 with ffmpeg.

## Installation

Requires **Python 3.10+** and **ffmpeg** (with `ffprobe`) on `PATH`.

```bash
pip install snippet-cast
# or
pixi add snippet-cast
# or
conda install -c munch-group snippet-cast
```

## Usage

```bash
snippet-cast snippet.py -o out.mp4 --tts silent --subtitles   # fast, voiceless proof
snippet-cast snippet.py -o out.mp4 --typing --subtitles       # type each new line in
snippet-cast loop.py    -o out.mp4 --every --subtitles        # animate each loop iteration
```

Or from Python:

```python
from snippet_cast import build

build("snippet.py", "out.mp4", tts="silent", subtitles=True)
```

Or in a Jupyter notebook — write the snippet directly in a cell instead of a
separate `.py` file:

```
pip install snippet-cast[jupyter]
```

```
%load_ext snippet_cast.magic
```

```
%%snippet-cast -o out.mp4 --tts silent --subtitles
def fib(n):             #: We define fib, taking one argument, n.
    a, b = 0, 1          #: Start from the first two Fibonacci numbers.
    for _ in range(n):   #: Loop n times.
        a, b = b, a + b  #: Advance the pair; b becomes the running sum.
    return a             #: Return a — the nth Fibonacci number.
result = fib(7)          #: Call fib with seven; result becomes {result}.
```

The cell magic takes the same flags as the CLI and displays the rendered MP4
inline.

`%%snippet-cast` has to be the cell's first line — and so do Quarto's `#|`
directives, so they cannot share a cell. `snippet_cast.video()` is the same
notebook front end as a plain function call, taking the snippet as a string:

```python
#| fig-column: margin
#| echo: false
from snippet_cast import video

video("""
def fib(n):              #: We define fib, taking one argument, n.
    a, b = 0, 1          #: Start from the first two Fibonacci numbers.
    for _ in range(n):   #: Loop n times.
        a, b = b, a + b  #: Advance the pair; b becomes the running sum.
    return a             #: Return a — the nth Fibonacci number.
result = fib(7)          #: Call fib with seven; result becomes {result}.
""", tts="silent", subtitles=True)
```

Every parameter is the same-named flag (`trace=False` for `--no-trace`),
resolved the same way — argument, then `SNIPPET_CAST_<NAME>`, then the
default — and it returns the video to display, so leave it as the cell's last
expression. Don't make the snippet an f-string: `{result}` is snippet-cast's
own interpolation. Given no `out`/`name`/`output_dir`, the video goes to
`.snippet-cast/<hash of the snippet>.mp4`, so re-running an unchanged cell
reuses its file. `build()` above is the equivalent for a snippet that already
lives in its own `.py` file.

See [SETUP.md](SETUP.md) for all TTS backends — the zero-setup `say` (macOS)
and `manual` (your own recordings, including `--record` for recording live
via the microphone) backends, plus configuring Piper (local) and ElevenLabs
(cloud) — and `snippet-cast --help` for all options.

## Configuration

`-o`/`--output` sets an explicit path; without it, `-n`/`--name` (default
`out`) and `-d`/`--output-dir` (default `.snippet-cast`, created if missing)
build one as `output-dir/name.mp4`:

```bash
snippet-cast snippet.py --tts silent                        # -> ./.snippet-cast/out.mp4
snippet-cast snippet.py --tts silent -n intro               # -> ./.snippet-cast/intro.mp4
snippet-cast snippet.py --tts silent -n intro -d ./videos   # -> ./videos/intro.mp4
```

`.snippet-cast/` is a hidden directory beside your work that ignores itself in
git, so renders don't scatter `out.mp4` through your folder or your history.
`%%snippet-cast` cells write there too (under a name hashed from the cell), so
both front ends keep their videos in one place. `-d .` puts them back in the
current directory.

Every option (except `-o`/`--output`) also has a `SNIPPET_CAST_<NAME>`
environment variable default — an explicit flag always wins over its env
var:

```python
import os
os.environ["SNIPPET_CAST_TTS"] = "say"
os.environ["SNIPPET_CAST_SUBTITLES"] = "1"
os.environ["SNIPPET_CAST_PAUSE"] = "0.6"
os.environ["SNIPPET_CAST_OUTPUT_DIR"] = "./videos"
```

set in one notebook cell, applies to every `%%snippet-cast` cell after it
(picked up fresh each time, so setting it in a later cell still works).

Runs are **quiet by default** — the per-beat progress and every `note:` need
`-v`/`--verbose`. A snippet that won't compile or raises part-way still
reports on stderr, as do errors. The other shipped defaults are `--tts say`
(macOS; pass `--tts silent` elsewhere) and `--order exec`, which highlights
each line on the way in and again on the way out, in the order Python visits
them; `--order source` gives the plain top-to-bottom playback.

Toggle flags (`--every`, `--subtitles`, `--typing`, `--record`,
`--export-script`) accept `--no-X` to override an env-var-forced default back
off for one run.

## Development

This repository is built from the munch-group library template.

### Initial set up

```bash
pixi run init
```

### Get updates to upstream fork

Add upstream if not already added

```bash
git remote add upstream https://github.com/munch-group/snippet-cast.git
```

Fetch upstream changes

```bash
git fetch upstream
```

Either rebase your changes on top of upstream (cleaner history)

```bash
git rebase upstream/main
```

Or, merge upstream into your fork (preserves history)

```bash
git merge upstream/main
```

If you want to see what's changed upstream before applying:

```bash
git log HEAD..upstream/main
```

See the actual diff

```bash
git diff HEAD...upstream/main
```

Then push your updated fork:

```bash
git push origin main
```

If you rebased and need to force push
    
```bash
git push origin main --force-with-lease
```
