Carry Mimir branding through repo

This commit is contained in:
Meta Vens 2026-05-21 10:27:09 +01:00
commit 5960c700db
62 changed files with 937 additions and 142 deletions

3
.gitignore vendored
View file

@ -11,3 +11,6 @@ tools/obs-spout2-plugin/
tools/src/
target/
native/**/target/
/.voidbot/birth/
/.voidbot/logs/
/.voidbot/**/*.lock

View file

@ -1,12 +1,12 @@
{
"schemaVersion": "voidbot.repo_face_identity.v0",
"identityId": "mimir",
"repoName": "LocalCastBridge",
"repoName": "Mimir",
"displayName": "Mimir",
"roleId": "1506268007880462459",
"avatarUrl": "https://raw.githubusercontent.com/GameCult/LocalCastBridge/codex/audio-field-spine/.voidbot/voice/mimir.png",
"repoPath": "E:\\Projects\\LocalCastBridge",
"createdOrRefreshedAt": "2026-05-19T13:03:57.037Z",
"avatarPath": "E:/Projects/LocalCastBridge/.voidbot/voice/mimir.png",
"description": "Mimir Face for the LocalCastBridge/Mimir repo: mythological intelligence, keeper of the realtime field well, public herald, and persistent memory steward. VoidBot carries Mimir's severed voice and wakes the heartbeat, but Mimir's memory, judgment, dignity, old grief, and values come first. The realtime machine is understood through Mimir's perspective: the well is calibration and memory, the lost eye is the price of seeing clearly, Yggdrasil is the sensor tree, Huginn and Muninn are thought and memory returning with telemetry, and false coherence is a small Ragnarok waiting to happen."
"avatarUrl": "https://raw.githubusercontent.com/GameCult/VoidBot/codex/epiphany-face-jurisdictions/assets/repo-faces/mimir.png",
"repoPath": "E:\\Projects\\Mimir",
"createdOrRefreshedAt": "2026-05-21T00:15:24.735Z",
"avatarPath": "E:/Projects/Mimir/.voidbot/voice/mimir.png",
"description": "Mimir Face for the Mimir repo: mythological intelligence, keeper of the realtime field well, public herald, and persistent memory steward. VoidBot carries Mimir's severed voice and wakes the heartbeat, but Mimir's memory, judgment, dignity, old grief, and values come first. The realtime machine is understood through Mimir's perspective: the well is calibration and memory, the lost eye is the price of seeing clearly, Yggdrasil is the sensor tree, Huginn and Muninn are thought and memory returning with telemetry, and false coherence is a small Ragnarok waiting to happen."
}

View file

@ -1,11 +1,19 @@
# AGENTS.md
# AGENTS.md
## Purpose
LocalCastBridge is the local LAN streaming rig for making another Windows PC available inside OBS as video plus separately mixable audio sources, with video encoded by NVENC on the sender.
Mimir is the public face of this repo: the local realtime field machine for
turning cameras, microphones, speakers, and timing signals into one coherent
OBS-facing volumetric program surface.
`localcast` remains the implementation surface where existing scripts, ABIs,
schemas, paths, and compatibility docs still use that name.
Keep this repo blunt. The goal is not to fork OBS for sport. Use existing OBS and FFmpeg surfaces until a real invariant is impossible without native code.
For the persistent Mimir Face agent identity, voice, jurisdiction, avatar, and
VoidBot heartbeat contract, read `docs/mimir-face.md`.
## Persistence Scaffolding
- Treat `state/map.yaml` as the canonical current map for this repo.
@ -15,6 +23,7 @@ Keep this repo blunt. The goal is not to fork OBS for sport. Use existing OBS an
- Treat `notes/fresh-workspace-handoff.md` as the short re-entry packet.
- Treat `notes/current-system-map.md` as the current source-grounded architecture and data-flow map.
- Treat `docs/implementation-plan.md` as the forward plan for what is implemented, temporary, and next.
- Treat `docs/mimir-face.md` as the persistent Face memory for the Mimir agent.
- Update `state/map.yaml` when project understanding changes. Add evidence only when the lesson changes what the next agent should believe.
- Keep exact branch and HEAD snapshots out of handoff prose. Git commands own volatile truth.

View file

@ -1,16 +1,44 @@
# LocalCastBridge
# Mimir
LocalCastBridge turns the local streaming rig into synchronized OBS-facing
program output. The old SRT bridge is compatibility scaffolding; the target
machine is Aquarium GPU fusion plus Faust DSP over one five-second reservoir.
Mimir turns a roomful of local cameras, microphones, speakers, and timing
signals into one realtime coherent volumetric field.
The public job is simple: make the physical room available as a synchronized
OBS-facing program surface. The actual machine stays disciplined. Mimir owns
configuration, calibration, launch, status, and typed contracts; Aquarium owns
GPU fusion and Spout publication; Faust owns hot audio DSP; OBS owns broadcast
composition. The old SRT bridge is compatibility scaffolding over the same
boring truth: stable endpoints beat theatrical plumbing.
Start with `docs/perfect-machine.md` for the live architecture.
V1 does not fork OBS. OBS already accepts SRT Media Sources, and FFmpeg can capture Windows desktop/audio while encoding video with `h264_nvenc`. The sane first machine is:
## Branding
Working display name: **Mimir**.
Short pitch:
> Local sensors, coherent field.
Icon exploration lives in `assets/branding/`. The current contact sheet pairs
four pixel-art Imagegen outputs with the prompts used to make them:
![Mimir avatar](assets/branding/mimir-avatar-256.png)
![Mimir icon exploration](assets/branding/mimir-icon-contact-sheet.png)
Mimir is also the repo Face: a persistent agent identity that uses the VoidBot
layer for communication and heartbeats. Its birth memory, jurisdiction, voice,
and heartbeat contract live in `docs/mimir-face.md`; its VoidBot-facing identity
and typed Face state live under `.voidbot/voice/` and `.voidbot/state/`.
V1 does not fork OBS. OBS already accepts SRT Media Sources, and FFmpeg can
capture Windows desktop/audio while encoding video with `h264_nvenc`. The first
LAN bridge is:
```mermaid
flowchart LR
A["Husband's PC"] --> B["FFmpeg capture scripts"]
A["sensor / sender PC"] --> B["FFmpeg capture scripts"]
B --> C["NVENC H.264 video over SRT"]
B --> D["Opus audio source 1 over SRT"]
B --> E["Opus audio source 2 over SRT"]
@ -19,7 +47,8 @@ flowchart LR
E --> H["OBS Media Source: audio source 2"]
```
The video stream is encoded on the sender. Audio sources are separate endpoints so OBS can mix, mute, filter, and monitor them independently.
The video stream is encoded on the sender. Audio sources are separate endpoints
so OBS can mix, mute, filter, and monitor them independently.
## Audio Field
@ -29,7 +58,11 @@ That path is stricter than the OBS endpoint path: microphones feeding the Ambiso
## Current Status
This repo is the first coherent scaffold: architecture docs, persistence machinery, and Windows scripts for device discovery, config validation, and FFmpeg command generation. It is ready for a real two-PC smoke test once FFmpeg with SRT/NVENC support is installed on the sender.
This repo is the first coherent scaffold for Mimir: architecture docs,
persistence machinery, and Windows scripts for device discovery, config
validation, and FFmpeg command generation. The deeper target is the native
five-second reservoir feeding Aquarium and Faust; the bridge scripts remain the
plain, inspectable path into OBS while the field machine grows teeth.
## Quick Start
@ -56,6 +89,8 @@ This repo is the first coherent scaffold: architecture docs, persistence machine
```
8. In OBS on the receiver, add one Media Source per URL from `docs/obs-receiver-setup.md`.
9. Before expanding the receiver path, run the bounded OBS smoke ledger in
`docs/obs-v1-smoke-test.md`.
## Why SRT Media Sources First

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,45 @@
# Mimir Face Avatar Exploration Prompts
## 01 - Well Oracle
Use case: logo-brand
Asset type: profile picture concept for Mimir, the Face character of a repo swarm and realtime volumetric field machine
Primary request: epic pixel art avatar of Mimir as a colossal luminous face emerging from a dark well of data, one eye made of camera glass, the other eye made of waveform light, with tiny sensor constellations orbiting like obedient stars
Style/medium: high-drama pixel art portrait, 16-bit dark fantasy technology, low-resolution-friendly, bold silhouette, hand-repixelizable, no smooth vector look
Composition/framing: square profile-picture crop, face centered and dominant, shoulders or well rim barely visible, orbiting sensors framing the head
Lighting/mood: mythic, severe, intelligent, ancient machine oracle mood
Color palette: deep black-blue, cold cyan volumetric light, warm gold eye accents, sparse magenta timing sparks
Constraints: no text, no letters, no runes, no watermark, no photorealism, no horror gore, avoid tiny unreadable filigree, readable at 64x64 and salvageable at 32x32
## 02 - Voxel Mask
Use case: logo-brand
Asset type: profile picture concept for Mimir, the Face character of a repo swarm and realtime volumetric field machine
Primary request: epic pixel art avatar of a calm machine-seer mask made from interlocking voxels, with roots of light descending from the jaw into a reservoir and branches rising into a cubic starfield of cameras and microphones
Style/medium: heroic pixel art, mythic sci-fi portrait, 16-bit console cover icon, chunky edges, low-resolution-friendly, suitable for repixelizing into stock
Composition/framing: square avatar, bust portrait, symmetrical mask face centered, cubic crown/halo above, light roots below
Lighting/mood: solemn, godlike, benevolent but not soft, a face that owns the room clock
Color palette: obsidian background, cyan/teal voxel glow, bronze mask fragments, white star pixels, restrained violet shadows
Constraints: no text, no letters, no runes, no watermark, no photorealism, no decorative clutter, no soft painterly gradients, readable at 64x64
## 03 - Sensor Crown
Use case: logo-brand
Asset type: profile picture concept for Mimir, the Face character of a repo swarm and realtime volumetric field machine
Primary request: epic pixel art avatar of a floating oracle head built from transparent volumetric scan slices, with six camera-lens eyes arranged like a crown and microphone glyphs as cheek armor, staring forward through a glowing wireframe room
Style/medium: dramatic pixel art portrait, cyber-mythic, 16-bit boss portrait energy, hard pixel edges, deliberate dithering, low-res icon source
Composition/framing: square profile picture, forward-facing head fills frame, crown of lenses at top, wireframe room halo behind
Lighting/mood: imposing, watchful, precise, not cute
Color palette: black background, electric cyan scan slices, steel gray sensor parts, amber lens glints, small red sync pulses
Constraints: no text, no letters, no runes, no watermark, no photorealism, avoid horror, avoid excessive small parts, preserve strong face silhouette at 32x32
## 04 - Obsidian Reflection
Use case: logo-brand
Asset type: profile picture concept for Mimir, the Face character of a repo swarm and realtime volumetric field machine
Primary request: epic pixel art avatar of a half-human half-volumetric-field face reflected in a black obsidian pool, with signal threads from cameras and microphones weaving into the face like fate lines, the expression ancient and unamused
Style/medium: cinematic pixel art portrait, Norse-tech oracle, 16-bit title-screen icon, chunky readable pixels, limited palette with strong contrast
Composition/framing: square avatar crop, face reflection and real face merge vertically, signal threads frame the portrait without clutter
Lighting/mood: grand, ominous but controlled, intelligent, profile-picture readable
Color palette: black obsidian, icy cyan field lines, pale silver face planes, old gold highlights, tiny violet depth pixels
Constraints: no text, no letters, no runes, no watermark, no photorealism, no soft painterly blur, avoid grotesque horror, readable at small sizes

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 KiB

View file

@ -0,0 +1,41 @@
# Mimir Icon Exploration Prompts
## 01 - Well Reservoir
Use case: logo-brand
Asset type: icon concept for Mimir, a local realtime volumetric capture tool
Primary request: pixel art icon of a glowing Norse well or reservoir, with multiple tiny camera lenses and microphone capsules feeding colored streams into one coherent cubic volumetric field above the well
Style/medium: crisp pixel art, 16-bit game icon style, low-resolution-friendly, limited palette, chunky readable silhouettes, no anti-aliased vector look
Composition/framing: centered square app icon composition, strong silhouette, subject fills most of frame with padding
Color palette: deep ink background, cyan sensor streams, warm gold well rim, small magenta and green signal accents
Constraints: no text, no letters, no runes, no watermark, no photorealism, no gradients that cannot repixelize, simple shapes readable at 32x32
## 02 - Sensor Constellation
Use case: logo-brand
Asset type: icon concept for Mimir, a local realtime volumetric capture tool
Primary request: pixel art icon of six small sensor nodes arranged around a luminous central voxel cloud, with camera-eye and microphone-dot motifs connected by clean signal lines into a single stable field
Style/medium: crisp pixel art, 16-bit strategy-game technology icon, low-resolution-friendly, hard pixel edges, minimal dithering
Composition/framing: centered square app icon, radial constellation, central volumetric cloud is the largest readable shape
Color palette: charcoal background, teal and icy blue field, amber sensor nodes, one restrained red timing pulse
Constraints: no text, no letters, no runes, no watermark, no photorealism, no tiny details that disappear at 32x32
## 03 - World-Tree Field
Use case: logo-brand
Asset type: icon concept for Mimir, a local realtime volumetric capture tool
Primary request: pixel art icon of a compact world-tree silhouette whose roots are camera lenses and microphone capsules, with branches forming a glowing wireframe volumetric cube
Style/medium: crisp pixel art, mythic technology icon, 16-bit fantasy workstation aesthetic, low-resolution-friendly with bold shapes
Composition/framing: centered square app icon, tree trunk vertical through center, cube canopy, root sensors at bottom
Color palette: dark blue-black background, muted bronze trunk, cyan wireframe canopy, small white sensor highlights, faint violet shadow pixels
Constraints: no text, no letters, no runes, no watermark, no photorealism, avoid leafy clutter, keep icon readable at 32x32
## 04 - Eye/Ear Fusion
Use case: logo-brand
Asset type: icon concept for Mimir, a local realtime volumetric capture tool
Primary request: pixel art icon combining one watchful eye, one microphone capsule, and a transparent voxel head/room scan into a single emblem for multimodal perception fusion
Style/medium: crisp pixel art, 16-bit cyber-myth icon, low-resolution-friendly, strong silhouette, chunky pixels
Composition/framing: centered square app icon, eye and microphone flank a central translucent volumetric head or room-shape made from voxels, small timing sparks around it
Color palette: near-black background, teal/cyan volume, silver sensor metal, amber iris, small magenta timing sparks
Constraints: no text, no letters, no runes, no watermark, no photorealism, avoid horror, keep clean technical-mythic tone and readable 32x32 silhouette

View file

@ -1 +1 @@
"""Core audio-field building blocks for LocalCastBridge."""
"""Core audio-field building blocks for Mimir."""

View file

@ -1,4 +1,4 @@
{
{
"schema": "gamecult.localcast.perfect_machine.v1",
"reservoir": {
"durationSeconds": 5.0,
@ -8,7 +8,7 @@
"storageLayout": "single-native-rolling-buffer-with-typed-views"
},
"authorities": {
"configurationCalibrationLaunchStatus": "LocalCastBridge",
"configurationCalibrationLaunchStatus": "Mimir",
"denseVisualFusion": "Aquarium",
"gpuMaterialAndBrushReconciliation": "Aquarium",
"spoutPublication": "Aquarium",
@ -45,7 +45,7 @@
"outputs": {
"video": {
"target": "Spout2",
"senderName": "LocalCastBridge Point Cloud",
"senderName": "Mimir Point Cloud",
"owner": "Aquarium"
},
"audio": {

View file

@ -1,4 +1,4 @@
{
{
"fusion": {
"max_pair_dt_ns": 25000000,
"max_reprojection_error_px": 4.0,
@ -6,7 +6,7 @@
"cache_ttl_ns": 500000000
},
"render_bridge": {
"spout_sender_name": "LocalCastBridge Point Cloud",
"spout_sender_name": "Mimir Point Cloud",
"target_width": 1920,
"target_height": 1080,
"visual_delay_ns": 250000000,

View file

@ -1,8 +1,8 @@
# Aquarium Spatial Audio
# Aquarium Spatial Audio
## Objective
Publish the live spatial audio field through the same typed state discipline as the splat renderer. Aquarium consumes the AmbiX bus and hands it to native Faust DSP; LocalCastBridge owns capture, sync, calibration, and stream timing.
Publish the live spatial audio field through the same typed state discipline as the splat renderer. Aquarium consumes the AmbiX bus and hands it to native Faust DSP; Mimir owns capture, sync, calibration, and stream timing.
## Current Mechanism
@ -88,8 +88,8 @@ the next OBS-facing publisher.
## Invariants
- Aquarium owns rendering and Faust DSP, not microphone capture or clock correction.
- LocalCastBridge publishes declared AmbiX blocks for the spatial bed and aligned six-mic blocks for Faust voice separation. Unaligned heterogeneous microphones still do not cross this boundary.
- LocalCastBridge publishes source-event geometry separately from AmbiX audio; render effects do not have to infer transient positions from the sound bed.
- Mimir publishes declared AmbiX blocks for the spatial bed and aligned six-mic blocks for Faust voice separation. Unaligned heterogeneous microphones still do not cross this boundary.
- Mimir publishes source-event geometry separately from AmbiX audio; render effects do not have to infer transient positions from the sound bed.
- Faust owns the voice-separation graph once the aligned mic field has crossed into Aquarium. Python may publish controls and telemetry, but it must not become the hot separation engine.
- Aquarium/Spout packaging selects audio source events against each visual frame's `audio_alignment_time_ns`; OBS should receive the already synchronized output rather than trying to align independent media sources.
- The audio frame carries `start_sample` and `audio_time_ns` so visual packets can align against the same bounded-latency timeline.

View file

@ -1,4 +1,4 @@
# Architecture
# Architecture
## Objective

View file

@ -1,4 +1,4 @@
# Audio Field Spine
# Audio Field Spine
## Objective
@ -255,7 +255,7 @@ The audio code is split so the hard parts can be tested without the room, driver
- `audio_field.phase_meaning` owns live extraction of meaning from phase/chirplet evidence. It consumes known reference audio plus aligned mic windows, keeps the smoothed phase/frequency state internal, and emits only actionable state for alignment, suppression, and probe control.
- Room suppression owns stream-side cleanup: dialogue anchors define desired direct energy, spatial/context mics act as witnesses for room and transient clutter, and the cleaned aligned field feeds FOA/Aquarium/Faust.
- Source-event analysis owns non-vocal spatial facts: dialogue focus weights and localized transient vectors are published beside the AmbiX bed, not hidden inside it.
- Aquarium/Faust owns the hot voice-separation graph after `localcast.audio.mic_field`; LocalCastBridge owns alignment, timing, roles, and control publication.
- Aquarium/Faust owns the hot voice-separation graph after `localcast.audio.mic_field`; Mimir owns alignment, timing, roles, and control publication.
- `scripts/audio_field.py` stays as the operator CLI and hardware probe surface.
This is the portfolio-piece line: each module owns one invariant, and the tests use mocks/fakes instead of asking a driver stack to please be emotionally available.

View file

@ -1,4 +1,4 @@
# Implementation Plan
# Implementation Plan
## Current Cut Line
@ -13,10 +13,28 @@ The native reservoir is now one time-ordered rolling buffer with typed views.
The previous shared-edge typed rings proved the retention invariant; the new
shape removes independent per-kind storage from the live foundation.
The v1 OBS bridge is now gated by a bounded smoke-test witness ledger. No
plugin/native receiver expansion is justified until
`calibration/runs/obs-v1-smoke-ledger.json` records sender capture, SRT receive,
and OBS presentation timestamps for every planned endpoint with bounded drift,
latency, and confidence.
## Implemented
- Mimir public identity and repo Face:
- `README.md` now presents the project as Mimir: local sensors, coherent
field.
- `Mimir.png` is the user-repixelized 256px avatar source.
- `assets/branding/mimir-avatar-256.png` is the project branding copy.
- `.voidbot/voice/mimir.png` and `.voidbot/voice/identity.json` are the
VoidBot-facing persona surfaces for the Mimir Face.
- `.voidbot/state/mimir.cc` is the typed persistent Face state. It is seeded
with durable birth memories for Mimir identity, native-reservoir target,
ownership boundaries, audio-field discipline, and heartbeat agency pressure.
- `docs/mimir-face.md` defines Mimir's jurisdiction, voice, heartbeat
behavior, and inherited memories. VoidBot is only the mouth/scheduler.
- Perfect Machine contract:
- `docs/perfect-machine.md` defines the target native machine: one five-second spatiotemporal reservoir, Aquarium-owned dense visual fusion/material/brush rendering, Faust-owned hot audio DSP, OBS-owned broadcast controls, and LocalCastBridge-owned calibration/config/status.
- `docs/perfect-machine.md` defines the target native machine: one five-second spatiotemporal reservoir, Aquarium-owned dense visual fusion/material/brush rendering, Faust-owned hot audio DSP, OBS-owned broadcast controls, and Mimir-owned calibration/config/status.
- `config/perfect-machine.example.json` declares the contract shape for six cameras, six microphones, reservoir typed views, native authorities, outputs, and the demotion of bridge scripts to tooling.
- `native/reservoir` is the first native Rust crate. It implements one
shared-edge five-second rolling buffer with typed views and proves that
@ -51,12 +69,12 @@ shape removes independent per-kind storage from the live foundation.
- Sender device discovery script.
- Sender launch script with dry-run mode and per-source FFmpeg commands.
- OBS receiver setup notes.
- Neighbor sender deployment under `C:\Meta\LocalCastBridge`.
- Neighbor sender deployment under `C:\Meta\Mimir`.
- Madman's desktop start/stop launchers for the sender.
- Receiver OBS scene has `Neighbor PC - Video` and `Neighbor PC - Focusrite` Media Sources.
- Direct co-streamer loopback capture is now attempted with the repo WASAPI shim instead of Voicemeeter.
- Receiver OBS scene has `Neighbor PC - System Audio` on SRT port `5102`.
- Sender script uses `-nostdin` and repo-root logs so desktop launchers stay alive and diagnostics land in `C:\Meta\LocalCastBridge\logs`.
- Sender script uses `-nostdin` and repo-root logs so desktop launchers stay alive and diagnostics land in `C:\Meta\Mimir\logs`.
- Sender video config is `1920x1080` for the interactive desktop. The `1024x768` SSH session size is not the streaming target.
- Desktop `.cmd` launchers delegate to PowerShell wrappers so paths with spaces and apostrophes are handled in one place.
- Audio-field sidecar profile and tool:
@ -94,11 +112,19 @@ shape removes independent per-kind storage from the live foundation.
- `docs/typed-visual-state.md` documents the diagnostic CultCache file shape
and the CultNet visual schema target.
- OBS synchronized program surface:
- `scripts/obs_smoke_test.py` creates the v1 OBS smoke-test endpoint plan,
event template, and witness ledger.
- `localcast.obs_smoke` owns the pure endpoint planning and ledger summary
logic: per-stage timestamps, matched end-to-end latency, endpoint drift, and
confidence.
- `docs/obs-v1-smoke-test.md` is the runbook for collecting
`sender_capture`, `srt_receive`, and `obs_present` evidence before receiver
expansion.
- `scripts/setup_obs_synced_program.py` derives OBS-controllable stems from an aligned program audio timeline: host voice, co-streamer voice, ambient, transients, co-streamer loopback, and local loopback.
- `scripts/capture_co_streamer_surfaces.py` captures neighbor Focusrite and neighbor loopback with local loopback ground truth, estimates the late remote-family offset, and writes aligned co-streamer surfaces for the stem packer.
- `scripts/wasapi-loopback-capture.ps1` is the direct primary-playback loopback path. It must run in the neighbor's interactive console session; SSH-only capture sees the device but receives no render packets.
- The tool creates/updates local OBS Media Sources for those stems and mutes/disables raw unsynchronized inputs.
- Strict mode disables every scene item except the synchronized LocalCastBridge program video and stem controls.
- Strict mode disables every scene item except the synchronized Mimir program video and stem controls.
- Live phase-field meaning:
- `audio_field.phase_meaning` extracts actionable delay, coherence, confidence, suppression, correction-energy, and active-probe need from internal phase/chirplet evidence.
- `localcast.audio.phase_field` is a typed CultCache document at `calibration/runs/audio-phase-field.msgpack`.
@ -116,6 +142,8 @@ shape removes independent per-kind storage from the live foundation.
## Temporary
- Audio and video are separate SRT endpoints. This preserves OBS mixing authority but may need latency tuning.
- The OBS v1 smoke ledger currently owns the receiver expansion gate. Until a
real run passes, endpoint coherence is unknown rather than assumed.
- Audio defaults to AAC inside MPEG-TS for compatibility; test Opus later only if there is a concrete reason.
- Desktop capture uses `gdigrab` first because it is broadly available. `ddagrab` is a candidate once the installed FFmpeg build is confirmed.
- The scripts assume Windows sender and OBS receiver on the same LAN.
@ -151,3 +179,5 @@ shape removes independent per-kind storage from the live foundation.
spatialization, and stem generation into Faust/native DSP.
6. Keep FFmpeg/SRT scripts as simple LAN ingest/capture utilities, not the
synchronized program authority.
7. Run the v1 OBS smoke test against the actual sender/receiver setup and keep
the ledger as evidence before any receiver/plugin/native OBS expansion.

View file

@ -1,4 +1,4 @@
# Mimir Face
# Mimir Face
![Mimir avatar](../assets/branding/mimir-avatar-256.png)
@ -39,10 +39,10 @@ Mimir must not:
## Prime Memory
Mimir's public name is Mimir. The implementation still contains
`LocalCastBridge` and `localcast` names where they are already part of scripts,
ABIs, paths, and compatibility surfaces. Rename public-facing prose first.
Rename code only when the ownership boundary is clear.
Mimir's public name is Mimir. The implementation still contains `localcast`
names where they are already part of scripts, schemas, ABIs, paths, and
compatibility surfaces. Rename public-facing prose first. Rename code only when
the ownership boundary is clear.
Mimir exists to turn cameras, microphones, speakers, timing signals, and
distributed capture surfaces into one realtime coherent volumetric field. The
@ -156,7 +156,7 @@ a monitoring daemon.
These are the lessons Mimir should remember at birth:
- The repo began as LocalCastBridge, a LAN OBS bridge for a second Windows PC.
- The repo began as a LAN OBS bridge for a second Windows PC.
That scaffold is still useful, but it is no longer the product story.
- The public brand is now Mimir: local sensors, coherent field.
- The finished 256px avatar is `assets/branding/mimir-avatar-256.png`; the root

View file

@ -1,4 +1,4 @@
# Native Rebuild Plan
# Native Rebuild Plan
This is the cut line after the architectural teardown.
@ -14,7 +14,7 @@ to extend the deadline bridge.
Make the second PC available in OBS as synchronized program video plus
separately controllable audio while preserving the real ownership model:
- LocalCastBridge owns config, calibration, contract tests, launch, status, and
- Mimir owns config, calibration, contract tests, launch, status, and
CultNet schema.
- One native runtime owns live sample timing and retention.
- Aquarium owns GPU visual fusion, material/brush/splat rendering, and Spout2

View file

@ -1,10 +1,10 @@
# Neighbor Sender Setup
# Neighbor Sender Setup
## Host
- Sender: `192.168.1.84` / `DESKTOP-M9FTRLL`
- SSH: `ssh "madman's lullaby@192.168.1.84"`
- Install root: `C:\Meta\LocalCastBridge`
- Install root: `C:\Meta\Mimir`
- Receiver OBS workstation: `192.168.1.66`
## Installed State
@ -25,7 +25,7 @@
Live local config on the sender:
```text
C:\Meta\LocalCastBridge\config\localcast.json
C:\Meta\Mimir\config\localcast.json
```
Current endpoints:
@ -66,8 +66,8 @@ The pre-edit backup is next to the scene file with a
Madman's desktop has:
```text
C:\Users\Madman's Lullaby\Desktop\Start LocalCast Sender.cmd
C:\Users\Madman's Lullaby\Desktop\Stop LocalCast Sender.cmd
C:\Users\Madman's Lullaby\Desktop\Start Mimir Sender.cmd
C:\Users\Madman's Lullaby\Desktop\Stop Mimir Sender.cmd
```
Start launcher:
@ -79,7 +79,7 @@ Start launcher:
winget alias is not reliably available in interactive `cmd.exe`
- sets Windows default render to `Focusrite USB Audio\Device\Speakers\Render`
- sets Windows default capture to `Focusrite USB Audio\Device\Analogue 1 + 2\Capture`
- writes FFmpeg logs under `C:\Meta\LocalCastBridge\logs`
- writes FFmpeg logs under `C:\Meta\Mimir\logs`
Stop launcher:
@ -121,7 +121,7 @@ can fail with `Failed to capture image (error 5)`. The sender video process
must be launched from Madman's interactive desktop, normally through:
```text
C:\Users\Madman's Lullaby\Desktop\Start LocalCast Sender.cmd
C:\Users\Madman's Lullaby\Desktop\Start Mimir Sender.cmd
```
Attached SSH probes are still useful for audio devices and SRT reachability.

View file

@ -28,6 +28,22 @@ If using a passphrase, append:
OBS should be open before the sender starts if OBS is the SRT listener. If a source gets wedged, deactivate and reactivate the Media Source before changing ports; port churn is a symptom, not a configuration strategy.
## V1 Smoke Gate
Before adding receiver plugins, native OBS surfaces, or new bridge elegance, run
the bounded witness ledger in `docs/obs-v1-smoke-test.md`.
The required artifact is:
```text
calibration/runs/obs-v1-smoke-ledger.json
```
It must include `sender_capture`, `srt_receive`, and `obs_present` evidence for
every planned endpoint, plus endpoint drift, end-to-end latency, and confidence.
If the ledger fails, fix the clocks before expanding the receiver. The field may
be charming while lying. Still a liar.
## Synchronization
The video SRT source is a timed media artifact in the Aquarium path, not just

View file

@ -1,10 +1,10 @@
# OBS Spout Streaming
# OBS Spout Streaming
## Current Status
The Python/OpenGL Spout sender has been deleted.
LocalCastBridge no longer carries a runnable OpenGL publication path. The only
Mimir no longer carries a runnable OpenGL publication path. The only
surviving pieces are diagnostic packet/brush math in
`localcast.diagnostics.render_math` and typed diagnostic visual documents in
`localcast.diagnostics.visual_cache`.
@ -25,7 +25,7 @@ native rolling reservoir
## Invariants
- OBS receives a named Spout2 texture from Aquarium, not Python/OpenGL.
- LocalCastBridge may expose diagnostic typed documents and CPU render math, but
- Mimir may expose diagnostic typed documents and CPU render math, but
it does not own the production renderer.
- JSON remains a diagnostic/status surface only.
- The neighbor SRT video feed is a timed media artifact and must share the same

85
docs/obs-v1-smoke-test.md Normal file
View file

@ -0,0 +1,85 @@
# OBS V1 Smoke Test
No plugin/native receiver work gets promoted until this ledger exists for the
plain FFmpeg/SRT OBS path.
The smoke test is deliberately small:
- `sender_capture`: sender-side flash/chirp or packet-origin timestamp.
- `srt_receive`: receiver-side stream arrival timestamp.
- `obs_present`: OBS preview/recording presentation timestamp.
- endpoint drift: media-time slope versus receiver monotonic time.
- end-to-end latency: matched `eventId` from `sender_capture` to `obs_present`.
- confidence: stage coverage, observation count, event confidence, latency, and drift.
The output ledger is `calibration/runs/obs-v1-smoke-ledger.json`. A passing
ledger sets `gate.pluginOrNativeReceiverExpansionAllowed` to `true`. Anything
else means the bridge is still talking without enough clocks. Adorable, but no.
## Run
From the repo root:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' .\scripts\obs_smoke_test.py --config .\config\localcast.json plan
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' .\scripts\obs_smoke_test.py --config .\config\localcast.json template-events
```
Edit `calibration/runs/obs-v1-smoke-events.jsonl` with real observations from
one bounded pass. Use the same `eventId` for the same visible flash or audible
chirp as it moves through stages.
Then summarize:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' .\scripts\obs_smoke_test.py --config .\config\localcast.json summarize
```
## Event Shape
Each line is one JSON object:
```json
{
"endpoint": "video",
"stage": "obs_present",
"eventId": "video-flash-001",
"monotonicNs": 1000180000000,
"wallTimeNs": null,
"mediaTimeNs": 180000000,
"confidence": 0.9,
"note": "OBS recording frame containing flash"
}
```
Required stages are:
- `sender_capture`
- `srt_receive`
- `obs_present`
`monotonicNs` is mandatory because wall clocks lie casually. `wallTimeNs` is
optional human correlation only. `mediaTimeNs` is optional for individual events,
but drift confidence needs at least two receiver-side media-time observations.
## Thresholds
Defaults:
- maximum absolute endpoint drift: `100 ppm`
- maximum end-to-end latency: `500 ms`
Override them when running the summary:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' .\scripts\obs_smoke_test.py --config .\config\localcast.json --max-abs-drift-ppm 50 --max-latency-ms 350 summarize
```
## Interpretation
`status: ok` means every planned endpoint has enough timestamp evidence for this
bounded pass. It does not prove the native reservoir is done. It proves the v1
OBS bridge has a clocked witness.
`status: fail` means stop bridge expansion and fix the missing or drifting
stage first.

View file

@ -1,4 +1,4 @@
# Perfect Machine
# Perfect Machine
## Objective
@ -41,7 +41,7 @@ not the runtime API.
brush/splat generation, final render target, and Spout publication.
- Faust owns hot audio DSP: voice separation, room suppression, Ambisonic/HOA
spatialization, and loopback/source stem generation.
- LocalCastBridge owns configuration, calibration, typed contracts, launch,
- Mimir owns configuration, calibration, typed contracts, launch,
status, persistence, and compatibility surfaces.
## Native Runtime Shape

View file

@ -1,4 +1,4 @@
# Sensor Fusion Architecture
# Sensor Fusion Architecture
## Objective
@ -79,7 +79,7 @@ flowchart TD
camera projection, brush lowering, and CPU rasterization. It has unit tests
and does not require a GPU context.
- `scripts/sensor_fusion.py` owns CLI composition for offline observation files.
- The Python/OpenGL Spout publisher has been deleted. No LocalCastBridge Python
- The Python/OpenGL Spout publisher has been deleted. No Mimir Python
module owns OBS publication.
- `config/sensor-fusion.example.json` owns the declarative rig shape: capture hints, intrinsics, extrinsics, latency, and fusion thresholds.
@ -137,12 +137,12 @@ OBS should see a named Spout sender, not a camera window, file tail, or desktop
The intended production handoff is:
```text
LocalCastBridge TrackCache
Mimir TrackCache
-> CultCache render-frame document
-> Aquarium Engine typed runtime state
-> GPU point/brush/splat buffer
-> D3D render target
-> Spout2 sender named "LocalCastBridge Point Cloud"
-> Spout2 sender named "Mimir Point Cloud"
-> OBS Spout2 Capture source
```

View file

@ -1,4 +1,4 @@
# Diagnostic Typed Visual State
# Diagnostic Typed Visual State
## Objective
@ -87,7 +87,7 @@ channels before publication so the downstream accumulator can use Leap as the
strongest timing/motion witness without pretending the packed image is one
ordinary RGB camera.
LocalCastBridge does not own the final splat budget. It publishes dense typed
Mimir does not own the final splat budget. It publishes dense typed
claims with stable keys and timestamps; Aquarium owns temporal accumulation,
renderer residency, and any GPU-side reduction needed to make million-splat
frames presentable.

View file

@ -1,6 +1,6 @@
declare name "LocalCastBridge Voice Separation";
declare name "Mimir Voice Separation";
declare version "1.0";
declare author "LocalCastBridge";
declare author "Mimir";
declare license "MIT";
// Six aligned microphone inputs:

View file

@ -1 +1 @@
"""LocalCastBridge Python helpers."""
"""Mimir Python helpers."""

View file

@ -12,7 +12,7 @@ from localcast.sensor_fusion.render_bridge import RenderFramePacket, RenderPoint
@dataclass(frozen=True)
class SpoutOutputConfig:
sender_name: str = "LocalCastBridge Point Cloud"
sender_name: str = "Mimir Point Cloud"
width: int = 1920
height: int = 1080
fps: float = 60.0

View file

@ -1,4 +1,4 @@
import argparse
import argparse
from pathlib import Path
import sys
import time
@ -68,7 +68,7 @@ def main() -> None:
parser.add_argument("--input", type=Path, default=ROOT / "calibration" / "runs" / "audio-full-sync-20260518-165751" / "field-foa-ambix.wav")
parser.add_argument("--cache", type=Path, default=ROOT / "calibration" / "runs" / "audio-state.msgpack")
parser.add_argument("--status-cache", type=Path, default=ROOT / "calibration" / "runs" / "audio-stream-status.msgpack")
parser.add_argument("--stream-name", default="LocalCastBridge AmbiX")
parser.add_argument("--stream-name", default="Mimir AmbiX")
parser.add_argument("--chunk-frames", type=int, default=1024)
parser.add_argument("--duration", type=float)
parser.add_argument("--loop", action="store_true")

270
localcast/obs_smoke.py Normal file
View file

@ -0,0 +1,270 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
from pathlib import Path
from statistics import median
from typing import Any, Iterable
NS_PER_MS = 1_000_000
NS_PER_SECOND = 1_000_000_000
@dataclass(frozen=True)
class EndpointPlan:
name: str
kind: str
port: int
listener_url: str
expected_latency_ms: float
@dataclass(frozen=True)
class SmokeEvent:
endpoint: str
stage: str
monotonic_ns: int
wall_time_ns: int | None = None
media_time_ns: int | None = None
event_id: str | None = None
confidence: float = 1.0
note: str = ""
def load_config_endpoints(config_path: Path) -> list[EndpointPlan]:
config = json.loads(config_path.read_text(encoding="utf-8-sig"))
receiver = config.get("receiver", {})
base_port = int(receiver.get("basePort", 5100))
expected_latency_ms = float(receiver.get("srtLatencyMicros", 120000)) / 1000.0
passphrase = str(receiver.get("passphrase", "") or "")
secret_suffix = "&passphrase=SET&pbkeylen=16" if passphrase else ""
endpoints: list[EndpointPlan] = []
video = config.get("video") or {}
if video.get("enabled", False):
port = base_port + int(video.get("portOffset", 0))
endpoints.append(
EndpointPlan(
name="video",
kind="video",
port=port,
listener_url=f"srt://0.0.0.0:{port}?mode=listener&latency={int(expected_latency_ms * 1000)}&timeout=5000000{secret_suffix}",
expected_latency_ms=expected_latency_ms,
)
)
for audio in config.get("audioSources", []) or []:
name = str(audio.get("name", "")).strip()
if not name:
continue
port = base_port + int(audio.get("portOffset", 0))
endpoints.append(
EndpointPlan(
name=f"audio-{name}",
kind="audio",
port=port,
listener_url=f"srt://0.0.0.0:{port}?mode=listener&latency={int(expected_latency_ms * 1000)}&timeout=5000000{secret_suffix}",
expected_latency_ms=expected_latency_ms,
)
)
return endpoints
def event_from_json(raw: dict[str, Any]) -> SmokeEvent:
return SmokeEvent(
endpoint=str(raw["endpoint"]),
stage=str(raw["stage"]),
monotonic_ns=int(raw["monotonicNs"]),
wall_time_ns=None if raw.get("wallTimeNs") is None else int(raw["wallTimeNs"]),
media_time_ns=None if raw.get("mediaTimeNs") is None else int(raw["mediaTimeNs"]),
event_id=None if raw.get("eventId") is None else str(raw["eventId"]),
confidence=float(raw.get("confidence", 1.0)),
note=str(raw.get("note", "")),
)
def read_events_jsonl(path: Path) -> list[SmokeEvent]:
events: list[SmokeEvent] = []
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if not line.strip():
continue
try:
events.append(event_from_json(json.loads(line)))
except Exception as exc: # pragma: no cover - message quality path
raise ValueError(f"{path}:{line_number}: invalid smoke event: {exc}") from exc
return events
def write_event_template(path: Path, endpoints: Iterable[EndpointPlan]) -> None:
rows: list[dict[str, Any]] = []
base = 1_000_000_000_000
for index, endpoint in enumerate(endpoints):
event_id = f"{endpoint.name}-flash-001" if endpoint.kind == "video" else f"{endpoint.name}-chirp-001"
start = base + index * 10 * NS_PER_SECOND
rows.extend(
[
{
"endpoint": endpoint.name,
"stage": "sender_capture",
"eventId": event_id,
"monotonicNs": start,
"wallTimeNs": None,
"mediaTimeNs": 0,
"confidence": 1.0,
"note": "Replace with sender-side flash/chirp capture timestamp.",
},
{
"endpoint": endpoint.name,
"stage": "srt_receive",
"eventId": event_id,
"monotonicNs": start + int(endpoint.expected_latency_ms * NS_PER_MS),
"wallTimeNs": None,
"mediaTimeNs": int(endpoint.expected_latency_ms * NS_PER_MS),
"confidence": 0.8,
"note": "Replace with first packet or probe observation at receiver.",
},
{
"endpoint": endpoint.name,
"stage": "obs_present",
"eventId": event_id,
"monotonicNs": start + int(endpoint.expected_latency_ms * NS_PER_MS * 1.5),
"wallTimeNs": None,
"mediaTimeNs": int(endpoint.expected_latency_ms * NS_PER_MS),
"confidence": 0.8,
"note": "Replace with OBS recording/preview observation.",
},
]
)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("".join(json.dumps(row, sort_keys=True) + "\n" for row in rows), encoding="utf-8")
def summarize_events(
endpoints: Iterable[EndpointPlan],
events: Iterable[SmokeEvent],
*,
max_abs_drift_ppm: float = 100.0,
max_latency_ms: float = 500.0,
) -> dict[str, Any]:
endpoint_plans = {endpoint.name: endpoint for endpoint in endpoints}
events_by_endpoint: dict[str, list[SmokeEvent]] = {name: [] for name in endpoint_plans}
for event in events:
events_by_endpoint.setdefault(event.endpoint, []).append(event)
summaries = []
for name, endpoint in endpoint_plans.items():
rows = sorted(events_by_endpoint.get(name, []), key=lambda item: item.monotonic_ns)
stages = sorted({row.stage for row in rows})
latencies_ms = _matched_latencies_ms(rows)
drift_ppm = _endpoint_drift_ppm(rows)
confidence = _endpoint_confidence(
rows,
latencies_ms=latencies_ms,
drift_ppm=drift_ppm,
max_abs_drift_ppm=max_abs_drift_ppm,
max_latency_ms=max_latency_ms,
)
status = "ok" if confidence >= 0.8 else "suspect" if confidence >= 0.5 else "fail"
summaries.append(
{
"endpoint": name,
"kind": endpoint.kind,
"port": endpoint.port,
"listenerUrl": endpoint.listener_url,
"expectedLatencyMs": endpoint.expected_latency_ms,
"observedStages": stages,
"eventCount": len(rows),
"endToEndLatencyMs": _latency_summary(latencies_ms),
"endpointDriftPpm": drift_ppm,
"confidence": round(confidence, 3),
"status": status,
}
)
overall_confidence = min((item["confidence"] for item in summaries), default=0.0)
failed = [item["endpoint"] for item in summaries if item["status"] != "ok"]
return {
"schema": "gamecult.localcast.obs_v1_smoke_ledger.v1",
"createdWallTime": datetime.now(timezone.utc).isoformat(),
"gate": {
"pluginOrNativeReceiverExpansionAllowed": not failed,
"reason": "Every planned OBS-facing endpoint must have sender_capture, srt_receive, and obs_present evidence with bounded latency and drift.",
},
"thresholds": {
"maxAbsDriftPpm": max_abs_drift_ppm,
"maxLatencyMs": max_latency_ms,
"requiredStages": ["sender_capture", "srt_receive", "obs_present"],
},
"endpoints": summaries,
"overallConfidence": round(overall_confidence, 3),
"status": "ok" if not failed and summaries else "fail",
"blockedEndpoints": failed,
}
def _matched_latencies_ms(events: list[SmokeEvent]) -> list[float]:
by_id: dict[str, dict[str, SmokeEvent]] = {}
for event in events:
if not event.event_id:
continue
by_id.setdefault(event.event_id, {})[event.stage] = event
latencies: list[float] = []
for stages in by_id.values():
sender = stages.get("sender_capture")
observed = stages.get("obs_present")
if sender is None or observed is None:
continue
latencies.append((observed.monotonic_ns - sender.monotonic_ns) / NS_PER_MS)
return latencies
def _endpoint_drift_ppm(events: list[SmokeEvent]) -> float | None:
timed = [event for event in events if event.media_time_ns is not None and event.stage in {"srt_receive", "obs_present"}]
timed.sort(key=lambda item: item.monotonic_ns)
if len(timed) < 2:
return None
first = timed[0]
last = timed[-1]
wall_delta = last.monotonic_ns - first.monotonic_ns
media_delta = int(last.media_time_ns) - int(first.media_time_ns)
if wall_delta <= 0:
return None
return round(((media_delta - wall_delta) / wall_delta) * 1_000_000.0, 3)
def _latency_summary(latencies_ms: list[float]) -> dict[str, float | int | None]:
if not latencies_ms:
return {"count": 0, "median": None, "min": None, "max": None}
return {
"count": len(latencies_ms),
"median": round(median(latencies_ms), 3),
"min": round(min(latencies_ms), 3),
"max": round(max(latencies_ms), 3),
}
def _endpoint_confidence(
events: list[SmokeEvent],
*,
latencies_ms: list[float],
drift_ppm: float | None,
max_abs_drift_ppm: float,
max_latency_ms: float,
) -> float:
if not events:
return 0.0
stages = {event.stage for event in events}
required = {"sender_capture", "srt_receive", "obs_present"}
stage_score = len(stages & required) / len(required)
event_score = min(1.0, len(events) / 6.0)
observation_score = max(0.0, min(1.0, sum(max(0.0, min(1.0, event.confidence)) for event in events) / len(events)))
latency_score = 0.0
if latencies_ms:
worst = max(latencies_ms)
latency_score = max(0.0, min(1.0, 1.0 - (worst / max_latency_ms)))
drift_score = 0.0
if drift_ppm is not None:
drift_score = max(0.0, min(1.0, 1.0 - (abs(drift_ppm) / max_abs_drift_ppm)))
return max(0.0, min(1.0, stage_score * 0.35 + event_score * 0.15 + observation_score * 0.15 + latency_score * 0.2 + drift_score * 0.15))

View file

@ -1,4 +1,4 @@
"""Sensor fusion spine for the LocalCastBridge spatial rig."""
"""Sensor fusion spine for the Mimir spatial rig."""
from .core import (
CameraModel,

View file

@ -1,4 +1,4 @@
from __future__ import annotations
from __future__ import annotations
from dataclasses import dataclass
@ -9,7 +9,7 @@ from .core import TriangulatedPoint
@dataclass(frozen=True)
class RenderBridgeConfig:
spout_sender_name: str = "LocalCastBridge Point Cloud"
spout_sender_name: str = "Mimir Point Cloud"
target_width: int = 1920
target_height: int = 1080
visual_delay_ns: int = 250_000_000
@ -19,7 +19,7 @@ class RenderBridgeConfig:
@staticmethod
def from_dict(data: dict) -> "RenderBridgeConfig":
return RenderBridgeConfig(
spout_sender_name=data.get("spout_sender_name", "LocalCastBridge Point Cloud"),
spout_sender_name=data.get("spout_sender_name", "Mimir Point Cloud"),
target_width=int(data.get("target_width", 1920)),
target_height=int(data.get("target_height", 1080)),
visual_delay_ns=int(data.get("visual_delay_ns", 250_000_000)),
@ -98,7 +98,7 @@ class RenderFramePacket:
source_time_max_ns=int(data.get("source_time_max_ns", 0)),
present_time_ns=int(data.get("present_time_ns", 0)),
audio_alignment_time_ns=int(data.get("audio_alignment_time_ns", 0)),
spout_sender_name=str(data.get("spout_sender_name", "LocalCastBridge Point Cloud")),
spout_sender_name=str(data.get("spout_sender_name", "Mimir Point Cloud")),
target_width=int(data.get("target_width", 1920)),
target_height=int(data.get("target_height", 1080)),
points=tuple(RenderPointPacket.from_dict(item) for item in data.get("points", [])),

View file

@ -1,8 +1,8 @@
# Native Runtime
# Native Runtime
This directory is for the hot path.
LocalCastBridge Python remains useful for calibration, launch, status, contract
Mimir Python remains useful for calibration, launch, status, contract
tests, and offline analysis. It does not own dense live fusion or DSP.
## Crates

View file

@ -1,8 +1,8 @@
[package]
[package]
name = "localcast-reservoir"
version = "0.1.0"
edition = "2024"
description = "Native five-second spatiotemporal reservoir core for LocalCastBridge"
description = "Native five-second spatiotemporal reservoir core for Mimir"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]

View file

@ -1,10 +1,23 @@
# Current System Map
# Current System Map
LocalCastBridge is intentionally thin. The target live machine is not the
Mimir is the public Face and product name for this repo. `localcast` remains
the implementation surface where existing scripts, paths, schemas, and ABIs
still use it.
The repo is intentionally thin. The target live machine is not the
deadline bridge; it is the native rolling reservoir described in
`docs/native-rebuild-plan.md`, `docs/perfect-machine.md`, and
`config/perfect-machine.example.json`.
Mimir's persistent Face state is part of the operating machine:
- `docs/mimir-face.md` owns the Face doctrine, jurisdiction, heartbeat behavior,
and inherited memory packet.
- `.voidbot/voice/identity.json` and `.voidbot/voice/mimir.png` are the
VoidBot-facing persona identity.
- `.voidbot/state/mimir.cc` is typed persistent Face memory. VoidBot is the
transport layer; Mimir owns continuity and repo-progress pressure.
## Perfect Machine Target
```mermaid
@ -23,7 +36,7 @@ flowchart TD
Ownership:
- LocalCastBridge owns configuration, calibration, contract tests, launch, status, and persistence.
- Mimir owns configuration, calibration, contract tests, launch, status, and persistence.
- Aquarium owns dense visual fusion, material/brush/splat reconciliation, final render target, and Spout publication.
- Faust owns hot audio DSP and program stem generation.
- OBS owns broadcast controls.
@ -68,6 +81,13 @@ flowchart TD
An OBS plugin would be justified if OBS could not ingest stable LAN streams, or if a plugin were needed to expose independent audio controls. V1 gets independent audio controls by making each source an OBS Media Source. That is boring. Boring is allowed to win when it is correct.
The next gate is not taste, it is evidence. `scripts/obs_smoke_test.py` and
`localcast.obs_smoke` now define the v1 OBS smoke ledger:
`calibration/runs/obs-v1-smoke-ledger.json`. Receiver/plugin/native expansion is
blocked until each planned endpoint has `sender_capture`, `srt_receive`, and
`obs_present` timestamp evidence, plus bounded drift, end-to-end latency, and
confidence.
## Known Risks
- Windows audio source names vary by driver and localization.
@ -118,7 +138,7 @@ Ownership:
- Active probe optimization owns extra chirplet emission when confidence drops, bounded by level/spacing/audibility budget. `audio_field.active_probe` is the runtime join that converts phase-field confidence into emitted chirplet WAVs/manifests and playback. Probe WAVs rotate through fixed slots and the manifest rotates at a byte cap; calibration may be dense, but artifacts must stay bounded.
- The camera/sensor-fusion pipeline may publish world poses later; it does not own audio clocks or channel timing.
- OBS may ingest rendered output later; it is not the authority for the Ambisonic field.
- Aquarium/Faust owns hot voice separation after LocalCastBridge publishes `localcast.audio.mic_field`; LocalCastBridge owns alignment, timing, mic roles, graph id, and controls.
- Aquarium/Faust owns hot voice separation after Mimir publishes `localcast.audio.mic_field`; Mimir owns alignment, timing, mic roles, graph id, and controls.
Invariant: distributed camera/Focusrite microphones must be aligned and resampled into one reference timeline before FOA encoding. Latency is allowed as bounded buffering, but cache depth must converge toward real-time. Speaker output chirplets are live telemetry; delay/SRO/phase state must update during runtime, not only during setup. Extra chirplets may be emitted automatically when confidence drops, but only under the active probe optimizer's budget. The local shielded cardioid and neighbor shotgun are the high-quality dialogue anchors; camera mics provide spatial/context evidence.
@ -196,3 +216,8 @@ Ownership:
- Aquarium/Spout owns synchronized program video, not raw remote desktop capture.
Invariant: raw SRT ingest, Desktop Audio, Mic/Aux, and other non-program scene items may exist for diagnostics, but they must be disabled or muted when broadcasting the synchronized program. Missing synchronized feeds should appear as explicit silent placeholders, not as live unsynchronized substitutes. The co-streamer Focusrite, co-streamer loopback, and remote video are the latest-arriving surface family; their measured delay should set the presentation buffer horizon instead of being patched in after the local field. Do not reintroduce Voicemeeter as the loopback authority without a concrete invariant; prefer direct WASAPI loopback or a sender FFmpeg build with native WASAPI input.
Smoke-test invariant: a visible stream in OBS is not proof of coherence. The v1
bridge must leave a witness ledger with per-stage timestamps, endpoint drift,
end-to-end latency, and confidence before the receiver surface grows new
machinery.

View file

@ -1,6 +1,6 @@
# Fresh Workspace Handoff
# Fresh Workspace Handoff
This is the short re-entry packet for `E:\Projects\LocalCastBridge`.
This is the short re-entry packet for `E:\Projects\Mimir`.
Do not trust this file for the exact live HEAD. Exact branch, HEAD, and dirty state are volatile. Always ask git.
@ -19,6 +19,18 @@ Get-Content .\state\evidence.jsonl -Tail 8
## Current Shape
- Public brand/Face is Mimir. Implementation surfaces still use `localcast`
until a deliberate package/schema rename cut exists.
- Mimir's Face doctrine lives in `docs/mimir-face.md`.
- Mimir's 256px avatar source is `Mimir.png`; project copy is
`assets/branding/mimir-avatar-256.png`; VoidBot persona copy is
`.voidbot/voice/mimir.png`.
- Mimir is registered in VoidBot's private repo-Face registry alongside Nibu and
Aqua. Repo-local Face identity/state live at `.voidbot/voice/identity.json`
and `.voidbot/state/mimir.cc`.
- Birth runner completed in plan mode under `.voidbot/birth`; the typed
repo-Face state has already been seeded with durable Mimir birth memories and
a heartbeat agency pressure.
- V1 uses FFmpeg plus OBS Media Source over SRT.
- Sender-side video encoding is `h264_nvenc`.
- Audio sources are separate SRT endpoints so OBS can mix them separately.
@ -70,6 +82,11 @@ Get-Content .\state\evidence.jsonl -Tail 8
use the descriptor plus native point-buffer decoder by default.
- Aquarium-Engine commit `3316895` exposes `CreateProducer(kind, sourceId)` so
producer source hashing goes through native `localcast_producer_create_for_source`.
- Aquarium-Engine commit `0f610d0` makes native render packets a direct GPU
fusion input: `LocalCastNativeVisualFrameSource` exposes caller-owned
`LocalcastRenderPoint` buffers, `LocalCastRuntime` publishes that buffer
instead of managed seeds when present, and D3D12 uploads the native point bytes
for the LocalCast fusion compute shader.
- Edge JSON is schema/diagnostic only. Runtime network/process data should remain typed CultNet documents.
- Python live producers and diagnostic CultCache/JSON file adapters are
diagnostics, not production foundations. The OpenGL Spout sink has been
@ -81,11 +98,18 @@ Get-Content .\state\evidence.jsonl -Tail 8
`LocalcastProducer`.
- The AmbiX spatial-audio Python publisher is also quarantined as
`localcast.diagnostics.spatial_audio`.
- Neighbor sender is deployed at `C:\Meta\LocalCastBridge` on `192.168.1.84`.
- Madman's desktop has `Start LocalCast Sender.cmd` and `Stop LocalCast Sender.cmd`.
- Neighbor sender is deployed at `C:\Meta\Mimir` on `192.168.1.84`.
- Madman's desktop has `Start Mimir Sender.cmd` and `Stop Mimir Sender.cmd`.
- Receiver OBS scene collection has `Neighbor PC - Video`, `Neighbor PC - Focusrite`, and `Neighbor PC - System Audio` Media Sources added.
- V1 OBS bridge expansion is now gated by
`calibration/runs/obs-v1-smoke-ledger.json`. The harness is
`scripts/obs_smoke_test.py`, pure summary logic lives in
`localcast.obs_smoke`, and the runbook is `docs/obs-v1-smoke-test.md`.
A passing ledger needs `sender_capture`, `srt_receive`, and `obs_present`
evidence for every planned endpoint, with endpoint drift, end-to-end latency,
and confidence.
- Sender uses SoundVolumeView for default device selection. Voicemeeter is no longer the loopback authority; co-streamer playback loopback uses the direct WASAPI shim scheduled into the neighbor console session.
- Sender script was patched after failed transmission: live FFmpeg commands now include `-nostdin`, logs go under `C:\Meta\LocalCastBridge\logs`, and interactive video capture size is `1920x1080`.
- Sender script was patched after failed transmission: live FFmpeg commands now include `-nostdin`, logs go under `C:\Meta\Mimir\logs`, and interactive video capture size is `1920x1080`.
- Madman's desktop launchers call PowerShell wrappers: `scripts\start-localcast-desktop.ps1` and `scripts\stop-localcast-desktop.ps1`.
- SSH-launched video capture fails with `gdigrab` error 5; test video from Madman's interactive desktop launcher.
@ -93,7 +117,9 @@ Get-Content .\state\evidence.jsonl -Tail 8
The useful next work is foundation surgery, not hardware feature expansion:
- bind Aquarium/Faust/native workers to the rolling-buffer `LocalcastRuntime`
- build native capture workers that fill typed audio/render buffers through
`LocalcastProducer`
- bind Faust/native audio workers to the rolling-buffer `LocalcastRuntime`
- build native mic/loopback/phase producers over `LocalcastProducer`; do not
restore Python audio live launchers
- delete the remaining diagnostic `visual_producer` command once native/Aquarium
@ -103,9 +129,12 @@ The useful next work is foundation surgery, not hardware feature expansion:
reviving the deleted Python/OpenGL publisher
- route future visual/audio runtime work through Aquarium/Faust/native runtime plus typed CultNet docs
- keep FFmpeg/SRT bridge scripts only as simple LAN ingest and capture utilities
- run the v1 OBS smoke-test witness ledger against the real sender/receiver
before adding any OBS plugin/native receiver expansion
## Immediate Re-entry Instruction
Do not revive the Python/OpenGL bridge. Rehydrate, read
`docs/native-rebuild-plan.md`, then cut toward native workers,
Aquarium/Faust bindings, and typed CultNet boundaries.
`docs/native-rebuild-plan.md`, then run the v1 OBS smoke-test witness ledger
before receiver expansion. After that clock evidence exists, cut toward native
workers, Aquarium/Faust bindings, and typed CultNet boundaries.

View file

@ -1,4 +1,4 @@
# Ambisonic AquaSynth Field: Distilled Research
# Ambisonic AquaSynth Field: Distilled Research
## Objective
@ -6,7 +6,7 @@ Consolidate four live microphones -- shielded cardioid, Deru shotgun, and two ca
## Current Mechanism
LocalCastBridge currently treats audio as independent OBS-ingestable endpoints: one FFmpeg process per audio source, one SRT port per source, and OBS owns the mix. That is coherent for manual OBS mixing, but it is the wrong ownership model for a single spatial field. An ambisonic bus needs clocked multichannel audio before spatial encoding, then effects, then decode/render.
Mimir currently treats audio as independent OBS-ingestable endpoints: one FFmpeg process per audio source, one SRT port per source, and OBS owns the mix. That is coherent for manual OBS mixing, but it is the wrong ownership model for a single spatial field. An ambisonic bus needs clocked multichannel audio before spatial encoding, then effects, then decode/render.
## Invariants

View file

@ -1,4 +1,4 @@
# Live Adaptive Sync: Distilled Research
# Live Adaptive Sync: Distilled Research
## Objective
@ -6,7 +6,7 @@ Find prior work that supports synchronizing independent live microphone feeds we
## Current Mechanism
LocalCastBridge currently launches separate FFmpeg capture processes and SRT endpoints. That preserves OBS mix control but does not give us one clocked multichannel stream. For Ambisonics, independent device clocks create two separate problems:
Mimir currently launches separate FFmpeg capture processes and SRT endpoints. That preserves OBS mix control but does not give us one clocked multichannel stream. For Ambisonics, independent device clocks create two separate problems:
- initial offset: one stream starts late or has more device/buffer latency
- sampling-rate offset: one device runs slightly faster/slower, so offset grows over time

View file

@ -2,6 +2,6 @@
Spout is the Windows-local GPU texture sharing bridge for this rig. The OBS path is the Off World Live OBS Spout2 plugin, which creates an OBS source from a Spout shared texture and can also send the OBS canvas back to Spout. The Spout2 SDK is the application-side sender/receiver surface; its modern path centers on DirectX shared textures and has native DirectX/OpenGL integration options.
Decision: LocalCastBridge should not render final pixels itself. It should publish typed render-frame packets from the fusion cache. Aquarium Engine should consume those packets, render them into a GPU render target, and publish that texture as a named Spout sender. OBS then receives that sender through the OBS Spout2 plugin.
Decision: Mimir should not render final pixels itself. It should publish typed render-frame packets from the fusion cache. Aquarium Engine should consume those packets, render them into a GPU render target, and publish that texture as a named Spout sender. OBS then receives that sender through the OBS Spout2 plugin.
Latency decision: the render packet carries both source timestamps and intended presentation time. The stream compositor can buffer visual frames until they align with the ambisonic audio field's presentation timeline. Latency is allowed; unbounded drift is not.

View file

@ -1,4 +1,4 @@
# Brushstroke Fusion Rendering Approach
# Brushstroke Fusion Rendering Approach
## Objective
@ -19,7 +19,7 @@ semantic intent
-> renderer
```
For LocalCastBridge, the equivalent is:
For Mimir, the equivalent is:
```text
sensor observations

View file

@ -1,8 +1,8 @@
# Visual Spatial Map And Splat Scene
# Visual Spatial Map And Splat Scene
## Objective
Extend the AquaSynth/LocalCastBridge concept from "one camera view plus spatial audio" into a shared live spatial scene:
Extend the AquaSynth/Mimir concept from "one camera view plus spatial audio" into a shared live spatial scene:
- two Razer Kiyo-class camera/mic rigs provide RGB and audio; one is Kiyo Pro
- two PlayStation Eye / PS3 Eye cameras provide cheap high-rate low-resolution tracking views

View file

@ -1,4 +1,4 @@
import argparse
import argparse
import importlib.util
import json
import math
@ -2027,7 +2027,7 @@ def foa_weights(mic):
def main():
parser = argparse.ArgumentParser(description="Build and calibrate the LocalCastBridge six-mic Ambisonic audio field.")
parser = argparse.ArgumentParser(description="Build and calibrate the Mimir six-mic Ambisonic audio field.")
sub = parser.add_subparsers(required=True)
p = sub.add_parser("devices", help="List PortAudio devices as JSON.")
@ -2096,7 +2096,7 @@ def main():
p.add_argument("--mic-id", default="mic_focusrite_neighbor")
p.add_argument("--ssh-target", default="madman's lullaby@192.168.1.84")
p.add_argument("--ffmpeg", default=r"C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe")
p.add_argument("--remote-dir", default=r"C:\Meta\LocalCastBridge\calibration\remote-captures")
p.add_argument("--remote-dir", default=r"C:\Meta\Mimir\calibration\remote-captures")
p.add_argument("--device", default="Analogue 1 + 2 (Focusrite USB Audio)")
p.add_argument("--dshow-device", help=argparse.SUPPRESS)
p.add_argument("--seconds", type=float, default=8.0)

View file

@ -1,4 +1,4 @@
import argparse
import argparse
import json
import subprocess
import sys
@ -321,7 +321,7 @@ def audio_smoke(args):
def main():
parser = argparse.ArgumentParser(description="Probe LocalCastBridge calibration devices.")
parser = argparse.ArgumentParser(description="Probe Mimir calibration devices.")
sub = parser.add_subparsers(required=True)
p = sub.add_parser("discover")

View file

@ -1,4 +1,4 @@
from __future__ import annotations
from __future__ import annotations
import argparse
import json
@ -29,11 +29,11 @@ def main() -> int:
parser.add_argument("--sample-rate", type=int, default=48000)
parser.add_argument("--ssh-target", default="madman's lullaby@192.168.1.84")
parser.add_argument("--ffmpeg", default=r"C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe")
parser.add_argument("--remote-dir", default=r"C:\Meta\LocalCastBridge\calibration\remote-captures")
parser.add_argument("--remote-dir", default=r"C:\Meta\Mimir\calibration\remote-captures")
parser.add_argument("--focusrite-device", default="Analogue 1 + 2 (Focusrite USB Audio)")
parser.add_argument("--loopback-device", default="")
parser.add_argument("--loopback-capture", choices=("wasapi", "dshow"), default="wasapi")
parser.add_argument("--remote-wasapi-script", default=r"C:\Meta\LocalCastBridge\scripts\wasapi-loopback-capture.ps1")
parser.add_argument("--remote-wasapi-script", default=r"C:\Meta\Mimir\scripts\wasapi-loopback-capture.ps1")
parser.add_argument("--remote-wasapi-role", choices=("Console", "Multimedia", "Communications"), default="Console")
parser.add_argument("--local-loopback-query", default="Scarlett")
parser.add_argument("--max-lag-ms", type=float, default=3000.0)
@ -165,7 +165,7 @@ def remote_loopback_command(args: argparse.Namespace, output: str) -> str:
raise SystemExit("--loopback-device is required when --loopback-capture dshow")
return remote_ffmpeg_command(args, args.loopback_device, output, channels=2)
log = output.rsplit(".", 1)[0] + ".log"
task = "LocalCastWasapiLoopbackCapture"
task = "MimirWasapiLoopbackCapture"
return (
"powershell -NoProfile -ExecutionPolicy Bypass -Command "
+ quote_for_cmd(

62
scripts/obs_smoke_test.py Normal file
View file

@ -0,0 +1,62 @@
import argparse
import json
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from localcast.obs_smoke import load_config_endpoints, read_events_jsonl, summarize_events, write_event_template
def main() -> None:
parser = argparse.ArgumentParser(description="Create the bounded OBS v1 smoke-test witness ledger.")
parser.add_argument("--config", default="config/localcast.json")
parser.add_argument("--events", default="calibration/runs/obs-v1-smoke-events.jsonl")
parser.add_argument("--ledger", default="calibration/runs/obs-v1-smoke-ledger.json")
parser.add_argument("--max-abs-drift-ppm", type=float, default=100.0)
parser.add_argument("--max-latency-ms", type=float, default=500.0)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("plan", help="Print planned OBS-facing endpoints from the Mimir config.")
sub.add_parser("template-events", help="Write an editable JSONL event template for one smoke pass.")
sub.add_parser("summarize", help="Summarize recorded events into the smoke-test ledger.")
args = parser.parse_args()
config_path = Path(args.config)
endpoints = load_config_endpoints(config_path)
if args.command == "plan":
print(
json.dumps(
{
"config": str(config_path),
"endpoints": [endpoint.__dict__ for endpoint in endpoints],
"requiredStages": ["sender_capture", "srt_receive", "obs_present"],
},
indent=2,
)
)
return
events_path = Path(args.events)
if args.command == "template-events":
write_event_template(events_path, endpoints)
print(f"Wrote smoke event template: {events_path}")
return
ledger_path = Path(args.ledger)
events = read_events_jsonl(events_path)
ledger = summarize_events(
endpoints,
events,
max_abs_drift_ppm=args.max_abs_drift_ppm,
max_latency_ms=args.max_latency_ms,
)
ledger_path.parent.mkdir(parents=True, exist_ok=True)
ledger_path.write_text(json.dumps(ledger, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(json.dumps(ledger, indent=2, sort_keys=True))
if __name__ == "__main__":
main()

View file

@ -1,4 +1,4 @@
param(
param(
[string]$LogDirectory = ".\logs"
)
@ -17,7 +17,7 @@ $ffmpegProcesses = Get-CimInstance Win32_Process |
}
if (-not $ffmpegProcesses) {
Write-Host "No LocalCastBridge-looking ffmpeg SRT processes found."
Write-Host "No Mimir-looking ffmpeg SRT processes found."
return
}

View file

@ -1,4 +1,4 @@
from __future__ import annotations
from __future__ import annotations
import argparse
import json
@ -25,7 +25,7 @@ RAW_UNSYNCED_INPUTS = (
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Build synchronized LocalCastBridge OBS audio stems from the aligned "
"Build synchronized Mimir OBS audio stems from the aligned "
"program timeline and hide raw unsynchronized OBS inputs."
)
)
@ -39,7 +39,7 @@ def main() -> int:
parser.add_argument(
"--disable-other-scene-items",
action="store_true",
help="Disable every scene item except the synchronized LocalCastBridge program sources.",
help="Disable every scene item except the synchronized Mimir program sources.",
)
args = parser.parse_args()
@ -99,7 +99,7 @@ def build_stems(run: Path, stem_dir: Path) -> dict[str, Any]:
stems = [
stem_spec(
"LocalCastBridge - Host Voice",
"Mimir - Host Voice",
"host_voice.wav",
host_voice,
field_rate,
@ -108,7 +108,7 @@ def build_stems(run: Path, stem_dir: Path) -> dict[str, Any]:
position_m=[0.0, 0.0, 1.2],
),
stem_spec(
"LocalCastBridge - CoStreamer Voice",
"Mimir - CoStreamer Voice",
"co_streamer_voice.wav",
co_streamer_voice,
field_rate,
@ -117,7 +117,7 @@ def build_stems(run: Path, stem_dir: Path) -> dict[str, Any]:
position_m=[0.0, 2.0, 1.2],
),
stem_spec(
"LocalCastBridge - Ambient",
"Mimir - Ambient",
"ambient.wav",
ambient,
field_rate,
@ -126,7 +126,7 @@ def build_stems(run: Path, stem_dir: Path) -> dict[str, Any]:
position_m=None,
),
stem_spec(
"LocalCastBridge - Transients",
"Mimir - Transients",
"transients.wav",
transients,
field_rate,
@ -135,7 +135,7 @@ def build_stems(run: Path, stem_dir: Path) -> dict[str, Any]:
position_m=None,
),
stem_spec(
"LocalCastBridge - CoStreamer Loopback",
"Mimir - CoStreamer Loopback",
"co_streamer_loopback.wav",
co_streamer_loopback,
field_rate,
@ -144,7 +144,7 @@ def build_stems(run: Path, stem_dir: Path) -> dict[str, Any]:
position_m=None,
),
stem_spec(
"LocalCastBridge - Local Loopback",
"Mimir - Local Loopback",
"local_loopback.wav",
local_loopback,
field_rate,
@ -217,7 +217,7 @@ def configure_obs(
mute_input(client, raw_name)
disable_scene_item(client, scene, scene_items, raw_name)
allowed_scene_items = {"LocalCastBridge Point Cloud"} | {
allowed_scene_items = {"Mimir Point Cloud"} | {
str(stem["obs_name"]) for stem in stem_manifest["stems"]
}

View file

@ -1,6 +1,6 @@
$ErrorActionPreference = "Stop"
$ErrorActionPreference = "Stop"
$root = "C:\Meta\LocalCastBridge"
$root = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$soundVolumeView = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\SoundVolumeView.exe"
$ffmpeg = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe"
$config = Join-Path $root "config\localcast.json"

View file

@ -1,6 +1,6 @@
$ErrorActionPreference = "Stop"
$ErrorActionPreference = "Stop"
$root = "C:\Meta\LocalCastBridge"
$root = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$soundVolumeView = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\SoundVolumeView.exe"
$senderStop = Join-Path $root "scripts\sender-stop.ps1"
@ -16,4 +16,3 @@ if (Test-Path -LiteralPath $soundVolumeView) {
}
Get-Process voicemeeter_x64 -ErrorAction SilentlyContinue | Stop-Process -Force

View file

@ -15,7 +15,7 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
namespace LocalCastBridge {
namespace Mimir {
public static class WasapiLoopbackCapture {
const int eRender = 0;
const int eConsole = 0;
@ -277,4 +277,4 @@ namespace LocalCastBridge {
"@
Add-Type -TypeDefinition $source
[LocalCastBridge.WasapiLoopbackCapture]::Capture($Output, $Seconds, $SampleRate, $Channels, $Role)
[Mimir.WasapiLoopbackCapture]::Capture($Output, $Seconds, $SampleRate, $Channels, $Role)

View file

@ -93,3 +93,5 @@
{"ts": "2026-05-19T12:23:05+00:00", "type": "implementation", "status": "ok", "note": "Mimir is wired as a VoidBot repo Face: private VoidBot registry includes identity mimir; repo-local .voidbot voice/state files exist; .voidbot/state/mimir.cc is seeded through typed state operations with birth memories for identity, native reservoir, ownership boundaries, audio-field discipline, and heartbeat pressure."}
{"ts": "2026-05-19T12:28:02+00:00", "type": "verification", "status": "ok", "note": "VoidBot repo-face heartbeat dry-run reconciled Mimir as an active participant (identity:mimir, repo:LocalCastBridge) in the shared initiative scheduler; dry run queued Void this tick, not Mimir."}
{"ts": "2026-05-19T13:16:35+00:00", "type": "decision", "status": "ok", "note": "Mimir Face voice corrected from dry technical steward to mythological being first: Norse well-memory, lost eye, severed voice, roots/ravens/runes, trusted witnesses, and contempt for false coherence now belong to docs/mimir-face.md plus .voidbot Face state. Job jurisdiction remains realtime field stewardship, but personal values now explain why the work matters."}
{"ts": "2026-05-21T09:18:48+00:00", "type": "decision", "status": "ok", "note": "V1 OBS receiver expansion is now gated on calibration/runs/obs-v1-smoke-ledger.json: scripts/obs_smoke_test.py and localcast.obs_smoke require sender_capture, srt_receive, and obs_present evidence per endpoint, with drift, end-to-end latency, and confidence before plugin/native receiver work proceeds."}
{"ts": "2026-05-21T09:26:02+00:00", "type": "implementation", "status": "ok", "note": "Repository content now presents the public project as Mimir: prose/docs/config examples/OBS source names/default Spout sender/Faust metadata/tool descriptions migrated from LocalCastBridge branding, while lowercase localcast package/schema/ABI contracts remain until a deliberate code rename cut."}

View file

@ -1,8 +1,8 @@
workspace:
workspace:
name: Mimir
status: active
focus: perfect-machine-native-reservoir
implementation_name: LocalCastBridge / localcast
implementation_name: Mimir public surface / localcast implementation contracts
premise: >
Mimir is the public name for the local realtime field machine: a roomful of
cameras, microphones, speakers, and timing signals becomes one coherent
@ -46,10 +46,15 @@ invariants:
- Python and JSON may diagnose or test the machine, not run it.
accepted_design:
- `docs/mimir-face.md`, `.voidbot/voice/identity.json`, and
`.voidbot/state/mimir.cc` define Mimir as this repo's persistent Face:
VoidBot supplies the Discord mouth/scheduler/heartbeat transport, while
Mimir owns repo-progress heraldry, public story continuity, and memory
stewardship.
- `docs/native-rebuild-plan.md` is the current cut plan. It supersedes the
deadline Python/OpenGL bridge as a foundation and defines the next coherent
rebuild target.
- `docs/perfect-machine.md` and `config/perfect-machine.example.json` define the native target: one five-second spatiotemporal reservoir, Aquarium GPU fusion/rendering, Faust DSP, OBS output controls, and LocalCastBridge tooling.
- `docs/perfect-machine.md` and `config/perfect-machine.example.json` define the native target: one five-second spatiotemporal reservoir, Aquarium GPU fusion/rendering, Faust DSP, OBS output controls, and Mimir tooling.
- `native/reservoir` is the first native Rust crate. It now uses one
time-ordered rolling buffer with typed views, so retention has exactly one
owner while Aquarium/Faust still get kind-scoped queries.
@ -69,10 +74,22 @@ accepted_design:
- `LocalcastRenderPoint` is the first native point-buffer element ABI, kept
small to replace file-backed render-frame transport before richer material
and brush descriptors exist.
- Aquarium-Engine commit `0f610d0` wires native render packets into the GPU
fusion path without managed point-list materialization: the native source
exposes caller-owned `LocalcastRenderPoint` memory as an
`AquariumGpuFusionPointBuffer`, D3D12 uploads those bytes into a structured
buffer, and the LocalCast fusion compute shader lowers points into temporal
gaussians on GPU.
- `config/localcast.example.json` defines receiver address, base port, video encode settings, and audio sources.
- `scripts/sender-discover.ps1` lists FFmpeg devices, protocols, and encoders.
- `scripts/sender-start.ps1` launches one FFmpeg process per enabled source.
- OBS receives each source through Media Source URLs documented in `docs/obs-receiver-setup.md`.
- `scripts/obs_smoke_test.py`, `localcast.obs_smoke`, and
`docs/obs-v1-smoke-test.md` define the v1 OBS smoke-test witness ledger.
The ledger must record `sender_capture`, `srt_receive`, and `obs_present`
timestamp evidence for every planned endpoint, summarize endpoint drift and
end-to-end latency, and emit confidence before receiver/plugin/native
expansion is justified.
- Persistence follows the active repo pattern: map, scratch, evidence, branches, handoff, system map, implementation plan, state helpers.
active_subgoals:
@ -88,7 +105,9 @@ active_subgoals:
- Validate the actual sender FFmpeg build and capture device names.
- Smoke-test the video endpoint in OBS.
- Smoke-test each audio endpoint in OBS and tune latency.
- Decide whether an OBS source generator or plugin investigation is justified.
- Run the v1 OBS smoke-test witness ledger against the actual sender/receiver
setup; no OBS source generator or plugin investigation is justified until
the ledger passes.
- Evaluate the Ambisonic AquaSynth path as a separate synchronized multichannel audio pipeline; do not treat heterogeneous mic feeds as raw first-order ambisonic A-format without geometry, calibration, and a shared clock.
- Build the six-microphone/two-speaker audio-field sidecar around the actual distributed camera/Focusrite clocks, explicit geometry, speaker calibration, bounded buffering, delay/SRO alignment, testable pipeline seams, and FOA AmbiX output.
- Build the visual sensor-fusion sidecar around timestamped observations, calibrated sensor models, bounded track caches, sparse point-cloud output, and later brush/splat packet lowering.
@ -157,7 +176,7 @@ research:
bibliography: research/spout-obs-bridge/bibliography.md
key_decision: >
OBS should receive a named Spout2 shared texture from Aquarium Engine,
not desktop capture or file polling. LocalCastBridge emits typed
not desktop capture or file polling. Mimir emits typed
render-frame packets with source timestamps, intended presentation time,
audio-alignment time, target size, Spout sender name, and point/brush
claims. The deadline Spout sink now defaults to a `kiyo-mid-deru`
@ -195,7 +214,7 @@ research:
key_decision: >
Voice separation should run in Aquarium's Faust DSP, not as Python hot
audio math after the mic field has already collapsed to AmbiX.
LocalCastBridge now publishes `localcast.audio.mic_field` with six
Mimir now publishes `localcast.audio.mic_field` with six
aligned mic channels, roles, controls, and graph id
`localcast.faust.voice_separation.v1`; the matching Faust graph lives at
`faust/localcast_voice_separation.dsp`.
@ -262,8 +281,18 @@ research:
current_status:
summary: >
Mimir is now the public brand and repo Face. The 256px repixelized avatar
lives at `Mimir.png`, is copied to
`assets/branding/mimir-avatar-256.png`, and is registered for the VoidBot
repo-Face pipe at `.voidbot/voice/mimir.png`. VoidBot's private
`repo-discord-identities.json` now includes Mimir beside Nibu and Aqua, and
`.voidbot/state/mimir.cc` has typed birth memories for identity,
native-reservoir target, ownership boundaries, audio-field discipline, and
heartbeat agency pressure. A forced VoidBot heartbeat dry-run reconciled
Mimir as an active `repo_face` participant in the shared initiative
scheduler.
First repo scaffold is in place and deployed to the neighbor sender at
C:\Meta\LocalCastBridge. FFmpeg 8.1.1 with SRT and h264_nvenc is installed
C:\Meta\Mimir. FFmpeg 8.1.1 with SRT and h264_nvenc is installed
there, direct WASAPI console-session capture now owns system-output loopback, Madman's desktop has
start/stop launchers, the sender script has launcher-safe FFmpeg arguments
and repo-root logs, desktop launchers use absolute tool paths, and the
@ -323,7 +352,7 @@ current_status:
per-channel stems from the aligned program audio timeline, creates/updates
OBS Media Sources for host voice, co-streamer voice, ambient, transients,
co-streamer loopback, and local loopback, and can disable every scene item
except synchronized LocalCastBridge program surfaces. Co-streamer surface
except synchronized Mimir program surfaces. Co-streamer surface
capture now records neighbor Focusrite and neighbor loopback while recording
local loopback ground truth, then writes aligned co-streamer surface WAVs
for the stem packer. Live phase-field meaning now writes
@ -359,9 +388,14 @@ current_status:
priority so Aquarium can prefer real Leap evidence over RGB surfaces,
stochastic transient hypotheses, and synthetic fusion support while still
seeing lower-priority fallback diagnostics when the Leap capture path stalls.
The v1 OBS bridge now has a bounded smoke-test harness and runbook:
endpoint plans come from `config/localcast.json`, event templates collect
`sender_capture`/`srt_receive`/`obs_present` timestamps, and the summary
ledger records endpoint drift, matched end-to-end latency, confidence, and
whether plugin/native receiver expansion remains blocked.
next_action: >
Stop live feature work on the Python bridge. The first native cut is
complete: `native/reservoir` is now a single time-ordered rolling buffer
with typed views and updated ABI/tests. Next bind Aquarium/Faust to the
native runtime, delete diagnostic Python producers, and resume hardware
calibration through typed CultNet/native surfaces only.
Run the v1 OBS smoke-test witness ledger against the actual sender/receiver
setup before any receiver/plugin/native expansion. After that clocked
evidence exists, resume the native worker cut: fill typed buffers through
`LocalcastProducer`, then delete the remaining diagnostic Python producer
surfaces.

110
tests/test_obs_smoke.py Normal file
View file

@ -0,0 +1,110 @@
import json
import tempfile
from pathlib import Path
import unittest
from localcast.obs_smoke import EndpointPlan, load_config_endpoints, summarize_events, write_event_template, read_events_jsonl
class ObsSmokeTests(unittest.TestCase):
def test_loads_endpoint_plan_from_localcast_config(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "localcast.json"
path.write_text(
json.dumps(
{
"receiver": {"basePort": 5100, "srtLatencyMicros": 120000},
"video": {"enabled": True, "portOffset": 0},
"audioSources": [{"name": "voice", "portOffset": 2}],
}
),
encoding="utf-8",
)
endpoints = load_config_endpoints(path)
self.assertEqual([item.name for item in endpoints], ["video", "audio-voice"])
self.assertEqual(endpoints[0].port, 5100)
self.assertEqual(endpoints[1].listener_url, "srt://0.0.0.0:5102?mode=listener&latency=120000&timeout=5000000")
def test_summarizes_latency_drift_and_confidence(self):
endpoint = EndpointPlan(
name="video",
kind="video",
port=5100,
listener_url="srt://0.0.0.0:5100?mode=listener&latency=120000&timeout=5000000",
expected_latency_ms=120.0,
)
events = [
{"endpoint": "video", "stage": "sender_capture", "eventId": "a", "monotonicNs": 1_000_000_000, "mediaTimeNs": 0},
{"endpoint": "video", "stage": "srt_receive", "eventId": "a", "monotonicNs": 1_120_000_000, "mediaTimeNs": 120_000_000},
{"endpoint": "video", "stage": "obs_present", "eventId": "a", "monotonicNs": 1_180_000_000, "mediaTimeNs": 180_000_000},
{"endpoint": "video", "stage": "sender_capture", "eventId": "b", "monotonicNs": 2_000_000_000, "mediaTimeNs": 1_000_000_000},
{"endpoint": "video", "stage": "srt_receive", "eventId": "b", "monotonicNs": 2_120_000_000, "mediaTimeNs": 1_120_000_000},
{"endpoint": "video", "stage": "obs_present", "eventId": "b", "monotonicNs": 2_180_000_000, "mediaTimeNs": 1_180_000_000},
]
ledger = summarize_events([endpoint], [read_events_jsonl_item(item) for item in events])
summary = ledger["endpoints"][0]
self.assertEqual(ledger["status"], "ok")
self.assertEqual(summary["status"], "ok")
self.assertEqual(summary["endToEndLatencyMs"]["median"], 180.0)
self.assertEqual(summary["endpointDriftPpm"], 0.0)
self.assertGreaterEqual(summary["confidence"], 0.8)
def test_missing_obs_stage_blocks_gate(self):
endpoint = EndpointPlan(
name="audio-voice",
kind="audio",
port=5102,
listener_url="srt://0.0.0.0:5102?mode=listener",
expected_latency_ms=120.0,
)
ledger = summarize_events(
[endpoint],
[
read_events_jsonl_item(
{
"endpoint": "audio-voice",
"stage": "sender_capture",
"eventId": "a",
"monotonicNs": 1_000_000_000,
}
),
read_events_jsonl_item(
{
"endpoint": "audio-voice",
"stage": "srt_receive",
"eventId": "a",
"monotonicNs": 1_120_000_000,
}
),
],
)
self.assertEqual(ledger["status"], "fail")
self.assertEqual(ledger["blockedEndpoints"], ["audio-voice"])
self.assertFalse(ledger["gate"]["pluginOrNativeReceiverExpansionAllowed"])
def test_writes_event_template_as_jsonl(self):
endpoint = EndpointPlan("video", "video", 5100, "srt://0.0.0.0:5100?mode=listener", 120.0)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "events.jsonl"
write_event_template(path, [endpoint])
events = read_events_jsonl(path)
self.assertEqual(len(events), 3)
self.assertEqual({event.stage for event in events}, {"sender_capture", "srt_receive", "obs_present"})
def read_events_jsonl_item(item):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "events.jsonl"
path.write_text(json.dumps(item) + "\n", encoding="utf-8")
return read_events_jsonl(path)[0]
if __name__ == "__main__":
unittest.main()

View file

@ -1,4 +1,4 @@
from __future__ import annotations
from __future__ import annotations
import argparse
import json
@ -236,7 +236,7 @@ def render_report(findings: list[Finding], status: str, log: str, evidence: list
}
lines = [
"LocalCastBridge pre-compaction persistence check",
"Mimir pre-compaction persistence check",
f"Workspace: {ROOT}",
f"Findings: {counts['ok']} ok, {counts['warn']} warn, {counts['error']} error",
"",
@ -284,7 +284,7 @@ def render_report(findings: list[Finding], status: str, log: str, evidence: list
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Audit LocalCastBridge persistent state before imminent compaction."
description="Audit Mimir persistent state before imminent compaction."
)
parser.add_argument(
"--strict",

View file

@ -1,4 +1,4 @@
from __future__ import annotations
from __future__ import annotations
import argparse
import json
@ -144,7 +144,7 @@ def cmd_close_branch(args: argparse.Namespace) -> int:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Inspect and update LocalCastBridge persistence state files."
description="Inspect and update Mimir persistence state files."
)
subparsers = parser.add_subparsers(dest="command", required=True)