@hxnnxs/opencode-voice 0.1.6 → 0.1.7
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 +7 -0
- package/bin/opencode-voice.js +42 -4
- package/lib/engine.js +37 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.1.7 - 2026-06-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Hardened Windows ffmpeg resolution to handle executable-resolution edge cases (extensionless bundled binary paths and local module fallback), so recorder startup can use the actual resolved binary path.
|
|
10
|
+
- Improved diagnostics to print exact recorder command paths and quick per-command probe results in `opencode-voice doctor` output.
|
|
11
|
+
|
|
5
12
|
## 0.1.6 - 2026-06-17
|
|
6
13
|
|
|
7
14
|
### Fixed
|
package/bin/opencode-voice.js
CHANGED
|
@@ -32,6 +32,34 @@ function packageName() {
|
|
|
32
32
|
return process.env.OPENCODE_VOICE_PACKAGE || manifest.name || "opencode-voice";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function probeCommand(command, args = ["-version"]) {
|
|
36
|
+
if (!command) return { ok: false, message: "missing" };
|
|
37
|
+
try {
|
|
38
|
+
const result = spawnSync(command, args, {
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
timeout: 2000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (result.error) {
|
|
45
|
+
return { ok: false, message: result.error.message };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
message: `exit ${result.status}`,
|
|
52
|
+
stderr: (result.stderr || "").trim(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`;
|
|
57
|
+
return { ok: true, versionLine: output.split("\n").filter(Boolean)[0] || "" };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
async function runtime() {
|
|
36
64
|
const [engine, models, engines] = await Promise.all([import("../lib/engine.js"), import("../lib/models.js"), import("../lib/engines.js")]);
|
|
37
65
|
return { ...engine, ...models, ...engines };
|
|
@@ -40,6 +68,7 @@ async function runtime() {
|
|
|
40
68
|
async function doctor() {
|
|
41
69
|
const {
|
|
42
70
|
commandExists,
|
|
71
|
+
resolveCommand,
|
|
43
72
|
getAudioDir,
|
|
44
73
|
getCacheDir,
|
|
45
74
|
getEngineStatus,
|
|
@@ -50,6 +79,9 @@ async function doctor() {
|
|
|
50
79
|
|
|
51
80
|
const engine = getEngineStatus("whisper.cpp");
|
|
52
81
|
const probe = engine.resolvedBinary ? await probeEngine("whisper.cpp", engine.resolvedBinary) : { ok: false, message: "missing binary" };
|
|
82
|
+
const ffmpeg = resolveCommand("ffmpeg");
|
|
83
|
+
const arecord = resolveCommand("arecord");
|
|
84
|
+
const sox = resolveCommand("sox");
|
|
53
85
|
const payload = {
|
|
54
86
|
platform: `${process.platform}-${process.arch}`,
|
|
55
87
|
cacheDir: getCacheDir(),
|
|
@@ -58,9 +90,15 @@ async function doctor() {
|
|
|
58
90
|
engine,
|
|
59
91
|
probe,
|
|
60
92
|
recorders: {
|
|
61
|
-
ffmpeg
|
|
62
|
-
arecord
|
|
63
|
-
sox
|
|
93
|
+
ffmpeg,
|
|
94
|
+
arecord,
|
|
95
|
+
sox,
|
|
96
|
+
ffmpegPresent: commandExists("ffmpeg"),
|
|
97
|
+
arecordPresent: commandExists("arecord"),
|
|
98
|
+
soxPresent: commandExists("sox"),
|
|
99
|
+
ffmpegProbe: probeCommand(ffmpeg),
|
|
100
|
+
arecordProbe: probeCommand(arecord, ["--help"]),
|
|
101
|
+
soxProbe: probeCommand(sox, ["--help"]),
|
|
64
102
|
},
|
|
65
103
|
microphones: listMicrophones(),
|
|
66
104
|
};
|
|
@@ -80,7 +118,7 @@ async function doctor() {
|
|
|
80
118
|
`Managed engine dir: ${engine.managedDir}`,
|
|
81
119
|
`whisper-cli: ${engine.resolvedBinary || "missing"}`,
|
|
82
120
|
`Probe: ${probe.ok ? "ok" : probe.message}`,
|
|
83
|
-
`Recorders: ffmpeg=${payload.recorders.
|
|
121
|
+
`Recorders: ffmpeg=${payload.recorders.ffmpegPresent ? "yes" : "no"}${payload.recorders.ffmpeg ? ` (${payload.recorders.ffmpeg})` : ""}, arecord=${payload.recorders.arecordPresent ? "yes" : "no"}${payload.recorders.arecord ? ` (${payload.recorders.arecord})` : ""}, sox=${payload.recorders.soxPresent ? "yes" : "no"}${payload.recorders.sox ? ` (${payload.recorders.sox})` : ""}`,
|
|
84
122
|
`Microphones: ${payload.microphones.join(", ")}`,
|
|
85
123
|
].join("\n"),
|
|
86
124
|
);
|
package/lib/engine.js
CHANGED
|
@@ -24,6 +24,31 @@ function isExecutable(file) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function executableExtensions() {
|
|
28
|
+
if (process.platform !== "win32") return [""];
|
|
29
|
+
return (process.env.PATHEXT || ".EXE;.CMD;.BAT")
|
|
30
|
+
.split(";")
|
|
31
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.filter((entry) => entry.startsWith("."));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveExecutable(file) {
|
|
37
|
+
const normalized = path.resolve(file);
|
|
38
|
+
if (isExecutable(normalized)) return normalized;
|
|
39
|
+
|
|
40
|
+
if (path.extname(normalized) || process.platform !== "win32") {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const ext of executableExtensions()) {
|
|
45
|
+
const candidate = `${normalized}${ext}`;
|
|
46
|
+
if (isExecutable(candidate)) return candidate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
function executableNames(command) {
|
|
28
53
|
const names = [command];
|
|
29
54
|
if (path.extname(command)) return names;
|
|
@@ -46,12 +71,18 @@ function bundledFfmpegPath() {
|
|
|
46
71
|
const resolveFromDependency = () => {
|
|
47
72
|
try {
|
|
48
73
|
const candidate = require("ffmpeg-static");
|
|
49
|
-
|
|
74
|
+
if (typeof candidate !== "string") return "";
|
|
75
|
+
return resolveExecutable(candidate);
|
|
50
76
|
} catch {
|
|
51
77
|
return "";
|
|
52
78
|
}
|
|
53
79
|
};
|
|
54
80
|
|
|
81
|
+
const resolveFromLocalModule = () => {
|
|
82
|
+
const fallback = path.join(PLUGIN_ROOT, "node_modules", "ffmpeg-static", "ffmpeg");
|
|
83
|
+
return resolveExecutable(fallback);
|
|
84
|
+
};
|
|
85
|
+
|
|
55
86
|
const installDependency = () => {
|
|
56
87
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
57
88
|
try {
|
|
@@ -66,6 +97,7 @@ function bundledFfmpegPath() {
|
|
|
66
97
|
};
|
|
67
98
|
|
|
68
99
|
let candidate = resolveFromDependency();
|
|
100
|
+
if (!candidate) candidate = resolveFromLocalModule();
|
|
69
101
|
if (!candidate && !ffmpegStaticInstallAttempted) {
|
|
70
102
|
ffmpegStaticInstallAttempted = true;
|
|
71
103
|
if (installDependency()) {
|
|
@@ -73,6 +105,8 @@ function bundledFfmpegPath() {
|
|
|
73
105
|
}
|
|
74
106
|
}
|
|
75
107
|
|
|
108
|
+
if (!candidate) candidate = resolveFromLocalModule();
|
|
109
|
+
|
|
76
110
|
ffmpegStaticPathCache = candidate || "";
|
|
77
111
|
return ffmpegStaticPathCache;
|
|
78
112
|
}
|
|
@@ -134,7 +168,8 @@ function candidateCommands(command, options = {}) {
|
|
|
134
168
|
export function resolveCommand(command, options = {}) {
|
|
135
169
|
for (const candidate of candidateCommands(command, options)) {
|
|
136
170
|
if (looksLikePath(candidate)) {
|
|
137
|
-
|
|
171
|
+
const resolved = resolveExecutable(candidate);
|
|
172
|
+
if (resolved) return resolved;
|
|
138
173
|
continue;
|
|
139
174
|
}
|
|
140
175
|
|