Files
gramps b216cbc56f tools: scripts/avsync.py measures A/V sync from the file, not from an ear
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.
2026-11-12 13:57:29 -08:00

12 KiB
Raw Permalink Blame History

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 → "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:

./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.

# 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:

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.