CLAUDE.md update

This commit is contained in:
Jakub Zych
2026-04-30 00:15:23 +02:00
parent 579e4984c2
commit c8f9c11c93
6 changed files with 300 additions and 277 deletions

87
docs/audio.md Normal file
View File

@@ -0,0 +1,87 @@
# Audio pipeline, MPD, OBS WS bus, systemd units
**Read when:** editing `bridges/`, `twitch-bot/`, `~/.config/mpd/mpd.conf`, `~/.config/pipewire/pipewire-pulse.conf.d/*.conf`, or working with `mpd:state` events on the OBS WebSocket bus. Also read before touching audio routing in scene JSON.
## Audio architecture
Two parallel isolated streams into OBS — one for MPD music, one for Discord voice — so neither mixes with the rest of the desktop, and OBS gets independent meters / per-source volume for each.
```
┌────────────┐ ┌─────────────┐
│ MPD │ systemd user │ Discord │ Flatpak / native
└──────┬─────┘ unit └──────┬──────┘ app
│ PipeWire target= │ pavucontrol → "Discord-Stream"
│ "mpd_stream" │ (one-time per rig)
▼ ▼
┌────────────────────────┐ ┌────────────────────────────┐
│ null sink "mpd_stream" │ │ null sink "discord_stream" │
└──────┬──────────┬──────┘ └──────┬─────────────┬───────┘
│ │ │ │
loopback│ │ monitor loopback│ │ monitor
▼ ▼ ▼ ▼
default OBS default OBS
sink pulse_output_ sink pulse_output_
capture capture
"MPD" "Discord"
(defined in pipewire-pulse.conf.d/
mpd-stream.conf, discord-stream.conf)
Twitch
```
The dedicated null sinks are the load-bearing piece: without them, OBS would either pick up *all* desktop audio (browser, system, every app) or *none*. The loopback modules mirror each null sink back to the user's actual default sink so MPD and Discord are still audible locally while OBS captures the isolated monitors.
**Routing the apps to their sinks:**
- **MPD** binds via `target "mpd_stream"` in `~/.config/mpd/mpd.conf` — automatic.
- **Discord** has no equivalent target setting. One-time per rig: open pavucontrol → Playback tab → while Discord plays sound, set its dropdown to **Discord-Stream**. PipeWire remembers across launches via `module-stream-restore`.
Sanity check after login:
```bash
pactl list short sinks | grep -E 'mpd_stream|discord_stream' # both should appear
```
If either is missing, `pipewire-pulse` didn't load the conf. Either relog or stop OBS and `systemctl --user restart pipewire-pulse.service` (do **not** restart pipewire-pulse while OBS is running — see CLAUDE.md "Critical gotcha").
## MPD setup
Config: `~/.config/mpd/mpd.conf`. Library: `~/HDD/Music`. Output target: `mpd_stream` (the null sink above). State files (`database`, `state`, `sticker.sql`, `playlists/`) live in `~/.config/mpd/`. Run via the system-provided units: `systemctl --user enable --now mpd.socket mpd.service`.
The `zeroconf_name` in `mpd.conf` is per-rig (`"MPD on Ignia"` / `"MPD on Midgolem"`) — `scripts/deploy-rig.sh` sets it to the local hostname when seeding the config.
`auto_update "yes"` means `inotify`-watch on the library — drop a file or `yt-dlp -P ~/HDD/Music/...` and MPD picks it up without `mpc update`.
## OBS WebSocket bus
Port 4455 (default). Password lives in `plugin_config/obs-websocket/config.json` (gitignored — secret). `scripts/setup.sh` extracts URL + password to `vendor/obs-config.js`, which every overlay loads as a `<script>` tag (no `fetch()` because CEF blocks it on `file://`).
Auth is HMAC-SHA256, handled by `vendor/obs-ws-mini.js`. The lib falls back to a pure-JS SHA-256 if `crypto.subtle` is unavailable (CEF on `file://` origins sometimes lacks it).
If `ss -tlnp | grep 4455` shows nothing, OBS's WS server is disabled — flip `Tools → WebSocket Server Settings → Enable` in the UI. The JSON file's `server_enabled` lags actual state until OBS shutdown, so trust the socket, not the file.
### Custom events on the bus
| Event | Publisher | Consumers |
|---|---|---|
| `mpd:state` | `bridges/mpd-state.py` (1 Hz + on track change) | `loading/index.html`, `game/index.html`, `desktop/index.html` (camera sync only), `goodbye/index.html`, `music-box/index.html`, `music-box/widget.html` |
| `mpd:cmd` | `music/index.html` (legacy; reads `loading/cmd.js`) | self-handled inside the music daemon |
`mpd:state` payload: `{_type, index, total, title, artist, trackTitle, file, currentTime, duration, paused, nextTitle, nextArtist, nextTrackTitle, nextFile, nextTitles}`. `nextTitles` is an array of up to 3 display strings peeked from the queue starting at the current song's successor (used by the Music Box overlays to show 3-up "coming next"). The single `nextTitle`/`nextArtist`/`nextTrackTitle`/`nextFile` fields mirror `nextTitles[0]` for overlays that only need one. All `next*` fields are populated when MPD has a queued follow-up (`status.nextsong` present); they're `None`/`[]` at the end of the queue.
## Systemd user units
Live in `~/.config/systemd/user/`. Two custom + two system-provided:
| Unit | Source | Purpose |
|---|---|---|
| `obs-mpd-bridge.service` | `bridges/mpd-state.py` | MPD → OBS WS state bridge |
| `obs-twitch-bot.service` | `twitch-bot/bot.py` | Twitch chat → MPD control |
| `mpd.service` + `mpd.socket` | system | MPD itself (socket-activated) |
Common ops:
```bash
systemctl --user status obs-mpd-bridge obs-twitch-bot mpd
systemctl --user restart obs-mpd-bridge
journalctl --user -u obs-twitch-bot -f
```
`scripts/deploy-rig.sh` ships the two custom units inline (heredoc) — single source of truth for unit content lives in the deploy script, not in this repo as a separate file.

39
docs/cheatsheet.md Normal file
View File

@@ -0,0 +1,39 @@
# One-liners
**Read when:** you want a quick command reference. These are pasted directly from typical workflows.
```bash
# Refresh a browser source after editing HTML
# In OBS: right-click the source → Refresh cache (no restart needed)
# Regenerate vendor/obs-config.js after WS password change
bash scripts/setup.sh
# Refresh telemetry (interactive — review every field)
bash scripts/telemetry.sh --collect
# Rebuild the playlist index after dropping new files into ~/HDD/Music
bash scripts/playlist.sh
# Deploy this rig from scratch (run on Midgolem)
git clone <repo> ~/.config/obs-studio && cd ~/.config/obs-studio && bash scripts/deploy-rig.sh
# Bridge / bot status + tail
systemctl --user status obs-mpd-bridge obs-twitch-bot
journalctl --user -u obs-twitch-bot -f
# Verify both audio sinks exist
pactl list short sinks | grep -E 'mpd_stream|discord_stream'
# Check OBS WS port is listening
ss -tlnp | grep 4455
# Is OBS running? (flatpak-aware — DO NOT use `pgrep -fa 'flatpak run …'`)
flatpak ps --columns=application | grep -qx com.obsproject.Studio && echo running
# Inspect a scene collection
python3 -m json.tool basic/scenes/Default_Stream_HUD.json | less
# List scenes in the active collection
python3 -c "import json; d=json.load(open('basic/scenes/Default_Stream_HUD.json')); print([s['name'] for s in d['scene_order']])"
```

69
docs/deployment.md Normal file
View File

@@ -0,0 +1,69 @@
# Deployment, per-rig state, secrets, sandbox
**Read when:** bringing up a fresh rig (Midgolem or otherwise), rotating tokens, deciding what to commit vs regenerate, or hitting Flatpak sandbox / hardcoded-asset issues.
## Deploy a fresh rig
```bash
git clone <repo> ~/.config/obs-studio
cd ~/.config/obs-studio
bash scripts/deploy-rig.sh
```
Idempotent. 9 phases: pacman + pip + flatpak + submodules + PipeWire sink + MPD + systemd units + OBS first-launch + secrets stub. See file header for details.
## Twitch identity & secrets
| Account | Purpose | Token file | Scopes |
|---|---|---|---|
| `ophi118` (broadcaster) | Channel title/category updates by `set-channel.py` | `twitch-bot/.env.ophi118` | `channel:manage:broadcast` |
| `vault118` (bot) | Handles `!skip` / `!queue` / `!info`, posts now-playing, optional Mattermost going-live notifier (outbound HTTP only — no extra Twitch scopes needed) | `twitch-bot/.env` | `chat:read`, `chat:edit` |
| `ophi118` (stream key) | RTMP stream key | `basic/profiles/ophi118/service.json` | n/a |
All three are gitignored. **Do not copy between rigs** — regenerate via https://twitchtokengenerator.com (for OAuth tokens) and the Twitch Dashboard (for the stream key).
## External assets referenced by absolute path
These hardcoded paths exist in `basic/scenes/Default_Stream_HUD.json`. On a fresh rig they need to exist or OBS will surface "missing source" errors:
| Path | Purpose |
|---|---|
| `/home/jin/HDD/Images/Gifs/stamd.png` | Game-scene "Starting Soon" image |
| `/home/jin/cloud.jakubzych.com/_img/logos/dgw.png` | Doomguard watermark (Game/Landing) |
| `/home/jin/HDD/Music/` | MPD library root |
| `/home/jin/Videos/` | Recording output |
| `/home/jin/.config/obs-studio/landing/static-hum.wav` | Landing audio |
`scripts/deploy-rig.sh` creates the directories and warns about missing image files; the user has to bring those over manually.
## Per-rig vs portable state
**Differs per rig** (don't copy from one to the other; let the deploy script or OBS itself regenerate):
- `global.ini` — ALSA device IDs, monitor IDs
- `~/.config/mpd/mpd.conf``zeroconf_name`
- `twitch-bot/.env*` — OAuth tokens
- `basic/profiles/ophi118/service.json` — stream key
- `plugin_config/obs-websocket/config.json` — WebSocket password
- `landing/telemetry.json``rig`, hardware specs, kernel
- `vendor/obs-config.js` — derived from WS password
**Identical across rigs** (commit and reuse):
- `basic/scenes/Default_Stream_HUD.json` — scenes + transitions + audio routing
- `basic/profiles/ophi118/{basic,streamEncoder,recordEncoder}.json` — encoder/output settings (minus `service.json`)
- All HTML overlays under `landing/`, `loading/`, `game/`, `desktop/`, `goodbye/`, `music-box/`, `music/`
- All Python in `bridges/`, `twitch-bot/`
- All scripts in `scripts/`
- `vendor/obs-ws-mini.js`
**Outside this repo but written by `scripts/deploy-rig.sh` (identical across rigs):**
- `~/.config/pipewire/pipewire-pulse.conf.d/mpd-stream.conf` — MPD null sink + loopback
- `~/.config/pipewire/pipewire-pulse.conf.d/discord-stream.conf` — Discord null sink + loopback
**Manual one-time per rig (no automation):**
- pavucontrol → Playback → Discord → set output to **Discord-Stream** (PipeWire remembers across launches; Discord has no equivalent of MPD's `target` config).
## Flatpak permissions — what the sandbox allows
OBS flatpak ships with `filesystems=host` by default — full host access. Asset paths under `/home/jin/HDD/`, `/home/jin/cloud.jakubzych.com/`, etc. work without `flatpak override`. PipeWire audio works via `xdg-run/pipewire-0`. NVIDIA acceleration works via `org.freedesktop.Platform.GL.nvidia-*` (auto-installed). If you ever need to widen permissions: `flatpak override --user com.obsproject.Studio --<flag>=<value>`.
`filesystems=host` is also the reason this config lives at `~/.config/obs-studio/` instead of the per-app sandboxed path — see CLAUDE.md "What this directory is".

12
docs/history.md Normal file
View File

@@ -0,0 +1,12 @@
# Migration history
**Read when:** encountering pre-migration artifacts (e.g. `linuxbrowser-source` source ids, references to native `obs` binary, browser-side music daemon code paths), reading old scene JSON, or auditing AI-written files that may target stale conventions.
- **Was:** native Arch `obs-studio` (no browser support) + AUR `obs-linuxbrowser-source` (id: `linuxbrowser-source`).
- **2026-04-25:** migrated to Flatpak `com.obsproject.Studio` with bundled CEF browser source (id: `browser_source`). Settings keys: `url`, `width`, `height`, `fps` map directly; CEF requires `fps_custom: true`, `reroute_audio: false`, `restart_when_active: false`, `shutdown: false`, `webpage_control_level: 1`. Stale `linuxbrowser.reloadpage` hotkey keys dropped. Pre-migration backups have been deleted as of 2026-04-26.
- **2026-04-26:** music architecture moved from a browser-side daemon (`music/index.html`) to MPD + a Python state bridge (`bridges/mpd-state.py`). Twitch chat bot (`twitch-bot/bot.py`) added with `!skip → MPD` integration. `station/` submodule added for the public rig-spec page. Game scene HUD added with live OBS WS sync. All `.sh` scripts consolidated under `scripts/`.
- **2026-04-26 (later):** Music Box scene added (`music-box/index.html`, full-screen) plus sidebar widget (`music-box/widget.html`) for the Desktop scene. Desktop scene reworked Game-style — camera in right sidebar with the Music Box widget directly below. Desktop HUD forked from Game HUD into `desktop/index.html` (no game-name slot). Bridge payload extended with `nextTitle`/`nextArtist`/`nextTrackTitle`/`nextFile`. Bot gained `!queue` (next 3 tracks) and `!info` (full metadata) commands; `!skip` cooldown shifted from global 5s to per-user 60s.
- **2026-04-27:** Music Box overlays rewritten as proper terminals (boot sequence + scrolling `[audio] now: …` history + static-HTML pinned block + ellipsis-clipped track titles). Music Box overlay gained a dedicated chat status strip between the terminal and the banner so the command help stays visible regardless of what's scrolling. Music Box widget resized to 549×880 to pair with camera width. Bridge payload gained `nextTitles[]` (array of next 3 display strings) so Music Box can show 3-up "coming next". Bot gained `live_watch()` — optional Mattermost going-live notifier driven by `MM_HOOK` env var. Landing top HUD identifier upgraded to `OPHI-118 // [<RIG>]` with cyberpunk decode/scramble reveal animation reading `window.__TEL.rig` from telemetry.
- **2026-04-28:** Discord audio split out to its own isolated stream — `~/.config/pipewire/pipewire-pulse.conf.d/discord-stream.conf` adds a `discord_stream` null sink + loopback (mirrors the existing `mpd-stream.conf` pattern). New OBS source `Discord` (`pulse_output_capture` on `discord_stream.monitor`) added to scenes Game / Desktop / Desktop (No Cam) / Music Box. `scripts/deploy-rig.sh` phase 5 now provisions both sinks and skips the `pipewire-pulse` restart when OBS is running (a restart silently detaches OBS audio sources — fix needs an OBS process restart, not just a settings refresh). Discord routing to its sink is the only manual per-rig step (pavucontrol → Playback → Discord → Discord-Stream).
If you encounter `linuxbrowser-source` anywhere — in a backup, a doc, a paste — assume it's pre-migration and migrate the same way.

View File

@@ -0,0 +1,49 @@
# Scene JSON, HUD overlays, OBS internals
**Read when:** editing `basic/scenes/Default_Stream_HUD.json`, building or modifying the HTML HUDs (`landing/`, `loading/`, `game/`, `desktop/`, `goodbye/`, `music-box/`, `music/`), debugging plugin behavior, or programmatically driving OBS.
## Scene collection JSON — structure
Top-level keys in `basic/scenes/Default_Stream_HUD.json`:
- `name` — collection name (matches filename stem).
- `sources`**flat array of every source, scene, and group**. Scenes are entries here with `"id": "scene"`; their `settings.items[]` references child sources **by name string**, not uuid. Renaming a source means updating every scene that references it.
- `scene_order` — array of `{name}` defining sidebar order.
- `current_scene` / `current_program_scene` — strings matching scene names.
- `transitions`, `quick_transitions`, `current_transition`, `transition_duration` — transition setup.
- `groups` — group definitions.
- `modules` — per-source-plugin state.
- `version`, `migration_resolution`, `resolution`, `canvases` — collection schema + canvas sizing. Canvas: **2560×1336**.
Each source entry has: `name`, `uuid`, `id`, `versioned_id`, `settings` (plugin-specific), plus filters/transform/audio fields. Scene items inside `settings.items[]` reference sources via `source_uuid`. **Use uuids when adding new items programmatically; use names when reading.**
Source `id` values currently in use:
- `scene` — a scene
- `pipewire-screen-capture-source` — Wayland/PipeWire screen/window capture
- `v4l2_input` — V4L2 webcam (legacy path)
- `browser_source` — CEF browser (post-migration)
- `image_source` — static image
- `text_ft2_source` — FreeType2 text
- `ffmpeg_source` — media file (audio/video) via ffmpeg
- `pulse_output_capture` / `pulse_input_capture` — PulseAudio sink/source capture (used for `mpd_stream.monitor` and the mic)
When editing scene JSON: keep `uuid` values stable across edits, preserve 4-space indentation, and validate with `python3 -m json.tool` before re-launching OBS.
## Game / Desktop / Music Box HUDs — browser-resolution constraints
The Game (`HUD`), Desktop (`Desktop HUD`), and Music Box (`Music Box HUD`) browser sources all stretch (`bounds_type: 1`, bounds 2560×1336) to fill the canvas. **Their `width` / `height` settings must match the canvas (2560×1336)**, not 16:9 like the output resolution. If the browser viewport is 1920×1080, the canvas stretch is non-uniform (1.333× horizontal, 1.237× vertical), which distorts every CSS box relative to the camera's transform (camera is positioned in canvas coords directly, so a CSS-positioned frame around it looks visibly off — wider/squashed than the camera). Symptom: cyan "camera frame" doesn't align with the camera feed inside it. Fix: keep all three browser sources at `width: 2560, height: 1336` so 1 CSS px = 1 canvas px, then position any frame using canvas coords.
The Camera frame in both `game/index.html` and `desktop/index.html` auto-syncs via OBS WebSocket: `GetSceneItemTransform` on connect + `SceneItemTransformChanged` event listener update four CSS vars (`--cam-x/y/w/h`). Move/resize the Camera item in OBS and the frame follows live. Assumes Camera item alignment 5 (top-left) — OBS default; if you change alignment, the math in `setCameraTransform()` needs adjusting. The `?scene=` and `?camera=` URL params let either HUD target a different scene or camera source name.
**The Music Box widget** (`music-box/widget.html`) at 549×880 is the exception: it sits in canvas coords as a native-sized scene item (`bounds_type: 0`), NOT stretched. Authoring viewport must match scene-item dimensions; do not toggle `bounds_type` on without resizing the source's `width`/`height` to match the new bounds. The 549 width is deliberate — camera renders at 1920×1080 × 0.286 = 549×309, so the widget pairs visually as a same-width column directly under the camera.
## Working with scene JSON & overlays
- **Confirm OBS is using THIS dir, not the ghost** before assuming an edit will land. While OBS is running: `ls -la /proc/$(pgrep -x obs)/cwd` and check `lsof -p $(pgrep -x obs) | grep scenes`. Or compare mtimes after an OBS session — the file OBS rewrites is the live one.
- **Inspecting a scene collection:** `python3 -m json.tool basic/scenes/Default_Stream_HUD.json | less`. List scenes: `python3 -c "import json; d=json.load(open('basic/scenes/Default_Stream_HUD.json')); print([s['name'] for s in d['scene_order']])"`.
- **Diffing after an OBS session:** OBS rewrites the file even when nothing visibly changed. Expect noisy diffs; focus on `sources[]` and `scene_order` for meaningful changes.
- **Backing up before edits:** copy the active `.json` somewhere outside this dir — OBS's own `.bak` will be overwritten on the next save.
- **Refreshing a browser source after editing HTML:** right-click source in OBS → **Refresh cache**. No restart needed — CEF re-reads on demand.
- **Plugin development:** Flatpak OBS plugins live inside the runtime; you can't drop `.so` files into the host filesystem and have them load. Plugin extensions for the OBS flatpak come as `com.obsproject.Studio.Plugin.<name>` flatpak extensions (search Flathub). If a plugin only has source/AUR distribution, it won't work in Flatpak — that's the tradeoff of this distribution choice.
- **WebSocket control:** `plugin_config/obs-websocket/config.json` holds connection settings. Programmatic scene edits while OBS is running should go through obs-websocket — file edits get clobbered on save.