speech-opencode 1.1.1 → 1.1.2
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/dist/index.d.ts +2 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -20
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -8,28 +8,9 @@ export interface VoicePluginOptions {
|
|
|
8
8
|
silenceDuration?: number;
|
|
9
9
|
/** Maximum recording duration in seconds as a safety timeout (default 300 = 5 minutes) */
|
|
10
10
|
maxDuration?: number;
|
|
11
|
+
/** Enable wake word trigger file watching (default true) */
|
|
12
|
+
enableWakeWord?: boolean;
|
|
11
13
|
}
|
|
12
|
-
/**
|
|
13
|
-
* OpenCode Voice Plugin
|
|
14
|
-
*
|
|
15
|
-
* Adds a 'voice' tool that records audio from the microphone and transcribes it
|
|
16
|
-
* using OpenAI's Whisper API.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```ts
|
|
20
|
-
* // In opencode.json
|
|
21
|
-
* {
|
|
22
|
-
* "plugin": ["opencode-voice"]
|
|
23
|
-
* }
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* // With options in .opencode/plugin/voice.ts
|
|
29
|
-
* import { VoicePlugin } from "opencode-voice"
|
|
30
|
-
* export default VoicePlugin({ language: "en", defaultDuration: 10 })
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
14
|
export declare const VoicePlugin: (options?: VoicePluginOptions) => Plugin;
|
|
34
15
|
declare const _default: Plugin;
|
|
35
16
|
export default _default;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;AA2GvD,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+DAA+D;IAC/D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAmID,eAAO,MAAM,WAAW,GACrB,UAAS,kBAAuB,KAAG,MAyDnC,CAAA;;AAGH,wBAA4B"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import OpenAI from "openai";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
|
-
import { unlinkSync, readFileSync } from "fs";
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
import { join } from "path";
|
|
4
|
+
import { unlinkSync, readFileSync, existsSync, watch, mkdirSync } from "fs";
|
|
5
|
+
import { tmpdir, homedir } from "os";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
// Trigger file path for wake word integration
|
|
8
|
+
const TRIGGER_FILE = join(homedir(), ".cache", "opencode", "voice_trigger");
|
|
7
9
|
/**
|
|
8
10
|
* Records audio from the microphone with automatic silence detection.
|
|
9
11
|
* Recording stops after the specified silence duration.
|
|
@@ -99,11 +101,101 @@ async function transcribeAudio(audioFilePath, apiKey, language) {
|
|
|
99
101
|
* export default VoicePlugin({ language: "en", defaultDuration: 10 })
|
|
100
102
|
* ```
|
|
101
103
|
*/
|
|
104
|
+
/**
|
|
105
|
+
* Records and transcribes audio, returning the transcription
|
|
106
|
+
*/
|
|
107
|
+
async function recordAndTranscribe(apiKey, maxDuration, silenceDuration, language) {
|
|
108
|
+
let audioFile = null;
|
|
109
|
+
try {
|
|
110
|
+
audioFile = await recordAudio(maxDuration, silenceDuration);
|
|
111
|
+
const transcription = await transcribeAudio(audioFile, apiKey, language);
|
|
112
|
+
if (!transcription || transcription.trim() === "") {
|
|
113
|
+
return "No speech detected. Please try again and speak clearly into your microphone.";
|
|
114
|
+
}
|
|
115
|
+
return transcription;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (audioFile) {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(audioFile);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Ignore cleanup errors
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Clears the wake word trigger file
|
|
130
|
+
*/
|
|
131
|
+
function clearTriggerFile() {
|
|
132
|
+
try {
|
|
133
|
+
if (existsSync(TRIGGER_FILE)) {
|
|
134
|
+
unlinkSync(TRIGGER_FILE);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Ignore errors
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Sets up wake word trigger file watching
|
|
143
|
+
* When the trigger file is written, it records audio, transcribes it, and appends to the TUI prompt
|
|
144
|
+
*/
|
|
145
|
+
function setupWakeWordWatcher(apiKey, maxDuration, silenceDuration, language, client // OpenCode SDK client
|
|
146
|
+
) {
|
|
147
|
+
// Ensure the directory exists
|
|
148
|
+
const triggerDir = dirname(TRIGGER_FILE);
|
|
149
|
+
mkdirSync(triggerDir, { recursive: true });
|
|
150
|
+
// Clear any existing trigger
|
|
151
|
+
clearTriggerFile();
|
|
152
|
+
console.log("[Voice Plugin] Wake word watcher enabled");
|
|
153
|
+
console.log(`[Voice Plugin] Watching: ${TRIGGER_FILE}`);
|
|
154
|
+
console.log("[Voice Plugin] Run 'python wakeword/listener.py' to enable 'Hey Jarvis' wake word");
|
|
155
|
+
// Watch the directory for the trigger file
|
|
156
|
+
let isRecording = false;
|
|
157
|
+
watch(triggerDir, async (eventType, filename) => {
|
|
158
|
+
if (filename !== "voice_trigger" || isRecording)
|
|
159
|
+
return;
|
|
160
|
+
if (!existsSync(TRIGGER_FILE))
|
|
161
|
+
return;
|
|
162
|
+
isRecording = true;
|
|
163
|
+
console.log("[Voice Plugin] Wake word triggered! Recording...");
|
|
164
|
+
try {
|
|
165
|
+
// Clear the trigger file immediately
|
|
166
|
+
clearTriggerFile();
|
|
167
|
+
// Record and transcribe
|
|
168
|
+
const transcription = await recordAndTranscribe(apiKey, maxDuration, silenceDuration, language);
|
|
169
|
+
if (transcription && !transcription.startsWith("No speech detected")) {
|
|
170
|
+
console.log(`[Voice Plugin] Transcribed: "${transcription}"`);
|
|
171
|
+
// Append transcription to the TUI prompt
|
|
172
|
+
try {
|
|
173
|
+
await client.tui.appendPrompt({ body: { text: transcription } });
|
|
174
|
+
// Auto-submit the prompt
|
|
175
|
+
await client.tui.submitPrompt();
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.error("[Voice Plugin] Failed to send to TUI:", err);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error("[Voice Plugin] Error:", error);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
isRecording = false;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
102
190
|
export const VoicePlugin = (options = {}) => async (ctx) => {
|
|
103
|
-
const { apiKey = process.env.OPENAI_API_KEY, language, silenceDuration = 7, maxDuration = 300, } = options;
|
|
191
|
+
const { apiKey = process.env.OPENAI_API_KEY, language, silenceDuration = 7, maxDuration = 300, enableWakeWord = true, } = options;
|
|
104
192
|
if (!apiKey) {
|
|
105
193
|
console.warn("[Voice Plugin] Warning: OPENAI_API_KEY not set. Voice transcription will fail.");
|
|
106
194
|
}
|
|
195
|
+
// Set up wake word watcher if enabled
|
|
196
|
+
if (enableWakeWord && apiKey && ctx.client) {
|
|
197
|
+
setupWakeWordWatcher(apiKey, maxDuration, silenceDuration, language, ctx.client);
|
|
198
|
+
}
|
|
107
199
|
return {
|
|
108
200
|
tool: {
|
|
109
201
|
voice: tool({
|
|
@@ -115,29 +207,14 @@ export const VoicePlugin = (options = {}) => async (ctx) => {
|
|
|
115
207
|
if (!apiKey) {
|
|
116
208
|
return "Error: OPENAI_API_KEY environment variable is not set. Please set it to use voice transcription.";
|
|
117
209
|
}
|
|
118
|
-
let audioFile = null;
|
|
119
210
|
try {
|
|
120
|
-
|
|
121
|
-
const transcription = await transcribeAudio(audioFile, apiKey, language);
|
|
122
|
-
if (!transcription || transcription.trim() === "") {
|
|
123
|
-
return "No speech detected. Please try again and speak clearly into your microphone.";
|
|
124
|
-
}
|
|
211
|
+
const transcription = await recordAndTranscribe(apiKey, maxDuration, silenceDuration, language);
|
|
125
212
|
return `Transcribed speech: "${transcription}"`;
|
|
126
213
|
}
|
|
127
214
|
catch (error) {
|
|
128
215
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
129
216
|
return `Voice recording/transcription failed: ${errorMessage}`;
|
|
130
217
|
}
|
|
131
|
-
finally {
|
|
132
|
-
if (audioFile) {
|
|
133
|
-
try {
|
|
134
|
-
unlinkSync(audioFile);
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
// Ignore cleanup errors
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
218
|
},
|
|
142
219
|
}),
|
|
143
220
|
},
|