From 68efe9c4200c95467179d2590e9dc1b17cf95912 Mon Sep 17 00:00:00 2001 From: StuBehan Date: Thu, 30 Apr 2026 13:53:22 +0200 Subject: [PATCH] fix: migrate to stackvox 0.3.x unified CLI; voice was silently broken on fresh installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stackvox 0.3.x consolidated its CLI — there is no separate `stackvox-say` console_script anymore; speech goes through `stackvox say ` as a subcommand. notify.sh and Speaker.swift both still pointed at the old binary, so on fresh installs (where only `stackvox` exists in the venv) the existence guard bailed silently and voice never fired. install looks healthy, daemon is running, models are downloaded, the manual `stackvox speak` works — but hooks never speak. Reported in #14 by @OMauriStkOne. notify.sh - STACKVOX_SAY removed; speak_notification now invokes "$STACKVOX" say - voice_phrase_for gates on STACKVOX existence (was STACKVOX_SAY) - nudge_debug() helper writes to stderr only when STACKNUDGE_DEBUG=true. Logs the missing-binary and missing-daemon-socket cases that were previously hidden by the silent bail. The "voice silently no-ops" failure mode is now diagnosable by setting STACKNUDGE_DEBUG=true. Speaker.swift - Same migration on the Swift side (used by the menu bar's voice toggle preview and the voice-cycle preview in Settings) - Removed the now-redundant separate stackvox / stackvoxSay paths Verified locally with stackvox 0.3.x in the venv: `stackvox say --voice af_aoede ...` runs cleanly; notify.sh's voice path completes without error when STACKNUDGE_VOICE=true. Closes #14 Co-Authored-By: Claude Opus 4.7 (1M context) --- install.sh | 2 +- notify.sh | 26 +++++++++++++++++++------- panel/Speaker.swift | 19 ++++++++++--------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/install.sh b/install.sh index 27679b6..a8748c2 100755 --- a/install.sh +++ b/install.sh @@ -48,7 +48,7 @@ find_python() { # Install the voice engine (stackvox) from PyPI into an isolated venv. echo "" echo "Setting up voice engine..." -STACKVOX_SPEC="stackvox>=0.3.0" +STACKVOX_SPEC="stackvox>=0.4.0" PYTHON=$(find_python) if [[ -z "$PYTHON" ]]; then echo " Could not find Python ≥ 3.10. Install one (e.g. 'brew install python@3.13')" diff --git a/notify.sh b/notify.sh index 2a631a8..ecc594b 100755 --- a/notify.sh +++ b/notify.sh @@ -146,7 +146,7 @@ voice_phrase_for() { local event="$1" local lang repo - if [[ -x "$STACKVOX_SAY" ]]; then + if [[ -x "$STACKVOX" ]]; then lang=$(voice_to_lang "$VOICE_NAME") repo=$(repo_name_raw) else @@ -196,25 +196,37 @@ agent_label() { esac } -# Bundled voice engine paths +# Bundled voice engine paths. stackvox 0.3.x consolidated the CLI — there +# is no separate `stackvox-say` console script anymore; speech goes through +# `stackvox say ` as a subcommand. VENV="${HOME}/.stack-nudge/venv" STACKVOX="${VENV}/bin/stackvox" -STACKVOX_SAY="${VENV}/bin/stackvox-say" + +# Log a debug line when STACKNUDGE_DEBUG=true. Used for "voice was +# requested but couldn't fire" cases that previously failed silently. +nudge_debug() { + [[ "${STACKNUDGE_DEBUG:-}" == "true" ]] || return 0 + printf '[stack-nudge] %s\n' "$*" >&2 +} # Speak a message aloud via the bundled StackVox daemon. # Auto-starts the daemon if it isn't running. Falls back silently if the -# venv isn't installed or the daemon fails to respond. +# venv isn't installed or the daemon fails to respond — set STACKNUDGE_DEBUG=true +# to surface why. speak_notification() { [[ "${VOICE_ENABLED}" != "true" ]] && return - [[ ! -x "$STACKVOX_SAY" ]] && return + if [[ ! -x "$STACKVOX" ]]; then + nudge_debug "voice requested but stackvox not found at $STACKVOX" + return + fi local text="$1" - # Start daemon if socket doesn't exist yet if [[ ! -S "${HOME}/.cache/stackvox/daemon.sock" ]]; then + nudge_debug "stackvox daemon socket missing — starting daemon" nohup "$STACKVOX" serve >/dev/null 2>&1 & fi local kokoro_lang kokoro_lang=$(voice_to_kokoro_lang "$VOICE_NAME") - "$STACKVOX_SAY" --voice "${VOICE_NAME}" --lang "${kokoro_lang}" --speed "${VOICE_SPEED}" "${text}" 2>/dev/null & + "$STACKVOX" say --voice "${VOICE_NAME}" --lang "${kokoro_lang}" --speed "${VOICE_SPEED}" "${text}" 2>/dev/null & } # Locate one of our .app bundles. Searches ~/Applications, the script's diff --git a/panel/Speaker.swift b/panel/Speaker.swift index 49c642a..69ecdf1 100644 --- a/panel/Speaker.swift +++ b/panel/Speaker.swift @@ -1,19 +1,20 @@ import Foundation -// Thin wrapper around stackvox-say. Spawns the daemon if its socket isn't -// up yet (mirrors notify.sh's auto-start) and falls back to a no-op if -// stackvox isn't installed in the venv. +// Thin wrapper around the stackvox CLI. Spawns the daemon if its socket +// isn't up yet (mirrors notify.sh's auto-start) and falls back to a no-op +// if stackvox isn't installed in the venv. +// +// stackvox 0.3.x consolidated its CLI — there's no separate `stackvox-say` +// binary anymore; speech goes through `stackvox say ` as a subcommand. enum Speaker { static func speak(_ text: String, voice: String? = nil, speed: String? = nil) { let venvBin = "\(NSHomeDirectory())/.stack-nudge/venv/bin" - let stackvoxSay = "\(venvBin)/stackvox-say" let stackvox = "\(venvBin)/stackvox" let socketPath = "\(NSHomeDirectory())/.cache/stackvox/daemon.sock" - guard FileManager.default.isExecutableFile(atPath: stackvoxSay) else { return } + guard FileManager.default.isExecutableFile(atPath: stackvox) else { return } - if !FileManager.default.fileExists(atPath: socketPath), - FileManager.default.isExecutableFile(atPath: stackvox) { + if !FileManager.default.fileExists(atPath: socketPath) { let serve = Process() serve.executableURL = URL(fileURLWithPath: stackvox) serve.arguments = ["serve"] @@ -24,8 +25,8 @@ enum Speaker { let resolvedVoice = voice ?? config["STACKNUDGE_VOICE_NAME"] ?? "af_aoede" let resolvedSpeed = speed ?? config["STACKNUDGE_VOICE_SPEED"] ?? "1.1" let say = Process() - say.executableURL = URL(fileURLWithPath: stackvoxSay) - say.arguments = ["--voice", resolvedVoice, "--speed", resolvedSpeed, text] + say.executableURL = URL(fileURLWithPath: stackvox) + say.arguments = ["say", "--voice", resolvedVoice, "--speed", resolvedSpeed, text] try? say.run() } }