Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
Expand Down
26 changes: 19 additions & 7 deletions notify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <text>` 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
Expand Down
19 changes: 10 additions & 9 deletions panel/Speaker.swift
Original file line number Diff line number Diff line change
@@ -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 <text>` 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"]
Expand All @@ -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()
}
}