opencode-smart-voice-notify 1.0.3 → 1.0.6

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/README.md CHANGED
@@ -36,7 +36,7 @@ The plugin automatically tries multiple TTS engines in order, falling back if on
36
36
 
37
37
  ### Option 1: From npm (Recommended)
38
38
 
39
- Add to your OpenCode config file (`~/.config/opencode/config.json`):
39
+ Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
40
40
 
41
41
  ```json
42
42
  {
@@ -74,10 +74,10 @@ Add to your OpenCode config file (`~/.config/opencode/config.json`):
74
74
 
75
75
  When you first run OpenCode with this plugin installed, it will **automatically create**:
76
76
 
77
- 1. **`~/.config/opencode/smart-voice-notify.jsonc`** - A default config file with sensible defaults
78
- 2. **`~/.config/opencode/assets/*.mp3`** - Bundled notification sound files
77
+ 1. **`~/.config/opencode/smart-voice-notify.jsonc`** - A comprehensive configuration file with all available options fully documented.
78
+ 2. **`~/.config/opencode/assets/*.mp3`** - Bundled notification sound files.
79
79
 
80
- You can then customize the config file as needed.
80
+ The auto-generated configuration includes all advanced settings, message arrays, and engine options, so you don't have to refer back to the documentation for available settings.
81
81
 
82
82
  ### Manual Configuration
83
83
 
@@ -86,49 +86,123 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
86
86
  ```jsonc
87
87
  {
88
88
  // ============================================================
89
- // NOTIFICATION MODE SETTINGS
89
+ // NOTIFICATION MODE SETTINGS (Smart Notification System)
90
90
  // ============================================================
91
- // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
92
- // 'tts-first' - Speak TTS immediately
93
- // 'both' - Play sound AND speak TTS immediately
94
- // 'sound-only' - Only play sound, no TTS
91
+ // Controls how notifications are delivered:
92
+ // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
93
+ // 'tts-first' - Speak TTS immediately, no sound
94
+ // 'both' - Play sound AND speak TTS immediately
95
+ // 'sound-only' - Only play sound, no TTS at all
95
96
  "notificationMode": "sound-first",
96
97
 
98
+ // ============================================================
99
+ // TTS REMINDER SETTINGS (When user doesn't respond to sound)
100
+ // ============================================================
101
+
102
+ // Enable TTS reminder if user doesn't respond after sound notification
103
+ "enableTTSReminder": true,
104
+
105
+ // Delay (in seconds) before TTS reminder fires
106
+ "ttsReminderDelaySeconds": 30, // Global default
107
+ "idleReminderDelaySeconds": 30, // For task completion notifications
108
+ "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
109
+
110
+ // Follow-up reminders if user STILL doesn't respond after first TTS
111
+ "enableFollowUpReminders": true,
112
+ "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
113
+ "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
114
+
97
115
  // ============================================================
98
116
  // TTS ENGINE SELECTION
99
117
  // ============================================================
100
- // 'elevenlabs' - Best quality (requires API key)
101
- // 'edge' - Free neural voices (requires: pip install edge-tts)
102
- // 'sapi' - Windows built-in (free, offline)
103
- "ttsEngine": "elevenlabs",
118
+ // 'elevenlabs' - Best quality, anime-like voices (requires API key)
119
+ // 'edge' - Good quality neural voices (free, requires: pip install edge-tts)
120
+ // 'sapi' - Windows built-in voices (free, offline)
121
+ "ttsEngine": "edge",
104
122
  "enableTTS": true,
105
123
 
106
124
  // ============================================================
107
- // ELEVENLABS SETTINGS
125
+ // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
108
126
  // ============================================================
109
127
  // Get your API key from: https://elevenlabs.io/app/settings/api-keys
110
- "elevenLabsApiKey": "your-api-key-here",
111
- "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9", // Jessica voice
128
+ // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
129
+ "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
112
130
  "elevenLabsModel": "eleven_turbo_v2_5",
131
+ "elevenLabsStability": 0.5,
132
+ "elevenLabsSimilarity": 0.75,
133
+ "elevenLabsStyle": 0.5,
113
134
 
114
135
  // ============================================================
115
- // TTS REMINDER SETTINGS
136
+ // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
116
137
  // ============================================================
117
- "enableTTSReminder": true,
118
- "idleReminderDelaySeconds": 30,
119
- "permissionReminderDelaySeconds": 20,
120
- "enableFollowUpReminders": true,
121
- "maxFollowUpReminders": 3,
138
+ "edgeVoice": "en-US-AnaNeural",
139
+ "edgePitch": "+50Hz",
140
+ "edgeRate": "+10%",
141
+
142
+ // ============================================================
143
+ // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
144
+ // ============================================================
145
+ "sapiVoice": "Microsoft Zira Desktop",
146
+ "sapiRate": -1,
147
+ "sapiPitch": "medium",
148
+ "sapiVolume": "loud",
149
+
150
+ // ============================================================
151
+ // INITIAL TTS MESSAGES (Used immediately or after sound)
152
+ // ============================================================
153
+ "idleTTSMessages": [
154
+ "All done! Your task has been completed successfully.",
155
+ "Hey there! I finished working on your request.",
156
+ "Task complete! Ready for your review whenever you are.",
157
+ "Good news! Everything is done and ready for you.",
158
+ "Finished! Let me know if you need anything else."
159
+ ],
160
+ "permissionTTSMessages": [
161
+ "Attention please! I need your permission to continue.",
162
+ "Hey! Quick approval needed to proceed with the task.",
163
+ "Heads up! There is a permission request waiting for you.",
164
+ "Excuse me! I need your authorization before I can continue.",
165
+ "Permission required! Please review and approve when ready."
166
+ ],
167
+
168
+ // ============================================================
169
+ // TTS REMINDER MESSAGES (Used after delay if no response)
170
+ // ============================================================
171
+ "idleReminderTTSMessages": [
172
+ "Hey, are you still there? Your task has been waiting for review.",
173
+ "Just a gentle reminder - I finished your request a while ago!",
174
+ "Hello? I completed your task. Please take a look when you can.",
175
+ "Still waiting for you! The work is done and ready for review.",
176
+ "Knock knock! Your completed task is patiently waiting for you."
177
+ ],
178
+ "permissionReminderTTSMessages": [
179
+ "Hey! I still need your permission to continue. Please respond!",
180
+ "Reminder: There is a pending permission request. I cannot proceed without you.",
181
+ "Hello? I am waiting for your approval. This is getting urgent!",
182
+ "Please check your screen! I really need your permission to move forward.",
183
+ "Still waiting for authorization! The task is on hold until you respond."
184
+ ],
122
185
 
123
186
  // ============================================================
124
187
  // SOUND FILES (relative to OpenCode config directory)
125
188
  // ============================================================
126
189
  "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
127
- "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3"
190
+ "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
191
+
192
+ // ============================================================
193
+ // GENERAL SETTINGS
194
+ // ============================================================
195
+ "wakeMonitor": true,
196
+ "forceVolume": true,
197
+ "volumeThreshold": 50,
198
+ "enableToast": true,
199
+ "enableSound": true,
200
+ "idleThresholdSeconds": 60,
201
+ "debugLog": false
128
202
  }
129
203
  ```
130
204
 
131
- See `example.config.jsonc` for the full configuration options.
205
+ See `example.config.jsonc` for more details.
132
206
 
133
207
  ## Requirements
134
208
 
package/index.js CHANGED
@@ -1,7 +1,6 @@
1
- import path from 'path';
2
- import os from 'os';
3
1
  import fs from 'fs';
4
- import { loadConfig } from './util/config.js';
2
+ import os from 'os';
3
+ import path from 'path';
5
4
  import { createTTS, getTTSConfig } from './util/tts.js';
6
5
 
7
6
  /**
@@ -183,6 +182,13 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
183
182
  : config.idleReminderTTSMessages;
184
183
 
185
184
  const reminderMessage = getRandomMessage(reminderMessages);
185
+
186
+ // Check for ElevenLabs API key configuration issues
187
+ // If user hasn't responded (reminder firing) and config is missing, warn about fallback
188
+ if (config.ttsEngine === 'elevenlabs' && (!config.elevenLabsApiKey || config.elevenLabsApiKey.trim() === '')) {
189
+ debugLog('ElevenLabs API key missing during reminder - showing fallback toast');
190
+ await showToast("⚠️ ElevenLabs API Key missing! Falling back to Edge TTS.", "warning", 6000);
191
+ }
186
192
 
187
193
  // Speak the reminder using TTS
188
194
  await tts.wakeMonitor();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-smart-voice-notify",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Smart voice notification plugin for OpenCode with multiple TTS engines (ElevenLabs, Edge TTS, Windows SAPI) and intelligent reminder system",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/util/config.js CHANGED
@@ -1,319 +1,319 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { fileURLToPath } from 'url';
5
-
6
- /**
7
- * Basic JSONC parser that strips single-line and multi-line comments.
8
- * @param {string} jsonc
9
- * @returns {any}
10
- */
11
- const parseJSONC = (jsonc) => {
12
- const stripped = jsonc.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m);
13
- return JSON.parse(stripped);
14
- };
15
-
16
- /**
17
- * Get the directory where this plugin is installed.
18
- * Used to find bundled assets like example.config.jsonc
19
- */
20
- const getPluginDir = () => {
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = path.dirname(__filename);
23
- return path.dirname(__dirname); // Go up from util/ to plugin root
24
- };
25
-
26
- /**
27
- * Generate a comprehensive default configuration file content.
28
- * This provides users with ALL available options fully documented.
29
- */
30
- const generateDefaultConfig = () => {
31
- return `{
32
- // ============================================================
33
- // OpenCode Smart Voice Notify - Configuration
34
- // ============================================================
35
- //
36
- // This file was auto-generated with all available options.
37
- // Customize the settings below to your preference.
38
- //
39
- // Sound files have been automatically copied to:
40
- // ~/.config/opencode/assets/
41
- //
42
- // Documentation: https://github.com/MasuRii/opencode-smart-voice-notify
43
- //
44
- // ============================================================
45
-
46
- // ============================================================
47
- // NOTIFICATION MODE SETTINGS (Smart Notification System)
48
- // ============================================================
49
- // Controls how notifications are delivered:
50
- // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
51
- // 'tts-first' - Speak TTS immediately, no sound
52
- // 'both' - Play sound AND speak TTS immediately
53
- // 'sound-only' - Only play sound, no TTS at all
54
- "notificationMode": "sound-first",
55
-
56
- // ============================================================
57
- // TTS REMINDER SETTINGS (When user doesn't respond to sound)
58
- // ============================================================
59
-
60
- // Enable TTS reminder if user doesn't respond after sound notification
61
- "enableTTSReminder": true,
62
-
63
- // Delay (in seconds) before TTS reminder fires
64
- // Set globally or per-notification type
65
- "ttsReminderDelaySeconds": 30, // Global default
66
- "idleReminderDelaySeconds": 30, // For task completion notifications
67
- "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
68
-
69
- // Follow-up reminders if user STILL doesn't respond after first TTS
70
- "enableFollowUpReminders": true,
71
- "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
72
- "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
73
-
74
- // ============================================================
75
- // TTS ENGINE SELECTION
76
- // ============================================================
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ /**
7
+ * Basic JSONC parser that strips single-line and multi-line comments.
8
+ * @param {string} jsonc
9
+ * @returns {any}
10
+ */
11
+ const parseJSONC = (jsonc) => {
12
+ const stripped = jsonc.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m);
13
+ return JSON.parse(stripped);
14
+ };
15
+
16
+ /**
17
+ * Get the directory where this plugin is installed.
18
+ * Used to find bundled assets like example.config.jsonc
19
+ */
20
+ const getPluginDir = () => {
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+ return path.dirname(__dirname); // Go up from util/ to plugin root
24
+ };
25
+
26
+ /**
27
+ * Generate a comprehensive default configuration file content.
28
+ * This provides users with ALL available options fully documented.
29
+ */
30
+ const generateDefaultConfig = () => {
31
+ return `{
32
+ // ============================================================
33
+ // OpenCode Smart Voice Notify - Configuration
34
+ // ============================================================
35
+ //
36
+ // This file was auto-generated with all available options.
37
+ // Customize the settings below to your preference.
38
+ //
39
+ // Sound files have been automatically copied to:
40
+ // ~/.config/opencode/assets/
41
+ //
42
+ // Documentation: https://github.com/MasuRii/opencode-smart-voice-notify
43
+ //
44
+ // ============================================================
45
+
46
+ // ============================================================
47
+ // NOTIFICATION MODE SETTINGS (Smart Notification System)
48
+ // ============================================================
49
+ // Controls how notifications are delivered:
50
+ // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
51
+ // 'tts-first' - Speak TTS immediately, no sound
52
+ // 'both' - Play sound AND speak TTS immediately
53
+ // 'sound-only' - Only play sound, no TTS at all
54
+ "notificationMode": "sound-first",
55
+
56
+ // ============================================================
57
+ // TTS REMINDER SETTINGS (When user doesn't respond to sound)
58
+ // ============================================================
59
+
60
+ // Enable TTS reminder if user doesn't respond after sound notification
61
+ "enableTTSReminder": true,
62
+
63
+ // Delay (in seconds) before TTS reminder fires
64
+ // Set globally or per-notification type
65
+ "ttsReminderDelaySeconds": 30, // Global default
66
+ "idleReminderDelaySeconds": 30, // For task completion notifications
67
+ "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
68
+
69
+ // Follow-up reminders if user STILL doesn't respond after first TTS
70
+ "enableFollowUpReminders": true,
71
+ "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
72
+ "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
73
+
74
+ // ============================================================
75
+ // TTS ENGINE SELECTION
76
+ // ============================================================
77
77
  // 'elevenlabs' - Best quality, anime-like voices (requires API key, free tier: 10k chars/month)
78
78
  // 'edge' - Good quality neural voices (free, requires: pip install edge-tts)
79
79
  // 'sapi' - Windows built-in voices (free, offline, robotic)
80
- "ttsEngine": "edge",
80
+ "ttsEngine": "elevenlabs",
81
81
 
82
82
  // Enable TTS for notifications (falls back to sound files if TTS fails)
83
- "enableTTS": true,
84
-
85
- // ============================================================
86
- // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
87
- // ============================================================
88
- // Get your API key from: https://elevenlabs.io/app/settings/api-keys
89
- // Free tier: 10,000 characters/month
90
- //
91
- // To use ElevenLabs:
92
- // 1. Uncomment elevenLabsApiKey and add your key
93
- // 2. Change ttsEngine above to "elevenlabs"
94
- //
95
- // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
96
-
97
- // Voice ID - Recommended cute/anime-like voices:
98
- // 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
99
- // 'FGY2WhTYpPnrIDTdsKH5' - Laura (Enthusiast, Quirky)
100
- // 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
101
- // 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
102
- // Browse more at: https://elevenlabs.io/voice-library
103
- "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
104
-
105
- // Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
106
- "elevenLabsModel": "eleven_turbo_v2_5",
107
-
108
- // Voice tuning (0.0 to 1.0)
109
- "elevenLabsStability": 0.5, // Lower = more expressive, Higher = more consistent
110
- "elevenLabsSimilarity": 0.75, // How closely to match the original voice
111
- "elevenLabsStyle": 0.5, // Style exaggeration (higher = more expressive)
112
-
113
- // ============================================================
114
- // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
115
- // ============================================================
116
- // Requires: pip install edge-tts
117
-
83
+ "enableTTS": true,
84
+
85
+ // ============================================================
86
+ // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
87
+ // ============================================================
88
+ // Get your API key from: https://elevenlabs.io/app/settings/api-keys
89
+ // Free tier: 10,000 characters/month
90
+ //
91
+ // To use ElevenLabs:
92
+ // 1. Uncomment elevenLabsApiKey and add your key
93
+ // 2. Change ttsEngine above to "elevenlabs"
94
+ //
95
+ // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
96
+
97
+ // Voice ID - Recommended cute/anime-like voices:
98
+ // 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
99
+ // 'FGY2WhTYpPnrIDTdsKH5' - Laura (Enthusiast, Quirky)
100
+ // 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
101
+ // 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
102
+ // Browse more at: https://elevenlabs.io/voice-library
103
+ "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
104
+
105
+ // Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
106
+ "elevenLabsModel": "eleven_turbo_v2_5",
107
+
108
+ // Voice tuning (0.0 to 1.0)
109
+ "elevenLabsStability": 0.5, // Lower = more expressive, Higher = more consistent
110
+ "elevenLabsSimilarity": 0.75, // How closely to match the original voice
111
+ "elevenLabsStyle": 0.5, // Style exaggeration (higher = more expressive)
112
+
113
+ // ============================================================
114
+ // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
115
+ // ============================================================
116
+ // Requires: pip install edge-tts
117
+
118
118
  // Voice options (run 'edge-tts --list-voices' to see all):
119
119
  // 'en-US-AnaNeural' - Young, cute, cartoon-like (RECOMMENDED)
120
120
  // 'en-US-JennyNeural' - Friendly, warm
121
121
  // 'en-US-AriaNeural' - Confident, clear
122
122
  // 'en-GB-SoniaNeural' - British, friendly
123
123
  // 'en-AU-NatashaNeural' - Australian, warm
124
- "edgeVoice": "en-US-AnaNeural",
124
+ "edgeVoice": "en-US-JennyNeural",
125
125
 
126
126
  // Pitch adjustment: +0Hz to +100Hz (higher = more anime-like)
127
- "edgePitch": "+50Hz",
128
-
129
- // Speech rate: -50% to +100%
130
- "edgeRate": "+10%",
131
-
132
- // ============================================================
133
- // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
134
- // ============================================================
135
-
136
- // Voice (run PowerShell to list all installed voices):
137
- // Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name }
138
- //
139
- // Common Windows voices:
140
- // 'Microsoft Zira Desktop' - Female, US English
141
- // 'Microsoft David Desktop' - Male, US English
142
- // 'Microsoft Hazel Desktop' - Female, UK English
143
- "sapiVoice": "Microsoft Zira Desktop",
144
-
145
- // Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
146
- "sapiRate": -1,
147
-
148
- // Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
149
- "sapiPitch": "medium",
150
-
151
- // Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
152
- "sapiVolume": "loud",
153
-
154
- // ============================================================
155
- // INITIAL TTS MESSAGES (Used immediately or after sound)
156
- // These are randomly selected each time for variety
157
- // ============================================================
158
-
159
- // Messages when agent finishes work (task completion)
160
- "idleTTSMessages": [
161
- "All done! Your task has been completed successfully.",
162
- "Hey there! I finished working on your request.",
163
- "Task complete! Ready for your review whenever you are.",
164
- "Good news! Everything is done and ready for you.",
165
- "Finished! Let me know if you need anything else."
166
- ],
167
-
168
- // Messages for permission requests
169
- "permissionTTSMessages": [
170
- "Attention please! I need your permission to continue.",
171
- "Hey! Quick approval needed to proceed with the task.",
172
- "Heads up! There is a permission request waiting for you.",
173
- "Excuse me! I need your authorization before I can continue.",
174
- "Permission required! Please review and approve when ready."
175
- ],
176
-
177
- // ============================================================
178
- // TTS REMINDER MESSAGES (More urgent - used after delay if no response)
179
- // These are more personalized and urgent to get user attention
180
- // ============================================================
181
-
182
- // Reminder messages when agent finished but user hasn't responded
183
- "idleReminderTTSMessages": [
184
- "Hey, are you still there? Your task has been waiting for review.",
185
- "Just a gentle reminder - I finished your request a while ago!",
186
- "Hello? I completed your task. Please take a look when you can.",
187
- "Still waiting for you! The work is done and ready for review.",
188
- "Knock knock! Your completed task is patiently waiting for you."
189
- ],
190
-
191
- // Reminder messages when permission still needed
192
- "permissionReminderTTSMessages": [
193
- "Hey! I still need your permission to continue. Please respond!",
194
- "Reminder: There is a pending permission request. I cannot proceed without you.",
195
- "Hello? I am waiting for your approval. This is getting urgent!",
196
- "Please check your screen! I really need your permission to move forward.",
197
- "Still waiting for authorization! The task is on hold until you respond."
198
- ],
199
-
200
- // ============================================================
201
- // SOUND FILES (For immediate notifications)
202
- // These are played first before TTS reminder kicks in
203
- // ============================================================
204
- // Paths are relative to ~/.config/opencode/ directory
205
- // Sound files are automatically copied here on first run
206
- // You can replace with your own custom MP3/WAV files
207
-
208
- "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
209
- "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
210
-
211
- // ============================================================
212
- // GENERAL SETTINGS
213
- // ============================================================
214
-
215
- // Wake monitor from sleep when notifying (Windows/macOS)
216
- "wakeMonitor": true,
217
-
218
- // Force system volume up if below threshold
219
- "forceVolume": true,
220
-
221
- // Volume threshold (0-100): force volume if current level is below this
222
- "volumeThreshold": 50,
223
-
224
- // Show TUI toast notifications in OpenCode terminal
225
- "enableToast": true,
226
-
227
- // Enable audio notifications (sound files and TTS)
228
- "enableSound": true,
229
-
230
- // Consider monitor asleep after this many seconds of inactivity (Windows only)
231
- "idleThresholdSeconds": 60,
232
-
233
- // Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
234
- // Useful for troubleshooting notification issues
235
- "debugLog": false
236
- }
237
- `;
238
- };
239
-
240
- /**
241
- * Copy bundled assets (sound files) to the OpenCode config directory.
242
- * @param {string} configDir - The OpenCode config directory path
243
- */
244
- const copyBundledAssets = (configDir) => {
245
- try {
246
- const pluginDir = getPluginDir();
247
- const sourceAssetsDir = path.join(pluginDir, 'assets');
248
- const targetAssetsDir = path.join(configDir, 'assets');
249
-
250
- // Check if source assets exist (they should be bundled with the plugin)
251
- if (!fs.existsSync(sourceAssetsDir)) {
252
- return; // No bundled assets to copy
253
- }
254
-
255
- // Create target assets directory if it doesn't exist
256
- if (!fs.existsSync(targetAssetsDir)) {
257
- fs.mkdirSync(targetAssetsDir, { recursive: true });
258
- }
259
-
260
- // Copy each asset file if it doesn't already exist in target
261
- const assetFiles = fs.readdirSync(sourceAssetsDir);
262
- for (const file of assetFiles) {
263
- const sourcePath = path.join(sourceAssetsDir, file);
264
- const targetPath = path.join(targetAssetsDir, file);
265
-
266
- // Only copy if target doesn't exist (don't overwrite user customizations)
267
- if (!fs.existsSync(targetPath) && fs.statSync(sourcePath).isFile()) {
268
- fs.copyFileSync(sourcePath, targetPath);
269
- }
270
- }
271
- } catch (error) {
272
- // Silently fail - assets are optional
273
- }
274
- };
275
-
276
- /**
277
- * Loads a configuration file from the OpenCode config directory.
278
- * If the file doesn't exist, creates a default config file.
279
- * @param {string} name - Name of the config file (without .jsonc extension)
280
- * @param {object} defaults - Default values if file doesn't exist or is invalid
281
- * @returns {object}
282
- */
283
- export const loadConfig = (name, defaults = {}) => {
284
- const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
285
- const filePath = path.join(configDir, `${name}.jsonc`);
286
-
287
- if (!fs.existsSync(filePath)) {
288
- // Auto-create the default config file
289
- try {
290
- // Ensure config directory exists
291
- if (!fs.existsSync(configDir)) {
292
- fs.mkdirSync(configDir, { recursive: true });
293
- }
294
-
295
- // Write the default config
296
- const defaultConfig = generateDefaultConfig();
297
- fs.writeFileSync(filePath, defaultConfig, 'utf-8');
298
-
299
- // Also copy bundled assets (sound files) to the config directory
300
- copyBundledAssets(configDir);
301
-
302
- // Parse and return the newly created config merged with defaults
303
- const config = parseJSONC(defaultConfig);
304
- return { ...defaults, ...config };
305
- } catch (error) {
306
- // If we can't create the file, just return defaults
307
- return defaults;
308
- }
309
- }
310
-
311
- try {
312
- const content = fs.readFileSync(filePath, 'utf-8');
313
- const config = parseJSONC(content);
314
- return { ...defaults, ...config };
315
- } catch (error) {
316
- // Silently return defaults - don't use console.error as it breaks TUI
317
- return defaults;
318
- }
319
- };
127
+ "edgePitch": "+0Hz",
128
+
129
+ // Speech rate: -50% to +100%
130
+ "edgeRate": "+10%",
131
+
132
+ // ============================================================
133
+ // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
134
+ // ============================================================
135
+
136
+ // Voice (run PowerShell to list all installed voices):
137
+ // Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name }
138
+ //
139
+ // Common Windows voices:
140
+ // 'Microsoft Zira Desktop' - Female, US English
141
+ // 'Microsoft David Desktop' - Male, US English
142
+ // 'Microsoft Hazel Desktop' - Female, UK English
143
+ "sapiVoice": "Microsoft Zira Desktop",
144
+
145
+ // Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
146
+ "sapiRate": -1,
147
+
148
+ // Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
149
+ "sapiPitch": "medium",
150
+
151
+ // Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
152
+ "sapiVolume": "loud",
153
+
154
+ // ============================================================
155
+ // INITIAL TTS MESSAGES (Used immediately or after sound)
156
+ // These are randomly selected each time for variety
157
+ // ============================================================
158
+
159
+ // Messages when agent finishes work (task completion)
160
+ "idleTTSMessages": [
161
+ "All done! Your task has been completed successfully.",
162
+ "Hey there! I finished working on your request.",
163
+ "Task complete! Ready for your review whenever you are.",
164
+ "Good news! Everything is done and ready for you.",
165
+ "Finished! Let me know if you need anything else."
166
+ ],
167
+
168
+ // Messages for permission requests
169
+ "permissionTTSMessages": [
170
+ "Attention please! I need your permission to continue.",
171
+ "Hey! Quick approval needed to proceed with the task.",
172
+ "Heads up! There is a permission request waiting for you.",
173
+ "Excuse me! I need your authorization before I can continue.",
174
+ "Permission required! Please review and approve when ready."
175
+ ],
176
+
177
+ // ============================================================
178
+ // TTS REMINDER MESSAGES (More urgent - used after delay if no response)
179
+ // These are more personalized and urgent to get user attention
180
+ // ============================================================
181
+
182
+ // Reminder messages when agent finished but user hasn't responded
183
+ "idleReminderTTSMessages": [
184
+ "Hey, are you still there? Your task has been waiting for review.",
185
+ "Just a gentle reminder - I finished your request a while ago!",
186
+ "Hello? I completed your task. Please take a look when you can.",
187
+ "Still waiting for you! The work is done and ready for review.",
188
+ "Knock knock! Your completed task is patiently waiting for you."
189
+ ],
190
+
191
+ // Reminder messages when permission still needed
192
+ "permissionReminderTTSMessages": [
193
+ "Hey! I still need your permission to continue. Please respond!",
194
+ "Reminder: There is a pending permission request. I cannot proceed without you.",
195
+ "Hello? I am waiting for your approval. This is getting urgent!",
196
+ "Please check your screen! I really need your permission to move forward.",
197
+ "Still waiting for authorization! The task is on hold until you respond."
198
+ ],
199
+
200
+ // ============================================================
201
+ // SOUND FILES (For immediate notifications)
202
+ // These are played first before TTS reminder kicks in
203
+ // ============================================================
204
+ // Paths are relative to ~/.config/opencode/ directory
205
+ // Sound files are automatically copied here on first run
206
+ // You can replace with your own custom MP3/WAV files
207
+
208
+ "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
209
+ "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
210
+
211
+ // ============================================================
212
+ // GENERAL SETTINGS
213
+ // ============================================================
214
+
215
+ // Wake monitor from sleep when notifying (Windows/macOS)
216
+ "wakeMonitor": true,
217
+
218
+ // Force system volume up if below threshold
219
+ "forceVolume": true,
220
+
221
+ // Volume threshold (0-100): force volume if current level is below this
222
+ "volumeThreshold": 50,
223
+
224
+ // Show TUI toast notifications in OpenCode terminal
225
+ "enableToast": true,
226
+
227
+ // Enable audio notifications (sound files and TTS)
228
+ "enableSound": true,
229
+
230
+ // Consider monitor asleep after this many seconds of inactivity (Windows only)
231
+ "idleThresholdSeconds": 60,
232
+
233
+ // Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
234
+ // Useful for troubleshooting notification issues
235
+ "debugLog": false
236
+ }
237
+ `;
238
+ };
239
+
240
+ /**
241
+ * Copy bundled assets (sound files) to the OpenCode config directory.
242
+ * @param {string} configDir - The OpenCode config directory path
243
+ */
244
+ const copyBundledAssets = (configDir) => {
245
+ try {
246
+ const pluginDir = getPluginDir();
247
+ const sourceAssetsDir = path.join(pluginDir, 'assets');
248
+ const targetAssetsDir = path.join(configDir, 'assets');
249
+
250
+ // Check if source assets exist (they should be bundled with the plugin)
251
+ if (!fs.existsSync(sourceAssetsDir)) {
252
+ return; // No bundled assets to copy
253
+ }
254
+
255
+ // Create target assets directory if it doesn't exist
256
+ if (!fs.existsSync(targetAssetsDir)) {
257
+ fs.mkdirSync(targetAssetsDir, { recursive: true });
258
+ }
259
+
260
+ // Copy each asset file if it doesn't already exist in target
261
+ const assetFiles = fs.readdirSync(sourceAssetsDir);
262
+ for (const file of assetFiles) {
263
+ const sourcePath = path.join(sourceAssetsDir, file);
264
+ const targetPath = path.join(targetAssetsDir, file);
265
+
266
+ // Only copy if target doesn't exist (don't overwrite user customizations)
267
+ if (!fs.existsSync(targetPath) && fs.statSync(sourcePath).isFile()) {
268
+ fs.copyFileSync(sourcePath, targetPath);
269
+ }
270
+ }
271
+ } catch (error) {
272
+ // Silently fail - assets are optional
273
+ }
274
+ };
275
+
276
+ /**
277
+ * Loads a configuration file from the OpenCode config directory.
278
+ * If the file doesn't exist, creates a default config file.
279
+ * @param {string} name - Name of the config file (without .jsonc extension)
280
+ * @param {object} defaults - Default values if file doesn't exist or is invalid
281
+ * @returns {object}
282
+ */
283
+ export const loadConfig = (name, defaults = {}) => {
284
+ const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
285
+ const filePath = path.join(configDir, `${name}.jsonc`);
286
+
287
+ if (!fs.existsSync(filePath)) {
288
+ // Auto-create the default config file
289
+ try {
290
+ // Ensure config directory exists
291
+ if (!fs.existsSync(configDir)) {
292
+ fs.mkdirSync(configDir, { recursive: true });
293
+ }
294
+
295
+ // Write the default config
296
+ const defaultConfig = generateDefaultConfig();
297
+ fs.writeFileSync(filePath, defaultConfig, 'utf-8');
298
+
299
+ // Also copy bundled assets (sound files) to the config directory
300
+ copyBundledAssets(configDir);
301
+
302
+ // Parse and return the newly created config merged with defaults
303
+ const config = parseJSONC(defaultConfig);
304
+ return { ...defaults, ...config };
305
+ } catch (error) {
306
+ // If we can't create the file, just return defaults
307
+ return defaults;
308
+ }
309
+ }
310
+
311
+ try {
312
+ const content = fs.readFileSync(filePath, 'utf-8');
313
+ const config = parseJSONC(content);
314
+ return { ...defaults, ...config };
315
+ } catch (error) {
316
+ // Silently return defaults - don't use console.error as it breaks TUI
317
+ return defaults;
318
+ }
319
+ };
package/util/tts.js CHANGED
@@ -20,8 +20,8 @@ export const getTTSConfig = () => {
20
20
  elevenLabsStability: 0.5,
21
21
  elevenLabsSimilarity: 0.75,
22
22
  elevenLabsStyle: 0.5,
23
- edgeVoice: 'en-US-AnaNeural',
24
- edgePitch: '+50Hz',
23
+ edgeVoice: 'en-US-JennyNeural',
24
+ edgePitch: '+0Hz',
25
25
  edgeRate: '+10%',
26
26
  sapiVoice: 'Microsoft Zira Desktop',
27
27
  sapiRate: -1,
@@ -245,7 +245,7 @@ export const createTTS = ({ $, client }) => {
245
245
 
246
246
  const ssml = `<?xml version="1.0" encoding="UTF-8"?>
247
247
  <speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
248
- <voice name="${voice.replace(/'/g, "''")}">
248
+ <voice name="${voice.replace(/"/g, '&quot;')}">
249
249
  <prosody rate="${ratePercent}" pitch="${pitch}" volume="${volume}">
250
250
  ${escapedText}
251
251
  </prosody>
@@ -255,14 +255,27 @@ export const createTTS = ({ $, client }) => {
255
255
  const scriptContent = `
256
256
  Add-Type -AssemblyName System.Speech
257
257
  $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
258
- $synth.Rate = ${rate}
259
- try { $synth.SelectVoice('${voice.replace(/'/g, "''")}') } catch {}
260
- $ssml = @'\\n${ssml}\\n'@
261
- try { $synth.SpeakSsml($ssml) } catch { $synth.Speak('${text.replace(/'/g, "''")}') }
262
- $synth.Dispose()
258
+ try {
259
+ $synth.Rate = ${rate}
260
+ try { $synth.SelectVoice("${voice.replace(/"/g, '""')}") } catch { }
261
+ $ssml = @"
262
+ ${ssml}
263
+ "@
264
+ $synth.SpeakSsml($ssml)
265
+ } catch {
266
+ [Console]::Error.WriteLine($_.Exception.Message)
267
+ exit 1
268
+ } finally {
269
+ if ($synth) { $synth.Dispose() }
270
+ }
263
271
  `;
264
272
  fs.writeFileSync(scriptPath, scriptContent, 'utf-8');
265
- await $`powershell.exe -NoProfile -ExecutionPolicy Bypass -File ${scriptPath}`.quiet();
273
+ const result = await $`powershell.exe -NoProfile -ExecutionPolicy Bypass -File ${scriptPath}`.nothrow().quiet();
274
+
275
+ if (result.exitCode !== 0) {
276
+ debugLog(`speakWithSAPI failed with code ${result.exitCode}: ${result.stderr}`);
277
+ return false;
278
+ }
266
279
  return true;
267
280
  } catch (e) {
268
281
  debugLog(`speakWithSAPI error: ${e.message}`);