@codexstar/pi-listen 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/daemon.py +517 -0
- package/docs/API.md +273 -0
- package/docs/ARCHITECTURE.md +114 -0
- package/docs/backends.md +196 -0
- package/docs/plans/2026-03-12-pi-voice-master-plan.md +613 -0
- package/docs/plans/2026-03-12-pi-voice-model-aware-execution-plan.md +256 -0
- package/docs/plans/2026-03-12-pi-voice-onboarding-remediation-plan.md +391 -0
- package/docs/plans/pi-voice-model-aware-review.md +196 -0
- package/docs/plans/pi-voice-model-detection-qa-plan.md +226 -0
- package/docs/plans/pi-voice-model-detection-research.md +483 -0
- package/docs/plans/pi-voice-onboarding-ux-plan.md +388 -0
- package/docs/plans/pi-voice-release-validation-plan.md +386 -0
- package/docs/plans/pi-voice-remaining-implementation-plan.md +524 -0
- package/docs/plans/pi-voice-review-findings.md +227 -0
- package/docs/plans/pi-voice-technical-remediation-plan.md +613 -0
- package/docs/qa-matrix.md +69 -0
- package/docs/qa-results.md +357 -0
- package/docs/troubleshooting.md +265 -0
- package/extensions/voice/config.ts +206 -0
- package/extensions/voice/diagnostics.ts +212 -0
- package/extensions/voice/install.ts +62 -0
- package/extensions/voice/onboarding.ts +315 -0
- package/extensions/voice.ts +1149 -0
- package/package.json +48 -0
- package/scripts/setup-macos.sh +374 -0
- package/scripts/setup-windows.ps1 +271 -0
- package/transcribe.py +497 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codexstar/pi-listen",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Voice input, first-run onboarding, and side-channel BTW conversations for Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"voice",
|
|
11
|
+
"stt"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "bunx tsc -p tsconfig.json",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"check": "bun run typecheck && bun run test && python3 -m py_compile daemon.py transcribe.py",
|
|
18
|
+
"release:dry": "bun run check && bun publish --dry-run",
|
|
19
|
+
"release": "bun run check && bun publish --access public"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/codexstar69/pi-listen",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/codexstar69/pi-listen.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/codexstar69/pi-listen/issues"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@mariozechner/pi-ai": "*",
|
|
31
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
32
|
+
"@mariozechner/pi-tui": "*"
|
|
33
|
+
},
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./extensions/voice.ts"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"extensions",
|
|
41
|
+
"docs",
|
|
42
|
+
"scripts",
|
|
43
|
+
"README.md",
|
|
44
|
+
"transcribe.py",
|
|
45
|
+
"daemon.py",
|
|
46
|
+
"package.json"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
PACKAGE_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
MODE="local"
|
|
8
|
+
BACKEND="faster-whisper"
|
|
9
|
+
SCOPE="global"
|
|
10
|
+
PROJECT_DIR="$PWD"
|
|
11
|
+
DEEPGRAM_KEY="${DEEPGRAM_API_KEY:-}"
|
|
12
|
+
SKIP_PI_INSTALL=0
|
|
13
|
+
PERSIST_DEEPGRAM_KEY=0
|
|
14
|
+
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<'EOF'
|
|
17
|
+
pi-listen macOS bootstrap
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
scripts/setup-macos.sh [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--mode local|api Setup mode (default: local)
|
|
24
|
+
--backend NAME Local backend: faster-whisper | moonshine | whisper-cpp | parakeet
|
|
25
|
+
--scope global|project Where to write settings and install the package (default: global)
|
|
26
|
+
--project-dir PATH Project directory used for --scope project (default: current directory)
|
|
27
|
+
--deepgram-key KEY Deepgram API key for api mode (defaults to current DEEPGRAM_API_KEY)
|
|
28
|
+
--persist-deepgram-key Append DEEPGRAM_API_KEY to ~/.zshrc when a key is available
|
|
29
|
+
--skip-pi-install Skip `pi install npm:@codexstar/pi-listen`
|
|
30
|
+
-h, --help Show this help
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
scripts/setup-macos.sh --mode local --backend faster-whisper
|
|
34
|
+
scripts/setup-macos.sh --mode local --backend whisper-cpp --scope project
|
|
35
|
+
scripts/setup-macos.sh --mode api --deepgram-key dg_xxx --persist-deepgram-key
|
|
36
|
+
EOF
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
log() {
|
|
40
|
+
printf '\n[%s] %s\n' "pi-listen" "$*"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warn() {
|
|
44
|
+
printf '\n[%s] WARNING: %s\n' "pi-listen" "$*" >&2
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
have() {
|
|
48
|
+
command -v "$1" >/dev/null 2>&1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
append_if_missing() {
|
|
52
|
+
local file="$1"
|
|
53
|
+
local line="$2"
|
|
54
|
+
mkdir -p "$(dirname "$file")"
|
|
55
|
+
touch "$file"
|
|
56
|
+
APPEND_FILE="$file" APPEND_LINE="$line" python3 - <<'PY'
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
import os
|
|
59
|
+
|
|
60
|
+
path = Path(os.environ["APPEND_FILE"])
|
|
61
|
+
line = os.environ["APPEND_LINE"]
|
|
62
|
+
existing = path.read_text() if path.exists() else ""
|
|
63
|
+
if line not in existing:
|
|
64
|
+
with path.open("a", encoding="utf8") as handle:
|
|
65
|
+
handle.write("\n" + line + "\n")
|
|
66
|
+
PY
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
selected_backend() {
|
|
70
|
+
if [[ "$MODE" == "api" ]]; then
|
|
71
|
+
printf 'deepgram\n'
|
|
72
|
+
else
|
|
73
|
+
printf '%s\n' "$BACKEND"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
selected_model() {
|
|
78
|
+
case "$(selected_backend)" in
|
|
79
|
+
faster-whisper) printf 'small\n' ;;
|
|
80
|
+
moonshine) printf 'moonshine/base\n' ;;
|
|
81
|
+
whisper-cpp) printf 'small\n' ;;
|
|
82
|
+
parakeet) printf 'nvidia/parakeet-tdt-0.6b-v2\n' ;;
|
|
83
|
+
deepgram) printf 'nova-3\n' ;;
|
|
84
|
+
*)
|
|
85
|
+
warn "No default model for backend $(selected_backend)"
|
|
86
|
+
exit 1
|
|
87
|
+
;;
|
|
88
|
+
esac
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
settings_path() {
|
|
92
|
+
if [[ "$SCOPE" == "project" ]]; then
|
|
93
|
+
printf '%s/.pi/settings.json\n' "$PROJECT_DIR"
|
|
94
|
+
else
|
|
95
|
+
printf '%s/.pi/agent/settings.json\n' "$HOME"
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
install_pi_package() {
|
|
100
|
+
if (( SKIP_PI_INSTALL )); then
|
|
101
|
+
return
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if ! have pi; then
|
|
105
|
+
warn "The 'pi' command was not found, so package installation was skipped. Install Pi first, then run: pi install npm:@codexstar/pi-listen"
|
|
106
|
+
return
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [[ "$SCOPE" == "project" ]]; then
|
|
110
|
+
log "Installing pi-listen into Pi project settings"
|
|
111
|
+
(
|
|
112
|
+
cd "$PROJECT_DIR"
|
|
113
|
+
pi install -l npm:@codexstar/pi-listen
|
|
114
|
+
)
|
|
115
|
+
else
|
|
116
|
+
log "Installing pi-listen into global Pi settings"
|
|
117
|
+
pi install npm:@codexstar/pi-listen
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
write_voice_config() {
|
|
122
|
+
local settings_file
|
|
123
|
+
settings_file="$(settings_path)"
|
|
124
|
+
local backend model
|
|
125
|
+
backend="$(selected_backend)"
|
|
126
|
+
model="$(selected_model)"
|
|
127
|
+
|
|
128
|
+
mkdir -p "$(dirname "$settings_file")"
|
|
129
|
+
SETTINGS_PATH="$settings_file" \
|
|
130
|
+
VOICE_MODE="$MODE" \
|
|
131
|
+
VOICE_BACKEND="$backend" \
|
|
132
|
+
VOICE_MODEL="$model" \
|
|
133
|
+
VOICE_SCOPE="$SCOPE" \
|
|
134
|
+
python3 - <<'PY'
|
|
135
|
+
import json
|
|
136
|
+
import os
|
|
137
|
+
from datetime import datetime, timezone
|
|
138
|
+
from pathlib import Path
|
|
139
|
+
|
|
140
|
+
settings_path = Path(os.environ["SETTINGS_PATH"])
|
|
141
|
+
if settings_path.exists():
|
|
142
|
+
try:
|
|
143
|
+
data = json.loads(settings_path.read_text())
|
|
144
|
+
except Exception:
|
|
145
|
+
data = {}
|
|
146
|
+
else:
|
|
147
|
+
data = {}
|
|
148
|
+
|
|
149
|
+
timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
150
|
+
data["voice"] = {
|
|
151
|
+
"version": 2,
|
|
152
|
+
"enabled": True,
|
|
153
|
+
"language": "en",
|
|
154
|
+
"mode": os.environ["VOICE_MODE"],
|
|
155
|
+
"backend": os.environ["VOICE_BACKEND"],
|
|
156
|
+
"model": os.environ["VOICE_MODEL"],
|
|
157
|
+
"scope": os.environ["VOICE_SCOPE"],
|
|
158
|
+
"btwEnabled": True,
|
|
159
|
+
"onboarding": {
|
|
160
|
+
"completed": True,
|
|
161
|
+
"schemaVersion": 2,
|
|
162
|
+
"completedAt": timestamp,
|
|
163
|
+
"lastValidatedAt": timestamp,
|
|
164
|
+
"source": "setup-command"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
settings_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
168
|
+
PY
|
|
169
|
+
|
|
170
|
+
log "Wrote ready-to-use voice config to $settings_file"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
smoke_test() {
|
|
174
|
+
local backend model socket log_file err_file
|
|
175
|
+
backend="$(selected_backend)"
|
|
176
|
+
model="$(selected_model)"
|
|
177
|
+
socket="$(mktemp -u /tmp/pi-listen-bootstrap-XXXXXX.sock)"
|
|
178
|
+
log_file="$(mktemp /tmp/pi-listen-daemon-stdout-XXXXXX.log)"
|
|
179
|
+
err_file="$(mktemp /tmp/pi-listen-daemon-stderr-XXXXXX.log)"
|
|
180
|
+
|
|
181
|
+
log "Checking backend availability via transcribe.py"
|
|
182
|
+
python3 "$PACKAGE_ROOT/transcribe.py" --list-backends | python3 -c '
|
|
183
|
+
import json
|
|
184
|
+
import sys
|
|
185
|
+
backend_name = sys.argv[1]
|
|
186
|
+
backends = json.load(sys.stdin)
|
|
187
|
+
selected = next((b for b in backends if b.get("name") == backend_name), None)
|
|
188
|
+
if not selected:
|
|
189
|
+
raise SystemExit(f"Backend {backend_name} was not returned by transcribe.py --list-backends")
|
|
190
|
+
if not selected.get("available"):
|
|
191
|
+
raise SystemExit(f"Backend {backend_name} is still not available after setup")
|
|
192
|
+
print(json.dumps({"backend": selected["name"], "available": selected["available"], "install_detection": selected.get("install_detection") or "unknown"}))
|
|
193
|
+
' "$backend"
|
|
194
|
+
|
|
195
|
+
log "Starting daemon smoke test for backend=$backend model=$model"
|
|
196
|
+
python3 "$PACKAGE_ROOT/daemon.py" start --socket "$socket" --backend "$backend" --model "$model" >"$log_file" 2>"$err_file" &
|
|
197
|
+
local daemon_pid=$!
|
|
198
|
+
|
|
199
|
+
cleanup() {
|
|
200
|
+
python3 "$PACKAGE_ROOT/daemon.py" stop --socket "$socket" >/dev/null 2>&1 || true
|
|
201
|
+
wait "$daemon_pid" >/dev/null 2>&1 || true
|
|
202
|
+
rm -f "$socket" "$socket.pid" "$log_file" "$err_file"
|
|
203
|
+
}
|
|
204
|
+
trap cleanup RETURN
|
|
205
|
+
|
|
206
|
+
local attempt
|
|
207
|
+
for attempt in {1..120}; do
|
|
208
|
+
if python3 "$PACKAGE_ROOT/daemon.py" ping --socket "$socket" >/dev/null 2>&1; then
|
|
209
|
+
break
|
|
210
|
+
fi
|
|
211
|
+
sleep 1
|
|
212
|
+
done
|
|
213
|
+
|
|
214
|
+
if ! python3 "$PACKAGE_ROOT/daemon.py" ping --socket "$socket" >/dev/null 2>&1; then
|
|
215
|
+
warn "Daemon failed to start cleanly"
|
|
216
|
+
warn "Daemon stdout log: $log_file"
|
|
217
|
+
warn "Daemon stderr log: $err_file"
|
|
218
|
+
tail -n 40 "$err_file" >&2 || true
|
|
219
|
+
return 1
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
python3 "$PACKAGE_ROOT/daemon.py" status --socket "$socket" >/dev/null
|
|
223
|
+
log "Daemon smoke test passed"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
while (($#)); do
|
|
227
|
+
case "$1" in
|
|
228
|
+
--mode)
|
|
229
|
+
MODE="${2:-}"
|
|
230
|
+
shift 2
|
|
231
|
+
;;
|
|
232
|
+
--backend)
|
|
233
|
+
BACKEND="${2:-}"
|
|
234
|
+
shift 2
|
|
235
|
+
;;
|
|
236
|
+
--scope)
|
|
237
|
+
SCOPE="${2:-}"
|
|
238
|
+
shift 2
|
|
239
|
+
;;
|
|
240
|
+
--project-dir)
|
|
241
|
+
PROJECT_DIR="${2:-}"
|
|
242
|
+
shift 2
|
|
243
|
+
;;
|
|
244
|
+
--deepgram-key)
|
|
245
|
+
DEEPGRAM_KEY="${2:-}"
|
|
246
|
+
shift 2
|
|
247
|
+
;;
|
|
248
|
+
--persist-deepgram-key)
|
|
249
|
+
PERSIST_DEEPGRAM_KEY=1
|
|
250
|
+
shift
|
|
251
|
+
;;
|
|
252
|
+
--skip-pi-install)
|
|
253
|
+
SKIP_PI_INSTALL=1
|
|
254
|
+
shift
|
|
255
|
+
;;
|
|
256
|
+
-h|--help)
|
|
257
|
+
usage
|
|
258
|
+
exit 0
|
|
259
|
+
;;
|
|
260
|
+
*)
|
|
261
|
+
warn "Unknown argument: $1"
|
|
262
|
+
usage
|
|
263
|
+
exit 1
|
|
264
|
+
;;
|
|
265
|
+
esac
|
|
266
|
+
done
|
|
267
|
+
|
|
268
|
+
if [[ "$MODE" != "local" && "$MODE" != "api" ]]; then
|
|
269
|
+
warn "--mode must be local or api"
|
|
270
|
+
exit 1
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
if [[ "$SCOPE" != "global" && "$SCOPE" != "project" ]]; then
|
|
274
|
+
warn "--scope must be global or project"
|
|
275
|
+
exit 1
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
|
279
|
+
|
|
280
|
+
case "$BACKEND" in
|
|
281
|
+
faster-whisper|moonshine|whisper-cpp|parakeet) ;;
|
|
282
|
+
*)
|
|
283
|
+
warn "Unsupported backend: $BACKEND"
|
|
284
|
+
exit 1
|
|
285
|
+
;;
|
|
286
|
+
esac
|
|
287
|
+
|
|
288
|
+
if ! have brew; then
|
|
289
|
+
warn "Homebrew is required for automatic macOS setup. Install it from https://brew.sh/ and rerun this script."
|
|
290
|
+
exit 1
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
if ! have python3; then
|
|
294
|
+
log "Installing python3 via Homebrew"
|
|
295
|
+
brew install python
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
if ! have rec; then
|
|
299
|
+
log "Installing SoX"
|
|
300
|
+
brew install sox
|
|
301
|
+
else
|
|
302
|
+
log "SoX already available"
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
log "Ensuring pip tooling is ready"
|
|
306
|
+
python3 -m pip install --upgrade pip >/dev/null
|
|
307
|
+
|
|
308
|
+
if [[ "$MODE" == "local" ]]; then
|
|
309
|
+
case "$BACKEND" in
|
|
310
|
+
faster-whisper)
|
|
311
|
+
log "Installing faster-whisper"
|
|
312
|
+
python3 -m pip install faster-whisper
|
|
313
|
+
;;
|
|
314
|
+
moonshine)
|
|
315
|
+
log "Installing moonshine"
|
|
316
|
+
python3 -m pip install 'useful-moonshine[onnx]'
|
|
317
|
+
;;
|
|
318
|
+
whisper-cpp)
|
|
319
|
+
log "Installing whisper-cpp"
|
|
320
|
+
brew install whisper-cpp
|
|
321
|
+
;;
|
|
322
|
+
parakeet)
|
|
323
|
+
log "Installing parakeet"
|
|
324
|
+
python3 -m pip install 'nemo_toolkit[asr]'
|
|
325
|
+
;;
|
|
326
|
+
esac
|
|
327
|
+
else
|
|
328
|
+
if [[ -z "$DEEPGRAM_KEY" ]]; then
|
|
329
|
+
warn "API mode requires DEEPGRAM_API_KEY. Pass --deepgram-key or export DEEPGRAM_API_KEY first."
|
|
330
|
+
exit 1
|
|
331
|
+
fi
|
|
332
|
+
export DEEPGRAM_API_KEY="$DEEPGRAM_KEY"
|
|
333
|
+
log "Loaded DEEPGRAM_API_KEY into the current shell process"
|
|
334
|
+
if (( PERSIST_DEEPGRAM_KEY )); then
|
|
335
|
+
append_if_missing "$HOME/.zshrc" "export DEEPGRAM_API_KEY=$DEEPGRAM_KEY"
|
|
336
|
+
log "Persisted DEEPGRAM_API_KEY to ~/.zshrc"
|
|
337
|
+
fi
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
install_pi_package
|
|
341
|
+
smoke_test
|
|
342
|
+
write_voice_config
|
|
343
|
+
|
|
344
|
+
cat <<EOF
|
|
345
|
+
|
|
346
|
+
Done.
|
|
347
|
+
|
|
348
|
+
Happy path result:
|
|
349
|
+
- dependencies installed
|
|
350
|
+
- selected backend validated
|
|
351
|
+
- daemon smoke test passed
|
|
352
|
+
- Pi voice settings written automatically
|
|
353
|
+
|
|
354
|
+
Selected configuration:
|
|
355
|
+
mode: $MODE
|
|
356
|
+
backend: $(selected_backend)
|
|
357
|
+
model: $(selected_model)
|
|
358
|
+
scope: $SCOPE
|
|
359
|
+
|
|
360
|
+
Next steps:
|
|
361
|
+
1. Grant microphone permission once:
|
|
362
|
+
System Settings -> Privacy & Security -> Microphone
|
|
363
|
+
Allow your terminal app (Ghostty / Terminal / iTerm)
|
|
364
|
+
|
|
365
|
+
2. Start Pi:
|
|
366
|
+
$( [[ "$SCOPE" == "project" ]] && printf 'cd %q && ' "$PROJECT_DIR" )pi
|
|
367
|
+
|
|
368
|
+
3. Try hold-to-talk in an empty editor.
|
|
369
|
+
|
|
370
|
+
You should not need to run /voice setup on the happy path.
|
|
371
|
+
If you want a quick verification inside Pi, run:
|
|
372
|
+
/voice test
|
|
373
|
+
/voice doctor
|
|
374
|
+
EOF
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[ValidateSet('local', 'api')]
|
|
3
|
+
[string]$Mode = 'local',
|
|
4
|
+
|
|
5
|
+
[ValidateSet('faster-whisper', 'moonshine', 'parakeet')]
|
|
6
|
+
[string]$Backend = 'faster-whisper',
|
|
7
|
+
|
|
8
|
+
[ValidateSet('global', 'project')]
|
|
9
|
+
[string]$Scope = 'global',
|
|
10
|
+
|
|
11
|
+
[string]$ProjectDir = (Get-Location).Path,
|
|
12
|
+
|
|
13
|
+
[string]$DeepgramKey = $env:DEEPGRAM_API_KEY,
|
|
14
|
+
|
|
15
|
+
[switch]$PersistDeepgramKey,
|
|
16
|
+
|
|
17
|
+
[switch]$SkipPiInstall
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
$ErrorActionPreference = 'Stop'
|
|
21
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
22
|
+
$PackageRoot = Split-Path -Parent $ScriptDir
|
|
23
|
+
|
|
24
|
+
function Write-Step {
|
|
25
|
+
param([string]$Message)
|
|
26
|
+
Write-Host "`n[pi-listen] $Message"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Write-Warn {
|
|
30
|
+
param([string]$Message)
|
|
31
|
+
Write-Warning $Message
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Test-Command {
|
|
35
|
+
param([string]$Name)
|
|
36
|
+
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Install-WingetPackage {
|
|
40
|
+
param([string]$Id)
|
|
41
|
+
if (-not (Test-Command winget)) {
|
|
42
|
+
throw "winget is required for automatic Windows setup. Install App Installer / winget first."
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
& winget install -e --id $Id --accept-package-agreements --accept-source-agreements
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Get-SelectedBackend {
|
|
49
|
+
if ($Mode -eq 'api') { return 'deepgram' }
|
|
50
|
+
return $Backend
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Get-SelectedModel {
|
|
54
|
+
switch (Get-SelectedBackend) {
|
|
55
|
+
'faster-whisper' { return 'small' }
|
|
56
|
+
'moonshine' { return 'moonshine/base' }
|
|
57
|
+
'parakeet' { return 'nvidia/parakeet-tdt-0.6b-v2' }
|
|
58
|
+
'deepgram' { return 'nova-3' }
|
|
59
|
+
default { throw "No default model for backend $(Get-SelectedBackend)" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Get-SettingsPath {
|
|
64
|
+
if ($Scope -eq 'project') {
|
|
65
|
+
return Join-Path $ProjectDir '.pi\settings.json'
|
|
66
|
+
}
|
|
67
|
+
return Join-Path $HOME '.pi\agent\settings.json'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Invoke-Python {
|
|
71
|
+
param([string[]]$Arguments)
|
|
72
|
+
& $script:PythonCmd @script:PythonArgs @Arguments
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Install-PiPackage {
|
|
76
|
+
if ($SkipPiInstall) { return }
|
|
77
|
+
|
|
78
|
+
if (-not (Test-Command pi)) {
|
|
79
|
+
Write-Warn "The 'pi' command was not found, so package installation was skipped. Install Pi first, then run: pi install npm:@codexstar/pi-listen"
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ($Scope -eq 'project') {
|
|
84
|
+
Write-Step 'Installing pi-listen into Pi project settings'
|
|
85
|
+
Push-Location $ProjectDir
|
|
86
|
+
try {
|
|
87
|
+
& pi install -l npm:@codexstar/pi-listen
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
Pop-Location
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
Write-Step 'Installing pi-listen into global Pi settings'
|
|
95
|
+
& pi install npm:@codexstar/pi-listen
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function Write-VoiceConfig {
|
|
100
|
+
$settingsPath = Get-SettingsPath
|
|
101
|
+
$settingsDir = Split-Path -Parent $settingsPath
|
|
102
|
+
$null = New-Item -ItemType Directory -Force -Path $settingsDir
|
|
103
|
+
|
|
104
|
+
$data = @{}
|
|
105
|
+
if (Test-Path $settingsPath) {
|
|
106
|
+
try {
|
|
107
|
+
$data = Get-Content $settingsPath -Raw | ConvertFrom-Json -AsHashtable
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
$data = @{}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$timestamp = (Get-Date).ToUniversalTime().ToString('o').Replace('+00:00', 'Z')
|
|
115
|
+
$data['voice'] = @{
|
|
116
|
+
version = 2
|
|
117
|
+
enabled = $true
|
|
118
|
+
language = 'en'
|
|
119
|
+
mode = $Mode
|
|
120
|
+
backend = (Get-SelectedBackend)
|
|
121
|
+
model = (Get-SelectedModel)
|
|
122
|
+
scope = $Scope
|
|
123
|
+
btwEnabled = $true
|
|
124
|
+
onboarding = @{
|
|
125
|
+
completed = $true
|
|
126
|
+
schemaVersion = 2
|
|
127
|
+
completedAt = $timestamp
|
|
128
|
+
lastValidatedAt = $timestamp
|
|
129
|
+
source = 'setup-command'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$data | ConvertTo-Json -Depth 8 | Set-Content -Path $settingsPath -Encoding UTF8
|
|
134
|
+
Write-Step "Wrote ready-to-use voice config to $settingsPath"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Smoke-Test {
|
|
138
|
+
$selectedBackend = Get-SelectedBackend
|
|
139
|
+
$selectedModel = Get-SelectedModel
|
|
140
|
+
$transcribePath = Join-Path $PackageRoot 'transcribe.py'
|
|
141
|
+
$daemonPath = Join-Path $PackageRoot 'daemon.py'
|
|
142
|
+
|
|
143
|
+
Write-Step 'Checking backend availability via transcribe.py'
|
|
144
|
+
$backends = Invoke-Python @($transcribePath, '--list-backends') | ConvertFrom-Json
|
|
145
|
+
$selected = $backends | Where-Object { $_.name -eq $selectedBackend } | Select-Object -First 1
|
|
146
|
+
if (-not $selected) {
|
|
147
|
+
throw "Backend $selectedBackend was not returned by transcribe.py --list-backends"
|
|
148
|
+
}
|
|
149
|
+
if (-not $selected.available) {
|
|
150
|
+
throw "Backend $selectedBackend is still not available after setup"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Write-Step "Starting daemon smoke test for backend=$selectedBackend model=$selectedModel"
|
|
154
|
+
$socket = Join-Path $env:TEMP ("pi-listen-bootstrap-" + [guid]::NewGuid().ToString() + '.sock')
|
|
155
|
+
$stdoutLog = Join-Path $env:TEMP ("pi-listen-daemon-stdout-" + [guid]::NewGuid().ToString() + '.log')
|
|
156
|
+
$stderrLog = Join-Path $env:TEMP ("pi-listen-daemon-stderr-" + [guid]::NewGuid().ToString() + '.log')
|
|
157
|
+
$pythonExe = (Get-Command $PythonCmd).Source
|
|
158
|
+
$daemonArgs = @($PythonArgs + @($daemonPath, 'start', '--socket', $socket, '--backend', $selectedBackend, '--model', $selectedModel))
|
|
159
|
+
$daemon = Start-Process -FilePath $pythonExe -ArgumentList $daemonArgs -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
$ready = $false
|
|
163
|
+
for ($i = 0; $i -lt 120; $i++) {
|
|
164
|
+
Invoke-Python @($daemonPath, 'ping', '--socket', $socket) *> $null
|
|
165
|
+
if ($LASTEXITCODE -eq 0) {
|
|
166
|
+
$ready = $true
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
Start-Sleep -Seconds 1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (-not $ready) {
|
|
173
|
+
$stderrTail = if (Test-Path $stderrLog) { Get-Content $stderrLog -Tail 40 | Out-String } else { '' }
|
|
174
|
+
throw "Daemon failed to start cleanly.`n$stderrTail"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Invoke-Python @($daemonPath, 'status', '--socket', $socket) | Out-Null
|
|
178
|
+
Write-Step 'Daemon smoke test passed'
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
Invoke-Python @($daemonPath, 'stop', '--socket', $socket) *> $null
|
|
182
|
+
if (-not $daemon.HasExited) {
|
|
183
|
+
Wait-Process -Id $daemon.Id -Timeout 20 -ErrorAction SilentlyContinue
|
|
184
|
+
}
|
|
185
|
+
Remove-Item -Force -ErrorAction SilentlyContinue $socket, "$socket.pid", $stdoutLog, $stderrLog
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (-not (Test-Command python) -and -not (Test-Command py)) {
|
|
190
|
+
Write-Step 'Installing Python 3.12'
|
|
191
|
+
Install-WingetPackage 'Python.Python.3.12'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (-not (Test-Command rec)) {
|
|
195
|
+
Write-Step 'Installing SoX'
|
|
196
|
+
Install-WingetPackage 'ChrisBagwell.SoX'
|
|
197
|
+
} else {
|
|
198
|
+
Write-Step 'SoX already available'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
$ProjectDir = (Resolve-Path $ProjectDir).Path
|
|
202
|
+
$script:PythonCmd = if (Test-Command py) { 'py' } else { 'python' }
|
|
203
|
+
$script:PythonArgs = if ($script:PythonCmd -eq 'py') { @('-3') } else { @() }
|
|
204
|
+
|
|
205
|
+
Write-Step 'Ensuring pip tooling is ready'
|
|
206
|
+
Invoke-Python @('-m', 'pip', 'install', '--upgrade', 'pip') | Out-Null
|
|
207
|
+
|
|
208
|
+
if ($Mode -eq 'local') {
|
|
209
|
+
switch ($Backend) {
|
|
210
|
+
'faster-whisper' {
|
|
211
|
+
Write-Step 'Installing faster-whisper'
|
|
212
|
+
Invoke-Python @('-m', 'pip', 'install', 'faster-whisper')
|
|
213
|
+
}
|
|
214
|
+
'moonshine' {
|
|
215
|
+
Write-Step 'Installing moonshine'
|
|
216
|
+
Invoke-Python @('-m', 'pip', 'install', 'useful-moonshine[onnx]')
|
|
217
|
+
}
|
|
218
|
+
'parakeet' {
|
|
219
|
+
Write-Step 'Installing parakeet'
|
|
220
|
+
Invoke-Python @('-m', 'pip', 'install', 'nemo_toolkit[asr]')
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (-not $DeepgramKey) {
|
|
226
|
+
throw 'API mode requires DEEPGRAM_API_KEY. Pass -DeepgramKey or set DEEPGRAM_API_KEY first.'
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$env:DEEPGRAM_API_KEY = $DeepgramKey
|
|
230
|
+
Write-Step 'Loaded DEEPGRAM_API_KEY into the current PowerShell session'
|
|
231
|
+
if ($PersistDeepgramKey) {
|
|
232
|
+
[Environment]::SetEnvironmentVariable('DEEPGRAM_API_KEY', $DeepgramKey, 'User')
|
|
233
|
+
Write-Step 'Persisted DEEPGRAM_API_KEY to the current user environment'
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
Install-PiPackage
|
|
238
|
+
Smoke-Test
|
|
239
|
+
Write-VoiceConfig
|
|
240
|
+
|
|
241
|
+
@"
|
|
242
|
+
|
|
243
|
+
Done.
|
|
244
|
+
|
|
245
|
+
Happy path result:
|
|
246
|
+
- dependencies installed
|
|
247
|
+
- selected backend validated
|
|
248
|
+
- daemon smoke test passed
|
|
249
|
+
- Pi voice settings written automatically
|
|
250
|
+
|
|
251
|
+
Selected configuration:
|
|
252
|
+
mode: $Mode
|
|
253
|
+
backend: $(Get-SelectedBackend)
|
|
254
|
+
model: $(Get-SelectedModel)
|
|
255
|
+
scope: $Scope
|
|
256
|
+
|
|
257
|
+
Next steps:
|
|
258
|
+
1. Grant microphone permission once:
|
|
259
|
+
Settings -> Privacy & security -> Microphone
|
|
260
|
+
Allow microphone access for your terminal app / shell host
|
|
261
|
+
|
|
262
|
+
2. Start Pi:
|
|
263
|
+
$(if ($Scope -eq 'project') { "Set-Location '$ProjectDir'; pi" } else { 'pi' })
|
|
264
|
+
|
|
265
|
+
3. Try hold-to-talk in an empty editor.
|
|
266
|
+
|
|
267
|
+
You should not need to run `/voice setup` on the happy path.
|
|
268
|
+
If you want a quick verification inside Pi, run:
|
|
269
|
+
/voice test
|
|
270
|
+
/voice doctor
|
|
271
|
+
"@ | Write-Host
|