zebra-spaces: persist transcribe state in localStorage + fix self-capture audioCtx
Fox 2026-06-05: "transcribe states should be saved in localstorage to
survive on refresh" and "self transcribe does not seem to work."
Two fixes:
1. localStorage persistence:
- TRANSCRIBE_KEY = 'zspc:transcribe-on'
- Loaded into transcribeEnabled at script top
- setTranscribe(want) replaces the old toggleTranscribe body and
writes localStorage on every state change
- applyTranscribeUI() reusable for both flip and refresh-restore
- At click-handler binding time, if transcribeEnabled was already
true (restored from storage), apply UI + pre-warm worker + kick
captures for any speakers already attached
2. Self-capture fixed:
- startSelfCapture used to early-return if audioCtx was null
- But audioCtx is only created when the FIRST remote speaker's
audio attaches via attachAudioStreamViaWorklet. A host alone in
the room (no remote audio yet) had no audioCtx → self capture
silently failed.
- Now startSelfCapture creates audioCtx if absent and resumes if
suspended, same pattern as the remote-attach path. Host-alone
captures their own voice immediately.
This commit is contained in:
parent
e23808d410
commit
cee13589d7
1 changed files with 61 additions and 12 deletions
|
|
@ -1627,6 +1627,24 @@ function renderIdentity(){
|
|||
|
||||
$('btn-vault').addEventListener('click', () => $('vault-panel').classList.toggle('hidden'));
|
||||
$('btn-transcribe').addEventListener('click', () => toggleTranscribe().catch(e => logLine('err','transcribe toggle: '+e.message)));
|
||||
/* If transcribe was on at the previous session, restore the state
|
||||
* now (UI label, transcript-log visibility, worker pre-warm).
|
||||
* Captures attach lazily as remote speakers arrive and self capture
|
||||
* fires when micStream is available — both paths already check the
|
||||
* transcribeEnabled flag. */
|
||||
if (transcribeEnabled){
|
||||
/* call setTranscribe with the SAME value but force-apply UI + spin
|
||||
* the worker. The function's "same-state" branch refreshes UI and
|
||||
* returns without flipping the flag. */
|
||||
applyTranscribeUI();
|
||||
ensureWhisperWorker();
|
||||
/* If captures need to be installed (e.g. listenerAudioNodes
|
||||
* populated before this code ran), kick them off. */
|
||||
for (const [uuid] of listenerAudioNodes){
|
||||
startCaptureForUuid(uuid).catch(()=>{});
|
||||
}
|
||||
startSelfCapture().catch(()=>{});
|
||||
}
|
||||
|
||||
/* log out — destructive: wipes Ed25519 + handle from localStorage and
|
||||
* generates a fresh identity. The booted/blocked window keys off the
|
||||
|
|
@ -2199,7 +2217,13 @@ function transcribeViaWorker(chunk){
|
|||
});
|
||||
}
|
||||
|
||||
/* Transcribe state persists in localStorage so refresh restores the
|
||||
* user's preference. Model is cached after first download so subsequent
|
||||
* loads spin the worker up fast. Fox 2026-06-05: "transcribe states
|
||||
* should be saved in localstorage to survive on refresh." */
|
||||
const TRANSCRIBE_KEY = 'zspc:transcribe-on';
|
||||
let transcribeEnabled = false;
|
||||
try { if (localStorage.getItem(TRANSCRIBE_KEY) === '1') transcribeEnabled = true; } catch(_){}
|
||||
/* whisper-tiny.en hallucinations on silence / low-energy chunks. The
|
||||
* RMS gate in the capture worklet catches dead silence; this set
|
||||
* catches what gets past it — quiet-room ambient with the same
|
||||
|
|
@ -2312,7 +2336,19 @@ async function handleWhisperChunk(uuid, chunk){
|
|||
* the user's handle. */
|
||||
let _selfCapture = null;
|
||||
async function startSelfCapture(){
|
||||
if (_selfCapture || !audioCtx || !micStream) return;
|
||||
if (_selfCapture || !micStream) return;
|
||||
/* audioCtx may not exist yet for a host alone in the room (no
|
||||
* remote speakers have triggered attachAudioStreamViaWorklet). Make
|
||||
* sure it's up before we try to attach a worklet to it. Resume it
|
||||
* too — first-attach on a browser may leave it suspended until a
|
||||
* user gesture has touched it. */
|
||||
if (!audioCtx){
|
||||
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
||||
catch(e){ logLine('err','self capture audioCtx: '+e.message); return; }
|
||||
}
|
||||
if (audioCtx.state === 'suspended'){
|
||||
try { audioCtx.resume(); } catch(_){}
|
||||
}
|
||||
const ok = await loadWhisperCaptureWorklet(audioCtx);
|
||||
if (!ok) return;
|
||||
try {
|
||||
|
|
@ -2368,13 +2404,15 @@ function stopCaptureForUuid(uuid){
|
|||
node.capture = null;
|
||||
}
|
||||
|
||||
async function toggleTranscribe(){
|
||||
transcribeEnabled = !transcribeEnabled;
|
||||
const btn = document.getElementById('btn-transcribe');
|
||||
const sec = document.getElementById('sec-transcript');
|
||||
if (btn) btn.classList.toggle('on', transcribeEnabled);
|
||||
if (btn) btn.textContent = transcribeEnabled ? 'transcribe (on)' : 'transcribe (off)';
|
||||
if (sec) sec.classList.toggle('hidden', !transcribeEnabled);
|
||||
async function setTranscribe(want){
|
||||
if (transcribeEnabled === want) {
|
||||
/* still refresh UI in case it's a load-time restore */
|
||||
applyTranscribeUI();
|
||||
return;
|
||||
}
|
||||
transcribeEnabled = want;
|
||||
try { localStorage.setItem(TRANSCRIBE_KEY, want ? '1' : '0'); } catch(_){}
|
||||
applyTranscribeUI();
|
||||
if (transcribeEnabled){
|
||||
/* pre-warm the worker so the first chunk doesn't wait on model load. */
|
||||
ensureWhisperWorker();
|
||||
|
|
@ -2384,14 +2422,25 @@ async function toggleTranscribe(){
|
|||
}
|
||||
/* Self (local mic). If the user is a listener with no mic yet,
|
||||
* this is a no-op — startSelfCapture early-returns. When the
|
||||
* user gets promoted and gets a mic, that path can also start
|
||||
* self capture (see getMic / applyMicMode). */
|
||||
* user gets promoted and gets a mic, getMic re-fires this path. */
|
||||
startSelfCapture().catch(e => logLine('err', 'self transcribe start: '+e.message));
|
||||
} else {
|
||||
for (const [uuid] of listenerAudioNodes) stopCaptureForUuid(uuid);
|
||||
stopSelfCapture();
|
||||
}
|
||||
}
|
||||
function applyTranscribeUI(){
|
||||
const btn = document.getElementById('btn-transcribe');
|
||||
const sec = document.getElementById('sec-transcript');
|
||||
if (btn){
|
||||
btn.classList.toggle('on', transcribeEnabled);
|
||||
btn.textContent = transcribeEnabled ? 'transcribe (on)' : 'transcribe (off)';
|
||||
}
|
||||
if (sec) sec.classList.toggle('hidden', !transcribeEnabled);
|
||||
}
|
||||
async function toggleTranscribe(){
|
||||
return setTranscribe(!transcribeEnabled);
|
||||
}
|
||||
function installJitterBuffer(uuid, node){
|
||||
if (!node || node.jbuf || !workletReady) return;
|
||||
const target = node.targetSeconds || RECV_PLAYOUT_DELAY_SEC;
|
||||
|
|
@ -7364,8 +7413,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> · built <span class="stamp-date">2026-06-05</span><br>
|
||||
md5 <span class="stamp-md5">6f6e30026fe83b275be830b2e135d1aa</span><br>
|
||||
sha256 <span class="stamp-sha">ab24f0b7071a56ed9b14fa3b096f1f4d2345847ffeb5d8ac5e2d64dd45d38e9e</span><br>
|
||||
md5 <span class="stamp-md5">f21110e1e23d66443bea1dc35f0f5fdc</span><br>
|
||||
sha256 <span class="stamp-sha">08d96f30022dda20daa30401ceda2912abc2f3889a28e7a8b52de4bfcc3684e3</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue