Three numbers, from the recording itself: each stream's length, the drift implied by a length mismatch (a clock problem), and the residual offset between mouth motion and speech (a constant problem). Per-3s window table with correlations, so a weak reading announces itself instead of reporting a confident wrong number, and a median over only the strongly-correlated windows — a whole-clip search on periodic speech locks onto the wrong beat and returns nonsense (it said -400 ms at the search limit on the clip where the windows said -185). Validated on ty-20261112-1228-457-0000.mp4: lengths 13.271/13.305 s (match, so no drift) and -185 ms (sound early), which agrees with the independent visual read of the same clip. The creator is deaf, so "does it sound right" cannot be the loop. This is. Needs numpy; uses the bundled ffmpeg.exe.
184 lines
12 KiB
Markdown
184 lines
12 KiB
Markdown
# Environment, build & tests
|
||
|
||
Lookup material: LAN hosts, the Windows build commands, where the DB/auth/log live, and the
|
||
test suite map. Read when you are about to build, run, or add a test — not on every session.
|
||
|
||
> Paths and secrets belong with the runbook in [`../../HANDOFF.md`](../../HANDOFF.md) →
|
||
> "Secrets / paths" until the queue trim moves them here.
|
||
## LAN infrastructure (192.168.50.x)
|
||
|
||
All LAN credentials live in `CREDENTIALS.md` (gitignored). Server IPs and roles:
|
||
|
||
| Host | IP | Role |
|
||
|------|----|------|
|
||
| corsair | .132 | Gaming/streaming primary — RTX 5070 Ti (this machine) |
|
||
| llamavault | .87 | ODROID-HC4, 12TB RAID5, media storage (NFS). SSH: `ssh mshallop@192.168.50.87`. **DHCP-bound — it moved off `.86` after the 2026-10 power outage, so confirm with a ping before trusting any path.** SMB is reached by the **`llamavault` name**, pinned in the Windows hosts file, never by literal IP: `\\llamavault\llamaVault\...` |
|
||
| pivault | .158 | RPi5 8GB, 10.83TB RAID5 NAS |
|
||
| jarvis | .210 | AMD Ryzen 5 5600X, RX 6600 XT — AI inference, SearXNG, Docker registry, Immich |
|
||
| ultron | .108 | AMD Ryzen 7 7840HS — AI orchestrator, llama-server, Qdrant |
|
||
| gordito | .144 | Home Assistant, Alarmo, Zigbee |
|
||
| officerfriendly | .26 | Pi-hole DNS |
|
||
| octopi | .197 | OctoPrint (Prusa MK4) |
|
||
| BigBlinkyRouter | .1 | ASUS ROG GT-BE98 Pro, WiFi 7 |
|
||
|
||
Web infra (DO droplet): `llamacasty.com` + `llamachile.tube` at `143.244.176.131`.
|
||
|
||
**Pushing to the Gitea instance (`llgit.llamachile.tube`).** The repo is **`gramps/LlamaCasty`**
|
||
(renamed from `gramps/ytLlive`; the old path 301-redirects). **Push over HTTPS**, not SSH:
|
||
`origin = https://llgit.llamachile.tube/gramps/LlamaCasty.git`, with the credential helper
|
||
`/home/gramps/bin/gitea-credential` (registered as
|
||
`credential.https://llgit.llamachile.tube.helper`) reading the account password from the
|
||
gitignored `CREDENTIALS.md` on demand — nothing stored in plaintext, nothing in a command line.
|
||
**The Gitea account password IS the portal password** in `CREDENTIALS.md`, and the same password
|
||
authenticates the REST API (`curl --config …/api/v1/user`) — **that API is the fastest way to ask
|
||
the server questions** (the repo's real name and clone URL, the account's SSH keys, a token).
|
||
⛔ **The SSH endpoint on `llgit:2214` is not a usable push path**: the account's SSH keys ARE
|
||
registered (key id 6 "Corsair VSC" = this machine's `~/.ssh/id_ed25519`) yet the server refuses
|
||
the publickey, and the port later refused connections outright. Do not spend a session on key
|
||
archaeology — ask the API, then push over HTTPS.
|
||
|
||
## Run
|
||
|
||
**First time on a fresh clone (or if `ffmpeg/` was deleted) — this is REQUIRED, not optional:**
|
||
|
||
```bash
|
||
./scripts/fetch-ffmpeg.sh # ~65 MB once; verifies SHA-256, extracts 9 files into ./ffmpeg
|
||
```
|
||
|
||
FFmpeg is **bundled, never downloaded at runtime** (Windows Store policy 10.2.2 — the old runtime
|
||
download 404'd on 2026-09-01 and would fail certification). The binaries are gitignored because
|
||
135 MB of DLLs in git would tax every commit on this 9p tree; the pinned URL + SHA-256 in the script
|
||
are the audit trail. `ytLive.csproj`'s `GuardBundledFfmpeg` target **fails the build** with that
|
||
exact instruction if the folder is missing. `FFMPEG_ZIP=<path>` reuses a pre-downloaded archive.
|
||
|
||
```bash
|
||
# From WSL, ALWAYS use the Windows dotnet host — never Linux `dotnet` for this project:
|
||
"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj"
|
||
"/mnt/c/Program Files/dotnet/dotnet.exe" run -c Release # the launch command (creator, 2026-10-16): Release, not Debug — perf takes must read Release numbers
|
||
```
|
||
|
||
**Run-mode convention (creator directive 2026-10-16):** the creator launches with `-c Release`
|
||
by DEFAULT. Debug runs happen ONLY when the AI explicitly names the Debug command for a
|
||
specific reason — never implicitly, and never for reading performance numbers (Debug pixel
|
||
paths are several × slower; every take in the 141ms saga read Debug numbers where Release is
|
||
the real reading).
|
||
|
||
**Pre-commit gate (write it down — I forgot once):** ALWAYS run
|
||
`./scripts/verify.sh "<file1>" "<file2>" ...` (declared scope) before committing. It forces a
|
||
**clean** build (0 warnings), the full test suite (passes only the 2 known pre-existing `[FAIL]`s), and
|
||
the scope check in one shot. An **incremental** build can report `0 Warning(s)` while skipping
|
||
recompiles — that's how `CS8601` slipped past on 2026-08-29. Never claim 0 warnings from anything but
|
||
`verify.sh`'s clean build.
|
||
|
||
`EnableWindowsTargeting=true` in `ytLive.csproj` lets a cold restore work from WSL, but a Linux
|
||
`dotnet run`/`build` re-downloads 100M+ of `windowsdesktop.app.*` packs into the Linux NuGet cache
|
||
(which lacks them) over the slow 9p `/mnt/c` bridge — twice, because the WPF `_wpftmp` generated
|
||
project triggers a second restore (203s observed). The Windows cache has the SQLite packages and the
|
||
packs resolve from `C:\Program Files\dotnet\packs`, so the Windows host never re-downloads.
|
||
Never use `--no-restore` right after an interrupted restore — the stale `project.assets.json`
|
||
produces misleading `NETSDK1064` "package not found" errors. Running requires Windows anyway.
|
||
|
||
## Tests
|
||
|
||
xUnit in `ytLive.Tests` (net8.0-windows10.0.19041.0, matches the app TFM). Run on Windows —
|
||
WSL can't run net8.0-windows tests:
|
||
|
||
```bash
|
||
dotnet.exe vstest "C:\...\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"
|
||
```
|
||
|
||
Good Dog Rule: ONE integration test per change (no feature branches pre-1.0 — work lands on
|
||
`main`, see AGENTS.md). Current: TokenStore DPAPI
|
||
roundtrip/corrupt/missing/clear, OAuth exchange/refresh/ClearSession, CameraManager refcount +
|
||
frame pump + failure handling (fakes for the WinRT seams), real-`MainWindow` round-clip
|
||
interaction test, LayoutStore delete roundtrip, LayoutStore pre-round-rect-dims roundtrip,
|
||
LayoutStore background roundtrip, LayoutStore HasBackground roundtrip + v5→v6 non-Live backfill,
|
||
ScreenCaptureManager refcount + shared-bitmap + coalescing
|
||
(fake `IScreenCaptureSource` + a real background-STA `Dispatcher`), BackgroundTests, SceneCatalogTests,
|
||
the TASK 25 dirty-layout heal (BackgroundHealIntegrationTests), WebcamSafeguardTests,
|
||
SceneCompositorTests (full-scene composite + vertical tier), StretchMathTests, FfmpegLocatorTests,
|
||
FfmpegEncoderTests, FramePumpTests, the TASK 8/11/12 audio chain (AudioPipelineTests + MasterLimiter),
|
||
the Socials fediverse-heal roundtrip, AboutHubTests, NotificationAreaIntegrationTests (TASK 24),
|
||
GlobalHotkeyTests + HotkeyConfigTests (TASK 20), WebcamMenuGateTests (TASK 26), ChatLayerGateTests
|
||
(TASK 27), BroadcastPullOutTests (TASK 29), DefaultRecordFolder fallback (TASK 30), WebView2ManagerTests
|
||
(TASK 17), RecordingFileTests + OnAirSignTests (TASK 18), SessionTeardownTests +
|
||
WebcamOutputKeyTests + GameMeterHonestyTests (2026-09-01 recording-verification fixes) — **ZERO
|
||
known failures as of 2026-09-01. `AudioPipelineTests` 25/25 green in 26ms —
|
||
the "known failing" `Mix_HonorsProviderGains…` and the class's notorious STANDALONE HANG shared
|
||
one root cause: TASK 22's `_delayedMix` (nullable, never initialized) was dereferenced
|
||
(`delayed.Length` on null) every live-mix tick — a swallowed NRE starved the pipe the tests
|
||
read and flooded startup.log. Test was right, code drifted from the map's own contract
|
||
(loopbackGain = GameAudioVolume — now honored; the "unity because loopback scales with the
|
||
endpoint" assumption was disproven by the creator's 20%-volume meter observation). The former
|
||
RoundClip "known failure" (stale-test layers: namescoped `FindName` + `VisualTreeHelper.HitTest`
|
||
where `UIElement.InputHitTest` models input — see MyMistakes) was fixed the same day.
|
||
Per-class runs through the Windows dotnet.exe host execute the RealApp/MainWindow suites fine;
|
||
only the FULL-suite run still hangs (WASAPI teardown, pre-existing) — and the whole-suite
|
||
"247 total" era count is stale; trust per-class results.**
|
||
|
||
Reward-event capture (monetization awareness, see the Monetization section) will add its integration
|
||
tests here when it ships: one real chat-poll payload containing all seven reward event types →
|
||
assert the persisted canonical `RewardEvent` rows (type + amount/currency/tier/memberLevel/participant
|
||
ids) round-trip into SQLite.
|
||
|
||
### Real-MainWindow tests MUST be hermetic (DB pollution bug)
|
||
|
||
The integration test boots a real `MainWindow` → `MainViewModel` → real `LayoutStore`
|
||
(`%APPDATA%\ytLlive\ytLlive.db`). `Shutdown()` on close **saves the layout** (full rewrite:
|
||
DELETE all scenes/sources, re-insert), so any source a test adds would be persisted over the
|
||
user's real ones — this happened and wiped the real webcam source (DeviceId replaced by the
|
||
test's fake `test-camera`). Rule: a test that constructs `MainWindow` MUST first set
|
||
`MainViewModel.LayoutPathOverride` to a temp DB path and reset it (plus
|
||
`SqliteConnection.ClearAllPools()` + delete) in `finally`. The seam is
|
||
`internal static string? LayoutPathOverride` (line ~529 in `MainViewModel.cs`),
|
||
`ytLive.csproj` has `InternalsVisibleTo("ytLive.Tests")`.
|
||
|
||
The layout DB is a **full rewrite per save** (delete all, re-insert from memory), so
|
||
save/load round trips are exact: an element removed in the UI (`RemoveElement` →
|
||
`scene.Elements.Remove` → `OnElementsChanged` → debounced `ScheduleSave`, plus `Shutdown` on
|
||
close) does **not** come back after reload (`LayoutStorePersistenceTests` guards this).
|
||
|
||
|
||
## A/V sync measurement
|
||
|
||
`scripts/avsync.py <recording> [--crop WxH+X+Y]` reports what no ear can: each stream's length,
|
||
whether they drift (a length mismatch is a clock problem), and the residual offset between mouth
|
||
motion and speech (a constant problem), with a per-3s window table and a correlation so a weak
|
||
reading says so. It uses the bundled `ffmpeg.exe` and needs `numpy`. The creator is deaf, so this —
|
||
not a listening test — is how sync gets judged: feed it the clip instead of guessing, and quote the
|
||
numbers in the commit.
|
||
|
||
## Session performance evidence
|
||
|
||
`python3 scripts/session-stats.py` mines every `startup*.log` (primary, rotated, and per-instance)
|
||
into one row per FramePump session: tier, scene, measured fps, drops, stalls, average/worst render,
|
||
capture rate — and the **sink**, which is the whole point:
|
||
|
||
| Sink | How it is recognised | What it proves |
|
||
|---|---|---|
|
||
| `RECORD` | `Recording ended` / `Recording discarded` | the machine alone — no socket |
|
||
| `TEST` | `Test session dock open` / `Test chat simulation` | the machine + socket, no audience |
|
||
| `LIVE` | `Broadcast created` / `Broadcast bound` | everything, network included |
|
||
| `UNKNOWN` | no marker (crashed or paused) | nothing — investigate before quoting it |
|
||
|
||
Verdicts: `holds` = 0 drops, 0 stalls, average render under **half** the frame budget; `strained` =
|
||
under the full budget; otherwise `over-budget`. Worst render is printed but never judged — it is
|
||
dominated by one-off startup hitches. This is the evidence source for the minimum-configuration
|
||
table in `product.md`: recordings first, because the network cannot pollute them.
|
||
|
||
## Persistence & external accounts (lookup)
|
||
|
||
- Layout: `%APPDATA%\ytLlive\ytLlive.db` (SQLite, schema v8 — scenes, sources, assets by SHA-256
|
||
content hash, socials, settings; **full rewrite per save**).
|
||
- Auth session: `%APPDATA%\ytLlive\ytLlive.auth` (DPAPI, CurrentUser scope).
|
||
- Startup log: `%APPDATA%\ytLlive\startup.log`, capped at 5 MB with one rolled copy
|
||
(`Helpers/LogRotation.cs`, TASK 55 slice 1).
|
||
- **Google Cloud project `198374416003`** — the OAuth ClientId prefix is the project number. It is
|
||
owned by **`llamachile.shop@gmail.com`** ("gramps"), which is **NOT** the Google account a creator
|
||
signs into YouTube with; console access and channel identity are separate logins. Direct link:
|
||
`https://console.cloud.google.com/apis/credentials?project=198374416003` — if the picker shows no
|
||
match it defaults to the **"Recent"** tab, so switch to **"All"**.
|
||
- LAN credentials live in the gitignored `CREDENTIALS.md`. ⚠️ It says Home Assistant is
|
||
`192.168.50.144`; the real HA is `192.168.50.145` (`.144` is `gordito`). Fix that file when next
|
||
editing it.
|