rotogod/DESIGN.md
m3ultra 874aaaefa3 feat: ROTOGOD v0 — SAM-2 rotoscoping web app (load/scrub/trim → click actor → track → alpha export)
FastAPI + PyAV + transformers SAM2.1 on MPS. Relative-URL frontend (works
bare or behind digalot.fyi/rotogod/), ROTOGOD_ROOTS clip-path allowlist,
fwd+rev propagation, ProRes 4444 / WebM / matte / greenscreen exports.
deploy/rotogod.launchd.plist = always-on tailnet-bound instance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:02:20 +10:00

69 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ROTOGOD — design
Local rotoscoping tool for m3ultra. Load a video → scrub → cut out a scene → click actors →
SAM 2 propagates masks across the clip → export alpha (ProRes 4444 / WebM / matte / greenscreen).
## Why these choices
| Decision | Choice | Why |
|---|---|---|
| UI | Local web app (FastAPI + vanilla JS canvas), port **8484** | Matches the fleet pattern (fluxgod :8890, paradramorama :8474, m3panel :8790). 84748479 are already taken on this box. Browser canvas is plenty fast for 7201080p preview; no Qt dependency. |
| Segmentation | **SAM 2.1 via `transformers`** (`Sam2VideoModel` / `Sam2VideoProcessor`) | pip-installable (no facebookresearch git clone, no hydra configs), weights from HF hub, actively maintained, pure-torch so MPS works. API verified against installed transformers 5.14. |
| Model size | `facebook/sam2.1-hiera-small` default, `-large` for finals | Small = snappy interactive loop; large = better masks. Both cached in `~/.cache/huggingface`. Switch with `ROTOGOD_SAM2=facebook/sam2.1-hiera-large`. |
| Python | 3.12 venv via uv (`.venv/`) | Homebrew python3 is 3.14 — no torch wheels yet. |
| Decode | PyAV (bundled ffmpeg), seek-to-keyframe + roll forward, RAM frame cache | Arbitrary scrubbing; 256GB UMA means the cache can be generous (default 600 frames ≈ 1.6GB at 1080p). |
| Precision | float32 on MPS | bf16 on MPS still has op gaps; fp32 is safe and fast enough on M3 Ultra. |
## Architecture
```
web/ (canvas UI: scrub, in/out, click prompts, overlays, export buttons)
│ JSON/JPEG/PNG over localhost:8484
rotogod/server.py FastAPI — endpoints below, serves web/
rotogod/video.py Clip: PyAV decode, frame(i) → RGB ndarray, JPEG previews, thumbstrip
rotogod/segment.py Sam2Engine (lazy model load) + RotoSession (frames, clicks, masks)
rotogod/jobs.py background threads with progress (propagate, exports)
rotogod/export.py ffmpeg: scene trim, ProRes 4444 alpha, VP9 alpha, matte, greenscreen
```
State is deliberately single-user/single-clip (module-level), same as fluxgod. Binds 127.0.0.1.
## The loop
1. **Open** a clip by path (`POST /api/open`). Meta + thumbstrip render; scrub via `GET /api/frame/{i}` (JPEG, cached decode).
2. **Cut the scene**: set in/out points (i/o keys), optional lossless keyframe-snapped trim or frame-exact re-encode (`POST /api/trim`).
3. **Roto session** (`POST /api/roto/start`): frames [in..out] are decoded once into RAM and handed to `processor.init_video_session(video=frames, inference_device="mps", video_storage_device="cpu")`.
4. **Click prompts**: left-click = positive, alt/right-click = negative, per object. The UI keeps the full point list per (frame, object) and sends it whole each time (SAM 2 replaces rather than accumulates per-frame inputs). Backend: `add_inputs_to_inference_session(...)` then `model(inference_session, frame_idx)` → instant mask overlay on that frame.
5. **Track** (`POST /api/roto/propagate`): background job runs `propagate_in_video_iterator` twice — forward from the prompted frame, then reverse to cover frames before it (SAM 2 only propagates one direction per pass); masks land per frame (packbits-compressed bools keyed by object) and the UI progress bar + overlay refresh as they stream in.
6. **Correct drift**: scrub to the bad frame, add +/- clicks there, re-track from that frame. SAM 2's memory bank handles re-propagation.
7. **Export** (`POST /api/export`): union of object masks → RGBA PNG sequence (optional edge feather) → ffmpeg:
- `prores`: ProRes 4444 `.mov`, `-pix_fmt yuva444p10le` (Resolve/FCP/Premiere-ready)
- `webm`: VP9 `yuva420p` (browser alpha)
- `matte`: grayscale h264 mp4
- `green`: comp over green, h264 mp4 (quick eyeball check)
## API surface
```
POST /api/open {path} GET /api/meta GET /api/frame/{i}?w=
GET /api/thumbs?n= POST /api/trim {in_f,out_f,exact}
POST /api/roto/start {in_f,out_f} POST /api/roto/click {frame,obj,points,labels}
POST /api/roto/propagate {start?} GET /api/roto/overlay/{frame}.png
GET /api/roto/status POST /api/roto/reset
POST /api/export {kind,feather} GET /api/job/{id} GET /api/status
```
Frame indices in the roto API are absolute clip frames; the session maps to local range internally.
Outputs land in `output/` (gitignored).
## Known limits / phase 2+
- **Hair & motion blur**: SAM 2 masks are binary. The feather param is a stopgap; real fix is a
matting pass — **MatAnyone** (preferred) or RVM taking SAM 2's mask as guidance → soft alpha.
Slots in as a second engine in `segment.py` + an export-time toggle.
- **Long clips**: propagate memory grows with range; UI warns > 2000 frames. Chunk-and-stitch later.
- **Audio** is dropped in roto exports (kept in scene trims).
- **Multi-clip / project files**: out of scope for v0; one clip + one session at a time.
- Possible future: farm propagation to MODELBEAST queue (:8777) as a job type; not needed while
this box is the only one running it.