Metadata-Version: 2.4
Name: open-montage
Version: 0.3.0
Summary: Describe media montages and render them into real movies.
Project-URL: Homepage, https://github.com/BAS-More/Open-Montage
Project-URL: Repository, https://github.com/BAS-More/Open-Montage
Project-URL: Issues, https://github.com/BAS-More/Open-Montage/issues
Author: BAS-More
License: MIT
License-File: LICENSE
Keywords: edit-decision-list,media,montage,timeline,video
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: render
Requires-Dist: moviepy<3,>=2.0; extra == 'render'
Description-Content-Type: text/markdown

# Open Montage

A Python toolkit to **describe media montages and render them into real movies**.

Open Montage has two layers:

1. **A tiny, dependency-free model** — build a montage out of clips and read back
   its timeline (the absolute start/end time of every clip), serialize it to JSON.
2. **A video renderer** — turn that montage into an actual MP4: still images are
   held for their duration, video clips are trimmed and stitched together, with an
   optional audio track. Rendering uses [MoviePy](https://zulko.github.io/moviepy/),
   which ships its own ffmpeg, so **no system ffmpeg install is needed**.

`v0.3.0`, part of the BAS-More software library.

## Install

```bash
pip install -e ".[render]"     # model + video renderer
pip install -e ".[dev,render]" # everything, incl. test/lint tools
pip install -e .               # model + CLI only (build/info), no rendering deps
```

The core model has **no third-party dependencies**; only the renderer pulls in
MoviePy + ffmpeg.

## Make a movie

```bash
# 1. Describe the montage (3 sources, 1.5s each)
open-montage build scene1.png clip.mp4 scene2.jpg --duration 1.5 --title "Reel" -o reel.json

# 2. Render it to a video file
open-montage render reel.json -o reel.mp4 --fps 24 --size 1280x720 --audio music.mp3
# Wrote 3 clip(s) → reel.mp4 (4.5s)

# ...with effects: letterbox, crossfades, slow zoom, a title card and captions
open-montage render reel.json -o reel.mp4 \
    --size 1920x1080 --fit contain --transition 0.5 \
    --ken-burns --title "Holiday" --captions
```

- **Images** (`.png .jpg .jpeg .bmp .gif .webp .tif`) are shown as stills for
  their duration.
- **Videos** (`.mp4 .mov .avi .mkv .webm .m4v .mpg`) are trimmed to their duration
  (or used whole if shorter).
- `--size` defaults to the first video clip's resolution, falling back to 1280×720.
- `--audio` lays a track under the whole montage, trimmed to its length.

### Effects

| Flag | What it does |
| --- | --- |
| `--fit contain\|cover\|stretch` | Fit clips to the frame: letterbox (default), fill-and-crop, or stretch. |
| `--transition SECONDS` | Crossfade between consecutive clips. Must be shorter than the shortest clip. |
| `--ken-burns` | Slow centered zoom on still images. |
| `--title TEXT` / `--title-duration S` | Show a title card before the montage. |
| `--captions` | Overlay each clip's `label` as a caption along the bottom. |

From Python:

```python
from open_montage import Clip, Montage, render_montage

montage = (
    Montage(title="Reel")
    .add(Clip("scene1.png", duration=1.5, label="Sunrise"))
    .add(Clip("clip.mp4", duration=3.0, label="Harbour"))
)
render_montage(
    montage, "reel.mp4",
    fps=24, size=(1920, 1080),
    fit="contain", transition=0.5, ken_burns=True,
    title="Holiday", captions=True, audio="music.mp3",
)
```

## Model & inspection (no rendering deps)

```python
from open_montage import Clip, Montage

montage = (
    Montage(title="Holiday")
    .add(Clip("beach.jpg", duration=2.0, label="Beach"))
    .add(Clip("sunset.mp4", duration=4.0))
)

print(montage.total_duration)        # 6.0
for segment in montage.timeline():
    print(segment.clip.source, segment.start, "->", segment.end)

montage.to_json()                    # serialize to a JSON document
Montage.from_json(text)              # ...and back again
```

```bash
open-montage info reel.json
# Reel — 3 clip(s), 4.5s total
#    1. scene1.png  [0s → 1.5s]
#    ...
```

You can also run the CLI as a module: `python -m open_montage ...`.

## Project layout

```
src/open_montage/
  __init__.py     # public API (Clip, Montage, Segment, render_montage, ...)
  montage.py      # pure domain model — no I/O, no dependencies
  render.py       # video rendering via MoviePy/ffmpeg (optional [render] extra)
  cli.py          # argparse command-line interface
  __main__.py     # `python -m open_montage`
tests/            # pytest suite
```

## Development

```bash
pip install -e ".[dev,render]"
ruff check .
pytest
```

Render tests that encode video are skipped automatically if the `render` extra
isn't installed.

## Publishing

Releases are built and published to [PyPI](https://pypi.org/) by
[`.github/workflows/release.yml`](.github/workflows/release.yml) when a `v*` tag
is pushed:

```bash
git tag v0.3.0
git push origin v0.3.0
```

The workflow builds an sdist + wheel, runs `twine check`, and uploads to PyPI.
Authentication uses a **PyPI API token**, configured once:

1. Create the project / reserve the name `open-montage` on PyPI, and generate an
   API token (PyPI → Account settings → API tokens).
2. Add it to this repo as a secret named **`PYPI_API_TOKEN`**
   (Settings → Secrets and variables → Actions → New repository secret).

Then publishing happens automatically on the next `v*` tag. (Prefer keyless
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) instead? Drop the
`with: password:` line from the publish step and configure a Trusted Publisher
for this repo + `release.yml`.)

Build locally to sanity-check before tagging:

```bash
python -m pip install build twine
python -m build && python -m twine check dist/*
```

## License

[MIT](LICENSE) © BAS-More
