@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/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