Skip to content

Feature/ux overhaul - #11

Merged
willwade merged 42 commits into
feature-installer-policy-and-setupfrom
feature/ux-overhaul
Aug 19, 2026
Merged

Feature/ux overhaul#11
willwade merged 42 commits into
feature-installer-policy-and-setupfrom
feature/ux-overhaul

Conversation

@willwade

Copy link
Copy Markdown
Collaborator

This is a BIG UX overhaul

  • Lots of thought gone into the UX steps
  • Make it more accessible
  • Simplified language
  • We've also cut a lot of code out we dont use any more (NaturalReader) and streamlined the install process (look for app running etc)
  • We've also updated the language strings

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
willwade merged commit da0b2e5 into feature-installer-policy-and-setup Aug 19, 2026
10 checks passed
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