opencode-smart-voice-notify 1.0.5 → 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/index.js +9 -3
- package/package.json +1 -1
- package/util/config.js +306 -306
- package/util/tts.js +22 -9
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
|
|
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
|
+
"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": "
|
|
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-
|
|
124
|
+
"edgeVoice": "en-US-JennyNeural",
|
|
125
125
|
|
|
126
126
|
// Pitch adjustment: +0Hz to +100Hz (higher = more anime-like)
|
|
127
|
-
"edgePitch": "+
|
|
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-
|
|
24
|
-
edgePitch: '+
|
|
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(/
|
|
248
|
+
<voice name="${voice.replace(/"/g, '"')}">
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
$
|
|
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}`);
|