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.
12 KiB
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 byllamachile.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 is192.168.50.144; the real HA is192.168.50.145(.144isgordito). Fix that file when next editing it.