Metadata-Version: 2.4
Name: northnoir
Version: 1.3.0
Summary: Local AI-drawn video: Ruvil 2.1 code-drawn scenes by default, or Ruvil 7.3 hand-drawn frame-by-frame JS/image/copy cels with your own API keys.
Author: North Noir
License: Proprietary
Project-URL: Homepage, https://northnoir.com/cli
Project-URL: Documentation, https://northnoir.com/cli
Project-URL: Source, https://github.com/syver777/NorthNoir/tree/main/packages/northnoir-cli
Keywords: ai-drawn-video,animation,cli,playwright,ffmpeg
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: playwright==1.63.0
Requires-Dist: Pillow==12.3.0
Requires-Dist: numpy==2.5.3; python_version >= "3.12"
Requires-Dist: numpy==2.2.6; python_version < "3.12"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"

# northnoir — the local CLI for AI-drawn video

Make **AI-drawn video** from your terminal. Two distinct models:

- **Ruvil 2.1 (`ruvil_2_1`, Code-drawn / `.js`-drawn) — the DEFAULT.**
  NorthNoir's default AI-drawn video model: a JavaScript (`.js`) canvas
  drawing engine inspired by hand-drawn style kits (AnidoodleDraw — 31
  styles — plus the RuvilMarks paper-and-ink marks) and closer to general
  AI-drawn video. The AI model writes a `.js` canvas drawing program
  (`scene.html`), renders every frame at 30 fps (the local CLI renders at
  24 fps by default), inspects its own frames, and fixes what is wrong
  across up to 3 review rounds.
- **Ruvil 7.3 (`ruvil_7_3`, public alias `latest`) — SELECTABLE.**
  Frame-by-frame hand-drawn animation where **each frame is drawn/generated
  as its own 2D image** (`draw_mode: "image"` — images as frames, classical
  hand-drawn animation timing: key poses, anticipation, ease-in/out
  breakdowns, secondary motion, anchored to reference art when provided).
  Every generated frame image is saved to the section frame strip to
  inspect.

Select Ruvil 7.3 with `northnoir r73`, `northnoir draw-frames`, or
`northnoir draw --model latest`. Local runs need no North Noir account or
cloud tokens: use your own provider keys (free/BYOK), or **one** NorthNoir
API key (`nn_live_…`) for image, sound, voice and LLM calls — see
"One NorthNoir API key" below.

Your agent (Claude Code, OpenCode, Codex, Gemini CLI, Cursor) writes the
drawing code, runs the checks, *looks at its own frames*, fixes what is wrong,
and renders the final MP4. That loop is the product.

## Install

Recommended (isolated, puts `northnoir` on your PATH):

```sh
pipx install northnoir
northnoir doctor --install   # installs Chromium via Playwright, then verifies
northnoir doctor             # Chromium, ffmpeg, pinned libraries (SHA256)
```

Alternatives:

```sh
uv tool install northnoir    # same PyPI package, via uv
pip install northnoir        # into the current Python environment
```

From source: `git clone https://github.com/syver777/NorthNoir.git`, then
`pipx install ./NorthNoir/packages/northnoir-cli`.

The npm package is **not published**. `bin/northnoir.js` in this repository is
a thin Node shim over the Python package: it needs Python 3.10+ with
`playwright`, `Pillow` and `numpy` already installed, so it is not a
stand-alone `npx` installer and is not advertised as one. The PyPI package
above is the one supported distribution.

Your host coding agent and your machine do the work: the agent's own
usage/subscription terms apply to its model use, and rendering uses your local
CPU and disk. The CLI is **free with your own API keys** (or one NorthNoir
API key); North Noir charges nothing for local rendering work, but
image/LLM/audio providers may charge for usage — and calls made through a
NorthNoir API key are charged from your NorthNoir token wallet.
Ruvil 2.1 code-drawn rendering requires no provider keys.

Requirements: Python 3.10+, `ffmpeg` on PATH (`brew install ffmpeg` on macOS,
`sudo apt install ffmpeg` on Debian/Ubuntu). Chromium is installed for you by
`northnoir doctor --install` (Playwright). Docker is an optional transparent
fallback when a local `ruvil-js-renderer:local` image exists.

## Quickstart

### Ruvil 7.3 — Hand-drawn frame-by-frame (explicit selection)

```sh
northnoir draw --model latest --ref reference.png --out film --plan-only --no-llm
northnoir draw --model ruvil_7_3 --draw-mode image --ref reference.png --out film --sfx elevenlabs
northnoir r73 --ref reference.png --out film --voice "A long day comes to a close."
```

`r73` and its alias `draw-frames` explicitly select Ruvil 7.3 (no `--model`
needed). The separate `draw` command has `--model latest | ruvil_7_3 |
ruvil_2_1`; omitting `--model` selects **Ruvil 2.1**, not the latest model.
The legacy `ruvil_2_1_js` alias still resolves to `ruvil_2_1`.
Ruvil 7.3 director films accept an arbitrary `--brief` or an editable JSON
`--plan` (see below). Without either, the CLI uses a six-section **9-to-7
Day in the Life** demo template. The hosted model draws at 6 fps; the CLI
demo draws `--fps 4` (default) frames/s, held into 24 fps video, with six
sections of four seconds by default. Director plans specify each drawing's
hold in 24 fps delivery frames.

**When to use which:** use Ruvil 2.1 for an arbitrary scene your agent authors
and animates directly in code (`.js`-drawn `scene.html`), with procedural
audio and no provider keys. Use Ruvil 7.3 for hand-drawn frame-by-frame films
where each frame is generated as its own image — custom director films, the
shipped Day-in-the-Life template, editable individual frames, X-sheets and
image-based character references. Copying an existing frame sequence is
available only as an internal/experimental training-R&D mode.

#### Director films (`--brief` / `--plan`)

```sh
northnoir r73 --brief "A cat follows a butterfly, then naps under a tree." \
  --target-seconds 12 --ref cat.png --out cat-film
northnoir r73 --plan film.json --music-file score.wav --sfx code --out film
# Fully local planning smoke: no image, audio or LLM API calls.
northnoir r73 --brief "A cat naps. It wakes up." --plan-only --no-llm --out plan
```

The staged planner (default) makes an outline, then plans individual
sections; `--planner single` opts into one larger planning call. Identity
references (`--ref`, repeatable), `--pose-ref`, `--style-ref` and
`--master-ref` guide the drawings. Plans retain editable poses, timing,
continuity, camera moves and sound cues. Use `--redraw <drawing-id>` for a
targeted drawing edit and `--only <section-slug>` to resume a section.

Use `--music-file` for a local score, or `--music` to request music from the
configured provider. `cost-ledger.json` is flushed after every provider
call, including planning, and accumulates costs across resumed runs.
`--max-image-calls 0` means **unlimited**, not a dry run. `--plan-only`
skips drawing and audio generation; add `--no-llm` (or supply `--plan`) to
avoid planning API calls as well. The bundled skill documents the plan schema.

#### One NorthNoir API key (frames, sound, voices and LLM through the API)

Sign up on a paid NorthNoir plan, create **one** `nn_live_…` API key at
[northnoir.com/developer](https://northnoir.com/developer), and give it to the
CLI once:

```sh
northnoir auth nn_live_...        # alias: northnoir login (chmod 0600 keys.json)
# or: export NORTHNOIR_API_KEY=nn_live_...
northnoir draw --model ruvil_7_3 --ref reference.png --out film --sfx elevenlabs
```

That single key replaces every separate provider key for local Ruvil 7.3
runs — the frame images (image model), the ElevenLabs sound effects, the
OpenAI or ElevenLabs voices and narration (`--voice-provider` /
`--voice-id`), and the X-sheet LLM enrichment all go through the NorthNoir
API. The same key also drives `northnoir cloud` (below). A direct
`OPENAI_API_KEY` / `ELEVENLABS_API_KEY` / LLM key still wins when present;
`--api-key nn_live_…` passes the key for one run without saving it. Local
rendering stays free; API-backed generation is charged from your NorthNoir
token wallet.

#### Ruvil 7.3 draw modes

`--draw-mode` (alias `--medium`) is **Ruvil 7.3-only** and **defaults to
`image`** — Ruvil 7.3 is strictly images as frames:

- **`image` (default):** hand-drawn frame-by-frame images — each frame is
  drawn/generated as its own 2D image with classical hand-drawn timing (key
  poses, anticipation, ease-in/out breakdowns, secondary motion), anchored
  to the reference drawing. Each key is anchored to the reference and
  previous key; in-betweens use the reference plus both surrounding keys.
  Requires your `OPENAI_API_KEY`; optional pose enrichment uses your LLM
  key. Every generated frame image is saved to the section frame strip to
  inspect or download.
- **`js` (internal/experimental — training R&D, not a user-facing choice):**
  local code-drawn ink frames from the shipped JS template. Saves editable
  `cels.js` and `scene.html`; no image or planning API call. `--ref` is
  accepted for workflow consistency but does not alter this template.
- **`copy` (internal/experimental — training R&D, not a user-facing choice):**
  a vision LLM writes a separate `.js` Canvas drawing of each source frame.
  Use `--source-frames <film-or-frames-dir>` instead of `--ref` and your
  `OPENROUTER_API_KEY` (optional `--llm-model`). Zero image API calls, no raster
  embedding or automatic tracing. It is an approximation, not a pixel-exact
  guarantee. Native-resolution copies, comparisons and metrics are retained;
  delivery frames fit the selected output canvas without stretching. Existing
  section audio is reused; `--sfx off` permits a silent copy without source audio.

**Why Ruvil 7.3 has no user-facing `.js` draw mode:** a custom-trained AI
model will later draw Ruvil 7.3 frame images as well as image-generation
models do — that model is in R&D and its quality is not there yet. Until it
is ready, `.js`-drawn is exclusively Ruvil 2.1 (the default) and Ruvil 7.3 is
strictly images as frames. The `js` and `copy` modes stay accepted by the CLI
only as internal/experimental flags for training R&D.

```sh
northnoir r73 --ref reference.png --aspect 4:5 --sfx code --out portrait
northnoir draw-frames --ref reference.png --draw-mode image --ar 9:16 --out phone
# internal/experimental (training R&D):
northnoir r73 --draw-mode copy --source-frames original --aspect 21:9 --sfx off --out copies
```

#### Six aspect ratios

`--aspect` (alias `--ar`) applies to **all three Ruvil 7.3 draw modes**
(including the internal `js` / `copy` modes), on
`r73`, `draw-frames` and `draw --model latest`. Unknown values normalise to
Desktop `16:9`. Plans/X-sheets, saved delivery frames and H.264 MP4s
record/use these dimensions; audio remains 48 kHz stereo.

| Format | Aspect | Output pixels |
|---|---|---|
| Desktop (default) | `16:9` | 1920×1080 |
| Phone | `9:16` | 1080×1920 |
| Square | `1:1` | 1080×1080 |
| Portrait feed | `4:5` | 1080×1350 |
| Cinematic | `21:9` | 2520×1080 |
| Retro TV | `4:3` | 1440×1080 |

Image mode (the default) asks the provider for landscape/portrait/square
frames, then centre-crops to the exact output dimensions; the internal
`js` / `copy` modes fit their drawings proportionally on paper, not stretched.
Use a fresh `--out` directory
when changing draw mode or aspect; existing film files are preserved.

Set provider keys in your environment or `~/.config/northnoir/keys.json`.
The file accepts provider ids (`openai`, `openrouter`, `deepinfra`,
`elevenlabs`, `fal`, `northnoir`) or environment-variable names, plus
`api_url` for a custom NorthNoir API base; environment values win. The
free/BYOK path uses your own provider keys; the one-key path above stores
`northnoir: nn_live_…` (via `northnoir auth`). Never put keys in briefs,
scene files, reports or source control.

- `OPENAI_API_KEY` is required for the default `--draw-mode image` (each
  frame generated as its own image) — unless a `NORTHNOIR_API_KEY` draws the
  frames through the NorthNoir API instead.
- `OPENROUTER_API_KEY` is required for new `--draw-mode copy` vision programs
  (internal/experimental, training R&D).
- Optional `OPENROUTER_API_KEY` / `DEEPINFRA_API_KEY` enrich poses (OpenAI is
  also supported); with only a `NORTHNOIR_API_KEY` the enrichment runs on the
  NorthNoir AI-drawn LLMs. `--no-llm` skips enrichment.
- `ELEVENLABS_API_KEY` supplies SFX and `--voice` narration.
  `ELEVENLABS_VOICE_ID` selects the voice. `FAL_KEY` is an SFX fallback.
  `--voice-provider openai|elevenlabs` and `--voice-id <voice>` pick the
  narration voice explicitly (OpenAI `alloy`/`ash`/`coral`/`echo`/`fable`/
  `nova`/`onyx`/`sage`/`shimmer`/`verse`, or an ElevenLabs voice id/name) —
  with one `NORTHNOIR_API_KEY` either voice provider runs through the API.
- `--sfx code` uses deterministic local Foley; `--sfx off` omits sound.
  Failed ElevenLabs SFX fall back to code Foley with the source recorded.
- `--plan-only` writes plans without image/audio calls. With `--brief`, add
  `--no-llm` to avoid planning provider calls too, or supply a local `--plan`.
  A fully local run of the internal `js` mode uses `--sfx code` or `--sfx off`,
  without `--voice`.

Outputs: `plan.json`, per-section `xsheet.json`, `frames/fNNNN.jpg`, contact
sheets, SFX WAVs, `audio.wav`, `section.mp4`, `all-frames/`, and the stitched
`day-in-the-life-r73.mp4` (image — the default), `day-in-the-life-r73-js.mp4`
(internal `js` mode), or `day-in-the-life-r73-copies.mp4` (internal `copy`
mode). Optional `narration.wav` is saved separately, not
muxed into the film. Inspect the saved frames/contact sheets and verify the
film with `ffprobe`. Use `--only <section-slug>` to resume selected sections.

### Ruvil 2.1 — Code-drawn / `.js`-drawn (default)

Ruvil 2.1 (`ruvil_2_1`) is NorthNoir's default AI-drawn video model: a
JavaScript (`.js`) canvas drawing engine inspired by the hand-drawn style
kits (AnidoodleDraw's 31 styles + the RuvilMarks paper-and-ink marks) and
closer to general AI-drawn video. The AI model writes a `.js` canvas drawing
program (`scene.html`) that paints every frame (30 fps in the hosted model;
the local CLI renders at 24 fps by default), then inspects its own frames
and fixes what is wrong across up to 3 review rounds.

```sh
northnoir init my-film --title "My Film"     # scaffold scene.html
northnoir check my-film/scene.html --still   # gate 1: frame 0 + report
northnoir check my-film/scene.html           # gate 2: sampled frames + quality
northnoir render my-film/scene.html -o my-film/my-film.mp4
# Equivalent explicit model route (writes output/scene.mp4):
northnoir draw --model ruvil_2_1 --scene my-film/scene.html --out output
# Omitted --model means Ruvil 2.1 too:
northnoir draw --scene my-film/scene.html --out output
```

Or hand the whole job to your agent: install the bundled skill (below), then
ask for a video. The skill teaches the agent the contract and the loop.

## The Ruvil 2.1 4-step agent loop

1. **Author** — write `scene.html`: one self-contained page whose JavaScript
   paints every frame of a 1920×1080 canvas. Frame 0 already shows the
   finished scene.
2. **Still check** — `northnoir check scene.html --still`, then *view*
   `frame-0000.png`. Fix composition, scale and contrast before animating.
3. **Motion check** — `northnoir check scene.html`, view `contact-sheet.png`,
   read the readability/collision/composition flags, fix, re-check (up to 3
   improvement rounds; keep the best version).
4. **Render** — `northnoir render scene.html -o out.mp4`, view the poster,
   verify with `ffprobe` (1920×1080, H.264, declared duration, stereo audio).

## `northnoir auth` + `northnoir cloud` (cloud rendering on one API key)

With one `nn_live_…` key — `northnoir auth nn_live_...` (alias
`northnoir login`), or `NORTHNOIR_API_KEY` — `northnoir cloud` generates in
the NorthNoir cloud and downloads the results locally: Ruvil 7.3
(`--model latest`) and Ruvil 2.1 (`--model ruvil_2_1`) videos with their
drawn frames, plus images, voices, sound effects and music.

```sh
northnoir auth nn_live_...          # save the key (or: northnoir login)
northnoir auth --status             # configured? (--logout removes it)
northnoir cloud models --json       # video/drawing/image/LLM models, voices, aspects
northnoir cloud generate --prompt "A lighthouse keeper climbs the stairs" \
  --model latest --aspect 16:9 --seconds 8 --sfx --out cloud-film
# polls the job, then writes cloud-film/video.mp4 and cloud-film/frames/f0001.jpg…
northnoir cloud frames <job_id> --out frames --reroll 1,2 --feedback "cleaner line"
northnoir cloud image --prompt "..." -o still.jpg --ref reference.png
northnoir cloud sfx --prompt "waves against rocks" -o waves.mp3 --seconds 4
northnoir cloud voice --text "A long day comes to a close." -o line.mp3 \
  --provider openai --voice alloy
northnoir cloud music --prompt "warm orchestral bed" -o bed.mp3 --seconds 30
northnoir cloud jobs [job_id] [--cancel] --json
```

`--no-wait` returns the job id instead of polling. `--out` takes a directory
(`video.mp4` + `frames/`) or a `.mp4` path. `--ref` sends a
`reference_image` to the run; `--drawing-model` picks the AI-drawn LLM.
Every command prints JSON. The routes behind them are documented at
[northnoir.com/api-docs](https://northnoir.com/api-docs) —
`POST /v1/drawn-video`, `GET /v1/jobs/{id}/frames`, `POST /v1/images`,
`GET /v1/voices`, `POST /v1/chat` and `POST /v1/audio/*`.

## Commands

| Command | What it does |
|---|---|
| `northnoir draw --scene scene.html --out <dir>` | Ruvil 2.1 Code-drawn / `.js`-drawn (default); `--model latest` / `ruvil_7_3` explicitly selects Ruvil 7.3 instead. |
| `northnoir r73` / `northnoir draw-frames --ref <drawing> --out <dir>` | Explicit Ruvil 7.3 Hand-drawn frame-by-frame — each frame generated as its own image. `--draw-mode image` (default); `js` / `copy` are internal/experimental (training R&D); six ratios via `--aspect` / `--ar`. Copy uses `--source-frames` instead of `--ref`. |
| `northnoir auth [nn_live_…] [--status] [--logout] [--api-url URL]` (alias `login`) | Save, show or remove the one NorthNoir API key in `~/.config/northnoir/keys.json` (0600). That key drives local Ruvil 7.3 image/SFX/voice/LLM calls and every `northnoir cloud` command. |
| `northnoir cloud <models\|voices\|generate\|frames\|image\|sfx\|voice\|music\|jobs> [...]` | Generate in the NorthNoir cloud on that key and download the results: videos (Ruvil 7.3 `--model latest` and Ruvil 2.1 `--model ruvil_2_1`) + drawn frames, images, voices, SFX and music. `generate` (alias `video`) polls the job unless `--no-wait`. Always prints JSON. |
| `northnoir doctor [--install] [--json]` | Verify Chromium/Playwright, `ffmpeg`/`ffprobe`, and the pinned scene libraries (SHA256). `--install` runs the guided setup (Playwright Chromium) first. |
| `northnoir init <dir> [--title "..."] [--duration 5] [--fps 24] [--style <id>] [--force] [--json]` | Scaffold `<dir>/scene.html` (working `window.SCENE` starter) + `<dir>/.northnoir/`. |
| `northnoir check <scene.html> [--still] [--frames 0,12,24,...] [--out <dir>] [--json]` | Deterministic observer + quality + audio check. Writes full-res `frame-NNNN.png` files, `contact-sheet.png`, `report.json` (and `audio.wav` when the scene has audio) to `<scene_dir>/.northnoir/check/` by default. Always prints JSON. |
| `northnoir render <scene.html> [-o <output.mp4>] [--fps 24] [--duration <sec>] [--no-audio] [--json]` | Render every frame at 1920×1080, render `SCENE.audio` offline (48 kHz stereo), mux H.264 + AAC. Writes `<output>.poster.png` and `<output>.sheet.png`. Always prints JSON. |
| `northnoir styles [--json]` | List the 31 AnidoodleDraw style ids and the RuvilMarks helpers. |
| `northnoir skill [install] [--install] [--json]` | Print the bundled `northnoir-draw` skill path; `install` copies it into the agent skill folders. |
| `northnoir --version` | Print the CLI version. |

Exit codes: `0` only when the report is `ok`, `1` on scene/encode failure,
`2` on usage errors.

`check` JSON: `ok`, `still_only`, `errors`, `quality` (readability / collision /
composition + `flags`), `cues`, `cue_errors`, `audio_ok`, `metrics`
(deterministic, canvas size, blank/frozen frames), `frame_paths`,
`contact_sheet_path`, `report_path`. Quality flags are review evidence — they
do not by themselves flip `ok`.

`render` JSON: `ok`, `output_mp4`, `poster_png`, `contact_sheet_png`,
`frames_rendered`, `duration_s`, `fps`, `has_audio`, `size_bytes`, `elapsed_s`.

## The `window.SCENE` contract (cheat-sheet)

- Canvas is **1920×1080**. Declare `SCENE.fps`, `SCENE.frames` (or
  `SCENE.duration`), `SCENE.width/height`, and `SCENE.canvas`.
- `SCENE.renderFrame(n)` is a **pure function of `n`**: no unseeded randomness,
  no wall-clock reads for drawing decisions (the renderer re-seeds per frame,
  so renders are deterministic and re-renders are byte-identical).
- `SCENE.init()` bakes textures once and registers subjects/blockers:
  `window.__ruvilSubjects.register(id, role, boxFn)` /
  `window.__ruvilBlockers.register(...)`, keeping the declarative
  `SCENE.subjects` / `SCENE.blockers` mirrors in sync. Draw the main subject
  inside `window.__ruvilSubjects.draw('hero', () => { ... })` so the checker
  can measure it.
- `SCENE.cues`: `[{ t, prompt, seconds }]` — timed sound beats, validated
  against the scene duration.
- `SCENE.audio(ctx, info)`: procedural audio scheduled on the
  `OfflineAudioContext` you are given (48 kHz stereo out). Keep peaks ≤ 0.7,
  seed every noise source, sync hits to your cues.
- Only five pinned library URLs may be loaded — every other network request is
  blocked by the renderer:
  `https://libs.ruvil/p5.min.js`, `rough.min.js`, `simplex-noise.js`,
  `ruvil-marks.js`, `anidoodle-draw.js` (served offline from the package,
  SHA256-verified by `doctor`).

## Kit reference

- **RuvilMarks** (`ruvil-marks.js`): `paper(ctx, o)` (bake the paper ground
  once in `init()`), `wash`, `stipple` (blue-noise dots; density = tone),
  `hatch` (spacing = tone), `rng(seed)` (mulberry32), `hash(a, b)` (boil
  outlines 1–3 px via `hash(boilIndex, strokeId)`).
- **AnidoodleDraw** (`anidoodle-draw.js`): `AnidoodleDraw.draw('<id>', ctx,
  frame, W, H)` plates one of 31 finished style scenes under your scene;
  `AnidoodleDraw.styles` lists the ids at runtime. Run `northnoir styles`.

## The agent skill

```sh
northnoir skill install
```

copies the bundled `northnoir-draw` skill into `.claude/skills/`,
`.agents/skills/`, `~/.config/opencode/skills/` and `~/.claude/skills/`, so
Claude Code, OpenCode, Codex, Gemini CLI and Cursor agents learn the contract,
the 4-step loop, the quality-flag fix table and the craft rules before they
draw. `northnoir skill` prints the bundled path.

## CLI vs Studio vs MCP

| | CLI (this package) | Web Studio | MCP server |
|---|---|---|---|
| Where it runs | Your machine | North Noir cloud | Your chat app |
| The model | Ruvil 2.1 (default); explicitly select Ruvil 7.3 | The model you pick in Studio | The model you pick |
| Cost | Free CLI with your own API keys; provider usage billed by providers | North Noir tokens (free plan: your own API key) | North Noir tokens |
| Best for | Saved frames + BYOK audio, or agent-authored `.js` canvas scenes | Timelines, voice/music, multi-section films at scale | Drawing from Claude/Cursor chats |

## Troubleshooting

- **`doctor` says Chromium will not launch** — run `northnoir doctor
  --install`. On Linux you may also need Playwright's system libraries:
  `python3 -m playwright install-deps chromium` (asks for sudo; the CLI never
  runs sudo itself).
- **`ffmpeg` missing** — install it with your OS package manager (above);
  `doctor` prints the exact hint for your platform.
- **The Node shim cannot start Python / deps missing** — the npm package is
  not published; `bin/northnoir.js` is a repo-only thin shim. Use
  `pipx install northnoir` (recommended), or install the pinned deps into the
  Python the shim finds and set `NORTHNOIR_PYTHON` to choose the interpreter.
- **A library SHA256 fails** — the vendored runtime is corrupted or hand-edited.
  Reinstall the package; maintainers refresh it with `tools/sync_runtime.py`
  (single source of truth: `cloudrun/ruvil-js-renderer/` in the repo).
- **Slow renders** — rendering is deterministic software rendering; heavy
  full-canvas grain costs time per frame. Check `avg_ms_per_frame` in the
  check report before rendering long scenes.

## Development

```sh
python3 tools/sync_runtime.py   # refresh northnoir_cli/runtime from cloudrun/
python3 -m pytest tests/ -q     # end-to-end suite (doctor/styles/init/check/render)
python3 northnoir_cli/cli.py doctor   # or: ./bin/northnoir doctor
```

Packaging choice: a **Python package** (the engine is Python + Playwright;
Chromium and ffmpeg are external tools `doctor` verifies). An unpublished
thin Node shim (`bin/northnoir.js`, `private: true` in `package.json`) is kept
for repo use only. A compiled single binary was rejected: it would have
to bundle a Python interpreter plus Playwright plus a browser download, which
is larger, more fragile across platforms, and harder to debug than `pipx`.

## Changelog

### 1.3.0

- Ruvil 7.3 director films from `--plan` / `--brief`, with reference art,
  editable drawings, camera assembly, music and sound effects.
- Streamed chat, transient-error retries/backoff and truncation-tolerant
  planning with section-level fallbacks.
- Per-call cost ledger flushed to disk, including planning and resumed runs.
- Clarified `--max-image-calls`: `0` means unlimited; use `--plan-only`
  (plus `--no-llm` for a brief) for a fully local dry run.

## Licenses

The CLI code is proprietary North Noir software (see `pyproject.toml`). The
bundled scene libraries keep their own licenses, shipped beside them in
`northnoir_cli/runtime/libs/`: `LICENSE.p5.txt` (p5.js), `LICENSE.roughjs.txt`
(rough.js), `LICENSE.simplex-noise.txt` (simplex-noise), `LICENSE.anidoodle.txt`
(AnidoodleDraw / RuvilMarks). Their pinned SHA256 digests are in
`northnoir_cli/runtime/libs/SHA256SUMS` and verified by `northnoir doctor`.
