Skip to content

Perf, stability & progress-bar fixes; relicense;#30

Open
julia-kafarska wants to merge 17 commits into
mainfrom
chore/updates
Open

Perf, stability & progress-bar fixes; relicense;#30
julia-kafarska wants to merge 17 commits into
mainfrom
chore/updates

Conversation

@julia-kafarska

@julia-kafarska julia-kafarska commented Jun 30, 2026

Copy link
Copy Markdown
Member

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 Kafarska
  • package.jsonlicense"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)

  • On detecting a newer release, the app also fetches WHATSNEW.txt from the repo root on main.
  • Each non-blank line → one bullet in the banner (leading -/*/ stripped; capped at 5 lines / 140 chars).
  • Empty/missing/offline → falls back to the generic "a newer version is ready" copy (offline-first preserved).
  • UpdateInfo gains highlights across update-check, preload, the renderer hook, and electron.d.ts; preload.cjs forwards it unchanged.
  • WHATSNEW.txt added (empty) so it exists on main.

3. TTS pipeline: time-to-first-audio 3-4x faster (9ccb028)

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 (60 ms idle → 5.6 s under contention).

  • Look-ahead 4/3 → 1: same throughput (~1.3× realtime on M1), 3x lower inference latency
  • Reader phonemizes lazily (bounded producer/consumer) instead of front-loading the whole batch
  • preload builds the ONNX session at launch (~650 ms off the first play) and replies preloadComplete
  • New opt-in fastStart flag on generateUnits carves a small word-boundary first chunk for fast first audio on play/seek; refill batches stay seamless

Measured: 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-quit waits for shutdown_complete (bounded) before app.exit(0). Verified: Play → Quit mid-generation exits 0.

5. Progress bar (827070f, 546929e)

  • Green fill = plain elapsed seconds / one estimate fixed at play start, calibrated to the measured Kokoro rate (~19 chars/s, was 14)
  • The bar was driven by 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

  • onnxruntime-node 1.20.1 → 1.23.2 (last version shipping Intel-Mac prebuilts; small CPU gain, all worker integration tests pass)
  • Run-time CPU fallback: a GPU provider that initializes but dies inside 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 retry
  • Session cache keyed by (model, providers) so acceleration changes rebuild; worker reports sessionInfo (requested vs effective providers) → log + telemetry

7. Removals (a8ba8ba, b9c14ae)

  • Slack integration (content script, manifest entry, Slack-only context menu, marketplace plan doc) — the generic Chrome extension is unchanged
  • Global read-selection hotkey (from the earlier WIP commit here) — the synthesized-copy approach needs macOS Accessibility permission and failed silently without it; the permission-free macOS "Read out loud" Service covers the use case and stays

Verified

  • npm run electron:compile, cd electron-ui && npm run build, eslint ., prettier --check
  • New tests/ suite (12 tests, vitest): unit tests for the first-chunk splitter + integration tests against the real worker & model pinning the preloadComplete/fastStart/lazy-prep contracts and shutdown safety mid-generation
  • Bench + live app runs for the latency and crash fixes (numbers above)

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.
@julia-kafarska julia-kafarska changed the title Relicense to PolyForm Noncommercial + release-highlights update banner Perf, stability & progress-bar fixes; relicense; drop Slack + global hotkey Jul 2, 2026
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.
@julia-kafarska julia-kafarska changed the title Perf, stability & progress-bar fixes; relicense; drop Slack + global hotkey Perf, stability & progress-bar fixes; relicense; Jul 2, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant