v3.23.2-127
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
81da657094 |
One voice per listener, and the voice process stops stalling itself
Build 127. Reported from a live test: part of the translation came out in a
different voice on an Android phone, and iPhones were skipping and repeating.
Three investigations against the campus Mac's own log and transcript, one
test harness driving the voice process directly, and the answer is not what
it sounded like.
WHAT THE MACHINE DID. One voice process ran all afternoon. Every language
kept a single resident engine from its first sentence (14:54:26) to STOP
(15:23:18); 2,800 clips, 560 per language, every one filed under its own
language and spoken by its own bundle. No synthesis-side voice change was
possible with that configuration. What the log shows at the time of the
complaint instead:
- Someone on the English stream heard the ROOM MICROPHONE for 21 s
(15:09:10-32). English was the first button on every phone's list, forced
there by the page whatever the operator had chosen (fixed in build 125).
- A listener tapped Chinese at 15:11:17. The 121 MB zh model was fetched
and unpacked THREE TIMES AT ONCE on the voice process's only thread.
Every stream stalled for about a minute, 26 sentences were dropped across
all five languages, and the catch-up rushed the Spanish voices to their
+16% cap. Same voice, sounding wrong.
WHAT WAS WRONG, AND IS NOW FIXED.
The live path filed every engine as an "audition". speak() resolved the
language's default voice, which always has a bundle, and passed it as
`explicit`, so the key was 'audition:'+bundle for every ordinary sentence
while the preload warmed copies under the language name that no sentence
ever read. Five languages asked for nine engines against a cap of six; every
service's first sentence built its real engines on the spot (1.3-2 s); the
operator's voice save flushed nothing that mattered. Engines are now keyed
by bundle and shared by every language, variant and audition that uses one,
auditions take at most two slots of their own and can never evict a live
engine, and the cap is 8, sized to the largest campus from measurement
(~165 MB per engine once it has spoken, 1.3 GB for eight).
Voice installs ran on the thread that answers every sentence, and were not
de-duplicated. They now run in a worker thread (voice-install.js), one per
bundle however many ask, staged and renamed into place whole so a half-
written bundle can never pass isInstalled().
Listener rows are { code, count } and engine.js read { lang, n } in five
places, so the set of "languages someone is on" was always empty — and a late
sentence keeps speech only for those. Under load the app dropped EVERY
language's speech instead of only the unchosen ones. Late became silent.
HLS segment numbers restarted at 1 for every new stream object, under a
'max-age=60, immutable' header. A phone back from the lock screen could be
answered from its cache with the previous stream's audio under the new
number: an earlier sentence, replayed. Numbers are seeded from the clock.
In clips mode a clip still downloading when the listener switched language
landed and played in the voice they had left; the frame now carries its
language and voice and the page checks both when the bytes land. In stream
mode the old playlist stayed loaded through a switch and three things could
restart it; it is stopped, not paused. A listener's alternate voice no longer
loses a round trip to the default on the original-text toggle.
One genuine wrong-voice path, dormant today because natural voices are off on
this campus: a variant waiting on another variant's premium synthesis
inherited that variant's Piper FALLBACK, in that variant's bundle — spa_ES
received the byte-identical Mexican clip and its own voice never loaded. A
follower now shares a fallback only when it is its own voice.
An audition (the Voices card's play button) played on the system default
output, which on the campus Mac is the Dante card feeding the hardware
channels. It now plays on another output, or refuses and says why.
VERIFIED: a harness drives the voice process through the baseline, a batch
of four auditions during live sentences, a voices save mid-stream, and nine
live languages against the cache — every clip spoken by its own bundle, and
auditions of a live voice now cost 78-311 ms with nothing rebuilt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
95d1a7b9ec |
Stop the stream seeking against itself, and stop the desk scrolling itself
Build 126. Two reports, unrelated to each other. IPHONES REPEATING AND SKIPPING PARTS OF EVERY SENTENCE. DRIFT_LAND was 0.6 s and one segment is 0.697 s, so the position the player was being held at sat INSIDE the newest segment — a point it very often did not have yet. It got dragged to the edge, ran out of audio and stalled. Then 'stalled' and 'waiting' were both wired to seekLive(), and a seek on a live HLS stream makes the element re-buffer, which raises those same two events. So the correction caused the stall that caused the correction. On Safari, where HLS is native and a seek lands on a segment boundary, each turn of that loop replays or drops a piece of a sentence — which is exactly what was described. Three changes, all of them about not fighting the player: those two events no longer seek, the target is three segments back (~2.1 s, what an HLS player picks for itself on a live playlist), and a correction now has 5 s to settle before another may run. Costs about 1.5 s of extra delay. A listener a second further behind is following the service; one whose audio keeps jumping backwards is not. NOT YET CONFIRMED ON A REAL IPHONE — the mechanism is read off the code and the measured segment length, and the next service is the test. CANNOT SCROLL THE MONITOR WHILE LIVE. Four boxes ended their update with box.scrollTop = box.scrollHeight, unconditionally, so every sentence yanked the view back to the live edge and reading back was impossible. They now follow the bottom only while the operator is already at the bottom, and resume when they return — nothing to press either way. Trimming the top is held off while they are scrolled away too, since dropping old lines moves the text being read; the cap comes back the moment they return, with a ceiling so nothing grows all service. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
acfca90836 |
The English switch did nothing, so English could not be turned off
Build 125. Reported from the desk: every language on the listener page can be
switched off except English.
Exactly right, and it was not subtle. renderFeatured() unshifted the SOURCE
language into the list on every render, whatever the operator had chosen:
if (list.indexOf(app.cfg.sourceLanguage) === -1)
list.unshift(app.cfg.sourceLanguage);
So English was not merely on, it was always FIRST — the leading button on the
page — on a desk whose saved list was ["spa_Latn","por_Latn","por_PT","spa_ES"]
with English deliberately off and allowAll false. The operator had turned it
off, the config recorded that it was off, and the page put it back anyway.
The reasoning behind it was sound: people do want the English captions for
accessibility rather than translation. But that is a choice for the desk, not
for this function, and the switch to make it is already there in Settings next
to every other language. English is in the catalogue (languages.js:19) and the
operator window reads that same file, so turning it back on is one click.
A control that silently refuses is worse than no control. The operator's list
is now the list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7291ff48f7 |
Save QR image stranded the window on a picture of itself
Build 124. Reported from the booth: the button did not download anything and there was no way back off the QR. Both halves are the same fault. The window is served from the asset port and the code comes off the congregation port, so the link the button built was CROSS-ORIGIN — and a cross-origin <a download> has its download attribute ignored and NAVIGATES instead. So the file never saved, and the window went to the image. The app has no back button, no reload item and no address bar, so that is a one-way trip: a headless booth Mac showing a full-screen QR with the operator locked out of the desk. It fetches the code and downloads a blob now, which is same-origin and actually saves. The second change is the one that matters more. Nothing was stopping the app window being replaced by any navigation at all, and the only reason this had never bitten before is that nothing had tried. will-navigate now refuses to replace the app page, logs what it refused, and sends anything external to the browser. Our own pages are unaffected — the operator console and the poster open as their own windows through setWindowOpenHandler, which is a different path entirely. Found because the build number went to the wrong field: buildNumber is the source of truth and build.buildVersion is synced from it by predist, which caught it and rebuilt correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c9eef0672c |
Church Translate, as it runs at the campuses
Live sermon translation into 100+ languages, running entirely on one Mac per campus: audio in, voice detection, Whisper on the GPU, NLLB-200 in its own process, and a built-in web server the congregation reads from on their phones. No audio leaves the building. This is the source only. What used to sit beside it — 14 GB of speech and translation models, 8 GB of voice bundles, 6 GB of natural voices, the Electron caches, and the app data holding the operator PIN and every service transcript — is machine state and downloaded assets, and stays in the NAS backup where it belongs. The vendored node, cloudflared and Windows ffmpeg binaries are out too (533 MB, re-fetched for packaging); the browser-side runtime beside them is kept, so a checkout still serves the congregation page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |