@hxnnxs/opencode-voice 0.1.4 → 0.1.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/CHANGELOG.md +12 -0
- package/bin/opencode-voice.js +7 -1
- package/index.js +4 -1
- package/lib/download.js +1 -1
- package/lib/engine.js +70 -28
- package/lib/engines.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.1.6 - 2026-06-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Windows recorder startup to use resolved recorder command paths (including bundled ffmpeg) directly when spawning, preventing startup failures when ffmpeg is present only by absolute path.
|
|
10
|
+
|
|
11
|
+
## 0.1.5 - 2026-06-17
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Recovered missing `ffmpeg-static` at runtime on Windows by installing it locally when not present, preventing `No recorder found` failures after fresh plugin installs.
|
|
16
|
+
|
|
5
17
|
## 0.1.4 - 2026-06-16
|
|
6
18
|
|
|
7
19
|
### Added
|
package/bin/opencode-voice.js
CHANGED
|
@@ -162,7 +162,13 @@ function printEngineRetry({ error, nextAttempt, attempts }) {
|
|
|
162
162
|
|
|
163
163
|
async function installCommand() {
|
|
164
164
|
const pluginArgs = args.filter((arg) => arg !== "--no-engine");
|
|
165
|
-
const
|
|
165
|
+
const spawnOptions = { stdio: "inherit" };
|
|
166
|
+
if (process.platform === "win32") spawnOptions.shell = true;
|
|
167
|
+
const result = spawnSync("opencode", ["plugin", packageName(), ...pluginArgs], spawnOptions);
|
|
168
|
+
if (result.error) {
|
|
169
|
+
console.error(`Failed to run opencode: ${result.error.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
166
172
|
if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1);
|
|
167
173
|
|
|
168
174
|
if (!hasFlag("--no-engine")) {
|
package/index.js
CHANGED
|
@@ -362,10 +362,13 @@ function showDiagnostics(ctx) {
|
|
|
362
362
|
const model = getModel(settings.model);
|
|
363
363
|
const commandOptions = { ...ctx.options, downloadDir: settings.downloadDir };
|
|
364
364
|
const whisperCli = resolveCommand("whisper-cli", commandOptions);
|
|
365
|
+
const ffmpeg = resolveCommand("ffmpeg", commandOptions);
|
|
366
|
+
const arecord = resolveCommand("arecord", commandOptions);
|
|
367
|
+
const sox = resolveCommand("sox", commandOptions);
|
|
365
368
|
const engine = getEngineStatus("whisper.cpp", ctx.options, settings);
|
|
366
369
|
const lines = [
|
|
367
370
|
`Platform: ${process.platform}-${process.arch}`,
|
|
368
|
-
`Recorder: ffmpeg=${
|
|
371
|
+
`Recorder: ffmpeg=${ffmpeg ? "yes" : "no"}${ffmpeg ? ` (${ffmpeg})` : ""}, arecord=${arecord ? "yes" : "no"}, sox=${sox ? "yes" : "no"}`,
|
|
369
372
|
`Engine: ${engine.id}`,
|
|
370
373
|
`Engine source: ${engine.source}`,
|
|
371
374
|
`whisper-cli: ${whisperCli || "missing"}`,
|
package/lib/download.js
CHANGED
|
@@ -70,7 +70,7 @@ function contentRangeStart(value) {
|
|
|
70
70
|
return match ? Number(match[1]) : null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async function replaceFile(source, destination) {
|
|
73
|
+
export async function replaceFile(source, destination) {
|
|
74
74
|
await fs.promises.unlink(destination).catch(() => {});
|
|
75
75
|
|
|
76
76
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
package/lib/engine.js
CHANGED
|
@@ -3,10 +3,15 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { ensureDir } from "./download.js";
|
|
7
8
|
import { getAudioDir, getEnginesDir, getModelPath } from "./models.js";
|
|
8
9
|
|
|
9
10
|
const require = createRequire(import.meta.url);
|
|
11
|
+
const FFMPEG_STATIC_PACKAGE = "ffmpeg-static@^5.2.0";
|
|
12
|
+
const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
let ffmpegStaticPathCache;
|
|
14
|
+
let ffmpegStaticInstallAttempted = false;
|
|
10
15
|
|
|
11
16
|
const RECORDING_MIN_BYTES = 44;
|
|
12
17
|
|
|
@@ -36,12 +41,40 @@ function executableNames(command) {
|
|
|
36
41
|
|
|
37
42
|
function bundledFfmpegPath() {
|
|
38
43
|
if (process.platform !== "win32") return "";
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
if (ffmpegStaticPathCache !== undefined) return ffmpegStaticPathCache;
|
|
45
|
+
|
|
46
|
+
const resolveFromDependency = () => {
|
|
47
|
+
try {
|
|
48
|
+
const candidate = require("ffmpeg-static");
|
|
49
|
+
return typeof candidate === "string" ? path.resolve(candidate) : "";
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const installDependency = () => {
|
|
56
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
57
|
+
try {
|
|
58
|
+
const result = spawnSync(npm, ["install", "--no-save", "--no-audit", "--no-fund", FFMPEG_STATIC_PACKAGE], {
|
|
59
|
+
cwd: PLUGIN_ROOT,
|
|
60
|
+
stdio: "ignore",
|
|
61
|
+
});
|
|
62
|
+
return result.status === 0;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let candidate = resolveFromDependency();
|
|
69
|
+
if (!candidate && !ffmpegStaticInstallAttempted) {
|
|
70
|
+
ffmpegStaticInstallAttempted = true;
|
|
71
|
+
if (installDependency()) {
|
|
72
|
+
candidate = resolveFromDependency();
|
|
73
|
+
}
|
|
44
74
|
}
|
|
75
|
+
|
|
76
|
+
ffmpegStaticPathCache = candidate || "";
|
|
77
|
+
return ffmpegStaticPathCache;
|
|
45
78
|
}
|
|
46
79
|
|
|
47
80
|
function platformKey(options = {}) {
|
|
@@ -149,8 +182,11 @@ function parseWindowsMicrophones(stderr) {
|
|
|
149
182
|
}
|
|
150
183
|
|
|
151
184
|
export function listMicrophones() {
|
|
152
|
-
|
|
153
|
-
|
|
185
|
+
const arecordCommand = resolveCommand("arecord");
|
|
186
|
+
const ffmpegCommand = resolveCommand("ffmpeg");
|
|
187
|
+
|
|
188
|
+
if (process.platform === "linux" && arecordCommand) {
|
|
189
|
+
const result = spawnSync(arecordCommand, ["-L"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
154
190
|
const devices = result.stdout
|
|
155
191
|
.split(/\r?\n/)
|
|
156
192
|
.map((line) => line.trim())
|
|
@@ -158,8 +194,8 @@ export function listMicrophones() {
|
|
|
158
194
|
return ["default", ...devices.filter((item) => item !== "default")];
|
|
159
195
|
}
|
|
160
196
|
|
|
161
|
-
if (process.platform === "darwin" &&
|
|
162
|
-
const result = spawnSync(
|
|
197
|
+
if (process.platform === "darwin" && ffmpegCommand) {
|
|
198
|
+
const result = spawnSync(ffmpegCommand, ["-hide_banner", "-f", "avfoundation", "-list_devices", "true", "-i", ""], {
|
|
163
199
|
encoding: "utf8",
|
|
164
200
|
stdio: ["ignore", "ignore", "pipe"],
|
|
165
201
|
});
|
|
@@ -170,8 +206,8 @@ export function listMicrophones() {
|
|
|
170
206
|
.map((id) => `:${id}`);
|
|
171
207
|
}
|
|
172
208
|
|
|
173
|
-
if (process.platform === "win32" &&
|
|
174
|
-
const result = spawnSync(
|
|
209
|
+
if (process.platform === "win32" && ffmpegCommand) {
|
|
210
|
+
const result = spawnSync(ffmpegCommand, ["-hide_banner", "-f", "dshow", "-list_devices", "true", "-i", "dummy"], {
|
|
175
211
|
encoding: "utf8",
|
|
176
212
|
stdio: ["ignore", "ignore", "pipe"],
|
|
177
213
|
});
|
|
@@ -185,54 +221,60 @@ export function listMicrophones() {
|
|
|
185
221
|
function buildRecorders(file, settings = {}) {
|
|
186
222
|
const mic = settings.mic || "";
|
|
187
223
|
const recorders = [];
|
|
224
|
+
const arecordCommand = resolveCommand("arecord", settings);
|
|
225
|
+
const ffmpegCommand = resolveCommand("ffmpeg", settings);
|
|
226
|
+
const soxCommand = resolveCommand("sox", settings);
|
|
188
227
|
|
|
189
|
-
if (process.platform === "linux" &&
|
|
228
|
+
if (process.platform === "linux" && arecordCommand) {
|
|
190
229
|
recorders.push({
|
|
191
230
|
label: mic ? `arecord (${mic})` : "arecord (default)",
|
|
192
|
-
command:
|
|
231
|
+
command: arecordCommand,
|
|
193
232
|
args: ["-q", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "wav", ...(mic ? ["-D", mic] : []), file],
|
|
194
233
|
});
|
|
195
234
|
}
|
|
196
235
|
|
|
197
|
-
if (process.platform === "linux" &&
|
|
236
|
+
if (process.platform === "linux" && ffmpegCommand) {
|
|
198
237
|
if (!mic) {
|
|
199
238
|
recorders.push({
|
|
200
239
|
label: "ffmpeg pulse (default)",
|
|
201
|
-
command:
|
|
240
|
+
command: ffmpegCommand,
|
|
202
241
|
args: ["-hide_banner", "-loglevel", "error", "-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", file],
|
|
203
242
|
});
|
|
204
243
|
}
|
|
205
244
|
|
|
206
245
|
recorders.push({
|
|
207
246
|
label: `ffmpeg alsa (${mic || "default"})`,
|
|
208
|
-
command:
|
|
247
|
+
command: ffmpegCommand,
|
|
209
248
|
args: ["-hide_banner", "-loglevel", "error", "-y", "-f", "alsa", "-i", mic || "default", "-ac", "1", "-ar", "16000", file],
|
|
210
249
|
});
|
|
211
250
|
}
|
|
212
251
|
|
|
213
|
-
if (process.platform === "darwin" &&
|
|
252
|
+
if (process.platform === "darwin" && ffmpegCommand) {
|
|
214
253
|
recorders.push({
|
|
215
254
|
label: `ffmpeg avfoundation (${mic || ":0"})`,
|
|
216
|
-
command:
|
|
255
|
+
command: ffmpegCommand,
|
|
217
256
|
args: ["-hide_banner", "-loglevel", "error", "-y", "-f", "avfoundation", "-i", mic || ":0", "-ac", "1", "-ar", "16000", file],
|
|
218
257
|
});
|
|
219
258
|
}
|
|
220
259
|
|
|
221
|
-
if (process.platform === "win32" &&
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
260
|
+
if (process.platform === "win32" && ffmpegCommand) {
|
|
261
|
+
const ffmpegCmd = ffmpegCommand;
|
|
262
|
+
if (ffmpegCmd) {
|
|
263
|
+
const inputs = [...new Set([normalizeWindowsAudioInput(mic), "audio=default"])];
|
|
264
|
+
for (const input of inputs) {
|
|
265
|
+
recorders.push({
|
|
266
|
+
label: `ffmpeg dshow (${input.replace(/^audio=/, "")})`,
|
|
267
|
+
command: ffmpegCmd,
|
|
268
|
+
args: ["-hide_banner", "-loglevel", "error", "-y", "-f", "dshow", "-i", input, "-ac", "1", "-ar", "16000", file],
|
|
269
|
+
});
|
|
270
|
+
}
|
|
229
271
|
}
|
|
230
272
|
}
|
|
231
273
|
|
|
232
|
-
if (
|
|
274
|
+
if (soxCommand) {
|
|
233
275
|
recorders.push({
|
|
234
276
|
label: "sox default",
|
|
235
|
-
command:
|
|
277
|
+
command: soxCommand,
|
|
236
278
|
args: ["-d", "-r", "16000", "-c", "1", "-b", "16", file],
|
|
237
279
|
});
|
|
238
280
|
}
|
package/lib/engines.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createGunzip } from "node:zlib";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { Readable, Transform } from "node:stream";
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
|
-
import { sha256, ensureDir } from "./download.js";
|
|
8
|
+
import { sha256, ensureDir, replaceFile } from "./download.js";
|
|
9
9
|
import { getCacheDir } from "./models.js";
|
|
10
10
|
import { getBundledEngineDir, resolveCommand } from "./engine.js";
|
|
11
11
|
|