agentvibes 5.12.0-alpha.0 → 5.12.0

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.
Files changed (71) hide show
  1. package/.agentvibes/config.json +15 -1
  2. package/.agentvibes/install-manifest.json +342 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  5. package/.claude/commands/agent-vibes-rdp.md +24 -24
  6. package/.claude/config/audio-effects.cfg +5 -5
  7. package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
  8. package/.claude/config/background-music-enabled.txt +1 -1
  9. package/.claude/config/background-music-position.txt +27 -0
  10. package/.claude/config/background-music-volume.txt +1 -0
  11. package/.claude/config/background-music.cfg +1 -0
  12. package/.claude/config/background-music.txt +1 -0
  13. package/.claude/config/language.txt +1 -0
  14. package/.claude/config/personality.txt +1 -0
  15. package/.claude/config/tts-speech-rate.txt +1 -0
  16. package/.claude/config/tts-verbosity.txt +1 -0
  17. package/.claude/docs/TERMUX_SETUP.md +408 -408
  18. package/.claude/github-star-reminder.txt +1 -1
  19. package/.claude/hooks/play-tts-ssh-remote.sh +42 -5
  20. package/.claude/hooks/play-tts.sh +21 -9
  21. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  22. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  23. package/.claude/hooks-windows/tts-watcher.ps1 +55 -0
  24. package/.claude/piper-voices-dir.txt +1 -0
  25. package/.clawdbot/README.md +105 -105
  26. package/.mcp.json +22 -0
  27. package/LICENSE +190 -190
  28. package/README.md +11 -6
  29. package/RELEASE_NOTES.md +38 -0
  30. package/WINDOWS-SETUP.md +208 -208
  31. package/bin/agent-vibes +39 -39
  32. package/bin/mcp-server.js +121 -121
  33. package/bin/test-bmad-pr +78 -78
  34. package/mcp-server/QUICK_START.md +203 -203
  35. package/mcp-server/README.md +345 -345
  36. package/mcp-server/examples/claude_desktop_config.json +11 -11
  37. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  38. package/mcp-server/examples/custom_instructions.md +169 -169
  39. package/mcp-server/pyproject.toml +52 -52
  40. package/mcp-server/requirements.txt +2 -2
  41. package/mcp-server/test_server.py +395 -395
  42. package/package.json +1 -1
  43. package/src/cli/list-personalities.js +110 -110
  44. package/src/cli/list-voices.js +114 -114
  45. package/src/commands/bmad-voices.js +394 -394
  46. package/src/console/app.js +1 -9
  47. package/src/console/brand-colors.js +13 -13
  48. package/src/console/constants/personalities.js +44 -44
  49. package/src/console/footer-config.js +0 -4
  50. package/src/console/navigation.js +0 -1
  51. package/src/console/tabs/agents-tab.js +14 -2
  52. package/src/console/tabs/help-tab.js +314 -314
  53. package/src/console/tabs/music-tab.js +101 -2
  54. package/src/console/tabs/placeholder-tab.js +0 -2
  55. package/src/console/tabs/readme-tab.js +272 -272
  56. package/src/console/tabs/settings-tab.js +13 -2
  57. package/src/console/tabs/setup-tab.js +30 -2
  58. package/src/console/tabs/voices-tab.js +19 -9
  59. package/src/console/widgets/destroy-list.js +25 -25
  60. package/src/console/widgets/notice.js +55 -55
  61. package/src/installer/language-screen.js +31 -31
  62. package/src/installer/music-file-input.js +304 -304
  63. package/src/services/language-service.js +47 -47
  64. package/src/services/navigation-service.js +1 -1
  65. package/src/utils/audio-format-validator.js +277 -277
  66. package/src/utils/dependency-checker.js +469 -469
  67. package/src/utils/file-ownership-verifier.js +358 -358
  68. package/src/utils/music-file-validator.js +285 -285
  69. package/src/utils/preview-list-prompt.js +144 -144
  70. package/src/utils/secure-music-storage.js +412 -412
  71. package/voice-assignments.json +8244 -8244
@@ -1 +1 @@
1
- 20260704
1
+ 20260703
@@ -34,8 +34,32 @@ fi
34
34
  # Default to "default" so remote always has an LLM key to look up
35
35
  LLM_NAME="${LLM_NAME:-default}"
36
36
 
37
- # Validate required input
38
- if [[ -z "$TEXT" ]]; then
37
+ # Music-only preview mode: AGENTVIBES_MUSIC_ONLY=<track.mp3> tells the receiver to
38
+ # play a standalone background-music track (no speech). Used by the Music tab's
39
+ # remote preview so a track can be auditioned on the receiver. In this mode empty
40
+ # TEXT is allowed and the track name is forwarded as the "music" field with
41
+ # kind=music; the receiver resolves the file from its own ~/.claude/audio/tracks/.
42
+ PAYLOAD_KIND="speak"
43
+ MUSIC_ONLY_TRACK=""
44
+ if [[ -n "${AGENTVIBES_MUSIC_STOP:-}" ]]; then
45
+ # Stop whatever music preview is currently playing on the receiver.
46
+ PAYLOAD_KIND="music-stop"
47
+ elif [[ -n "${AGENTVIBES_MUSIC_ONLY:-}" ]]; then
48
+ # Only a bare .mp3 filename is allowed (no path separators, no leading dash) —
49
+ # the receiver resolves it against its own tracks dir, so reject path-like input.
50
+ if [[ "$AGENTVIBES_MUSIC_ONLY" =~ ^[A-Za-z0-9._][A-Za-z0-9._-]*\.mp3$ ]]; then
51
+ PAYLOAD_KIND="music"
52
+ MUSIC_ONLY_TRACK="$AGENTVIBES_MUSIC_ONLY"
53
+ else
54
+ echo "Invalid music track name: $AGENTVIBES_MUSIC_ONLY" >&2
55
+ exit 1
56
+ fi
57
+ fi
58
+
59
+ # Validate required input (music / music-stop modes carry no speech text)
60
+ if [[ "$PAYLOAD_KIND" == "music" || "$PAYLOAD_KIND" == "music-stop" ]]; then
61
+ TEXT="" # no speech in a music preview / stop
62
+ elif [[ -z "$TEXT" ]]; then
39
63
  echo "Usage: $0 <text> [voice] [agent_name]" >&2
40
64
  exit 1
41
65
  fi
@@ -46,6 +70,9 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
46
70
 
47
71
  # Derive project name from directory
48
72
  PROJECT_NAME=$(basename "$PROJECT_ROOT")
73
+ # Absolute path, forwarded so the receiver-side avatar can show/learn the real
74
+ # remote folder (mirrors the local forward-to-avatar.sh's projectPath field).
75
+ PROJECT_PATH="$PROJECT_ROOT"
49
76
 
50
77
  # ---------------------------------------------------------------------------
51
78
  # Get SSH connection details from config
@@ -365,24 +392,34 @@ build_json_payload() {
365
392
  --arg music "$BG_FILE" \
366
393
  --arg volume "$BG_VOLUME" \
367
394
  --arg project "$PROJECT_NAME" \
395
+ --arg projectPath "$PROJECT_PATH" \
368
396
  --arg pretext "$PRETEXT" \
369
397
  --arg speed "$SPEED" \
370
398
  --arg provider "$PROVIDER" \
371
399
  --arg llm "$LLM_NAME" \
372
400
  --arg mute "$MUTE" \
373
401
  --arg language "$LANGUAGE" \
374
- '{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm, mute: $mute, language: $language}'
402
+ --arg kind "$PAYLOAD_KIND" \
403
+ '{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, projectPath: $projectPath, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm, mute: $mute, language: $language, kind: $kind}'
375
404
  else
376
405
  # Manual JSON — escape backslashes, quotes, control chars
377
406
  local escaped_text
378
407
  escaped_text=$(printf '%s' "$TEXT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ' | sed 's/\r//g')
379
408
  local escaped_pretext
380
409
  escaped_pretext=$(printf '%s' "$PRETEXT" | sed 's/\\/\\\\/g; s/"/\\"/g')
381
- printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s","mute":"%s","language":"%s"}' \
382
- "$escaped_text" "$VOICE" "$SOX_EFFECTS" "$BG_FILE" "$BG_VOLUME" "$PROJECT_NAME" "$escaped_pretext" "$SPEED" "$PROVIDER" "$LLM_NAME" "$MUTE" "$LANGUAGE"
410
+ local escaped_project_path
411
+ escaped_project_path=$(printf '%s' "$PROJECT_PATH" | sed 's/\\/\\\\/g; s/"/\\"/g')
412
+ printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","projectPath":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s","mute":"%s","language":"%s","kind":"%s"}' \
413
+ "$escaped_text" "$VOICE" "$SOX_EFFECTS" "$BG_FILE" "$BG_VOLUME" "$PROJECT_NAME" "$escaped_project_path" "$escaped_pretext" "$SPEED" "$PROVIDER" "$LLM_NAME" "$MUTE" "$LANGUAGE" "$PAYLOAD_KIND"
383
414
  fi
384
415
  }
385
416
 
417
+ # Music-only preview: force the track as the "music" field (overriding any
418
+ # per-LLM/agent background-music config) so the receiver plays exactly this track.
419
+ if [[ "$PAYLOAD_KIND" == "music" ]]; then
420
+ BG_FILE="$MUSIC_ONLY_TRACK"
421
+ fi
422
+
386
423
  JSON_PAYLOAD=$(build_json_payload)
387
424
 
388
425
  # SECURITY: Base64-encode entire payload — safe for SSH transport
@@ -329,7 +329,12 @@ case "$ACTIVE_PROVIDER" in
329
329
  # Transport — keep it. The receiver's audio-effects.cfg picks the engine.
330
330
  ;;
331
331
  *)
332
- if [[ -n "$_LLM_ENGINE" ]]; then
332
+ if [[ "$VOICE_OVERRIDE" =~ ^(af|am|bf|bm|jf|jm|kf|km|zf|zm|ff|fm|hf|hm|if|im|pf|pm|ef|em|nf|nm)_[a-zA-Z0-9]+$ ]]; then
333
+ # Kokoro-shaped voice override (e.g. af_heart) must win over the LLM row's
334
+ # engine column, else it is synthesized as a Piper voice, 404s downloading
335
+ # a model that doesn't exist, and stays silent. (Grafted from master WIP.)
336
+ ACTIVE_PROVIDER="kokoro"
337
+ elif [[ -n "$_LLM_ENGINE" ]]; then
333
338
  ACTIVE_PROVIDER="$_LLM_ENGINE"
334
339
  fi
335
340
  ;;
@@ -513,12 +518,17 @@ speak_text() {
513
518
  termux-ssh)
514
519
  bash "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$text" "$voice"
515
520
  ;;
516
- ssh-remote)
521
+ ssh-remote|agentvibes-receiver)
522
+ # Both route through the base64-JSON sender that the modern receiver
523
+ # (templates/agentvibes-receiver.ps1 / .sh, installed as play-remote.*)
524
+ # understands. The old voiceless-connections sender spoke a 3-positional-arg
525
+ # protocol for a legacy ~/.agentvibes/play-remote.sh receiver that no current
526
+ # receiver install produces, so it delivered a non-base64 command the modern
527
+ # receiver rejects ("Payload must be base64-encoded"). play-tts-ssh-remote.sh
528
+ # resolves the host from any transport-config mode=remote entry (Priority 2b),
529
+ # so the agentvibes-receiver section is found without an ssh-remote section.
517
530
  bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$text" "$voice" "" "${profile_file:-}"
518
531
  ;;
519
- agentvibes-receiver)
520
- bash "$SCRIPT_DIR/play-tts-agentvibes-receiver-for-voiceless-connections.sh" "$text" "$voice"
521
- ;;
522
532
  *)
523
533
  echo "❌ Unknown provider: $provider" >&2
524
534
  return 1
@@ -653,12 +663,14 @@ case "$ACTIVE_PROVIDER" in
653
663
  termux-ssh)
654
664
  exec bash "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
655
665
  ;;
656
- ssh-remote)
666
+ ssh-remote|agentvibes-receiver)
667
+ # Both route through the base64-JSON sender the modern receiver understands.
668
+ # The legacy voiceless-connections sender spoke a 3-arg protocol for an old
669
+ # ~/.agentvibes/play-remote.sh receiver that no current install produces, so it
670
+ # delivered a non-base64 command the modern receiver rejects with
671
+ # "Payload must be base64-encoded".
657
672
  exec bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$TEXT" "$VOICE_OVERRIDE" "" "${AGENT_PROFILE_FILE:-}"
658
673
  ;;
659
- agentvibes-receiver)
660
- exec bash "$SCRIPT_DIR/play-tts-agentvibes-receiver-for-voiceless-connections.sh" "$TEXT" "$VOICE_OVERRIDE"
661
- ;;
662
674
  *)
663
675
  echo "❌ Unknown provider: $ACTIVE_PROVIDER" >&2
664
676
  echo " Run: /agent-vibes:provider list" >&2
@@ -0,0 +1,119 @@
1
+ #
2
+ # File: .claude/hooks-windows/audio-cache-utils.ps1
3
+ #
4
+ # AgentVibes Audio Cache Utilities for Windows
5
+ #
6
+
7
+ param(
8
+ [Parameter(Position = 0)]
9
+ [ValidateSet('cleanup', 'stats', 'clear')]
10
+ [string]$Command
11
+ )
12
+
13
+ # Detect project-local audio dir (same logic as TTS scripts)
14
+ $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
15
+ $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
16
+ if (Test-Path $ProjectClaudeDir) {
17
+ $AudioDir = "$ProjectClaudeDir\audio"
18
+ } else {
19
+ $AudioDir = "$env:USERPROFILE\.claude\audio"
20
+ }
21
+
22
+ function Ensure-AudioDir {
23
+ if (-not (Test-Path $AudioDir)) {
24
+ New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null
25
+ }
26
+ }
27
+
28
+ function Get-AudioCacheSize {
29
+ Ensure-AudioDir
30
+
31
+ if (-not (Test-Path $AudioDir)) {
32
+ return 0
33
+ }
34
+
35
+ $files = Get-ChildItem -Path $AudioDir -Filter "*.wav" -ErrorAction SilentlyContinue
36
+ $totalSize = 0
37
+
38
+ foreach ($file in $files) {
39
+ $totalSize += $file.Length
40
+ }
41
+
42
+ return $totalSize
43
+ }
44
+
45
+ function Format-FileSize {
46
+ param([long]$Size)
47
+
48
+ if ($Size -lt 1KB) { return "$Size B" }
49
+ if ($Size -lt 1MB) { return "{0:N2} KB" -f ($Size / 1KB) }
50
+ if ($Size -lt 1GB) { return "{0:N2} MB" -f ($Size / 1MB) }
51
+ return "{0:N2} GB" -f ($Size / 1GB)
52
+ }
53
+
54
+ function Get-CacheStats {
55
+ Ensure-AudioDir
56
+
57
+ $files = Get-ChildItem -Path $AudioDir -Filter "*.wav" -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum
58
+
59
+ $count = if ($files.Count -eq $null) { 0 } else { $files.Count }
60
+ $totalSize = if ($files.Sum -eq $null) { 0 } else { $files.Sum }
61
+
62
+ return @{
63
+ FileCount = $count
64
+ TotalSize = $totalSize
65
+ FormattedSize = Format-FileSize $totalSize
66
+ }
67
+ }
68
+
69
+ function Clear-Cache {
70
+ Ensure-AudioDir
71
+
72
+ $files = Get-ChildItem -Path $AudioDir -Filter "*.wav" -ErrorAction SilentlyContinue
73
+
74
+ if ($files.Count -eq 0) {
75
+ Write-Host "[OK] Cache already empty" -ForegroundColor Green
76
+ return
77
+ }
78
+
79
+ $stats = Get-CacheStats
80
+ Write-Host "[CLEANUP] Clearing $($stats.FileCount) audio files ($($stats.FormattedSize))" -ForegroundColor Yellow
81
+
82
+ foreach ($file in $files) {
83
+ Remove-Item $file.FullName -Force -ErrorAction SilentlyContinue
84
+ }
85
+
86
+ Write-Host "[OK] Cache cleared" -ForegroundColor Green
87
+ }
88
+
89
+ function Show-CacheStats {
90
+ Ensure-AudioDir
91
+
92
+ $stats = Get-CacheStats
93
+
94
+ Write-Host ""
95
+ Write-Host "[STATS] Audio Cache Statistics" -ForegroundColor Cyan
96
+ Write-Host " Location: $AudioDir"
97
+ Write-Host " Files: $($stats.FileCount)"
98
+ Write-Host " Total Size: $($stats.FormattedSize)"
99
+ Write-Host ""
100
+ }
101
+
102
+ # Main command routing
103
+ switch ($Command) {
104
+ 'stats' {
105
+ Show-CacheStats
106
+ }
107
+
108
+ 'cleanup' {
109
+ Clear-Cache
110
+ }
111
+
112
+ 'clear' {
113
+ Clear-Cache
114
+ }
115
+
116
+ default {
117
+ Show-CacheStats
118
+ }
119
+ }
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # File: .claude/hooks-windows/soprano-gradio-synth.py
4
+ #
5
+ # AgentVibes - Finally, your AI Agents can Talk Back!
6
+ # Website: https://agentvibes.org
7
+ # Repository: https://github.com/paulpreibisch/AgentVibes
8
+ #
9
+ # Co-created by Paul Preibisch with Claude AI
10
+ # Copyright (c) 2025 Paul Preibisch
11
+ #
12
+ # Licensed under the Apache License, Version 2.0 (the "License");
13
+ # you may not use this file except in compliance with the License.
14
+ # You may obtain a copy of the License at
15
+ #
16
+ # http://www.apache.org/licenses/LICENSE-2.0
17
+ #
18
+ # Unless required by applicable law or agreed to in writing, software
19
+ # distributed under the License is distributed on an "AS IS" BASIS,
20
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ # See the License for the specific language governing permissions and
22
+ # limitations under the License.
23
+ #
24
+ # ---
25
+ #
26
+ # Soprano Gradio WebUI synthesizer helper.
27
+ # Calls the Soprano WebUI's Gradio API and saves the result as a WAV file.
28
+ # Uses only Python stdlib (json, sys, urllib) — no extra dependencies.
29
+ #
30
+ # Usage: python3 soprano-gradio-synth.py "text to speak" output.wav [port]
31
+ #
32
+ """
33
+ Soprano Gradio WebUI synthesizer helper for AgentVibes.
34
+
35
+ Calls the Soprano WebUI's Gradio API via the Server-Sent Events (SSE) protocol
36
+ and downloads the generated audio as a WAV file.
37
+
38
+ Flow:
39
+ 1. Submit generation request -> get event_id
40
+ 2. Poll SSE stream for audio file URL
41
+ 3. Download WAV file to output path
42
+
43
+ See: https://github.com/ekwek1/soprano
44
+ """
45
+ import json
46
+ import sys
47
+ import urllib.request
48
+ import urllib.error
49
+
50
+
51
+ def synth(text: str, output_path: str, port: int = 7860) -> None:
52
+ base = f"http://127.0.0.1:{port}"
53
+
54
+ # Step 1: Submit generation request
55
+ # Args: text, temperature, top_p, repetition_penalty, chunk_size, streaming
56
+ payload = json.dumps({
57
+ "data": [text, 0.0, 0.95, 1.2, 1, False]
58
+ }).encode()
59
+
60
+ event_id = submit_request(base, payload)
61
+
62
+ # Step 2: Poll SSE stream for audio file URL
63
+ audio_url = poll_for_result(base, event_id)
64
+
65
+ # Step 3: Download the audio file (validate localhost to prevent SSRF)
66
+ download_file(audio_url, output_path, port)
67
+
68
+
69
+ def submit_request(base: str, payload: bytes) -> str:
70
+ """Submit generation request to Gradio API, return event_id."""
71
+ for api_base in ["/gradio_api/call", "/call"]:
72
+ url = f"{base}{api_base}/generate_speech"
73
+ req = urllib.request.Request(
74
+ url,
75
+ data=payload,
76
+ headers={"Content-Type": "application/json"},
77
+ )
78
+ try:
79
+ with urllib.request.urlopen(req, timeout=30) as resp:
80
+ return json.loads(resp.read())["event_id"]
81
+ except urllib.error.URLError:
82
+ continue
83
+
84
+ raise RuntimeError("Could not reach Soprano WebUI API")
85
+
86
+
87
+ def poll_for_result(base: str, event_id: str) -> str:
88
+ """Poll SSE endpoint until audio file URL is returned."""
89
+ for api_base in ["/gradio_api/call", "/call"]:
90
+ url = f"{base}{api_base}/generate_speech/{event_id}"
91
+ req = urllib.request.Request(url)
92
+ try:
93
+ with urllib.request.urlopen(req, timeout=120) as resp:
94
+ for raw_line in resp:
95
+ line = raw_line.decode("utf-8", errors="replace").strip()
96
+ if not line.startswith("data: "):
97
+ continue
98
+ try:
99
+ parsed = json.loads(line[6:])
100
+ except json.JSONDecodeError:
101
+ continue
102
+ # Response format: [{"path": "...", "url": "...", ...}, "status string"]
103
+ if isinstance(parsed, list) and len(parsed) >= 1:
104
+ audio = parsed[0]
105
+ if isinstance(audio, dict) and "url" in audio:
106
+ return audio["url"]
107
+ break
108
+ except urllib.error.URLError:
109
+ continue
110
+
111
+ raise RuntimeError("No audio URL in Soprano response")
112
+
113
+
114
+ def download_file(url: str, output_path: str, port: int = 7860) -> None:
115
+ """Download audio file from Gradio file server.
116
+
117
+ Security: Validates URL points to localhost to prevent SSRF.
118
+ """
119
+ # Validate URL is localhost to prevent SSRF via crafted Gradio responses
120
+ allowed_prefixes = [
121
+ f"http://127.0.0.1:{port}/",
122
+ f"http://localhost:{port}/",
123
+ ]
124
+ if not any(url.startswith(prefix) for prefix in allowed_prefixes):
125
+ raise RuntimeError(f"Refused download from non-localhost URL: {url[:80]}")
126
+
127
+ req = urllib.request.Request(url)
128
+ with urllib.request.urlopen(req, timeout=30) as resp:
129
+ with open(output_path, "wb") as f:
130
+ while True:
131
+ chunk = resp.read(8192)
132
+ if not chunk:
133
+ break
134
+ f.write(chunk)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ if len(sys.argv) < 3:
139
+ print(f"Usage: {sys.argv[0]} \"text\" output.wav [port]", file=sys.stderr)
140
+ sys.exit(1)
141
+
142
+ text = sys.argv[1]
143
+ output = sys.argv[2]
144
+ port = int(sys.argv[3]) if len(sys.argv) > 3 else 7860
145
+ if not (1 <= port <= 65535):
146
+ print(f"Error: Invalid port number: {port}", file=sys.stderr)
147
+ sys.exit(1)
148
+
149
+ try:
150
+ synth(text, output, port)
151
+ except Exception as e:
152
+ print(f"Error: {e}", file=sys.stderr)
153
+ sys.exit(1)
@@ -31,10 +31,65 @@ try {
31
31
  try { Rename-Item $f.FullName $procFile -ErrorAction Stop } catch { continue }
32
32
  try {
33
33
  $req = Get-Content $procFile -Raw | ConvertFrom-Json
34
+
35
+ # Music preview: play/stop a standalone background-music track
36
+ # ASYNCHRONOUSLY (Start-Process) so it never blocks the TTS queue,
37
+ # tracking the ffplay PID so a new request (or an explicit stop)
38
+ # replaces the previous track instead of stacking playback.
39
+ # kind=music → stop any current preview, then play the track
40
+ # kind=music-stop → stop any current preview (no new playback)
41
+ if ($req.kind -eq 'music' -or $req.kind -eq 'music-stop') {
42
+ $pidFile = Join-Path $env:USERPROFILE '.agentvibes\music-preview.pid'
43
+ # Stop the currently-playing preview, if any. Verify the PID is
44
+ # actually ffplay so a recycled PID can't kill an unrelated proc.
45
+ if (Test-Path $pidFile) {
46
+ $oldPid = (Get-Content $pidFile -Raw -ErrorAction SilentlyContinue).Trim()
47
+ if ($oldPid -match '^\d+$') {
48
+ $op = Get-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
49
+ if ($op -and $op.ProcessName -eq 'ffplay') { Stop-Process -Id ([int]$oldPid) -Force -ErrorAction SilentlyContinue }
50
+ }
51
+ Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
52
+ }
53
+ if ($req.kind -eq 'music') {
54
+ $trackName = [string]$req.music
55
+ if ($trackName -match '^[A-Za-z0-9._][A-Za-z0-9._\-]*\.mp3$') {
56
+ $tracksDir = Join-Path $env:USERPROFILE '.claude\audio\tracks'
57
+ $full = [System.IO.Path]::GetFullPath((Join-Path $tracksDir $trackName))
58
+ $baseFull = [System.IO.Path]::GetFullPath($tracksDir)
59
+ if (-not $baseFull.EndsWith([IO.Path]::DirectorySeparatorChar)) { $baseFull += [IO.Path]::DirectorySeparatorChar }
60
+ if ($full.StartsWith($baseFull, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path $full)) {
61
+ $ffplay = Get-Command ffplay -ErrorAction SilentlyContinue
62
+ if ($ffplay) {
63
+ Write-WatcherLog "INFO" "music-preview id=$($req.id) track=$trackName"
64
+ $mp = Start-Process -FilePath $ffplay.Source -ArgumentList @('-autoexit','-nodisp','-loglevel','quiet','-volume','80',$full) -WindowStyle Hidden -PassThru
65
+ if ($mp) { Set-Content -Path $pidFile -Value $mp.Id -NoNewline -ErrorAction SilentlyContinue }
66
+ } else {
67
+ Write-WatcherLog "WARN" "ffplay not found - cannot preview music id=$($req.id)"
68
+ }
69
+ } else {
70
+ Write-WatcherLog "WARN" "music track not found id=$($req.id) track=$trackName"
71
+ }
72
+ } else {
73
+ Write-WatcherLog "WARN" "invalid music track name id=$($req.id)"
74
+ }
75
+ } else {
76
+ Write-WatcherLog "INFO" "music-stop id=$($req.id)"
77
+ }
78
+ Remove-Item $procFile -Force -ErrorAction SilentlyContinue
79
+ continue
80
+ }
81
+
34
82
  # Validate voice before passing to command line
35
83
  $safeVoice = if ($req.voice -and $req.voice -match '^[a-zA-Z0-9_\-\. :]+$') { $req.voice } else { "" }
36
84
  $env:CLAUDE_PROJECT_DIR = $env:USERPROFILE
37
85
  $env:AGENTVIBES_NO_PRETEXT = "1"
86
+ # Carry the sender's project (folder name/path) through to the
87
+ # TalkingHead forward so the avatar badge/tab shows the real remote
88
+ # origin instead of this machine's own profile dir. (Grafted from master WIP.)
89
+ if ($req.project) { $env:AGENTVIBES_PROJECT = $req.project }
90
+ else { [System.Environment]::SetEnvironmentVariable("AGENTVIBES_PROJECT", $null, "Process") }
91
+ if ($req.projectPath) { $env:AGENTVIBES_PROJECT_PATH = $req.projectPath }
92
+ else { [System.Environment]::SetEnvironmentVariable("AGENTVIBES_PROJECT_PATH", $null, "Process") }
38
93
  # Use SetEnvironmentVariable to truly unset (assignment to $null leaves empty string)
39
94
  if ($req.music) { $env:AGENTVIBES_OVERRIDE_MUSIC = $req.music }
40
95
  else { [System.Environment]::SetEnvironmentVariable("AGENTVIBES_OVERRIDE_MUSIC", $null, "Process") }
@@ -0,0 +1 @@
1
+ C:\Users\Paul\.claude\piper-voices