Perf, stability & progress-bar fixes; relicense;#30
Open
julia-kafarska wants to merge 17 commits into
Open
Conversation
Removes the files.downloads.read-write and network.server rows (neither is declared in the MAS build) and corrects the network.client description, so the table matches build-resources/entitlements.mas.plist.
Switch the project license to PolyForm Noncommercial License 1.0.0 (source-available, free for non-commercial use), copyright 2026 Julia Kafarska. Update package.json license field to "SEE LICENSE IN LICENSE" and adjust README wording (source-available, not open-source).
On detecting a newer release the app now also fetches WHATSNEW.txt from the repo root on main. Each non-blank line becomes a bullet in the update banner (leading list markers stripped, capped at 5 lines / 140 chars). Empty/missing/offline falls back to the generic 'a newer version is ready' copy, preserving offline-first behavior. UpdateInfo gains a highlights field across update-check, preload, the renderer hook and electron.d.ts; preload.cjs forwards it unchanged.
…peak 7.9s -> 2.8s) Profiling on M1 (real worker, q8f16) showed one session.run already saturates the ONNX CPU thread pool: the look-ahead concurrency added zero throughput while tripling per-chunk latency and starving espeak phonemization (60ms idle -> 5.6s under contention). - Look-ahead 4/3 -> 1: same throughput (RTF ~2.5), 3x lower inference latency - Reader phonemizes lazily (bounded producer/consumer in generateUnitsToStream) instead of front-loading the whole batch before the first inference - preload builds the ONNX session (not just the model bytes) and replies preloadComplete, moving ~650ms of session creation to app launch - New opt-in fastStart flag on generateUnits carves a ~48-phoneme first chunk at a word boundary for fast first audio on play/seek; refill batches stay seamless (flag plumbed through preload.ts and ReaderGenerateParams) New electron/phoneme-split.ts with unit tests, plus worker integration tests pinning the preloadComplete/fastStart/lazy-prep message contracts.
Quitting mid-generation aborted the whole process with an uncaught native
Napi::Error: main never sent the worker its shutdown message, and the
worker's cleanup released the ONNX session while the generation loop was
still using it. app.exit() alone does not avoid the teardown on current
Electron, despite the comment claiming so.
- Worker tracks in-flight session.run promises, refuses new inference once
shutting down, and drains in-flight runs (bounded 10s) before releasing
- before-quit sends {type:"shutdown"} and waits for shutdown_complete
(bounded 12s) alongside the telemetry flush before app.exit(0)
- Both fill loops get rejection handlers so an aborted inference can't
become an unhandled rejection; the reader drive loop unparks its
producer in a finally
Verified live: Play -> Quit mid-generation now exits 0 with a graceful
worker shutdown (was SIGABRT every time).
The bar's denominator was a chars/14 text estimate blended with a chunk-based extrapolation whose weighting broke down under playback backpressure, so the green fill ran 25-35% behind the audio and snapped to 100% at the end. Show plain elapsed seconds against ONE estimate fixed at play start instead (corrected to the actual total once generation completes), and calibrate it to the measured Kokoro rate of 10.2 chars/s (was 14).
…ered
The bar was driven by requestAnimationFrame, which Chromium stops firing
for occluded/hidden windows — but audio keeps playing in the background,
so the bar froze at its last position until the user interacted with the
window again ("progress bar only shows when I pause or reading ends").
Drive the progress loop with a 200ms setTimeout tick instead, and set
backgroundThrottling: false on the main window so the tick isn't
throttled while hidden.
Drop the Slack-specific content script (slack.js/slack.css), its manifest registration, the Slack-only "Read message out loud" context menu, and the Slack marketplace plan doc. The generic read-selection context menu, side panel, and the OS-level selection-reader hotkey (which works in any app, including Slack desktop) are unchanged.
The synthesized-copy approach needs macOS Accessibility permission and failed silently without it (selection unread, stale clipboard read out instead) — not worth the permission friction. Removes the selection reader, its shortcut-recorder settings UI, KeyCombo/platform helpers, and the About-dialog copy for it. The macOS "Read out loud" right-click Service stays — it receives the selection from the OS without any permission, over the same external:speak channel.
b9c14ae to
359c734
Compare
main.ts imports it via the compiled "./mac-service.js" specifier, so knip's resolver follows the committed .js and flags the .ts source as unused — same pattern as the other electron/*.ts entries.
package.json version (GitHub release / CFBundleShortVersionString) and electron-builder buildVersion (App Store CFBundleVersion) had drifted (2.0.1 vs 2.0.2); align both on 2.0.4. Version bump only — no release.
The worker integration tests boot the real ONNX model (~100s in CI). Move them behind a *.integration.test.ts naming convention excluded from "npm test"; run the full suite locally with "npm run test:integration".
Three versions of CPU kernel improvements (measured on M1: reader RTF 2.56 -> 2.62, quick-speak 2.85 -> 2.89, first-audio unchanged) and the newest release that still ships darwin/x64 prebuilts (1.24+ drops Intel Mac support). macOS binaries now register cpu, coreml, and webgpu backends; the default stays cpu. Full worker integration suite passes.
- Run-time CPU fallback: a GPU provider that initializes but fails inside
session.run (CoreML on 1.23.2 does exactly this: "Error in building
plan"; DirectML on Windows is the known equivalent) now rebuilds the
session CPU-only, retries the inference once, and latches that provider
list for the worker's lifetime. Previously this surfaced as a generation
error with no retry.
- Session cache is keyed by (model, resolved providers), so changing the
acceleration mode rebuilds instead of silently reusing the old session.
- Worker emits sessionInfo {model, requested, effective, fallback} on
every session build; preloadComplete now carries the providers. Main
logs it and records a tts_session_created telemetry event — the
go/no-go data for any future GPU work.
- Correct the stale comment about which EPs onnxruntime-node prebuilts
actually bundle.
Verified live on this machine: acceleration=coreml creates a session,
dies at first inference, falls back to CPU, and the generation still
delivers audio (covered by new integration tests).
The worker's chunk WAVs are float32 (4 bytes/sample); the earlier calibration divided byte counts by 2, halving the measured speech rate and making the green bar run at half speed before leaping at the end. Re-measured correctly: 1132 chars -> 55.6s, 2203 chars -> 120.4s.
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.
This branch grew beyond the original two changes — full contents below.
1. Relicense MIT → PolyForm Noncommercial 1.0.0
LICENSE— full PolyForm Noncommercial 1.0.0 text, © 2026 Julia Kafarskapackage.json—license→"SEE LICENSE IN LICENSE"(no SPDX id for PolyForm)README.md— wording → "source-available" (not "open source")Free for non-commercial use; commercial use no longer permitted. Source-available, not OSI open-source. Copyright holder retains the right to relicense future releases.
2. Release highlights in the update banner (WHATSNEW.txt)
WHATSNEW.txtfrom the repo root onmain.-/*/•stripped; capped at 5 lines / 140 chars).UpdateInfogainshighlightsacrossupdate-check,preload, the renderer hook, andelectron.d.ts;preload.cjsforwards it unchanged.WHATSNEW.txtadded (empty) so it exists onmain.3. TTS pipeline: time-to-first-audio 3-4x faster (
9ccb028)Profiling on M1 (real worker, q8f16) showed one
session.runalready saturates the ONNX CPU thread pool — the look-ahead concurrency added zero throughput while tripling per-chunk latency and starving espeak (60 ms idle → 5.6 s under contention).preloadbuilds the ONNX session at launch (~650 ms off the first play) and repliespreloadCompletefastStartflag ongenerateUnitscarves a small word-boundary first chunk for fast first audio on play/seek; refill batches stay seamlessMeasured: reader first audio 6.5 s → 1.6 s, quick-speak 7.9 s → 2.8 s, throughput unchanged.
4. Quit-while-playing crash (
997c47d)Quitting mid-generation aborted the process with an uncaught native
Napi::Error(SIGABRT): main never sent the worker its shutdown message and cleanup released the ONNX session while in use. Now: the worker tracks in-flight runs and drains them (bounded) before releasing;before-quitwaits forshutdown_complete(bounded) beforeapp.exit(0). Verified: Play → Quit mid-generation exits 0.5. Progress bar (
827070f,546929e)requestAnimationFrame, which Chromium stops for hidden/occluded windows while audio keeps playing — it froze until the user interacted. Now a 200 ms timer +backgroundThrottling: false.6. Acceleration path: hardened, instrumented, upgraded
session.run(CoreML does exactly this on 1.23.2; DirectML on Windows is the known equivalent) now rebuilds CPU-only, retries the inference, and latches — previously a generation error with no retrysessionInfo(requested vs effective providers) → log + telemetry7. Removals (
a8ba8ba,b9c14ae)Verified
npm run electron:compile,cd electron-ui && npm run build,eslint .,prettier --checktests/suite (12 tests, vitest): unit tests for the first-chunk splitter + integration tests against the real worker & model pinning thepreloadComplete/fastStart/lazy-prep contracts and shutdown safety mid-generation