* test build for 0.16.0
* macos 10.15?
* format
* shorter dir for build on windows?
* Update build.yml
* move
* Update build.yml
* long path support
* clang
* try
* try
* move to ort rc12
* use ms prebuilt?
* use lower version for 1.23.1
* download my prebuilt
* Update build.yml
* Update build.yml
* fix?
* fix(nix): use dynamic linking for ort-sys 2.0.0-rc.12
ort-sys rc.12 removed pkg-config support; without ORT_PREFER_DYNAMIC_LINK
it defaults to static linking against ORT_LIB_LOCATION, which fails
because nixpkgs only provides shared libraries (.so).
* fix(nix): override onnxruntime to 1.24.2 for ort-sys rc.12 compatibility
ort 2.0.0-rc.12 enables API v24 by default, but nixpkgs only ships
onnxruntime 1.23.2 (API v23), causing a runtime panic on model load.
Use Microsoft's prebuilt binaries for onnxruntime 1.24.2 via overlay,
patched with autoPatchelfHook for NixOS compatibility.
This overlay should be removed once nixpkgs merges onnxruntime ≥ 1.24:
https://github.com/NixOS/nixpkgs/pull/499389
* updated deps for whisper
* fix(nix): sync devShell with packages for ort-sys rc.12 compatibility
- Extract shared definitions (commonNativeDeps, gstPlugins, commonEnv,
onnxruntimeOverlay) to avoid duplication between packages and devShells
- Add onnxruntime 1.24.2 overlay to devShell (was only in packages)
- Add BINDGEN_EXTRA_CLANG_ARGS to devShell so bindgen can find stdio.h
and generate correct vulkan bindings for whisper-rs-sys
- Add ORT_LIB_LOCATION and ORT_PREFER_DYNAMIC_LINK to devShell
- Replace libappindicator with libayatana-appindicator in devShell
- Add onnxruntime and vulkan-loader to devShell LD_LIBRARY_PATH
* fix missing lib in the handy blob dl
* scope lib verification
* dont run bun2nix on windows
---------
Co-authored-by: Evgeny <evgeny.khudoba@yandex.ru>
* feat(audio): opt-in lazy stream close for bluetooth mic latency
keep the microphone stream open for 30s after recording stops to
reduce latency on back-to-back transcriptions. gated behind an
experimental setting (off by default) since it keeps the mic actively
capturing while idle — degrading bluetooth audio quality on macOS.
adds lazy_stream_close setting, tauri command, toggle in experimental
settings section, and i18n strings.
* fix: gate lazy close on setting in cancel_recording path
cancel_recording was unconditionally calling schedule_lazy_close,
keeping the mic open for 30s even when the setting was disabled.
also bump close_generation on AlwaysOn→OnDemand switch to cancel
any stale lazy close timers.
* feat(audio): opt-in lazy stream close for bluetooth mic latency
keep the microphone stream open for 30s after recording stops to
reduce latency on back-to-back transcriptions. gated behind an
experimental setting (off by default) since it keeps the mic actively
capturing while idle — degrading bluetooth audio quality on macOS.
adds lazy_stream_close setting, tauri command, toggle in experimental
settings section, and i18n strings.
* fix: gate lazy close on setting in cancel_recording path
cancel_recording was unconditionally calling schedule_lazy_close,
keeping the mic open for 30s even when the setting was disabled.
also bump close_generation on AlwaysOn→OnDemand switch to cancel
any stale lazy close timers.
* chore: fix cargo fmt on model.rs line from main merge
* fix race
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* fix: prevent idle watcher from unloading model during recording
the idle watcher treats ModelUnloadTimeout::Immediately as a 0s
timeout, causing idle_ms > 0 to always be true. this unloads the
model on the next 10s tick — even while the user is still recording.
the Immediately variant is already handled correctly by
maybe_unload_immediately() which fires after each transcription
completes. the idle watcher should skip it entirely.
fixes regression introduced in d1da935.
* fix race condition
* cleanup
* format
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat(audio): use device default sample rate and always downsample
instead of forcing the microphone to open at 16kHz (which can cause
issues with bluetooth codecs, some ALSA drivers, and other devices
that advertise 16kHz support but produce suboptimal audio), use the
device's native/default sample rate and let the existing FrameResampler
downsample to 16kHz for the whisper pipeline.
the resampling infrastructure (rubato FftFixedIn) already exists in
run_consumer() and short-circuits when in_hz == out_hz, so devices
that natively default to 16kHz are unaffected.
* fix: graceful fallback when supported_input_configs fails
some ALSA/PipeWire drivers support default_input_config but have
a broken supported_input_configs implementation. fall back to the
default config instead of propagating the error. also add a warn
log when no config matches the device's default rate.
- Default model_unload_timeout from Never to Min5
- Fix Drop impl: use take() on watcher handle so clones from
initiate_model_load() don't kill the watcher thread
- Reset last_activity on model load to prevent immediate unload
- Upgrade watcher logging from debug to info level
- Remove duplicate "unloaded" event (unload_model already emits it)
Co-authored-by: CJ Pais <cj@cjpais.com>
* first pass at gpu acceleration
* unload the model when changing
* mock accelerator
* translations and build fix
* format
* clean up helptext
* type cleanup
* format
* lint
* refactor: migrate transcribe-rs from v0.2.8 to v0.3.0
Breaking API migration:
- engines::* → onnx::* module paths, whisper_cpp::* for Whisper
- TranscriptionEngine trait → SpeechModel trait
- Two-step new()+load_model() → one-step Model::load()
- transcribe_samples(Vec<f32>) → transcribe(&[f32])
- Explicit unload_model() → RAII drop
- Canary engine now from transcribe-rs instead of local crate
Removes canary-engine local crate dependency.
Uses path dependency to local transcribe-rs clone (temporary).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: upgrade transcribe-rs to v0.3.1, add Canary models
Update transcribe-rs dependency from v0.2.8 to v0.3.1 with
restructured features (whisper + onnx). Add Canary 180M Flash
(4 languages, 146MB) and Canary 1B v2 (25 EU languages, 692MB)
with translation support. Add i18n translations for all 17 locales.
Languages verified against HuggingFace model cards:
- Canary 180M Flash: en, de, es, fr
- Canary 1B v2: bg, hr, cs, da, nl, en, et, fi, fr, de, el, hu,
it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, ru, uk
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* make sure when changing models the language changes with it
* format
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
Whisper supports an `initial_prompt` parameter that biases the model's
vocabulary during transcription. For Whisper models, we now join the
user's custom words list into a comma-separated string and pass it as
the initial_prompt. This guides the model to prefer those spellings
during decoding rather than relying solely on fuzzy post-correction.
For non-Whisper engines (Parakeet, Moonshine, SenseVoice, GigaAM),
the existing apply_custom_words post-correction continues to apply
since those engines don't support prompt-based vocabulary biasing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Handle microphone start failures without aborting
Return recorder init errors instead of panicking/aborting in the worker thread and reset UI state when recording start fails.
Refs #436
* prop errors to ui
* format
* Add missing recording error translation keys
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* Remove filler words that conflict with European languages
Removed three filler words from the transcription filter that are actual
words in European languages:
- 'um' - Portuguese/Spanish indefinite article meaning 'a/an' (masculine)
Example: Portuguese 'Isto é um teste' (This is a test)
Removing this was breaking Portuguese transcriptions
- 'ha' - Spanish/Italian/Norwegian/Swedish auxiliary verb meaning 'has/have'
Example: Spanish 'Él ha comido' (He has eaten)
This is a very common verb form in Romance and Scandinavian languages
- 'eh' - Italian interjection and Canadian English discourse marker
While less critical, this can appear in legitimate Italian speech
The remaining filler words are primarily English
vocalized hesitations that don't conflict with common words in other
European languages.
Updated tests to use 'uhm' instead of 'um' where needed.
Fixes#941
* customize list
---------
Co-authored-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
* ci: add temporary test workflow for portable ZIP validation
* ci: fix mock setup in test workflow
* ci: remove temporary test workflow
* feat: add portable mode option to NSIS installer
Custom NSIS template based on tauri-v2.9.1 upstream with 6 targeted
modifications for portable mode support:
- New PortableMode variable and /PORTABLE CLI flag for silent installs
- Install type selection page with Normal/Portable radio buttons
- Conditional default directory (Desktop for portable, LocalAppData for normal)
- Skip registry writes, file associations, deep links, and uninstaller for portable
- Skip Start Menu and desktop shortcuts for portable installs
- Create portable marker file and Data/ directory on portable install
The existing portable.rs runtime detection is unchanged — it looks for the
same marker file this installer creates.
Usage:
GUI: select "Portable Installation" on the install type page
Silent: Handy_setup.exe /S /PORTABLE /D=C:\path\to\Handy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: implement true portable mode via source code path override
Detect a `portable` marker file next to the executable at startup.
When present, redirect all data paths (settings, models, recordings,
database, logs) to a local `Data/` directory instead of %APPDATA%.
This approach works because it intercepts path resolution at the Rust
level, bypassing Tauri/Windows Shell API limitations that made the
previous environment-variable and junction approaches unreliable.
Changes:
- Add src-tauri/src/portable.rs with OnceLock-based detection
- Replace all app.path().app_data_dir() calls with portable helper
- Configure tauri-plugin-log to use Folder target when portable
- Configure tauri-plugin-store with absolute path when portable
- Simplify build.yml portable ZIP (exe + resources + marker file)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add missing Manager trait import in portable.rs
The .path() method on AppHandle requires `tauri::Manager` in scope.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: redirect WebView2 cache to portable Data dir
Move main window creation from tauri.conf.json to setup closure so
we can set data_directory when in portable mode. This prevents
WebView2 from creating %LOCALAPPDATA%\com.pais.handy.
Also set data_directory on the recording overlay window.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: auto-detect portable mode during updates
When the Tauri updater runs the installer with /UPDATE, the install type
page is skipped and /PORTABLE is not passed. Detect the existing portable
marker file in .onInit so updates preserve portable mode automatically.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix prettier formatting in portable.rs and lib.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: add GigaAM v3 model for Russian speech recognition
Add GigaAM v3 e2e_ctc as a new transcription engine using
transcribe-rs 0.2.7 gigaam feature. Russian speech recognition
with punctuation, Latin characters and digit support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Keep the file name of the model download the same as the file on the
blob website.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* fix: upgrade tauri-plugin-updater to v2.10.0 to fix duplicate registry entries (#873)
Upgrade tauri and tauri-plugin-updater to get bundle-specific target
resolution, which prevents NSIS-installed users from receiving MSI
updates via OTA and creating duplicate registry entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* move to my branch
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
The tray locale lookup truncated "zh-TW" to "zh", always hitting
Simplified Chinese. Now tries full locale first, then language prefix,
then English.
Follow-up fix for #796.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: add CLI parameters for Linux (--start-hidden, --no-tray, --toggle-transcription, --debug)
Add command-line interface using clap for better Linux desktop integration:
- --start-hidden: launch without showing the main window
- --no-tray: launch without system tray icon (closing window quits app)
- --toggle-transcription: toggle recording on/off on a running instance
via tauri_plugin_single_instance
- --debug: enable debug mode with Trace-level logging (runtime-only)
Extract toggle_transcription() from signal_handle.rs into a reusable
function shared between SIGUSR2 handler and CLI single-instance callback.
Update CLAUDE.md and README.md with CLI documentation including setup
instructions for GNOME, KDE Plasma, Sway/i3, and Hyprland.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add cancel and post processs
* cleanup
* format
* docs
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: implement structured outputs for Cerebras, OpenRouter, OpenAI, and Apple Intelligence
- Add structured output support with JSON schema in llm_client.rs
- Update actions.rs to use system prompt + user content approach
- Remove legacy ${output} variable substitution for supported providers
- Update Apple Intelligence Swift code to accept system prompts
- All providers now use consistent structured output format
- Remove duplicate check_apple_intelligence_availability function
* wip changes
* fix(structured-outputs): address PR #706 review comments
- Add settings migration to sync supports_structured_output field for existing providers
- Fix fallback behavior: structured output failures now fall through to legacy mode
- Clone api_key to prevent ownership issues in fallback path
- Clean up build_system_prompt(): remove placeholder entirely
(instead of replacing with 'the user's message' which reads awkwardly)
- Add warn import from log crate
* refactor(structured-outputs): apply best practice improvements
- Optimize settings migration: use single match instead of double iteration
- Add TRANSCRIPTION_FIELD constant to replace magic strings
- Keep Apple Intelligence behavior unchanged (no API fallback for privacy)
Addresses code review feedback on PR #706:
1. More efficient provider lookup in ensure_post_process_defaults()
2. Eliminates hardcoded 'transcription' string in JSON parsing
3. Maintains privacy-first approach for Apple Intelligence
* fix groq output
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat(linux): add SIGUSR1 signal for transcribe_with_post_process
Register SIGUSR1 alongside SIGUSR2 in the signal handler so Linux
users can trigger transcription with post-processing via system
signals (e.g. `pkill -SIGUSR1 -x handy`). This mirrors the existing
SIGUSR2 toggle pattern and maps to the `transcribe_with_post_process`
action already present in ACTION_MAP.
Closes#758
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* readme
* format
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* fix: add TranscriptionState to prevent race conditions & add debounce
* refactor(transcription): replace state machine with actor coordinator
* some cleanup
* format
* minor cleanup
* format
---------
Co-authored-by: CJ Pais <cj@cjpais.com>