Feature/ux overhaul - #11
Merged
willwade merged 42 commits intoAug 19, 2026
Merged
Conversation
NuGet 0.3.17 brings progressive/streamed on_audio delivery (cloud SharedPipe + IncrementalDecoder, sherpa sentence-batched streaming), licence metadata, the 2026-08-10 model registry, Google 24 kHz pin and numeric creds acceptance.
rust-tts-wrapper 0.3.17 streams audio incrementally (cloud engines via SharedPipe + IncrementalDecoder; sherpa via sentence batches), so onAudioData no longer sees a single complete buffer per utterance. - OnAudioData: a fully-silent chunk arriving after >= 1s of held silence fell through to Write(data, len - m_lastSilentBytes), underflowing ULONG and passing a huge length to the SAPI site. Unreachable with single-buffer delivery; crash-grade with streamed chunks. Flush the held silence and deliver the chunk in full. - InitRustTtsVoice: registry v2026-08-10 canonicalised model IDs and the wrapper hard-fails on unknown IDs while resolving files at <modelPath>/<modelId>. Migrate installed model directories from the 14 legacy names on voice init (idempotent) and best-effort rewrite the token's stored Sherpa paths onto the renamed directory. Silence compensation otherwise works per-chunk by design (holds fully silent chunks, flushes on non-silent audio); boundaries arrive on the rate-1.0 baseline anchored to delivered samples, matching the existing WaveTicksToBytes mapping.
…ies) Sync the bundled catalog from rust-tts-wrapper 0.3.17's src/models.json (sha256 e00175e8..., matching upstream registry-version.txt) and rename merged_models.json -> models.json to follow upstream. The refreshed catalog adds license/license_url, min_sherpa_onnx_version, sha256 and deprecated metadata, and canonicalises model IDs (the adapter migrates installed directories separately). Updated references: SherpaOnnxConfig.csproj, VoiceGarden.UI.csproj, SherpaModelService (models.json first with legacy merged_models.json sidecar fallback), SherpaOnnxConfig MainForm, msbuild.yml, build-release-local.ps1, create-setup-payload.ps1, redesign doc.
…review rate - SherpaModelService: expose the legacy->canonical ID map and rename installed model directories on scan (idempotent; the adapter retries on voice init for locked dirs). Installed models keep their promoted status when still referenced by the legacy Sherpa-<old-id> token. - Catalog DTO: license/license_url/min_sherpa_onnx_version/deprecated from registry v2026-08-10. - Model list: hide fp16-URL builds (183 entries) — they SIGABRT the wrapper's CPU-only ONNX runtime and Rust cannot catch the foreign exception; the hidden count is surfaced in the status line. - Row tooltip surfaces licence, sample rate, size, min sherpa-onnx version and deprecation state. min_sherpa_onnx_version is advisory only: the wrapper DLL statically links sherpa-onnx 1.13, so models needing 1.13.x still work despite the 1.12.23 sherpa-c-api bundled for legacy paths. - Sherpa preview: wrap PCM in WAV at the model's catalog sample rate instead of a hard-coded 24 kHz (non-24 kHz models played back pitch-shifted). Cloud previews stay 24 kHz — Google MP3 is pinned to 24 kHz since wrapper #12 and Azure delivers 24 kHz PCM.
Update test-word-boundaries(.live), test-sherpa-e2e, test-boundary-crash scripts and README/Program.cs examples to the canonical kokoro-en-v0_19 registry ID; the legacy name hard-fails in rust-tts-wrapper >= 0.3.17.
CI composes rust_tts_wrapper.dll into payload\x64 during setup (msbuild.yml build-setup job); build-release-local.ps1 had no equivalent, so MSI payloads built locally shipped without the wrapper DLL and every voice fell back to legacy engines. Mirror the CI step (and additionally cover win-x86, which the NuGet package also ships).
Three latent local-build bugs surfaced on 'C:\Users\Will Wade\...': - MSBuild /p:OutDir with a trailing backslash before the closing quote escapes it, so every following argument is mis-parsed (the AzureSpeechSDKShim project path was chopped at the space). Pass OutDir with forward slashes instead. - build-setup.ps1 had the same escape on -p:OutputPath, sending the MSI to Setup\obj\Release instead of installer-output. - -BrandingFile with an empty string drops the argument in PS 5.1 native invocation, leaving the parameter dangling; only pass it when a branding file is set. - run-sherpa-smoke-test.ps1 no longer exists (CI doesn't run it); skip it when absent instead of failing step 6.
Promoted Sherpa voices are named by catalog name ('v0_19') when
promoted via SherpaOnnxConfig but by model id when promoted via
VoiceGarden.UI; resolve either by matching name, description or
substring before SelectVoice. Also rewrite the scripts as pure ASCII
(PS 5.1 reads no-BOM UTF-8 as ANSI, and the em-dash bytes contain
0x94 which derails string parsing).
One wrapper commit: surfaces SherpaModelInfo.quality from the registry (free-form tiers high/medium/low/x_low plus int8/fp16 variant markers, 'unknown' for ~1196 mostly-MMS entries). No C ABI change; .NET bindings and catalog only. Bundled models.json already carried quality per entry since the Phase 2 registry sync.
- SherpaModelItem carries the catalog quality tier and a derived gender; quality shows as a row badge and in the tooltip alongside gender, licence and sample rate. 'unknown' tiers show no badge. - Quality filter combo (All/high/medium/low/x_low/int8/fp16/unknown) and list ordering high -> low with unknown last. - DeriveSherpaGender: whole-token female/woman/girl (checked before male/man/boy, since 'female' contains 'male'), the piper af_/am_ underscore prefixes, and mimic3's m/f (m-ailabs/f-ailabs) variants. Multi-speaker models and the MMS family stay Neutral: MMS ids are language codes and names are language names (e.g. the 'Male' language of Ethiopia), and mojibake in MMS names splits into stray single letters — guessing there would put wrong genders into SAPI tokens. 'af-'/'am-' dash segments are Afrikaans/Armenian language codes, not gender markers, so only the underscore form matches.
The wrapper's get_voices carries Gender for cloud engines (exposed by the .NET bindings); pass it through Promote/PromoteElevated so HKLM token Attributes get Gender=Male/Female instead of hardcoded Neutral — Grid 3 and other SAPI apps filter on it. The voice list gains a gender column (hidden when the engine reports nothing useful) and promotes with the selected voices' gender.
SAPI voice-token Attributes now carry derived metadata so Grid 3 and
other SAPI clients can filter voices:
- SherpaModelService promotion (direct HKLM and elevated .reg paths,
Speech and OneCore): Gender from the shared DeriveSherpaGender
rules plus custom Quality attribute; installed models are enriched
from the catalog before promoting. Stale Quality values are
deleted when the tier is unknown/empty.
- SherpaOnnxConfig promote paths: PersistentTokenMetadata carries
Gender + Quality; gender inference upgraded from naive substring
matching ('German' contains 'man', the MMS 'Male' language) to the
same token/prefix rules as the UI, and multi-speaker models are
left Neutral.
Also guards SherpaOnnxConfig against fp16 builds: the CLI download
command refuses them and the GUI catalog list hides them, matching
the VoiceGarden.UI filter — verified the hard way that a promoted
fp16 model SIGABRTs the SAPI host process on first Speak.
Verified: piper-en-southern_english_female_medium promotes with
Gender=Female; kokoro-en-v0_19 stays Neutral with no Quality value.
Move card/button/badge styles from MainWindow into App.axaml and add Phase 4 sizing: 44px buttons and touch targets, 52px list rows, 48px tabs, 14px base font, visible :focus-visible outlines, wrapping filter rows. Add neutral-English strings for the Engines/Credentials/Voices tabs.
…sherpa promotion TtsCredentialBuilder derives the wrapper credential dictionary from each engine's declared keys; AudioPreview centralises WAV wrapping, background playback and language-appropriate Sherpa preview text; SherpaModelService gains GetInstalledModel and PromoteModelsElevated(modelIds) so selected models can be promoted without registering everything. SherpaModelsView filter row moves from fixed-width grid columns to a wrapping panel.
…nced Replace the cramped single-page layout with a 4-tab shell: - Tab 1 Voice Engines: selectable catalogue of every wrapper engine (SherpaOnnx offline, Edge free cloud, 19 credentialed cloud engines) with online/offline, credentials and language filters; chunky 64px check rows; language support is learned from the Sherpa catalog and from voices fetched in Tab 3 (with a visible note while unknown). - Tab 2 Credentials: per-engine forms with key/secondary fields labelled from the engine's credential keys, verify per engine; tab disables with a 'not needed' hint when no selected engine needs credentials. - Tab 3 Voices: aggregated voices from selected engines (Sherpa catalog installed+downloadable, Edge, cloud via creds) with engine/language/ gender/quality filters, search, preview, download, single and bulk promote/unpromote to SAPI. - Advanced tab keeps every prior function: adapter registration, analytics, log level, logs folder, full Sherpa model manager, about. VoiceConfigView/VoiceConfigViewModel are superseded by the aggregated Voices tab and removed. CLI subcommands untouched.
The Fluent Button theme leaves VerticalContentAlignment unset, so with MinHeight the content pinned to the top of tall buttons with dead space below. Set Horizontal/VerticalContentAlignment=Center on the shared style and drop base buttons 44->36dp tall, MinWidth 96->80, with symmetric padding (16,8). Compact list-row buttons 40->32dp. Focus outlines unchanged.
The two-line tab headers (title + status) sat flush against the tab bottom, where the Fluent selected pipe overlays the last 4px, so the grey sub-text visually overlapped the blue indicator. Give every header StackPanel an 8dp bottom margin (the margin travels with the content, so it holds at any font scale) and reduce TabItem vertical padding 12->8dp to keep overall tab height stable. Applied to all four tabs including Advanced.
The star-sized list rows carried MinHeight=220: when the window was shorter than header+filters+220+footer, the list overflowed its grid cell and painted over the Install/Remove Selected action bar. Drop the minimum — the grid bounds the row and the ListBox scrolls inside it. Audited all tabs: Credentials (ScrollViewer) and Advanced (outer ScrollViewer) were already bounded; action bars all have their own grid rows.
The user never had to press Load: selecting the Voices tab now kicks off the existing load (sherpa catalog + Edge + credentialed engines with keys) when the list is empty or the engine selection changed since the last load. Engines without keys are skipped quietly — one per-engine hint when it's the only one, a single 'N engines skipped' count otherwise — so there is no error spam without credentials. The manual Load button stays for refresh.
'ara' and 'arabic' now surface Arabic-capable engines: the search prefix-matches (case-insensitive) engine name/id/description (still contains, as before) plus per-engine search tokens derived from language data — English + native names, BCP-47 tags and ISO 639-1/-3 codes (via Windows culture data). Sources: the sherpa models.json language arrays (names + 639-3 codes), the Edge voice list fetched key-free at startup, and a static per-engine list for the six cloud engines whose voice lists are hardcoded in rust-tts-wrapper src/cloud_engine.rs static_voices() @703f27c — nothing invented. Search box watermark now says what it searches: 'Search engines, languages…'.
Remove the large 'VoiceGarden' heading and the custom Close button from the main window — the standard title bar already provides both, and the header consumed ~46px of content height (32px text + 14px spacing). The tab control now starts at the top content margin; tab headers, status sub-texts and the invisible screen-reader announcement region are unchanged. Also remove the now-unused Close_Click handler and MainViewModel.AppName property (window title uses the localized AppName resource). OnboardingWindow checked: no equivalent redundant pattern (no custom close; its large text is page content).
C ABI unchanged since 0.3.18 (zipvoice/pocket synthesis, registry sync and Edge/SSML fixes are internal); the engine list and P/Invoke surface are identical, so this is a drop-in bump.
Straight copy from rust-tts-wrapper v0.3.20 src/models.json (was v0.3.17, 1399 entries). Adds 589 models: zipvoice and pocket zero-shot cloning, fp32/int8/fp16 quantization variants for piper and kokoro, and language-tagged piper/mimic3 IDs (piper-en_GB-alan-low style). 228 previous IDs are gone — handled by the migration map in the next commit.
The 2026-08-18 registry renamed 228 IDs (language-tagged piper/mimic3 names, -fs- developer fixes, quantization suffixes). rust-tts-wrapper hard-fails on unknown IDs, so both migration tables (C# SherpaModelService.LegacyModelIds and the adapter's kLegacySherpaModelIds) grow from 14 to 242 entries, mapping every gone ID to its canonical replacement. Pre-2026-08-10 names chain straight to their 2026-08-18 canonical ID, and URL-identical fp16 targets resolve to the plain fp32 entry where one exists (same on-disk layout, and the plain ID is not hidden in the UI). Verified live: an installed piper-en-southern_english_female_medium renames to piper-en_GB-southern_english_female_medium on scan and its promoted-token status still resolves via the legacy token-name lookup.
- DownloadModelAsync fetches the non-bundled vocos_24khz.onnx vocoder into the shared models dir when a zipvoice model is downloaded, so synthesis works without a manual extra step (the wrapper resolves it from the base dir). - CatalogModel parses the new quantization field; tooltips show the weight type (int8/fp16/...) when it differs from fp32. - Model-type labels pass the raw registry model_type through (zipvoice/pocket/supertonic/kitten) instead of collapsing to vits.
The registry gives several models the same display name — the MMS
family ships one model per script (mms_urd-script_arabic/-devanagari/
-latin all named 'Urdu') and piper ships fp32+int8 twins ('fasih').
Voice rows now show the model ID on the second line and a quantization
badge (int8/fp16/…) next to the quality badge; screen-reader labels
use 'Name (Id)' so identical names stay distinguishable non-visually.
Advanced tab model list gets the same quantization badge.
The getting-started steps still described the old single-page layout
('main screen Install button', 'SherpaOnnx Models tab', 'Cloud Engines
tab'). Rewritten as four steps matching the actual flow: register the
adapter (Advanced), choose engines (Voice Engines), add credentials
only when the tab is enabled (Credentials), then load/preview/install
voices (Voices). The en-US alias tip now points at the Advanced tab's
model manager. Onboarding version bumped to 2 so existing users see
the updated walkthrough once.
…settings Promoted voices now register with their REAL language instead of a hard-coded en-US: sherpa models carry their catalog language and cloud/Edge voices carry the locale reported by the wrapper (mapped to SAPI hex LANGIDs by the new SapiLanguage service, covering catalog lang codes, ISO 639-3, piper region tags and English names). Two Advanced-tab settings (both default on, persisted) add alias tokens so language-filtered apps can find every voice: - 'Add an en-US alias for non-English voices' — a '…-enUS' token (Language 409) so English-only apps see it; - 'Also alias right-to-left voices under Arabic' — a '…-ar' token (ar-SA / 1401) for RTL languages (Urdu, Arabic, Hebrew, Persian, Pashto…). Aliases are rebuilt on every promote (both the direct HKLM and the elevated .reg paths emit deletions first), unpromote removes the whole token family, and unknown languages keep the en-US primary with no redundant alias. Verified: ur-PK/urd/'Urdu' → ur-PK/420 with both aliases, nl_BE → nl-BE/813 with en-US only, en-US → none.
build-release-local.ps1 never passed -Version to build-setup.ps1, and the payload has no Installer.exe to sniff a version from, so every local build shipped ProductVersion 0.5.0.0. Windows Installer treats a same-version MSI as a no-op — setup.exe 'succeeded' but kept the old files (verified: installed exe stayed at the Aug 17 build while the payload contained the new code). Now defaults to a date-based version (0.<yy>.<mmdd>, today 0.26.818) which sorts above any 0.5.x install and triggers the MajorUpgrade path; -InstallerVersion overrides for same-day rebuilds.
MajorUpgrade Schedule=afterInstallFinalize uninstalls the previous version only after the new one is in place — if that pass hiccups (e.g. the app running mid-upgrade) the machine ends up registered at the new version with the old files, and every later setup run enters maintenance mode and silently does nothing. afterInstallValidate (the WiX default) removes the old product first, so an interrupted upgrade just leaves the working old version.
Downloading a model wrote the archive directly to its final name with no synchronisation, while the Voices tab's auto-rescan (ScanInstalledModels -> TryExtractArchives) could open/extract/delete the same file mid-download — the loser failed with 'being used by another process' (surfacing to the user as an access-denied warning), and a stalled download held the handle for its 30-minute HttpClient timeout, failing every retry. Downloads now stage as '<archive>.part' (invisible to scans), are renamed and extracted under a process-wide archive semaphore shared with TryExtractArchives, and a sharing violation surfaces as an actionable message instead of raw IOException text. Verified end-to-end: cantonese-yue-xiaomaiiwn downloads, extracts and scans clean while the repro (previous build) failed on a locked file.
Addresses the external UI/UX critique in one revertable commit:
1. Contrast & state: new Secondary button style (white, dark outline,
dark text — 4.5:1+) for every active secondary action (Verify,
Preview, Download, Select/Clear shown, adapters, logs). Flat
low-contrast grey is now reserved for :disabled only, so active
buttons never look disabled. Errors/warnings pair colour with an
icon and text (WCAG 1.4.1 / 1.4.3).
2. Workflow clarity: Voices tab gains an empty state ('No voices yet —
choose engines in the Voice Engines tab first…') and engine
descriptions now say WHY you'd pick them (SherpaOnnx: reliability
and privacy; Edge: best free online option) instead of just what
they are.
3. Errors: failures surface in a banner at the TOP of the Voices tab
(was a red string under the footer) with a ⚠ icon, a human-readable
sentence ('check your internet connection… close other voice apps
and restart') and the raw technical message tucked into a small
detail line. Load failures, missing keys, promote/unpromote and
download errors all route there.
4. Polish: internal model IDs hidden from voice rows except where the
display name is duplicated (the three-Urdu-scripts case), tooltip
keeps the full ID; the whole text area of a voice row toggles
selection (chunky target, WCAG 2.5.8); log-level dropdown lists
spdlog levels with Info preselected (was a blank box, and the
stored default now matches the adapter's info default); adapter
register/unregister rows sit in tinted panels tying status and
actions together.
…e UI thread PromoteSherpaModels' direct-write path (ScanInstalledModels + EnrichWithCatalog + HKLM writes) and Preview's GetInstalledModel both ran on the UI thread. ScanInstalledModels takes the archive lock introduced for the download race; when a download had just finished, its continuation needed the UI thread (Progress<T> posts to the sync context) to release the lock while the UI thread was blocked waiting for it — a classic deadlock that froze the whole window after Install. Both paths now run entirely inside Task.Run. Also adds right padding to the status badges column in voice rows so 'Downloaded' no longer collides with the Preview button.
…grade Running setup.exe while VoiceGarden was open failed the upgrade (locked files) or wedged the install into a mixed state. The SetupLauncher bootstrapper now checks for running VoiceGarden.UI / SherpaOnnxConfig before invoking msiexec: interactive runs prompt 'close it and continue / cancel' (auto-closing on OK), silent runs close everything automatically, and a stubborn process produces a plain-English message instead of a file-in-use MSI error. Also in this commit: - The UI's single-instance logic now takes over from a windowless zombie instance (kill after 30s windowless + retake the mutex) instead of deferring to it forever, which previously made the app unlaunchable after a bad install. - util:CloseApplication was evaluated and rejected: the PanelSw WiX build cannot load official wixext packages (symbol version mismatch), so the bootstrapper is the right home for this. Verified: app running + setup --silent -> auto-close, exit 0, files replaced, version 0.26.824 installed.
VoiceGarden.UI.exe replaced all of SherpaOnnxConfig's functionality in Phase 2 (model manager, promote, rescan) — nothing launches the old exe and its Start-Menu shortcut targets VoiceGarden.UI.exe — yet the payload still shipped the 146 MB single-file binary, roughly half the installer for zero function. Dropped from the local build script, the payload curate allow/exclude lists, and CI (the utilities matrix job now runs the existing 'remove the exe, keep models.json' step for every platform, not just ARM64). MSI shrinks 137 MB -> 80 MB. SpeechSDKPatcher.exe (0.2 MB) stays: it is the documented Windows 7 repair tool for the Azure Speech SDK chain the adapter delay-loads via SpeechSDKShim.dll. Also de-duplicated CliDispatcher's hardcoded engine credential map onto the shared TtsCredentialBuilder so CLI and UI cannot drift.
Local Narrator/natural voices were deliberately dropped (TOS concerns): EnumLocalVoices/EnumLocalVoicesInFolder have no callers and TTSEngine has no SDK synthesis path, so the adapter never loads Microsoft.CognitiveServices.Speech.* at runtime — yet the payload still shipped the whole chain plus its Win7 companions (SpeechSDKShim.dll, SpeechSDKPatcher.exe) and CI built the shim project on every run. - payload curate + CI compose + release ZIPs exclude the SDK chain - AzureSpeechSDKShim build steps removed (local + CI) - adapter code untouched: framework.h's PCH still compiles against the SDK headers (TTSEngine.h needs sapi_category), and the delay-load path is inert — removing it would cascade through the PCH for no runtime gain - onnxruntime.dll stays: sherpa-onnx-c-api.dll links it dynamically Edge/cloud/offline engines are unaffected (rust wrapper); SAPI enumeration unaffected (the SDK path was already dead). MSI shrinks 80.5 -> 73.5 MB. Verified on a live install: 11 runtime files, CLI and adapter registration intact.
Follows up on dropping the SDK runtime from the payload: the local Narrator/natural voice feature was removed for TOS reasons and had already lost all callers, so the code went too. Removed: - EnumLocalVoices / EnumLocalVoicesInFolder, MakeLocalVoiceToken, FindVoiceFolders, IsRunningInWin11Narrator + IsWindows10BuildOrGreater (~280 lines in VoiceTokenEnumerator.cpp) and their header decls - delaydll.cpp entirely: the delay-load hook + TryLoadAzureSpeechSDK existed only to load the SDK from the adapter folder, and only the SDK was ever delay-loaded - AzacException.h (SDK exception-macro override) + its PCH include and the azac_category branch in TTSEngine.h - speechapi_cxx.h includes from framework.h and TTSEngine.h (the 'needed for sapi_category' comment was stale — that class is ours) - vcxproj: delaydll.cpp entry, all DelayLoadDLLs, the four CognitiveServices .targets imports + guard errors; packages.config drops the SDK packages (YY-Thunks stays — the adapter uses it) Edge/Azure token enumeration, sherpa voices and rust synthesis are untouched. Verified on a live install: SAPI enumerates all voices and a sherpa voice synthesizes through the new adapter DLL. Also adds translation-strings.json at the repo root: the 103 keys missing from every locale file (plus the 58 dead keys listed inside) for the external translation tool.
The 3-tab overhaul added 103 keys that existed only in neutral
English; every locale file was stuck at the pre-overhaul 135-key set
(46 of which were no longer referenced). Generated machine
translations for all 36 locales (Bing batch pass; rate-limit
survivors and the fully-untranslated zh-CN set recovered via a
batched gpt-4o-mini run; a handful of genuine loanwords —
de/nl 'Offline', 'Token', 'Region' — correctly stay identical).
{0}/{1} placeholders verified intact for every entry, and the 46
dead keys are pruned from neutral and all locales so every file now
carries the same 180 keys.
translation-strings.json removed — it was the extraction scaffold
for this sweep.
A checkbox in the filter row restricts the aggregated voice list to voices already installed to SAPI — previously the only way to see what's installed was to scan the list for the green Installed badge. Filter composes with engine/language/gender/quality/search. String translated into all 36 locales (Bing, with pt/zh-CN recovered via gpt-4o-mini after rate limits); every resx now carries 181 keys.
willwade
merged commit Aug 19, 2026
da0b2e5
into
feature-installer-policy-and-setup
10 checks passed
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 is a BIG UX overhaul