Metadata-Version: 2.4
Name: navig-obs
Version: 0.6.0
Summary: Control OBS Studio from navig — scenes, sources, streams, recording, studio mode, whole obs-websocket v5 surface — its own navig-obs command, and `navig obs` inside navig.
Author-email: NAVIG Development Team <opensource@navig.run>
License-Expression: Apache-2.0
Project-URL: Homepage, https://navig.run
Project-URL: Documentation, https://navig.run/docs
Keywords: navig,cli,agent,automation,plugin,obs,obs-websocket,streaming,recording
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: navig-sdk>=2.7.0
Requires-Dist: typer>=0.14.0
Requires-Dist: rich>=13.7.0
Requires-Dist: websockets>=12.0
Dynamic: license-file

# navig-obs

<!-- navig:plugin-card:start -->
<!-- Generated by scripts/gen_plugin_catalog.py — edit the plugin's pyproject.toml, not this block. -->

> Control OBS Studio from navig — scenes, sources, streams, recording, studio mode, whole obs-websocket v5 surface — its own navig-obs command, and `navig obs` inside navig.

**Works on its own** — its own CLI, no navig required · v0.6.0 · not on PyPI yet (install from source below)

**On its own** (no navig needed)

```bash
pip install "git+https://github.com/navig-run/plugins#subdirectory=navig-obs"
navig-obs --help   # no navig needed
```

**With navig**

```bash
pip install "git+https://github.com/navig-run/plugins#subdirectory=navig-obs"
navig obs --help
```

<details><summary>Commands</summary>

| Command | What it does |
|---|---|
| `navig obs clips` | Cut the Grid Pigeon's moments from a recording into 9:16 clips, captions burned in. |
| `navig obs doctor` | Local OBS health check: install, websocket config, plugin load errors, missing files. |
| `navig obs events` | Stream OBS events live (scene changes, output state, media, …). |
| `navig obs marker` | Append a marker to the session's recording clock (OBS must be recording). |
| `navig obs request` | Send any obs-websocket v5 request and print its responseData. |
| `navig obs screenshot` | Capture a source/scene image from OBS and save it locally (proof shot). |
| `navig obs stats` | OBS resource stats: CPU, memory, FPS, render/output frame health. |
| `navig obs status` | Live OBS state: version, current scene, stream/record/replay/vcam/studio. |

</details>

<!-- navig:plugin-card:end -->

Control **OBS Studio** from NAVIG — locally or on any machine on your LAN — over
OBS's built-in **obs-websocket v5** protocol (port 4455). One command surface,
two front doors: `navig obs …` inside navig, `navig-obs …` standalone.

No agent on the OBS machine is required: OBS ≥ 28 ships the websocket server
itself. You only need it enabled (Tools → WebSocket Server Settings).

## Quick start

```bash
navig obs doctor            # local health: websocket, plugins, missing files, log errors
navig obs status            # version, current scene, stream/record/vcam/studio state
navig obs scenes list
navig obs scenes activate "BRB"
navig obs stream start
navig obs screenshot "Program" --out proof.png
```

Remote machine: `navig obs --host 192.168.1.50 status` or `NAVIG_OBS_HOST=…`.

## Connection & auth

Resolution order per field: `--host/--port/--password` flags → `NAVIG_OBS_HOST` /
`NAVIG_OBS_PORT` / `NAVIG_OBS_PASSWORD` env → navig config `obs.host` / `obs.port` /
`obs.password` → OBS's own local config file (loopback only, read-only) → defaults
`127.0.0.1:4455`. The password is never printed or logged. The v5 challenge/
response handshake is implemented in `navig_obs/engine/client.py` (stdlib +
`websockets`, no third-party OBS wrapper).

## Verbs

| Group | Actions |
|---|---|
| `status` · `doctor` · `stats` · `events` · `screenshot` | top-level probes |
| `request` | top-level — raw obs-websocket v5 request: `request GetVersion` (its `availableRequests` lists every valid name) · `--data '{…}'` |
| `scenes` | list · current · create · remove · activate |
| `items` | list · visible · transform · remove · duplicate |
| `sources` | list · kinds · add · remove · settings · active · defaults · refresh (browser source, no cache) |
| `audio` | monitor — read, or `--type none\|monitor\|both` |
| `filters` | list · add · remove · settings · enable |
| `transitions` | list · current · set · settings |
| `stream` | status · start · stop · pause · resume · settings |
| `record` | status · start · stop · pause · resume · directory |
| `replay` | status · start · stop · save |
| `vcam` | status · start · stop |
| `studio` | status · enable · disable · to-program |
| `collections` | list · current · create · use |
| `profiles` | list · current · use |
| `media` | status · play · pause · restart · stop |
| `show` | status · setup · cue · toast · poll · pigeon · civic · say — deploy toasts, store-sale ring, the Grid Pigeon (below) |
| `hotkeys` | list · send |
| `session` | start · stop |
| `marker` | top-level — append to the session's recording clock |

Every verb accepts `--json` for scripts/agents. Exit codes are part of the
contract: usage errors exit 2, operation failures exit 1, and an **unreachable
OBS on `status` is a skip (exit 0)** — "not running" is an answer, not a failure.
Destructive verbs (`scenes remove`, `items remove`, `sources remove`) confirm
unless `-y`.

`stream settings` masks the stream key in every rendering (including `--json`)
unless `--reveal`; writes go through `--set-key` / `--set-server` and never echo
the key.

## Integration contract (for SCHEMA BROADCAST and other consumers)

Consumers drive OBS through this CLI (or the same verbs through the agent).
Stable mapping for the SCHEMA BROADCAST integration spec:

| Consumer intent | NAVIG OBS action |
|---|---|
| Select broadcast scene | `navig obs scenes activate "<scene>"` |
| Trigger transition | `scenes activate …` (applies the current transition); studio mode: `studio to-program` |
| Set scene data (overlay payload) | `navig obs sources settings <browser-source> --set '{"url":"…"}'` (or any source's settings) |
| Display mission alert / Disorder Protocol / BRB / Emergency Cut | `scenes activate "<pack scene name>"` — names come from the scene pack, no hardcoding here |
| Read OBS connection status | `navig obs status --json` → `{"reachable": true, …}` (exit 0 + `reachable:false` when down) |
| Read recording status | `navig obs record status --json` |
| Start/stop stream & recording | `stream start\|stop`, `record start\|stop` |
| Anchor a recording session (clock, scene, canvas) | `navig obs session start --lang fr` → prints the session dir (default `~/.navig/obs-sessions/<id>/`, override `--dir` / `NAVIG_OBS_SESSIONS`) |
| Mark an event during a recording | `navig obs marker <label> --kind cue\|character\|scene\|mission\|anomaly\|bookmark\|incident` (needs an active recording; `--session <dir>` or `NAVIG_OBS_SESSION`) |
| Close a session + locate the file | `navig obs session stop` (sets `endEpoch`, finds the newest recording since `startEpoch`) |
| Archive a session + cut clips from its markers | `navig obs session archive --clip` → `obs.archive_dir/<id>/` (sha256-verified copy + `edl.json` → `navig video cut`) |
| React to OBS state changes | `navig obs events --json` (JSONL stream: scene changes, output state, media, …; `--topics` filters, `--timeout` bounds it) |

Guarantees for consumers:

- **All verbs and their `--json` shapes are stable** within a major version;
  new verbs are additive.
- `--host` targets any OBS on the network; auth per the resolution order above.
- The controller never mutates the scene pack — scene names are data supplied
  by the consumer.
- **Sessions are the writer of SCHEMA broadcast contract 7**
  (`session.json` + `markers.jsonl`, shapes frozen in the SCHEMA repo under
  `design/broadcast/contracts/`): media never enters a repository; the session
  dir is the hand-off unit for ingest and the EDL.
- Designed for **adapter-style consumption**: run the CLI, parse `--json`,
  branch on exit code (`0` ok/skip, `1` failed, `2` usage).

## Show events — deploys, sales and the Grid Pigeon on the stream

`navig_obs.show` puts three off-stream moments on the stream; it is off until
`obs.show.enabled` is true, then the gateway runs it (restart the gateway):

- **A deploy** — polls `obs.show.patch_feed` (default Schema's public Source log,
  `https://api.schema.cx/api/source/patches`, every `obs.show.poll_seconds` = 60):
  a deploy landing (`compiling`) or a published patch becomes an `obs-browser`
  `emit_event` named `obs.show.toast_event` (`schema-toast`) that the broadcast
  overlay draws, plus the `patch-landed` cue. The first look only seeds the
  position (`~/.navig/obs-show/feed-state.json`); after downtime only the newest
  patch shows.
- **A store sale** — navig's `sale` notification (navig-msstore) plays the
  `sale-ring` cue. Sound only.
- **A civic event** — polls `obs.show.civic_feed` (default Schema's public
  civic feed, `https://api.schema.cx/api/civic/events`, same interval): each new
  row becomes an `emit_event` named `obs.show.civic_event` (`pigeonCivic`) carrying
  the row; the Grid Pigeon overlay reacts to its `kind`. Same seeding rule
  (`civic-state.json`); after downtime at most three fire, oldest first. The
  overlay is a local file (origin `null`) the API does not admit — reading the
  feed here is what lets it react without loosening the API's CORS. An empty
  `civic_feed` turns it off.

By hand: `navig obs show pigeon <beat> [--mode observer|chaos|courier]` sends
the `pigeon` page event (`obs.show.pigeon_event`) — any beat of the overlay;
`navig obs show civic [--fire]` reads the civic feed once.

**A spoken line** — `navig obs show say [TEXT] [--title T] [--kicker K]
[--tone deploy|game|live|passport|info] [--lang en|fr|ru] [--template deploy]
[--image https://…] [--provider P] [--voice V] [--fresh] [--no-voice] [--dry-run]
[--json]`.

- **The stage's language decides the voice.** The loaded collection names it
  (`SCHEMA BROADCAST (ru)` → `ru`; `--lang` overrides; else `obs.show.say_lang`,
  default `en`). Each language has its own voice — `obs.show.say_voices`
  `{lang: {provider, voice, style, fx}}` over `voices.DEFAULTS` (en MiMo Dean with
  the pigeon's style note, fr Edge Henri, ru Edge Dmitry). `--provider`/`--voice`
  override one line; `obs.show.say_provider`/`say_voice`, when set, force one
  voice for every language (the old behaviour).
- **Names are said the language's way**: synthesis is `navig voice speak TEXT
  --provider P [--voice V] --lang L [--style S] --output FILE --no-play`, and
  navig-audio's lexicon rewrites the SPOKEN text only (SCHEMA → Skeema / schéma /
  Схема; `audio.tts.lexicon.<lang>` adds your own). The card keeps the text.
- **`fx`** (`pitch=<±semitones>,tempo=<factor>`) is the pigeon's character,
  applied by ffmpeg after synthesis (pitch keeps the speed; loudness evened).
- The clip is cached at `~/.navig/obs-show/tts/<sha16(provider,voice,text,lang,
  style,fx)>.mp3` and never synthesized again (`--fresh` forces it); measured
  with ffprobe (fallback `max(3500, words × 380)` ms). Then, in one OBS session:
  the `schema-toast` alert card `{kind:"alert", tone, kicker, title, text, ms,
  image?}` (card `ms` = clip + 1 s; `image` only an https URL), the `pigeon`
  `{act:"speak", ms}` event, and the clip through the cue input.
- A failed synthesis still shows the card (warning, exit 0). A dry run never
  touches OBS. With `obs.show.civic_say` on (default off — TTS can bill), each
  civic beat is spoken the same way, in the stage's language, coloured by kind
  (`flash_live` amber, `passport_forged` passport, `ignition` live).

**Deploys, spoken** — with `obs.show.deploy_say` on, a published patch that changed something a citizen can see is SAID in the stage's language instead of the silent card + sting (one cue input: the sting would cut the voice). English reads the patch's world-voiced headline; French and Russian name the layers it touched (`voices.LAYER_NAMES`, the app's own translations; at most three, then "and N more"). An internal-only patch keeps the plain card. **On air only:** `obs.show.say_on_air_only` (default on) lets the bridge speak — deploys and civic beats alike — only while OBS is streaming or recording; off air the cards and the pigeon still react, silently. A hand-run `show say` always speaks.

**Voices** — `navig obs show voices` lists each language's voice; `--sample
[--play]` renders (and plays on stream, one by one) four directions × three
languages; `--use <direction>` keeps one (writes `obs.show.say_voices`).

**Flash on air** — `navig obs show flash <zone-slug> [--api URL]` calls the
Architects' flash door (`POST /api/admin/flash/deal`) with the session in the
vault (`schema_operator_session`, or `NAVIG_SCHEMA_SESSION`). It deals a REAL
mission and notifies that Zone — an operator act, never automatic. Refusals are
printed in plain words (exit 1).

**Voice remote (Alexa)** — a voice assistant drives the stage through NAVIG's
signed ingest door. `navig signals add alexa` creates the source (URL + secret,
shown once); the gateway checks the HMAC, timestamp window and replays, then
hands the verified intent to `navig_obs.show.remote` (an *ingest action*, so it
never rings as a notification). The menu is fixed: `SwitchScene{scene}` (a
number or the closest name), `EmergencyCut`, `Say{text}`, `RecordStart`,
`RecordStop`, `Status`, `FreeGames`, `OpenPage{page}` (allow-listed https pages,
`obs.show.voice_pages`). **Going live is not on it.** The reply (`speech`) is in
the request's locale (en/fr). Source name: `obs.show.voice_source` (default
`alexa`). No source = the door stays closed.

Cues come from a contract-04 cue book (`obs.show.cues_file`, default
`~/.navig/obs-show/cues.json`; provenance required, `cooldownMs` honoured) and
play through ONE media input, `obs.show.cue_input` (`SCHEMA · Cue`) — set the
file, restart the input. `navig obs show setup --all-scenes` creates that input
and places it in every scene (monitor + output). An OBS that is closed is a
logged no-op. `GET /api/deck/obs/show` reports the running bridge.

## Layout

```
navig_obs/
  engine/client.py   obs-websocket v5 protocol (handshake, auth, requests, keepalives, events)
  engine/config.py   target resolution (flags > env > navig config > local OBS file)
  doctor.py          local health checks (install, websocket config, plugins, logs, missing files)
  commands/obs.py    top-level verbs + group mounts; sibling modules = resource groups
  show/              show-events bridge: feed watcher, cue book, OBS moves, gateway service
  skills/obs-control/SKILL.md   agent routing
```

## Install (dev)

```bash
pip install -e plugins/navig-obs     # from the navig repo root
navig obs doctor
```
