zebra-spaces: deploy lip-sync flap fix + transcript autoscroll

This commit is contained in:
russell@unturf.com 2026-06-05 14:04:26 -04:00
parent 6d24cf2d8f
commit 4533e4e4b6
No known key found for this signature in database

View file

@ -2298,11 +2298,15 @@ function appendTranscriptLine(uuid, text){
line.appendChild(tsEl); line.append(' ');
line.appendChild(nameEl); line.append(' ');
line.appendChild(txtEl);
/* Check "near bottom" BEFORE appending — scrollHeight grows the
* moment the line is in the DOM, which makes a post-append check
* report "not at bottom" even when the user was. ~120px tolerance
* also covers touch-scroll inertia leaving a small gap. Fox
* 2026-06-05: "when i am at the bottom and new transcriptions
* flow in, keep scrolling as they arrive unless ive scrolled up." */
const wasAtBottom = (log.scrollTop + log.clientHeight) >= (log.scrollHeight - 120);
log.appendChild(line);
/* auto-scroll only if user is already at the bottom (let them scroll
* up to read older transcript without being yanked back). */
const nearBottom = (log.scrollTop + log.clientHeight) >= (log.scrollHeight - 40);
if (nearBottom) log.scrollTop = log.scrollHeight;
if (wasAtBottom) log.scrollTop = log.scrollHeight;
}
/* Shared handler — used by both remote-speaker capture (one per uuid
@ -2590,7 +2594,7 @@ function resetListenerBufferReady(){
const lipSync = new Map(); /* pubHex → state */
const sfuAudioReceivers = new Map(); /* pubHex → RTCRtpReceiver (SFU audio) — cached so we can restore as the lip-sync source after mesh fail */
const httpLipSyncOverride = new Map(); /* pubHex → fixed delay (sec). When set, lip-sync uses this directly and skips worklet-derived measurement — for HTTP /stream toggle ON case where the audio source isn't the worklet anymore. */
const LIP_SYNC_HISTORY = 5;
const LIP_SYNC_HISTORY = 10;
const LIP_SYNC_THRESHOLD = 0.05; /* 50ms — below this is within perception noise */
function lipSyncEntry(pubHex){
@ -2615,7 +2619,13 @@ function registerLipSyncVideo(pubHex, kind, receiver){
function medianOf(arr){
if (arr.length === 0) return 0;
const s = arr.slice().sort((a,b) => a-b);
return s[Math.floor(s.length/2)];
const mid = Math.floor(s.length / 2);
/* For even-length windows return the AVERAGE of the two middle
* values. With odd length and alternating samples [A,B,A,B,A] the
* median is A; next push flips it to B → the lip-sync target
* whipsaws each tick. Even length + average makes the median
* stable at (A+B)/2 even when two sources alternate. */
return (s.length % 2 === 0) ? (s[mid - 1] + s[mid]) / 2 : s[mid];
}
async function refreshLipSyncForUuid(uuid){
/* find which publisher this audio uuid belongs to */
@ -2646,6 +2656,17 @@ async function refreshLipSyncForUuid(uuid){
const node = listenerAudioNodes.get(uuid);
if (!node) return;
const workletBufferedSec = node.bufferedSeconds || 0;
/* Stale-source filter. If the worklet's buffer has drained to
* essentially zero AND we know the publisher's actual native
* receiver still has audio coming in (e.notable audioReceiver
* jbuf > 1s), this worklet is from an OLD subscription that no
* longer carries audio — refresh-with-zero would pollute the
* lipSync history and the median would alternate between this
* dead value and the live worklet's value. Skip. The live
* worklet's refresh continues to drive the lip-sync target. */
if (workletBufferedSec < 0.5 && e.nativeJbufSec > 1.0){
return;
}
/* refresh native jbuf via getStats; this is a slow path but we
* only do it on the ~683ms cadence the worklet posts, and getStats
* is cheap (~1-2ms on Firefox Android). */
@ -7456,8 +7477,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-05</span><br>
md5 <span class="stamp-md5">e2a9c5eb310147cb147f54dc2495f3f6</span><br>
sha256 <span class="stamp-sha">076444a5567d418cb48557ee1b1e0c88539534a1532df4d6f6be98a7e40b0343</span><br>
md5 <span class="stamp-md5">90ea35ac08110f785509c523db5c720a</span><br>
sha256 <span class="stamp-sha">73faa630db78a68abfaf5b70e122b7f9c408bf5f69ccb8086025c28540decfc9</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>