feat(blocks): make the Speak block work again, with an optional neural voice - #8013
Open
7se7en72025 wants to merge 1 commit into
Open
feat(blocks): make the Speak block work again, with an optional neural voice#80137se7en72025 wants to merge 1 commit into
7se7en72025 wants to merge 1 commit into
Conversation
7se7en72025
requested review from
Ashutoshx7,
omsuneri,
ssz2605 and
walterbender
as code owners
August 10, 2026 16:16
The block has been dead since meSpeak was pulled out of the codebase. Logo.processSpeak() was left as an empty method, the block set hidden = true so it never showed up in the Media palette, and SpeakBlock.flow() still guarded on logo.meSpeak, which nothing sets anymore. The default is now the Web Speech API. It is already in every browser we target, adds nothing to the bundle, starts talking immediately, and works offline, which matters for a tool children open on school hardware and sometimes without a connection. It picks a voice matching the current locale, falls back to any voice for the same language, and otherwise lets the browser choose. An optional Kokoro neural voice is also wired up, off by default, switched on with a "kokoroSpeech" entry set to "on" in localStorage. It sounds far more human than the OS voices, but the weights are about 97 MB on first use, which is too much to ask of every user by default. None of it ships with the app: there is no bundler here, so js/kokoro-speech.js reaches kokoro-js through a native dynamic import of a pinned CDN URL, only once the neural voice is switched on and a phrase is actually spoken. dist/, the service-worker precache and package.json are all untouched. Transformers.js keeps the weights in Cache Storage, so it is one download per browser rather than per run. Synthesis for the next phrase starts while the current one is still playing, so consecutive Speak blocks have as little of a gap as possible between them. Consecutive Speak blocks now queue instead of cutting each other off, on both engines. Logo._cancelSpeech(), called from runLogoCommands() and doStopTurtles(), clears whichever engine is talking, so speech from an old run never bleeds into a new one and Stop actually stops the talking. Also unhides the block, simplifies SpeakBlock.flow() by dropping the dead meSpeak guard, and removes the stale meSpeak references left in index.html and the test mocks.
7se7en72025
force-pushed
the
feat/speak-block
branch
from
August 10, 2026 16:20
7fef195 to
54c72cc
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #8013 +/- ##
==========================================
+ Coverage 62.19% 62.24% +0.05%
==========================================
Files 187 188 +1
Lines 58324 58469 +145
==========================================
+ Hits 36273 36396 +123
- Misses 22051 22073 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Category
The problem
The Speak block has been dead for a while. When meSpeak was pulled out of the codebase:
Logo.processSpeak()was left as an empty method with a// meSpeak was removed from the codebase.commentthis.hidden = true, so it stopped appearing in the Media paletteSpeakBlock.flow()still guarded onlogo.meSpeak !== null, which nothing sets anymoreSo a child cannot find the block at all, and if they do get one onto the canvas from an older project file, it silently does nothing.
What I went with, and why
The default is the Web Speech API. It is already in every browser we target, it adds nothing to the bundle, it starts talking immediately, and it works offline. For a tool that children open on school hardware and sometimes without a connection, that felt like the right thing to have on by default.
I also wired up Kokoro, an 82M-parameter neural voice that runs in the browser, because it sounds dramatically more human than anything the OS ships. It is opt-in and off by default, and I want to be upfront about why: it is heavy.
Here is what it actually costs on first use, measured rather than estimated:
q8buildThat is a lot to ask of a child on a slow connection, which is exactly why it is not the default. Turning it on is a
kokoroSpeechentry set toonin localStorage for now.The shipped bundle genuinely does not change.
dist/is byte-for-byte the same, the service-worker precache list is untouched, and no dependency is added topackage.json. Music Blocks has no module bundler, sojs/kokoro-speech.jsreaches kokoro-js through a native dynamicimport()of a pinned CDN URL, evaluated only when someone has switched the neural voice on and a phrase is actually spoken. Transformers.js keeps the weights in the Cache Storage API, so it is one download per browser rather than per run, and it works offline afterwards.There are other options here, and I would welcome direction
I do not think the split above is the only reasonable answer, and I would rather agree on the direction with maintainers than assume. The realistic alternatives, with sizes:
en_US-amy-lowis 63.1 MB. Either would be a meaningful step up from the OS voices at a fraction of Kokoro's download. The engine sits behind a small interface, so swapping it is a contained change and I am happy to do it.Happy to go whichever way the team prefers.
What changed
Logo.processSpeak()validates the text and routes it to the active engine. Empty and whitespace-only text is ignored, and non-string values are coerced.getVoices()on the first call, which is treated as "browser decides" rather than a failure.js/kokoro-speech.jsfor the neural path. It lazily imports kokoro-js, memoises the model load so ten Speak blocks trigger one download, queues phrases, plays the raw samples through a singleAudioContext, and gives up quietly with a console warning if the weights cannot be fetched.Logo._cancelSpeech(), called fromrunLogoCommands()anddoStopTurtles(), so speech from an old run never talks over a new one and Stop actually stops the talking.activity/kokoro-speechinjs/loader.jsand added it to theactivity/logodependency list.SpeakBlock.flow(): dropped the deadlogo.meSpeakguard and flattened the nesting. Behaviour inside a note block is unchanged, it still hands off to the embedded-graphics scheduler so the speech lands on the note's timing.lib/mespeak.jsscript tag fromindex.htmland the stalemeSpeakentries in the test mocks. That file has not existed for some time.Tests
js/__tests__/logo.test.jscovers both engines: the happy path, empty and whitespace input, non-string coercion, a missing API, exact-locale and same-language voice selection, the emptygetVoices()case, queueing, cancellation, that the neural voice stays off unless explicitly switched on, and that it falls back cleanly if switched on without the module present.js/__tests__/kokoro-speech.test.jscovers the engine itself: the phrase queue, cancelling mid-phrase, one-time model loading, a custom voice, and the path where the weights cannot be fetched. The model is never downloaded in tests; the engine boundary is stubbed.All suites pass locally. The one unrelated failure on this branch is
RhythmBlocks.test.js(setupBlockDragController is not defined), which fails the same way onmaster.Demo
Two videos attached, sound on.
speak-block-demo.mp4shows the default browser voice, since that is what ships enabled.speak-block-demo-kokoro.mp4shows the same flow with the neural voice switched on, recorded against a warm cache so it reflects synthesis speed rather than the one-time download. In both, the Speak block is picked out of the Media palette, dropped under Start, given a phrase, and run; the second half adds a second Speak block and runs both, so you can hear the two phrases read in order rather than the first getting cut off.