omnius 1.0.417 → 1.0.418
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.js +1508 -1236
- package/dist/scripts/live-whisper.py +18 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41899,7 +41899,7 @@ var init_transcribe_tool = __esm({
|
|
|
41899
41899
|
"-U",
|
|
41900
41900
|
"pip",
|
|
41901
41901
|
"wheel",
|
|
41902
|
-
"setuptools",
|
|
41902
|
+
"setuptools<81",
|
|
41903
41903
|
"numpy",
|
|
41904
41904
|
"openai-whisper"
|
|
41905
41905
|
], {
|
|
@@ -602840,20 +602840,1381 @@ var init_image_ascii_preview = __esm({
|
|
|
602840
602840
|
}
|
|
602841
602841
|
});
|
|
602842
602842
|
|
|
602843
|
+
// packages/cli/src/api/py-embed.ts
|
|
602844
|
+
var py_embed_exports = {};
|
|
602845
|
+
__export(py_embed_exports, {
|
|
602846
|
+
ensureEmbedDeps: () => ensureEmbedDeps,
|
|
602847
|
+
getVenvPython: () => getVenvPython,
|
|
602848
|
+
runEmbedAudio: () => runEmbedAudio,
|
|
602849
|
+
runEmbedImage: () => runEmbedImage,
|
|
602850
|
+
runEmbedText: () => runEmbedText,
|
|
602851
|
+
runTranscribeFile: () => runTranscribeFile
|
|
602852
|
+
});
|
|
602853
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
602854
|
+
import { existsSync as existsSync109, mkdirSync as mkdirSync67, writeFileSync as writeFileSync56 } from "node:fs";
|
|
602855
|
+
import { join as join124 } from "node:path";
|
|
602856
|
+
import { homedir as homedir38 } from "node:os";
|
|
602857
|
+
function getVenvDir() {
|
|
602858
|
+
return join124(homedir38(), ".omnius", "venv");
|
|
602859
|
+
}
|
|
602860
|
+
function getVenvPython() {
|
|
602861
|
+
const base3 = getVenvDir();
|
|
602862
|
+
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
602863
|
+
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
602864
|
+
return join124(base3, bin, exe);
|
|
602865
|
+
}
|
|
602866
|
+
function ensureEmbedDeps() {
|
|
602867
|
+
const venv = getVenvDir();
|
|
602868
|
+
let log22 = "";
|
|
602869
|
+
try {
|
|
602870
|
+
mkdirSync67(venv, { recursive: true });
|
|
602871
|
+
} catch {
|
|
602872
|
+
}
|
|
602873
|
+
const venvExisted = existsSync109(getVenvPython());
|
|
602874
|
+
if (!venvExisted) {
|
|
602875
|
+
const mk = spawnSync8("python3", ["-m", "venv", venv], { encoding: "utf8" });
|
|
602876
|
+
log22 += mk.stdout + mk.stderr;
|
|
602877
|
+
}
|
|
602878
|
+
if (venvExisted && !(process.env["OMNIUS_INSTALL_FULL_EMBED_DEPS"] === "1")) {
|
|
602879
|
+
const check = spawnSync8(
|
|
602880
|
+
getVenvPython(),
|
|
602881
|
+
["-c", "import numpy, soundfile, whisper, faster_whisper, PIL"],
|
|
602882
|
+
{ encoding: "utf8", timeout: 3e4 }
|
|
602883
|
+
);
|
|
602884
|
+
if (check.status === 0) {
|
|
602885
|
+
return { ok: true, log: "embed deps already satisfied" };
|
|
602886
|
+
}
|
|
602887
|
+
}
|
|
602888
|
+
const pip = process.platform === "win32" ? join124(venv, "Scripts", "pip.exe") : join124(venv, "bin", "pip");
|
|
602889
|
+
const up = spawnSync8(pip, ["install", "--upgrade", "pip", "setuptools<81", "wheel"], { encoding: "utf8", timeout: 3e5 });
|
|
602890
|
+
log22 += up.stdout + up.stderr;
|
|
602891
|
+
const asrPkgs = [
|
|
602892
|
+
"numpy==1.26.4",
|
|
602893
|
+
"soundfile",
|
|
602894
|
+
"faster-whisper",
|
|
602895
|
+
"openai-whisper",
|
|
602896
|
+
// Embedding helpers used elsewhere (kept lightweight compared to full torch stack)
|
|
602897
|
+
"Pillow"
|
|
602898
|
+
];
|
|
602899
|
+
const ins = spawnSync8(pip, ["install", ...asrPkgs], { encoding: "utf8", timeout: 9e5 });
|
|
602900
|
+
log22 += ins.stdout + ins.stderr;
|
|
602901
|
+
let fullOk = true;
|
|
602902
|
+
if (process.env["OMNIUS_INSTALL_FULL_EMBED_DEPS"] === "1") {
|
|
602903
|
+
const fullPkgs = ["torch", "torchaudio", "open_clip_torch", "speechbrain"];
|
|
602904
|
+
const full = spawnSync8(pip, ["install", "--upgrade", ...fullPkgs], { encoding: "utf8", timeout: 18e5 });
|
|
602905
|
+
log22 += full.stdout + full.stderr;
|
|
602906
|
+
fullOk = full.status === 0;
|
|
602907
|
+
}
|
|
602908
|
+
const ok3 = ins.status === 0 && up.status === 0 && fullOk;
|
|
602909
|
+
return { ok: ok3, log: log22 };
|
|
602910
|
+
}
|
|
602911
|
+
function runEmbedImage(input) {
|
|
602912
|
+
const py = getVenvPython();
|
|
602913
|
+
const script = locateScript("embed-image.py");
|
|
602914
|
+
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
602915
|
+
try {
|
|
602916
|
+
const out = JSON.parse(p2.stdout || "{}");
|
|
602917
|
+
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
602918
|
+
} catch {
|
|
602919
|
+
}
|
|
602920
|
+
return null;
|
|
602921
|
+
}
|
|
602922
|
+
function runEmbedAudio(input) {
|
|
602923
|
+
const py = getVenvPython();
|
|
602924
|
+
const script = locateScript("embed-audio.py");
|
|
602925
|
+
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
602926
|
+
try {
|
|
602927
|
+
const out = JSON.parse(p2.stdout || "{}");
|
|
602928
|
+
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
602929
|
+
} catch {
|
|
602930
|
+
}
|
|
602931
|
+
return null;
|
|
602932
|
+
}
|
|
602933
|
+
function runEmbedText(text2) {
|
|
602934
|
+
const py = getVenvPython();
|
|
602935
|
+
const script = locateScript("embed-text.py");
|
|
602936
|
+
const p2 = spawnSync8(py, [script], { input: JSON.stringify({ text: text2 }), encoding: "utf8" });
|
|
602937
|
+
try {
|
|
602938
|
+
const out = JSON.parse(p2.stdout || "{}");
|
|
602939
|
+
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
602940
|
+
} catch {
|
|
602941
|
+
}
|
|
602942
|
+
return null;
|
|
602943
|
+
}
|
|
602944
|
+
function runTranscribeFile(input) {
|
|
602945
|
+
const py = getVenvPython();
|
|
602946
|
+
const script = locateScript("transcribe-file.py");
|
|
602947
|
+
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8", timeout: 6e5 });
|
|
602948
|
+
try {
|
|
602949
|
+
const out = JSON.parse(p2.stdout || "{}");
|
|
602950
|
+
if (typeof out.text === "string") return out.text;
|
|
602951
|
+
} catch {
|
|
602952
|
+
}
|
|
602953
|
+
return null;
|
|
602954
|
+
}
|
|
602955
|
+
function locateScript(name10) {
|
|
602956
|
+
const candidates = [
|
|
602957
|
+
join124(process.cwd(), "dist", "scripts", name10),
|
|
602958
|
+
join124(process.cwd(), name10),
|
|
602959
|
+
join124(process.cwd(), "scripts", name10)
|
|
602960
|
+
];
|
|
602961
|
+
for (const c9 of candidates) if (existsSync109(c9)) return c9;
|
|
602962
|
+
const fallback = join124(process.cwd(), name10);
|
|
602963
|
+
try {
|
|
602964
|
+
writeFileSync56(fallback, 'print("missing script")\n');
|
|
602965
|
+
} catch {
|
|
602966
|
+
}
|
|
602967
|
+
return fallback;
|
|
602968
|
+
}
|
|
602969
|
+
var init_py_embed = __esm({
|
|
602970
|
+
"packages/cli/src/api/py-embed.ts"() {
|
|
602971
|
+
"use strict";
|
|
602972
|
+
}
|
|
602973
|
+
});
|
|
602974
|
+
|
|
602975
|
+
// packages/cli/src/tui/listen.ts
|
|
602976
|
+
var listen_exports = {};
|
|
602977
|
+
__export(listen_exports, {
|
|
602978
|
+
ListenEngine: () => ListenEngine,
|
|
602979
|
+
ensureTranscribeCliBackground: () => ensureTranscribeCliBackground,
|
|
602980
|
+
getListenEngine: () => getListenEngine,
|
|
602981
|
+
getListenLiveState: () => getListenLiveState,
|
|
602982
|
+
isAudioPath: () => isAudioPath,
|
|
602983
|
+
isTranscribablePath: () => isTranscribablePath,
|
|
602984
|
+
isVideoPath: () => isVideoPath,
|
|
602985
|
+
transcribeFileViaWhisper: () => transcribeFileViaWhisper,
|
|
602986
|
+
waitForTranscribeCli: () => waitForTranscribeCli
|
|
602987
|
+
});
|
|
602988
|
+
import { spawn as spawn29 } from "node:child_process";
|
|
602989
|
+
import {
|
|
602990
|
+
existsSync as existsSync110,
|
|
602991
|
+
mkdirSync as mkdirSync68,
|
|
602992
|
+
writeFileSync as writeFileSync57,
|
|
602993
|
+
readdirSync as readdirSync35
|
|
602994
|
+
} from "node:fs";
|
|
602995
|
+
import { join as join125, dirname as dirname37 } from "node:path";
|
|
602996
|
+
import { homedir as homedir39 } from "node:os";
|
|
602997
|
+
import { fileURLToPath as fileURLToPath16 } from "node:url";
|
|
602998
|
+
import { EventEmitter as EventEmitter6 } from "node:events";
|
|
602999
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
603000
|
+
function isAudioPath(path12) {
|
|
603001
|
+
const ext = path12.toLowerCase().split(".").pop();
|
|
603002
|
+
return ext ? AUDIO_EXTENSIONS.has(`.${ext}`) : false;
|
|
603003
|
+
}
|
|
603004
|
+
function isVideoPath(path12) {
|
|
603005
|
+
const ext = path12.toLowerCase().split(".").pop();
|
|
603006
|
+
return ext ? VIDEO_EXTENSIONS2.has(`.${ext}`) : false;
|
|
603007
|
+
}
|
|
603008
|
+
function isTranscribablePath(path12) {
|
|
603009
|
+
return isAudioPath(path12) || isVideoPath(path12);
|
|
603010
|
+
}
|
|
603011
|
+
function updateListenLiveState(patch) {
|
|
603012
|
+
Object.assign(listenLiveState, patch, { updatedAt: Date.now() });
|
|
603013
|
+
}
|
|
603014
|
+
function getListenLiveState() {
|
|
603015
|
+
return listenLiveState;
|
|
603016
|
+
}
|
|
603017
|
+
async function resolveMicSource() {
|
|
603018
|
+
const configured = String(process.env["OMNIUS_MIC_DEVICE"] ?? "").trim();
|
|
603019
|
+
if (configured.startsWith("hw:") || configured.startsWith("plughw:")) {
|
|
603020
|
+
return { alsaDevice: configured, label: configured };
|
|
603021
|
+
}
|
|
603022
|
+
const configuredPulse = configured.startsWith("pulse:") ? configured.slice("pulse:".length) : configured || void 0;
|
|
603023
|
+
let sources = [];
|
|
603024
|
+
try {
|
|
603025
|
+
const raw = await execFileText4("pactl", ["list", "short", "sources"], { timeout: 5e3 });
|
|
603026
|
+
for (const line of raw.split(/\r?\n/).filter(Boolean)) {
|
|
603027
|
+
const parts = line.split(" ");
|
|
603028
|
+
const name10 = parts[1]?.trim();
|
|
603029
|
+
if (!name10) continue;
|
|
603030
|
+
const spec = parts[3] ?? "";
|
|
603031
|
+
const chMatch = spec.match(/(\d+)ch/);
|
|
603032
|
+
sources.push({
|
|
603033
|
+
name: name10,
|
|
603034
|
+
channels: chMatch ? Math.max(1, Number(chMatch[1])) : 1,
|
|
603035
|
+
state: (parts[4] ?? "").trim().toUpperCase()
|
|
603036
|
+
});
|
|
603037
|
+
}
|
|
603038
|
+
} catch {
|
|
603039
|
+
sources = [];
|
|
603040
|
+
}
|
|
603041
|
+
if (configuredPulse) {
|
|
603042
|
+
const match = sources.find((s2) => s2.name === configuredPulse);
|
|
603043
|
+
return {
|
|
603044
|
+
pulseSource: configuredPulse,
|
|
603045
|
+
channels: match?.channels,
|
|
603046
|
+
label: `pulse:${configuredPulse}`
|
|
603047
|
+
};
|
|
603048
|
+
}
|
|
603049
|
+
const candidates = sources.filter((s2) => !/\.monitor$/i.test(s2.name));
|
|
603050
|
+
if (candidates.length === 0) return null;
|
|
603051
|
+
const rank = (s2) => {
|
|
603052
|
+
let score = 0;
|
|
603053
|
+
if (s2.state === "RUNNING") score -= 20;
|
|
603054
|
+
if (s2.state === "SUSPENDED") score += 5;
|
|
603055
|
+
if (/usb|mic|array|respeaker|seeed/i.test(s2.name)) score -= 10;
|
|
603056
|
+
return score;
|
|
603057
|
+
};
|
|
603058
|
+
candidates.sort((a2, b) => rank(a2) - rank(b));
|
|
603059
|
+
const best = candidates[0];
|
|
603060
|
+
return { pulseSource: best.name, channels: best.channels, label: `pulse:${best.name}` };
|
|
603061
|
+
}
|
|
603062
|
+
function micFfmpegFilter(channels) {
|
|
603063
|
+
const parts = [];
|
|
603064
|
+
if ((channels ?? 1) > 2) parts.push("pan=mono|c0=c0");
|
|
603065
|
+
parts.push("highpass=f=80");
|
|
603066
|
+
const gain2 = Number(process.env["OMNIUS_MIC_GAIN_DB"] ?? "");
|
|
603067
|
+
if (Number.isFinite(gain2) && gain2 !== 0) parts.push(`volume=${gain2}dB`);
|
|
603068
|
+
return parts.join(",");
|
|
603069
|
+
}
|
|
603070
|
+
async function findMicCaptureCommand() {
|
|
603071
|
+
const platform7 = process.platform;
|
|
603072
|
+
if (platform7 === "linux") {
|
|
603073
|
+
const resolved = await resolveMicSource();
|
|
603074
|
+
if (resolved?.alsaDevice && await commandExists2("arecord")) {
|
|
603075
|
+
return {
|
|
603076
|
+
cmd: "arecord",
|
|
603077
|
+
args: ["-D", resolved.alsaDevice, "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-q", "-"],
|
|
603078
|
+
device: resolved.alsaDevice
|
|
603079
|
+
};
|
|
603080
|
+
}
|
|
603081
|
+
if (resolved?.pulseSource && await commandExists2("ffmpeg")) {
|
|
603082
|
+
return {
|
|
603083
|
+
cmd: "ffmpeg",
|
|
603084
|
+
args: [
|
|
603085
|
+
"-f",
|
|
603086
|
+
"pulse",
|
|
603087
|
+
"-i",
|
|
603088
|
+
resolved.pulseSource,
|
|
603089
|
+
"-af",
|
|
603090
|
+
micFfmpegFilter(resolved.channels),
|
|
603091
|
+
"-ar",
|
|
603092
|
+
"16000",
|
|
603093
|
+
"-ac",
|
|
603094
|
+
"1",
|
|
603095
|
+
"-f",
|
|
603096
|
+
"s16le",
|
|
603097
|
+
"-loglevel",
|
|
603098
|
+
"quiet",
|
|
603099
|
+
"pipe:1"
|
|
603100
|
+
],
|
|
603101
|
+
device: resolved.label
|
|
603102
|
+
};
|
|
603103
|
+
}
|
|
603104
|
+
if (await commandExists2("arecord")) {
|
|
603105
|
+
return {
|
|
603106
|
+
cmd: "arecord",
|
|
603107
|
+
args: ["-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-q", "-"],
|
|
603108
|
+
device: "default (arecord)"
|
|
603109
|
+
};
|
|
603110
|
+
}
|
|
603111
|
+
}
|
|
603112
|
+
if (platform7 === "darwin") {
|
|
603113
|
+
if (await commandExists2("sox")) {
|
|
603114
|
+
return {
|
|
603115
|
+
cmd: "sox",
|
|
603116
|
+
args: [
|
|
603117
|
+
"-d",
|
|
603118
|
+
"-t",
|
|
603119
|
+
"raw",
|
|
603120
|
+
"-r",
|
|
603121
|
+
"16000",
|
|
603122
|
+
"-c",
|
|
603123
|
+
"1",
|
|
603124
|
+
"-b",
|
|
603125
|
+
"16",
|
|
603126
|
+
"-e",
|
|
603127
|
+
"signed-integer",
|
|
603128
|
+
"-"
|
|
603129
|
+
],
|
|
603130
|
+
device: "default (sox)"
|
|
603131
|
+
};
|
|
603132
|
+
}
|
|
603133
|
+
}
|
|
603134
|
+
if (await commandExists2("ffmpeg")) {
|
|
603135
|
+
if (platform7 === "linux") {
|
|
603136
|
+
return {
|
|
603137
|
+
cmd: "ffmpeg",
|
|
603138
|
+
args: [
|
|
603139
|
+
"-f",
|
|
603140
|
+
"pulse",
|
|
603141
|
+
"-i",
|
|
603142
|
+
"default",
|
|
603143
|
+
"-ar",
|
|
603144
|
+
"16000",
|
|
603145
|
+
"-ac",
|
|
603146
|
+
"1",
|
|
603147
|
+
"-f",
|
|
603148
|
+
"s16le",
|
|
603149
|
+
"-loglevel",
|
|
603150
|
+
"quiet",
|
|
603151
|
+
"pipe:1"
|
|
603152
|
+
],
|
|
603153
|
+
device: "pulse:default"
|
|
603154
|
+
};
|
|
603155
|
+
} else if (platform7 === "darwin") {
|
|
603156
|
+
return {
|
|
603157
|
+
cmd: "ffmpeg",
|
|
603158
|
+
args: [
|
|
603159
|
+
"-f",
|
|
603160
|
+
"avfoundation",
|
|
603161
|
+
"-i",
|
|
603162
|
+
":0",
|
|
603163
|
+
"-ar",
|
|
603164
|
+
"16000",
|
|
603165
|
+
"-ac",
|
|
603166
|
+
"1",
|
|
603167
|
+
"-f",
|
|
603168
|
+
"s16le",
|
|
603169
|
+
"-loglevel",
|
|
603170
|
+
"quiet",
|
|
603171
|
+
"pipe:1"
|
|
603172
|
+
],
|
|
603173
|
+
device: "avfoundation:0"
|
|
603174
|
+
};
|
|
603175
|
+
}
|
|
603176
|
+
}
|
|
603177
|
+
return null;
|
|
603178
|
+
}
|
|
603179
|
+
async function findTranscribeFileScript() {
|
|
603180
|
+
const thisDir = dirname37(fileURLToPath16(import.meta.url));
|
|
603181
|
+
const candidates = [
|
|
603182
|
+
join125(thisDir, "../../../../packages/execution/scripts/transcribe-file.py"),
|
|
603183
|
+
join125(thisDir, "../../../packages/execution/scripts/transcribe-file.py"),
|
|
603184
|
+
join125(thisDir, "../../execution/scripts/transcribe-file.py"),
|
|
603185
|
+
join125(thisDir, "../scripts/transcribe-file.py"),
|
|
603186
|
+
join125(thisDir, "../../scripts/transcribe-file.py")
|
|
603187
|
+
];
|
|
603188
|
+
for (const p2 of candidates) {
|
|
603189
|
+
if (existsSync110(p2)) return p2;
|
|
603190
|
+
}
|
|
603191
|
+
try {
|
|
603192
|
+
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
603193
|
+
const candidates2 = [
|
|
603194
|
+
join125(globalRoot, "omnius", "dist", "scripts", "transcribe-file.py"),
|
|
603195
|
+
join125(globalRoot, "omnius", "scripts", "transcribe-file.py")
|
|
603196
|
+
];
|
|
603197
|
+
for (const p2 of candidates2) {
|
|
603198
|
+
if (existsSync110(p2)) return p2;
|
|
603199
|
+
}
|
|
603200
|
+
} catch {
|
|
603201
|
+
}
|
|
603202
|
+
return null;
|
|
603203
|
+
}
|
|
603204
|
+
async function transcribeFileViaWhisper(filePath, model) {
|
|
603205
|
+
const script = await findTranscribeFileScript();
|
|
603206
|
+
if (!script) return null;
|
|
603207
|
+
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
603208
|
+
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
603209
|
+
const venvPython2 = join125(homedir39(), ".omnius", "venv", bin, exe);
|
|
603210
|
+
if (!existsSync110(venvPython2)) return null;
|
|
603211
|
+
return new Promise((resolve76) => {
|
|
603212
|
+
const child = spawn29(venvPython2, [script], {
|
|
603213
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603214
|
+
env: process.env
|
|
603215
|
+
});
|
|
603216
|
+
let out = "";
|
|
603217
|
+
let err = "";
|
|
603218
|
+
let timer = null;
|
|
603219
|
+
const stop2 = (val) => {
|
|
603220
|
+
if (timer) clearTimeout(timer);
|
|
603221
|
+
try {
|
|
603222
|
+
child.kill("SIGTERM");
|
|
603223
|
+
} catch {
|
|
603224
|
+
}
|
|
603225
|
+
resolve76(val ? parse3(val) : null);
|
|
603226
|
+
};
|
|
603227
|
+
function parse3(raw) {
|
|
603228
|
+
try {
|
|
603229
|
+
const j = JSON.parse(raw);
|
|
603230
|
+
if (j.error) return null;
|
|
603231
|
+
if (typeof j.text !== "string") return null;
|
|
603232
|
+
return {
|
|
603233
|
+
text: j.text.trim(),
|
|
603234
|
+
duration: null,
|
|
603235
|
+
speakers: [],
|
|
603236
|
+
segments: []
|
|
603237
|
+
};
|
|
603238
|
+
} catch {
|
|
603239
|
+
return null;
|
|
603240
|
+
}
|
|
603241
|
+
}
|
|
603242
|
+
child.stdout?.on("data", (d2) => {
|
|
603243
|
+
out += d2.toString("utf-8");
|
|
603244
|
+
});
|
|
603245
|
+
child.stderr?.on("data", (d2) => {
|
|
603246
|
+
err += d2.toString("utf-8");
|
|
603247
|
+
});
|
|
603248
|
+
child.once("error", () => stop2(null));
|
|
603249
|
+
child.once("close", () => {
|
|
603250
|
+
if (out.trim()) resolve76(parse3(out));
|
|
603251
|
+
else {
|
|
603252
|
+
void err;
|
|
603253
|
+
resolve76(null);
|
|
603254
|
+
}
|
|
603255
|
+
});
|
|
603256
|
+
timer = setTimeout(() => stop2(null), 12e4);
|
|
603257
|
+
try {
|
|
603258
|
+
child.stdin?.write(JSON.stringify({ path: filePath, model }) + "\n");
|
|
603259
|
+
child.stdin?.end();
|
|
603260
|
+
} catch {
|
|
603261
|
+
stop2(null);
|
|
603262
|
+
}
|
|
603263
|
+
});
|
|
603264
|
+
}
|
|
603265
|
+
async function findLiveWhisperScript() {
|
|
603266
|
+
const thisDir = dirname37(fileURLToPath16(import.meta.url));
|
|
603267
|
+
const candidates = [
|
|
603268
|
+
join125(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
|
|
603269
|
+
join125(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
|
|
603270
|
+
join125(thisDir, "../../execution/scripts/live-whisper.py"),
|
|
603271
|
+
// npm install layout — scripts bundled alongside dist
|
|
603272
|
+
join125(thisDir, "../scripts/live-whisper.py"),
|
|
603273
|
+
join125(thisDir, "../../scripts/live-whisper.py")
|
|
603274
|
+
];
|
|
603275
|
+
for (const p2 of candidates) {
|
|
603276
|
+
if (existsSync110(p2)) return p2;
|
|
603277
|
+
}
|
|
603278
|
+
try {
|
|
603279
|
+
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
603280
|
+
const candidates2 = [
|
|
603281
|
+
join125(globalRoot, "omnius", "dist", "scripts", "live-whisper.py"),
|
|
603282
|
+
join125(globalRoot, "omnius", "scripts", "live-whisper.py")
|
|
603283
|
+
];
|
|
603284
|
+
for (const p2 of candidates2) {
|
|
603285
|
+
if (existsSync110(p2)) return p2;
|
|
603286
|
+
}
|
|
603287
|
+
} catch {
|
|
603288
|
+
}
|
|
603289
|
+
const nvmBase = join125(homedir39(), ".nvm", "versions", "node");
|
|
603290
|
+
if (existsSync110(nvmBase)) {
|
|
603291
|
+
try {
|
|
603292
|
+
for (const ver of readdirSync35(nvmBase)) {
|
|
603293
|
+
const p2 = join125(
|
|
603294
|
+
nvmBase,
|
|
603295
|
+
ver,
|
|
603296
|
+
"lib",
|
|
603297
|
+
"node_modules",
|
|
603298
|
+
"omnius",
|
|
603299
|
+
"dist",
|
|
603300
|
+
"scripts",
|
|
603301
|
+
"live-whisper.py"
|
|
603302
|
+
);
|
|
603303
|
+
if (existsSync110(p2)) return p2;
|
|
603304
|
+
}
|
|
603305
|
+
} catch {
|
|
603306
|
+
}
|
|
603307
|
+
}
|
|
603308
|
+
return null;
|
|
603309
|
+
}
|
|
603310
|
+
async function ensureVenvForTranscribeCli() {
|
|
603311
|
+
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
603312
|
+
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
603313
|
+
const venvBin = join125(homedir39(), ".omnius", "venv", bin);
|
|
603314
|
+
const venvPython2 = join125(venvBin, exe);
|
|
603315
|
+
if (!existsSync110(venvPython2)) {
|
|
603316
|
+
try {
|
|
603317
|
+
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
603318
|
+
if (typeof mod3.ensureEmbedDeps === "function") mod3.ensureEmbedDeps();
|
|
603319
|
+
} catch {
|
|
603320
|
+
}
|
|
603321
|
+
}
|
|
603322
|
+
if (!existsSync110(venvPython2)) {
|
|
603323
|
+
try {
|
|
603324
|
+
mkdirSync68(join125(homedir39(), ".omnius", "venv"), { recursive: true });
|
|
603325
|
+
await execFileText4("python3", ["-m", "venv", join125(homedir39(), ".omnius", "venv")], { timeout: 6e4 });
|
|
603326
|
+
} catch {
|
|
603327
|
+
return false;
|
|
603328
|
+
}
|
|
603329
|
+
}
|
|
603330
|
+
process.env.TRANSCRIBE_PYTHON = venvPython2;
|
|
603331
|
+
const pathSep2 = process.platform === "win32" ? ";" : ":";
|
|
603332
|
+
const currentPath = process.env.PATH || "";
|
|
603333
|
+
if (!currentPath.split(pathSep2).includes(venvBin)) {
|
|
603334
|
+
process.env.PATH = `${venvBin}${pathSep2}${currentPath}`;
|
|
603335
|
+
}
|
|
603336
|
+
try {
|
|
603337
|
+
await execFileText4(venvPython2, ["-c", "import numpy"], { timeout: 1e4 });
|
|
603338
|
+
} catch {
|
|
603339
|
+
try {
|
|
603340
|
+
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "pip", "setuptools<81", "wheel", "numpy==1.26.4"], { timeout: 3e5 });
|
|
603341
|
+
} catch {
|
|
603342
|
+
return false;
|
|
603343
|
+
}
|
|
603344
|
+
}
|
|
603345
|
+
try {
|
|
603346
|
+
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
603347
|
+
return true;
|
|
603348
|
+
} catch {
|
|
603349
|
+
}
|
|
603350
|
+
try {
|
|
603351
|
+
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "transcribe-cli"], { timeout: 3e5 });
|
|
603352
|
+
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
603353
|
+
return true;
|
|
603354
|
+
} catch {
|
|
603355
|
+
return false;
|
|
603356
|
+
}
|
|
603357
|
+
}
|
|
603358
|
+
async function requireTranscribeCliFrom2(packageDir) {
|
|
603359
|
+
try {
|
|
603360
|
+
const { createRequire: createRequire11 } = await import("node:module");
|
|
603361
|
+
const req3 = createRequire11(import.meta.url);
|
|
603362
|
+
const distPath = join125(packageDir, "dist", "index.js");
|
|
603363
|
+
if (existsSync110(distPath)) return req3(distPath);
|
|
603364
|
+
return req3(packageDir);
|
|
603365
|
+
} catch {
|
|
603366
|
+
return null;
|
|
603367
|
+
}
|
|
603368
|
+
}
|
|
603369
|
+
async function loadManagedTranscribeCli() {
|
|
603370
|
+
const packageDir = join125(MANAGED_TRANSCRIBE_CLI_DIR2, "node_modules", "transcribe-cli");
|
|
603371
|
+
if (!existsSync110(join125(packageDir, "dist", "index.js"))) return null;
|
|
603372
|
+
return requireTranscribeCliFrom2(packageDir);
|
|
603373
|
+
}
|
|
603374
|
+
async function ensureManagedTranscribeCliNode() {
|
|
603375
|
+
if (await loadManagedTranscribeCli()) return true;
|
|
603376
|
+
if (_managedTranscribeCliInstallPromise) return _managedTranscribeCliInstallPromise;
|
|
603377
|
+
_managedTranscribeCliInstallPromise = (async () => {
|
|
603378
|
+
try {
|
|
603379
|
+
mkdirSync68(MANAGED_TRANSCRIBE_CLI_DIR2, { recursive: true });
|
|
603380
|
+
if (!existsSync110(join125(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"))) {
|
|
603381
|
+
writeFileSync57(
|
|
603382
|
+
join125(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"),
|
|
603383
|
+
JSON.stringify({ private: true, dependencies: {} }, null, 2),
|
|
603384
|
+
"utf8"
|
|
603385
|
+
);
|
|
603386
|
+
}
|
|
603387
|
+
await execFileText4("npm", ["install", "--prefix", MANAGED_TRANSCRIBE_CLI_DIR2, "transcribe-cli@^2.0.1"], {
|
|
603388
|
+
timeout: 18e4
|
|
603389
|
+
});
|
|
603390
|
+
return Boolean(await loadManagedTranscribeCli());
|
|
603391
|
+
} catch {
|
|
603392
|
+
return false;
|
|
603393
|
+
}
|
|
603394
|
+
})();
|
|
603395
|
+
return _managedTranscribeCliInstallPromise;
|
|
603396
|
+
}
|
|
603397
|
+
function ensureTranscribeCliBackground() {
|
|
603398
|
+
if (_bgInstallPromise) return _bgInstallPromise;
|
|
603399
|
+
_bgInstallPromise = (async () => {
|
|
603400
|
+
const nodeReady = await ensureManagedTranscribeCliNode();
|
|
603401
|
+
const pyReady = await ensureVenvForTranscribeCli();
|
|
603402
|
+
return nodeReady && pyReady;
|
|
603403
|
+
})();
|
|
603404
|
+
return _bgInstallPromise;
|
|
603405
|
+
}
|
|
603406
|
+
async function waitForTranscribeCli() {
|
|
603407
|
+
if (_bgInstallPromise) {
|
|
603408
|
+
return _bgInstallPromise;
|
|
603409
|
+
}
|
|
603410
|
+
return Boolean(await loadManagedTranscribeCli()) && await ensureVenvForTranscribeCli();
|
|
603411
|
+
}
|
|
603412
|
+
function getListenEngine(config) {
|
|
603413
|
+
if (!_engine) {
|
|
603414
|
+
_engine = new ListenEngine(config);
|
|
603415
|
+
}
|
|
603416
|
+
return _engine;
|
|
603417
|
+
}
|
|
603418
|
+
var MANAGED_TRANSCRIBE_CLI_DIR2, AUDIO_EXTENSIONS, VIDEO_EXTENSIONS2, listenLiveState, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _managedTranscribeCliInstallPromise, _engine;
|
|
603419
|
+
var init_listen = __esm({
|
|
603420
|
+
"packages/cli/src/tui/listen.ts"() {
|
|
603421
|
+
"use strict";
|
|
603422
|
+
init_typed_node_events();
|
|
603423
|
+
init_async_process();
|
|
603424
|
+
MANAGED_TRANSCRIBE_CLI_DIR2 = join125(homedir39(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
|
|
603425
|
+
AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
603426
|
+
".mp3",
|
|
603427
|
+
".wav",
|
|
603428
|
+
".flac",
|
|
603429
|
+
".aac",
|
|
603430
|
+
".m4a",
|
|
603431
|
+
".ogg",
|
|
603432
|
+
".wma",
|
|
603433
|
+
".opus"
|
|
603434
|
+
]);
|
|
603435
|
+
VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
603436
|
+
".mp4",
|
|
603437
|
+
".mkv",
|
|
603438
|
+
".avi",
|
|
603439
|
+
".mov",
|
|
603440
|
+
".webm",
|
|
603441
|
+
".flv",
|
|
603442
|
+
".wmv",
|
|
603443
|
+
".m4v",
|
|
603444
|
+
".ts"
|
|
603445
|
+
]);
|
|
603446
|
+
listenLiveState = {
|
|
603447
|
+
phase: "idle",
|
|
603448
|
+
backend: "",
|
|
603449
|
+
model: "",
|
|
603450
|
+
micDevice: "",
|
|
603451
|
+
micLevelDb: null,
|
|
603452
|
+
noiseFloorDb: null,
|
|
603453
|
+
speechActive: false,
|
|
603454
|
+
lastTranscript: "",
|
|
603455
|
+
lastTranscriptAt: 0,
|
|
603456
|
+
lastStatus: "",
|
|
603457
|
+
updatedAt: 0
|
|
603458
|
+
};
|
|
603459
|
+
WhisperFallbackTranscriber = class extends EventEmitter6 {
|
|
603460
|
+
constructor(model, scriptPath2) {
|
|
603461
|
+
super();
|
|
603462
|
+
this.model = model;
|
|
603463
|
+
this.scriptPath = scriptPath2;
|
|
603464
|
+
}
|
|
603465
|
+
model;
|
|
603466
|
+
scriptPath;
|
|
603467
|
+
process = null;
|
|
603468
|
+
_ready = false;
|
|
603469
|
+
get ready() {
|
|
603470
|
+
return this._ready;
|
|
603471
|
+
}
|
|
603472
|
+
async start() {
|
|
603473
|
+
let pyPath = "python3";
|
|
603474
|
+
try {
|
|
603475
|
+
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
603476
|
+
const ensure = mod3.ensureEmbedDeps;
|
|
603477
|
+
const getPy = mod3.getVenvPython;
|
|
603478
|
+
if (typeof ensure === "function") {
|
|
603479
|
+
try {
|
|
603480
|
+
const res = ensure();
|
|
603481
|
+
if (res?.log) this.emit("status", res.log.slice(-500));
|
|
603482
|
+
} catch {
|
|
603483
|
+
}
|
|
603484
|
+
}
|
|
603485
|
+
if (typeof getPy === "function") {
|
|
603486
|
+
pyPath = getPy();
|
|
603487
|
+
}
|
|
603488
|
+
} catch {
|
|
603489
|
+
}
|
|
603490
|
+
updateListenLiveState({
|
|
603491
|
+
phase: "loading-model",
|
|
603492
|
+
backend: "openai-whisper",
|
|
603493
|
+
model: this.model,
|
|
603494
|
+
lastStatus: `loading whisper ${this.model} model...`
|
|
603495
|
+
});
|
|
603496
|
+
this.process = spawn29(
|
|
603497
|
+
pyPath,
|
|
603498
|
+
[
|
|
603499
|
+
this.scriptPath,
|
|
603500
|
+
"--model",
|
|
603501
|
+
this.model,
|
|
603502
|
+
"--chunk-seconds",
|
|
603503
|
+
"3",
|
|
603504
|
+
"--window-seconds",
|
|
603505
|
+
"10"
|
|
603506
|
+
],
|
|
603507
|
+
{
|
|
603508
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603509
|
+
env: { ...process.env }
|
|
603510
|
+
}
|
|
603511
|
+
);
|
|
603512
|
+
return new Promise((resolve76, reject) => {
|
|
603513
|
+
const timeout2 = setTimeout(() => {
|
|
603514
|
+
reject(
|
|
603515
|
+
new Error(
|
|
603516
|
+
"Whisper fallback: model load timeout (5 min). First run downloads the model."
|
|
603517
|
+
)
|
|
603518
|
+
);
|
|
603519
|
+
}, 3e5);
|
|
603520
|
+
const rl = createInterface2({ input: this.process.stdout });
|
|
603521
|
+
onReadlineLine(rl, (line) => {
|
|
603522
|
+
try {
|
|
603523
|
+
const evt = JSON.parse(line);
|
|
603524
|
+
switch (evt.type) {
|
|
603525
|
+
case "status":
|
|
603526
|
+
updateListenLiveState({ lastStatus: String(evt.message ?? "").slice(0, 160) });
|
|
603527
|
+
this.emit("status", evt.message);
|
|
603528
|
+
break;
|
|
603529
|
+
case "ready":
|
|
603530
|
+
this._ready = true;
|
|
603531
|
+
clearTimeout(timeout2);
|
|
603532
|
+
updateListenLiveState({ phase: "ready", lastStatus: "whisper model loaded" });
|
|
603533
|
+
this.emit("ready");
|
|
603534
|
+
resolve76();
|
|
603535
|
+
break;
|
|
603536
|
+
case "transcript":
|
|
603537
|
+
this.emit("transcript", {
|
|
603538
|
+
text: evt.text,
|
|
603539
|
+
isFinal: evt.isFinal ?? false
|
|
603540
|
+
});
|
|
603541
|
+
break;
|
|
603542
|
+
case "error":
|
|
603543
|
+
this.emit("error", new Error(evt.message));
|
|
603544
|
+
break;
|
|
603545
|
+
}
|
|
603546
|
+
} catch {
|
|
603547
|
+
}
|
|
603548
|
+
});
|
|
603549
|
+
this.process.stderr?.on("data", (data) => {
|
|
603550
|
+
const text2 = data.toString().trim();
|
|
603551
|
+
if (text2) this.emit("status", text2);
|
|
603552
|
+
});
|
|
603553
|
+
onChildError(this.process, (err) => {
|
|
603554
|
+
clearTimeout(timeout2);
|
|
603555
|
+
reject(err);
|
|
603556
|
+
});
|
|
603557
|
+
onChildClose(this.process, (code8) => {
|
|
603558
|
+
if (!this._ready) {
|
|
603559
|
+
clearTimeout(timeout2);
|
|
603560
|
+
reject(
|
|
603561
|
+
new Error(`Whisper worker exited with code ${code8} before ready`)
|
|
603562
|
+
);
|
|
603563
|
+
}
|
|
603564
|
+
});
|
|
603565
|
+
});
|
|
603566
|
+
}
|
|
603567
|
+
/** Write PCM16 audio data to the worker's stdin. */
|
|
603568
|
+
write(chunk) {
|
|
603569
|
+
if (this.process?.stdin?.writable) {
|
|
603570
|
+
this.process.stdin.write(chunk);
|
|
603571
|
+
}
|
|
603572
|
+
}
|
|
603573
|
+
/** Stop the worker. */
|
|
603574
|
+
stop() {
|
|
603575
|
+
if (this.process) {
|
|
603576
|
+
try {
|
|
603577
|
+
this.process.stdin?.end();
|
|
603578
|
+
this.process.kill("SIGTERM");
|
|
603579
|
+
} catch {
|
|
603580
|
+
}
|
|
603581
|
+
this.process = null;
|
|
603582
|
+
}
|
|
603583
|
+
this._ready = false;
|
|
603584
|
+
}
|
|
603585
|
+
};
|
|
603586
|
+
ListenEngine = class extends EventEmitter6 {
|
|
603587
|
+
config;
|
|
603588
|
+
micProcess = null;
|
|
603589
|
+
liveTranscriber = null;
|
|
603590
|
+
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
603591
|
+
active = false;
|
|
603592
|
+
paused = false;
|
|
603593
|
+
// Pause state for voicechat (stops mic but keeps transcriber)
|
|
603594
|
+
silenceTimer = null;
|
|
603595
|
+
countdownInterval = null;
|
|
603596
|
+
lastTranscriptTime = 0;
|
|
603597
|
+
pendingText = "";
|
|
603598
|
+
blinkTimer = null;
|
|
603599
|
+
blinkState = false;
|
|
603600
|
+
transcribeCliAvailable = null;
|
|
603601
|
+
constructor(config) {
|
|
603602
|
+
super();
|
|
603603
|
+
this.config = {
|
|
603604
|
+
model: config?.model ?? "base",
|
|
603605
|
+
mode: config?.mode ?? "confirm",
|
|
603606
|
+
silenceTimeoutMs: config?.silenceTimeoutMs ?? 3e3
|
|
603607
|
+
};
|
|
603608
|
+
}
|
|
603609
|
+
get isActive() {
|
|
603610
|
+
return this.active;
|
|
603611
|
+
}
|
|
603612
|
+
get isBlinking() {
|
|
603613
|
+
return this.active && this.blinkState;
|
|
603614
|
+
}
|
|
603615
|
+
get isPaused() {
|
|
603616
|
+
return this.paused;
|
|
603617
|
+
}
|
|
603618
|
+
get currentModel() {
|
|
603619
|
+
return this.config.model;
|
|
603620
|
+
}
|
|
603621
|
+
get currentMode() {
|
|
603622
|
+
return this.config.mode;
|
|
603623
|
+
}
|
|
603624
|
+
get pendingTranscript() {
|
|
603625
|
+
return this.pendingText;
|
|
603626
|
+
}
|
|
603627
|
+
setModel(model) {
|
|
603628
|
+
this.config.model = model;
|
|
603629
|
+
}
|
|
603630
|
+
setMode(mode) {
|
|
603631
|
+
this.config.mode = mode;
|
|
603632
|
+
}
|
|
603633
|
+
// Mic level metering state (dBFS)
|
|
603634
|
+
micLevelDb = null;
|
|
603635
|
+
micNoiseFloorDb = null;
|
|
603636
|
+
micSpeechActive = false;
|
|
603637
|
+
lastLevelEmit = 0;
|
|
603638
|
+
/**
|
|
603639
|
+
* Spawn the mic capture process and wire PCM piping + level metering.
|
|
603640
|
+
* Shared by start() and resume() so both paths meter identically.
|
|
603641
|
+
*/
|
|
603642
|
+
spawnMicProcess(micCmd) {
|
|
603643
|
+
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
603644
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603645
|
+
env: { ...process.env }
|
|
603646
|
+
});
|
|
603647
|
+
updateListenLiveState({ micDevice: micCmd.device });
|
|
603648
|
+
this.micProcess.stdout?.on("data", (chunk) => {
|
|
603649
|
+
this.meterMicChunk(chunk);
|
|
603650
|
+
if (this.active && !this.paused && this.liveTranscriber) {
|
|
603651
|
+
this.liveTranscriber.write(chunk);
|
|
603652
|
+
}
|
|
603653
|
+
});
|
|
603654
|
+
this.micProcess.stderr?.on("data", () => {
|
|
603655
|
+
});
|
|
603656
|
+
onChildError(this.micProcess, (err) => {
|
|
603657
|
+
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
603658
|
+
this.stop();
|
|
603659
|
+
});
|
|
603660
|
+
onChildClose(this.micProcess, () => {
|
|
603661
|
+
if (this.active && !this.paused) {
|
|
603662
|
+
this.stop();
|
|
603663
|
+
}
|
|
603664
|
+
});
|
|
603665
|
+
}
|
|
603666
|
+
/**
|
|
603667
|
+
* Compute the PCM16 chunk level (dBFS), track an adaptive noise floor, and
|
|
603668
|
+
* flag speech when the level rises meaningfully above it. Drives the status
|
|
603669
|
+
* bar mic indicator and the /live dashboard ASR line.
|
|
603670
|
+
*/
|
|
603671
|
+
meterMicChunk(chunk) {
|
|
603672
|
+
const samples = Math.floor(chunk.length / 2);
|
|
603673
|
+
if (samples <= 0) return;
|
|
603674
|
+
let sumSquares = 0;
|
|
603675
|
+
const stride = samples > 4096 ? 4 : 1;
|
|
603676
|
+
let counted = 0;
|
|
603677
|
+
for (let i2 = 0; i2 + 1 < chunk.length; i2 += 2 * stride) {
|
|
603678
|
+
const v = chunk.readInt16LE(i2) / 32768;
|
|
603679
|
+
sumSquares += v * v;
|
|
603680
|
+
counted++;
|
|
603681
|
+
}
|
|
603682
|
+
if (counted === 0) return;
|
|
603683
|
+
const rms = Math.sqrt(sumSquares / counted);
|
|
603684
|
+
const levelDb = 20 * Math.log10(Math.max(1e-6, rms));
|
|
603685
|
+
this.micLevelDb = this.micLevelDb === null ? levelDb : levelDb > this.micLevelDb ? 0.6 * levelDb + 0.4 * this.micLevelDb : 0.2 * levelDb + 0.8 * this.micLevelDb;
|
|
603686
|
+
if (this.micNoiseFloorDb === null) this.micNoiseFloorDb = levelDb;
|
|
603687
|
+
else if (levelDb < this.micNoiseFloorDb) this.micNoiseFloorDb = 0.3 * levelDb + 0.7 * this.micNoiseFloorDb;
|
|
603688
|
+
else this.micNoiseFloorDb = 0.02 * levelDb + 0.98 * this.micNoiseFloorDb;
|
|
603689
|
+
const floor = this.micNoiseFloorDb;
|
|
603690
|
+
const speech = this.micLevelDb > Math.max(-52, floor + 8);
|
|
603691
|
+
const changed = speech !== this.micSpeechActive;
|
|
603692
|
+
this.micSpeechActive = speech;
|
|
603693
|
+
const now2 = Date.now();
|
|
603694
|
+
if (changed || now2 - this.lastLevelEmit >= 300) {
|
|
603695
|
+
this.lastLevelEmit = now2;
|
|
603696
|
+
updateListenLiveState({
|
|
603697
|
+
micLevelDb: Number(this.micLevelDb.toFixed(1)),
|
|
603698
|
+
noiseFloorDb: Number(floor.toFixed(1)),
|
|
603699
|
+
speechActive: speech
|
|
603700
|
+
});
|
|
603701
|
+
this.emit("level", {
|
|
603702
|
+
levelDb: this.micLevelDb,
|
|
603703
|
+
noiseFloorDb: floor,
|
|
603704
|
+
speechActive: speech
|
|
603705
|
+
});
|
|
603706
|
+
}
|
|
603707
|
+
}
|
|
603708
|
+
/** Check if transcribe-cli is available. */
|
|
603709
|
+
async isAvailable() {
|
|
603710
|
+
if (this.transcribeCliAvailable !== null)
|
|
603711
|
+
return this.transcribeCliAvailable;
|
|
603712
|
+
const tc = await this.loadTranscribeCli() || (await ensureManagedTranscribeCliNode() ? await this.loadTranscribeCli() : null);
|
|
603713
|
+
const transcribeCliReady = Boolean(tc && await ensureVenvForTranscribeCli());
|
|
603714
|
+
const whisperFallbackReady = Boolean(await findLiveWhisperScript());
|
|
603715
|
+
this.transcribeCliAvailable = transcribeCliReady || whisperFallbackReady;
|
|
603716
|
+
return this.transcribeCliAvailable;
|
|
603717
|
+
}
|
|
603718
|
+
/** Load transcribe-cli — bundled as a dependency of omnius. */
|
|
603719
|
+
async loadTranscribeCli() {
|
|
603720
|
+
try {
|
|
603721
|
+
const { createRequire: createRequire11 } = await import("node:module");
|
|
603722
|
+
const req3 = createRequire11(import.meta.url);
|
|
603723
|
+
return req3("transcribe-cli");
|
|
603724
|
+
} catch {
|
|
603725
|
+
}
|
|
603726
|
+
const managed = await loadManagedTranscribeCli();
|
|
603727
|
+
if (managed) return managed;
|
|
603728
|
+
try {
|
|
603729
|
+
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
603730
|
+
const tcPath = join125(globalRoot, "transcribe-cli");
|
|
603731
|
+
if (existsSync110(join125(tcPath, "dist", "index.js"))) {
|
|
603732
|
+
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
603733
|
+
if (loaded) return loaded;
|
|
603734
|
+
}
|
|
603735
|
+
} catch {
|
|
603736
|
+
}
|
|
603737
|
+
const nvmBase = join125(homedir39(), ".nvm", "versions", "node");
|
|
603738
|
+
if (existsSync110(nvmBase)) {
|
|
603739
|
+
try {
|
|
603740
|
+
const { readdirSync: readdirSync61 } = await import("node:fs");
|
|
603741
|
+
for (const ver of readdirSync61(nvmBase)) {
|
|
603742
|
+
const tcPath = join125(
|
|
603743
|
+
nvmBase,
|
|
603744
|
+
ver,
|
|
603745
|
+
"lib",
|
|
603746
|
+
"node_modules",
|
|
603747
|
+
"transcribe-cli"
|
|
603748
|
+
);
|
|
603749
|
+
if (existsSync110(join125(tcPath, "dist", "index.js"))) {
|
|
603750
|
+
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
603751
|
+
if (loaded) return loaded;
|
|
603752
|
+
}
|
|
603753
|
+
}
|
|
603754
|
+
} catch {
|
|
603755
|
+
}
|
|
603756
|
+
}
|
|
603757
|
+
return null;
|
|
603758
|
+
}
|
|
603759
|
+
/**
|
|
603760
|
+
* Start listening — captures microphone audio and feeds to TranscribeLive.
|
|
603761
|
+
*/
|
|
603762
|
+
async start() {
|
|
603763
|
+
if (this.active) return "Already listening.";
|
|
603764
|
+
updateListenLiveState({
|
|
603765
|
+
phase: "bootstrapping",
|
|
603766
|
+
model: this.config.model,
|
|
603767
|
+
backend: "",
|
|
603768
|
+
lastStatus: "starting ASR backend...",
|
|
603769
|
+
speechActive: false
|
|
603770
|
+
});
|
|
603771
|
+
const micCmd = await findMicCaptureCommand();
|
|
603772
|
+
if (!micCmd) {
|
|
603773
|
+
updateListenLiveState({ phase: "error", lastStatus: "no mic capture tool (arecord/sox/ffmpeg)" });
|
|
603774
|
+
return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
|
|
603775
|
+
}
|
|
603776
|
+
let tc = await this.loadTranscribeCli();
|
|
603777
|
+
if (!tc) {
|
|
603778
|
+
if (_bgInstallPromise) {
|
|
603779
|
+
this.emit("info", "Waiting for transcribe-cli install...");
|
|
603780
|
+
await _bgInstallPromise;
|
|
603781
|
+
this.transcribeCliAvailable = null;
|
|
603782
|
+
tc = await this.loadTranscribeCli();
|
|
603783
|
+
}
|
|
603784
|
+
if (!tc) {
|
|
603785
|
+
try {
|
|
603786
|
+
await ensureManagedTranscribeCliNode();
|
|
603787
|
+
this.transcribeCliAvailable = null;
|
|
603788
|
+
tc = await this.loadTranscribeCli();
|
|
603789
|
+
} catch {
|
|
603790
|
+
}
|
|
603791
|
+
}
|
|
603792
|
+
}
|
|
603793
|
+
let usedFallback = false;
|
|
603794
|
+
let transcribeCliError = null;
|
|
603795
|
+
if (tc) {
|
|
603796
|
+
const TranscribeLive = tc.TranscribeLive;
|
|
603797
|
+
if (TranscribeLive) {
|
|
603798
|
+
let tcUpToDate = false;
|
|
603799
|
+
try {
|
|
603800
|
+
const tcPkgPath = join125(
|
|
603801
|
+
dirname37(__require.resolve?.("transcribe-cli/package.json") || ""),
|
|
603802
|
+
"package.json"
|
|
603803
|
+
);
|
|
603804
|
+
if (existsSync110(tcPkgPath)) {
|
|
603805
|
+
const tcPkg = JSON.parse(
|
|
603806
|
+
__require("fs").readFileSync(tcPkgPath, "utf8")
|
|
603807
|
+
);
|
|
603808
|
+
tcUpToDate = tcPkg.version && tcPkg.version >= "2.0.1";
|
|
603809
|
+
}
|
|
603810
|
+
} catch {
|
|
603811
|
+
try {
|
|
603812
|
+
const out = await execFileText4("npm", ["list", "-g", "transcribe-cli", "--depth=0", "--json"], {
|
|
603813
|
+
timeout: 1e4
|
|
603814
|
+
});
|
|
603815
|
+
const parsed = JSON.parse(out);
|
|
603816
|
+
const ver = parsed?.dependencies?.["transcribe-cli"]?.version || "";
|
|
603817
|
+
tcUpToDate = ver >= "2.0.1";
|
|
603818
|
+
} catch {
|
|
603819
|
+
}
|
|
603820
|
+
}
|
|
603821
|
+
if (!tcUpToDate) {
|
|
603822
|
+
try {
|
|
603823
|
+
await ensureManagedTranscribeCliNode();
|
|
603824
|
+
this.transcribeCliAvailable = null;
|
|
603825
|
+
const reloaded = await this.loadTranscribeCli();
|
|
603826
|
+
if (reloaded?.TranscribeLive) {
|
|
603827
|
+
tc = reloaded;
|
|
603828
|
+
}
|
|
603829
|
+
} catch {
|
|
603830
|
+
}
|
|
603831
|
+
}
|
|
603832
|
+
const venvReady = await ensureVenvForTranscribeCli();
|
|
603833
|
+
if (!venvReady) {
|
|
603834
|
+
transcribeCliError = "venv Python missing numpy (required by transcribe-cli) — using whisper fallback";
|
|
603835
|
+
} else {
|
|
603836
|
+
let caughtUncaught = null;
|
|
603837
|
+
const uncaughtHandler = (err) => {
|
|
603838
|
+
const msg = err?.message || String(err);
|
|
603839
|
+
if (msg.includes("live_worker") || msg.includes("numpy") || msg.includes("transcribe_cli") || msg.includes("ModuleNotFoundError") || msg.includes("Traceback")) {
|
|
603840
|
+
caughtUncaught = err;
|
|
603841
|
+
} else {
|
|
603842
|
+
throw err;
|
|
603843
|
+
}
|
|
603844
|
+
};
|
|
603845
|
+
process.on("uncaughtException", uncaughtHandler);
|
|
603846
|
+
try {
|
|
603847
|
+
this.liveTranscriber = new TranscribeLive({
|
|
603848
|
+
model: this.config.model,
|
|
603849
|
+
sampleRate: 16e3,
|
|
603850
|
+
channels: 1,
|
|
603851
|
+
sampleWidth: 2,
|
|
603852
|
+
chunkDuration: 3
|
|
603853
|
+
});
|
|
603854
|
+
this.liveTranscriber.on(
|
|
603855
|
+
"transcript",
|
|
603856
|
+
(evt) => {
|
|
603857
|
+
if (!evt.text.trim()) return;
|
|
603858
|
+
this.lastTranscriptTime = Date.now();
|
|
603859
|
+
this.pendingText = evt.text.trim();
|
|
603860
|
+
updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
|
|
603861
|
+
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
603862
|
+
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
603863
|
+
}
|
|
603864
|
+
);
|
|
603865
|
+
this.liveTranscriber.on("error", (err) => {
|
|
603866
|
+
this.emit("error", err);
|
|
603867
|
+
});
|
|
603868
|
+
await new Promise((resolve76, reject) => {
|
|
603869
|
+
const timeout2 = setTimeout(
|
|
603870
|
+
() => reject(new Error("Model load timeout (60s)")),
|
|
603871
|
+
6e4
|
|
603872
|
+
);
|
|
603873
|
+
this.liveTranscriber.on("ready", () => {
|
|
603874
|
+
clearTimeout(timeout2);
|
|
603875
|
+
resolve76();
|
|
603876
|
+
});
|
|
603877
|
+
this.liveTranscriber.on("error", (err) => {
|
|
603878
|
+
clearTimeout(timeout2);
|
|
603879
|
+
reject(err);
|
|
603880
|
+
});
|
|
603881
|
+
});
|
|
603882
|
+
if (caughtUncaught) {
|
|
603883
|
+
throw caughtUncaught;
|
|
603884
|
+
}
|
|
603885
|
+
} catch (err) {
|
|
603886
|
+
try {
|
|
603887
|
+
this.liveTranscriber?.stop();
|
|
603888
|
+
} catch {
|
|
603889
|
+
}
|
|
603890
|
+
this.liveTranscriber = null;
|
|
603891
|
+
transcribeCliError = err instanceof Error ? err.message : String(err);
|
|
603892
|
+
} finally {
|
|
603893
|
+
process.removeListener("uncaughtException", uncaughtHandler);
|
|
603894
|
+
}
|
|
603895
|
+
}
|
|
603896
|
+
}
|
|
603897
|
+
}
|
|
603898
|
+
if (!this.liveTranscriber) {
|
|
603899
|
+
const scriptPath2 = await findLiveWhisperScript();
|
|
603900
|
+
if (!scriptPath2) {
|
|
603901
|
+
const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
|
|
603902
|
+
return `No transcription backend available.${hint} live-whisper.py not found.`;
|
|
603903
|
+
}
|
|
603904
|
+
try {
|
|
603905
|
+
const fallback = new WhisperFallbackTranscriber(
|
|
603906
|
+
this.config.model,
|
|
603907
|
+
scriptPath2
|
|
603908
|
+
);
|
|
603909
|
+
usedFallback = true;
|
|
603910
|
+
fallback.on("status", (msg) => {
|
|
603911
|
+
this.emit("info", msg);
|
|
603912
|
+
});
|
|
603913
|
+
await fallback.start();
|
|
603914
|
+
fallback.on("transcript", (evt) => {
|
|
603915
|
+
if (!evt.text.trim()) return;
|
|
603916
|
+
this.lastTranscriptTime = Date.now();
|
|
603917
|
+
this.pendingText = evt.text.trim();
|
|
603918
|
+
updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
|
|
603919
|
+
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
603920
|
+
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
603921
|
+
});
|
|
603922
|
+
fallback.on("error", (err) => {
|
|
603923
|
+
this.emit("error", err);
|
|
603924
|
+
});
|
|
603925
|
+
this.liveTranscriber = fallback;
|
|
603926
|
+
} catch (err) {
|
|
603927
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
603928
|
+
const tcHint = transcribeCliError ? `
|
|
603929
|
+
transcribe-cli error: ${transcribeCliError}` : "";
|
|
603930
|
+
updateListenLiveState({ phase: "error", lastStatus: msg.slice(0, 160) });
|
|
603931
|
+
return `Failed to start live transcription: ${msg}${tcHint}`;
|
|
603932
|
+
}
|
|
603933
|
+
}
|
|
603934
|
+
this.spawnMicProcess(micCmd);
|
|
603935
|
+
this.active = true;
|
|
603936
|
+
this.blinkState = true;
|
|
603937
|
+
this.blinkTimer = setInterval(() => {
|
|
603938
|
+
this.blinkState = !this.blinkState;
|
|
603939
|
+
this.emit("recording", this.blinkState);
|
|
603940
|
+
}, 500);
|
|
603941
|
+
this.lastTranscriptTime = Date.now();
|
|
603942
|
+
this.emit("started");
|
|
603943
|
+
this.emit("recording", true);
|
|
603944
|
+
const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
|
|
603945
|
+
const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
|
|
603946
|
+
updateListenLiveState({
|
|
603947
|
+
phase: "listening",
|
|
603948
|
+
backend,
|
|
603949
|
+
model: this.config.model,
|
|
603950
|
+
lastStatus: `listening (${modeDesc})`
|
|
603951
|
+
});
|
|
603952
|
+
return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
|
|
603953
|
+
}
|
|
603954
|
+
/**
|
|
603955
|
+
* Stop listening — cleanup mic, transcriber, timers.
|
|
603956
|
+
*/
|
|
603957
|
+
async stop() {
|
|
603958
|
+
if (!this.active) return "Not listening.";
|
|
603959
|
+
this.active = false;
|
|
603960
|
+
this.blinkState = false;
|
|
603961
|
+
updateListenLiveState({ phase: "idle", speechActive: false, micLevelDb: null, lastStatus: "stopped" });
|
|
603962
|
+
if (this.blinkTimer) {
|
|
603963
|
+
clearInterval(this.blinkTimer);
|
|
603964
|
+
this.blinkTimer = null;
|
|
603965
|
+
}
|
|
603966
|
+
if (this.silenceTimer) {
|
|
603967
|
+
clearTimeout(this.silenceTimer);
|
|
603968
|
+
this.silenceTimer = null;
|
|
603969
|
+
}
|
|
603970
|
+
if (this.countdownInterval) {
|
|
603971
|
+
clearInterval(this.countdownInterval);
|
|
603972
|
+
this.countdownInterval = null;
|
|
603973
|
+
}
|
|
603974
|
+
if (this.micProcess) {
|
|
603975
|
+
try {
|
|
603976
|
+
this.micProcess.kill("SIGTERM");
|
|
603977
|
+
} catch {
|
|
603978
|
+
}
|
|
603979
|
+
this.micProcess = null;
|
|
603980
|
+
}
|
|
603981
|
+
if (this.liveTranscriber) {
|
|
603982
|
+
try {
|
|
603983
|
+
this.liveTranscriber.stop();
|
|
603984
|
+
} catch {
|
|
603985
|
+
}
|
|
603986
|
+
this.liveTranscriber = null;
|
|
603987
|
+
}
|
|
603988
|
+
this.emit("recording", false);
|
|
603989
|
+
this.emit("stopped");
|
|
603990
|
+
const result = this.pendingText;
|
|
603991
|
+
this.pendingText = "";
|
|
603992
|
+
return result || "Stopped listening.";
|
|
603993
|
+
}
|
|
603994
|
+
/**
|
|
603995
|
+
* Accept the current pending transcript (for confirm mode).
|
|
603996
|
+
* Returns the text to inject into the input line.
|
|
603997
|
+
*/
|
|
603998
|
+
acceptTranscript() {
|
|
603999
|
+
const text2 = this.pendingText;
|
|
604000
|
+
this.pendingText = "";
|
|
604001
|
+
return text2;
|
|
604002
|
+
}
|
|
604003
|
+
/**
|
|
604004
|
+
* Pause microphone capture (for voicechat during TTS output).
|
|
604005
|
+
* Keeps transcriber alive but stops feeding it audio.
|
|
604006
|
+
*/
|
|
604007
|
+
pause() {
|
|
604008
|
+
if (!this.active || this.paused) return;
|
|
604009
|
+
this.paused = true;
|
|
604010
|
+
if (this.blinkTimer) {
|
|
604011
|
+
clearInterval(this.blinkTimer);
|
|
604012
|
+
this.blinkTimer = null;
|
|
604013
|
+
}
|
|
604014
|
+
this.blinkState = false;
|
|
604015
|
+
if (this.micProcess) {
|
|
604016
|
+
try {
|
|
604017
|
+
this.micProcess.kill("SIGTERM");
|
|
604018
|
+
} catch {
|
|
604019
|
+
}
|
|
604020
|
+
this.micProcess = null;
|
|
604021
|
+
}
|
|
604022
|
+
updateListenLiveState({ phase: "paused", speechActive: false, lastStatus: "mic paused (TTS speaking)" });
|
|
604023
|
+
this.emit("paused");
|
|
604024
|
+
this.emit("recording", false);
|
|
604025
|
+
}
|
|
604026
|
+
/**
|
|
604027
|
+
* Resume microphone capture after pause.
|
|
604028
|
+
*/
|
|
604029
|
+
async resume() {
|
|
604030
|
+
if (!this.active || !this.paused) return "Not paused.";
|
|
604031
|
+
this.paused = false;
|
|
604032
|
+
const micCmd = await findMicCaptureCommand();
|
|
604033
|
+
if (!micCmd) {
|
|
604034
|
+
return "No microphone capture tool found.";
|
|
604035
|
+
}
|
|
604036
|
+
this.spawnMicProcess(micCmd);
|
|
604037
|
+
this.blinkState = true;
|
|
604038
|
+
this.blinkTimer = setInterval(() => {
|
|
604039
|
+
this.blinkState = !this.blinkState;
|
|
604040
|
+
this.emit("recording", this.blinkState);
|
|
604041
|
+
}, 500);
|
|
604042
|
+
updateListenLiveState({ phase: "listening", lastStatus: "listening" });
|
|
604043
|
+
this.emit("resumed");
|
|
604044
|
+
this.emit("recording", true);
|
|
604045
|
+
return "Resumed listening.";
|
|
604046
|
+
}
|
|
604047
|
+
/**
|
|
604048
|
+
* Create a standalone transcriber for call sessions.
|
|
604049
|
+
* No microphone capture — just an ASR engine you feed PCM chunks to.
|
|
604050
|
+
* Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
|
|
604051
|
+
* Caller is responsible for calling .stop() when done.
|
|
604052
|
+
*/
|
|
604053
|
+
async createCallTranscriber() {
|
|
604054
|
+
let tc = await this.loadTranscribeCli();
|
|
604055
|
+
if (!tc && _bgInstallPromise) {
|
|
604056
|
+
await _bgInstallPromise;
|
|
604057
|
+
this.transcribeCliAvailable = null;
|
|
604058
|
+
tc = await this.loadTranscribeCli();
|
|
604059
|
+
}
|
|
604060
|
+
if (!tc) {
|
|
604061
|
+
try {
|
|
604062
|
+
await ensureManagedTranscribeCliNode();
|
|
604063
|
+
this.transcribeCliAvailable = null;
|
|
604064
|
+
tc = await this.loadTranscribeCli();
|
|
604065
|
+
} catch {
|
|
604066
|
+
}
|
|
604067
|
+
}
|
|
604068
|
+
if (tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
|
|
604069
|
+
try {
|
|
604070
|
+
const transcriber = new tc.TranscribeLive({
|
|
604071
|
+
model: this.config.model,
|
|
604072
|
+
sampleRate: 16e3,
|
|
604073
|
+
channels: 1,
|
|
604074
|
+
sampleWidth: 2,
|
|
604075
|
+
chunkDuration: 3
|
|
604076
|
+
});
|
|
604077
|
+
await new Promise((resolve76, reject) => {
|
|
604078
|
+
const timeout2 = setTimeout(
|
|
604079
|
+
() => reject(new Error("Model load timeout (60s)")),
|
|
604080
|
+
6e4
|
|
604081
|
+
);
|
|
604082
|
+
transcriber.on("ready", () => {
|
|
604083
|
+
clearTimeout(timeout2);
|
|
604084
|
+
resolve76();
|
|
604085
|
+
});
|
|
604086
|
+
transcriber.on("error", (err) => {
|
|
604087
|
+
clearTimeout(timeout2);
|
|
604088
|
+
reject(err);
|
|
604089
|
+
});
|
|
604090
|
+
});
|
|
604091
|
+
return transcriber;
|
|
604092
|
+
} catch {
|
|
604093
|
+
}
|
|
604094
|
+
}
|
|
604095
|
+
const scriptPath2 = await findLiveWhisperScript();
|
|
604096
|
+
if (!scriptPath2) return null;
|
|
604097
|
+
try {
|
|
604098
|
+
const fallback = new WhisperFallbackTranscriber(
|
|
604099
|
+
this.config.model,
|
|
604100
|
+
scriptPath2
|
|
604101
|
+
);
|
|
604102
|
+
await fallback.start();
|
|
604103
|
+
return fallback;
|
|
604104
|
+
} catch {
|
|
604105
|
+
return null;
|
|
604106
|
+
}
|
|
604107
|
+
}
|
|
604108
|
+
/**
|
|
604109
|
+
* Transcribe a file (audio or video) using transcribe-cli.
|
|
604110
|
+
* Returns the transcription result.
|
|
604111
|
+
*/
|
|
604112
|
+
async transcribeFile(filePath, outputDir2) {
|
|
604113
|
+
let tc = await this.loadTranscribeCli();
|
|
604114
|
+
if (!tc && _bgInstallPromise) {
|
|
604115
|
+
await _bgInstallPromise;
|
|
604116
|
+
this.transcribeCliAvailable = null;
|
|
604117
|
+
tc = await this.loadTranscribeCli();
|
|
604118
|
+
}
|
|
604119
|
+
if (!tc) {
|
|
604120
|
+
try {
|
|
604121
|
+
await ensureManagedTranscribeCliNode();
|
|
604122
|
+
this.transcribeCliAvailable = null;
|
|
604123
|
+
tc = await this.loadTranscribeCli();
|
|
604124
|
+
} catch {
|
|
604125
|
+
}
|
|
604126
|
+
}
|
|
604127
|
+
await ensureVenvForTranscribeCli();
|
|
604128
|
+
let lastErr = null;
|
|
604129
|
+
if (tc) {
|
|
604130
|
+
try {
|
|
604131
|
+
const result = await tc.transcribe(filePath, {
|
|
604132
|
+
model: this.config.model,
|
|
604133
|
+
format: "json",
|
|
604134
|
+
diarize: false,
|
|
604135
|
+
wordTimestamps: false
|
|
604136
|
+
});
|
|
604137
|
+
if (outputDir2) {
|
|
604138
|
+
const { basename: basename43 } = await import("node:path");
|
|
604139
|
+
const transcriptDir = join125(outputDir2, ".omnius", "transcripts");
|
|
604140
|
+
mkdirSync68(transcriptDir, { recursive: true });
|
|
604141
|
+
const outFile = join125(transcriptDir, `${basename43(filePath)}.txt`);
|
|
604142
|
+
writeFileSync57(outFile, result.text, "utf-8");
|
|
604143
|
+
}
|
|
604144
|
+
return {
|
|
604145
|
+
text: result.text,
|
|
604146
|
+
duration: result.duration,
|
|
604147
|
+
speakers: result.speakers,
|
|
604148
|
+
segments: result.segments
|
|
604149
|
+
};
|
|
604150
|
+
} catch (err) {
|
|
604151
|
+
lastErr = err;
|
|
604152
|
+
}
|
|
604153
|
+
}
|
|
604154
|
+
const fb = await transcribeFileViaWhisper(filePath, this.config.model);
|
|
604155
|
+
if (fb) {
|
|
604156
|
+
if (outputDir2) {
|
|
604157
|
+
const { basename: basename43 } = await import("node:path");
|
|
604158
|
+
const transcriptDir = join125(outputDir2, ".omnius", "transcripts");
|
|
604159
|
+
mkdirSync68(transcriptDir, { recursive: true });
|
|
604160
|
+
const outFile = join125(transcriptDir, `${basename43(filePath)}.txt`);
|
|
604161
|
+
writeFileSync57(outFile, fb.text, "utf-8");
|
|
604162
|
+
}
|
|
604163
|
+
return fb;
|
|
604164
|
+
}
|
|
604165
|
+
if (lastErr) {
|
|
604166
|
+
this.emit(
|
|
604167
|
+
"error",
|
|
604168
|
+
lastErr instanceof Error ? lastErr : new Error(String(lastErr))
|
|
604169
|
+
);
|
|
604170
|
+
}
|
|
604171
|
+
return null;
|
|
604172
|
+
}
|
|
604173
|
+
// -------------------------------------------------------------------------
|
|
604174
|
+
// Auto-mode silence detection
|
|
604175
|
+
// -------------------------------------------------------------------------
|
|
604176
|
+
resetSilenceTimer() {
|
|
604177
|
+
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
604178
|
+
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
|
604179
|
+
const timeoutMs = this.config.silenceTimeoutMs;
|
|
604180
|
+
const startTime = Date.now();
|
|
604181
|
+
this.countdownInterval = setInterval(() => {
|
|
604182
|
+
const elapsed = Date.now() - startTime;
|
|
604183
|
+
const remaining = Math.max(0, Math.ceil((timeoutMs - elapsed) / 1e3));
|
|
604184
|
+
this.emit("countdown", remaining);
|
|
604185
|
+
if (remaining <= 0 && this.countdownInterval) {
|
|
604186
|
+
clearInterval(this.countdownInterval);
|
|
604187
|
+
this.countdownInterval = null;
|
|
604188
|
+
}
|
|
604189
|
+
}, 1e3);
|
|
604190
|
+
this.silenceTimer = setTimeout(() => {
|
|
604191
|
+
if (this.active && this.pendingText) {
|
|
604192
|
+
this.emit("countdown", 0);
|
|
604193
|
+
this.emit("transcript", this.pendingText, true);
|
|
604194
|
+
}
|
|
604195
|
+
}, timeoutMs);
|
|
604196
|
+
}
|
|
604197
|
+
};
|
|
604198
|
+
_bgInstallPromise = null;
|
|
604199
|
+
_managedTranscribeCliInstallPromise = null;
|
|
604200
|
+
_engine = null;
|
|
604201
|
+
}
|
|
604202
|
+
});
|
|
604203
|
+
|
|
602843
604204
|
// packages/cli/src/tui/live-sensors.ts
|
|
602844
|
-
import { mkdirSync as
|
|
602845
|
-
import { basename as basename23, join as
|
|
604205
|
+
import { mkdirSync as mkdirSync69, readFileSync as readFileSync89, writeFileSync as writeFileSync58 } from "node:fs";
|
|
604206
|
+
import { basename as basename23, join as join126, relative as relative11 } from "node:path";
|
|
602846
604207
|
function liveDir(repoRoot) {
|
|
602847
|
-
return
|
|
604208
|
+
return join126(repoRoot, ".omnius", "live");
|
|
602848
604209
|
}
|
|
602849
604210
|
function liveConfigPath(repoRoot) {
|
|
602850
|
-
return
|
|
604211
|
+
return join126(liveDir(repoRoot), "config.json");
|
|
602851
604212
|
}
|
|
602852
604213
|
function liveSnapshotPath(repoRoot) {
|
|
602853
|
-
return
|
|
604214
|
+
return join126(liveDir(repoRoot), "latest.json");
|
|
602854
604215
|
}
|
|
602855
604216
|
function ensureLiveDir(repoRoot) {
|
|
602856
|
-
|
|
604217
|
+
mkdirSync69(liveDir(repoRoot), { recursive: true });
|
|
602857
604218
|
}
|
|
602858
604219
|
function nowIso3(ms) {
|
|
602859
604220
|
return new Date(ms).toISOString();
|
|
@@ -603175,7 +604536,7 @@ function sanitizeLiveAudioError(value2) {
|
|
|
603175
604536
|
}
|
|
603176
604537
|
function readPcm16WavActivity(filePath, bins = 36) {
|
|
603177
604538
|
try {
|
|
603178
|
-
const buf =
|
|
604539
|
+
const buf = readFileSync89(filePath);
|
|
603179
604540
|
if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") return void 0;
|
|
603180
604541
|
let offset = 12;
|
|
603181
604542
|
let channels = 1;
|
|
@@ -603753,15 +605114,32 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
|
|
|
603753
605114
|
const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
|
|
603754
605115
|
const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
|
|
603755
605116
|
const audioError = sanitizeLiveAudioError([snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | "));
|
|
605117
|
+
const asr = getListenLiveState();
|
|
605118
|
+
const asrPhaseLabel = {
|
|
605119
|
+
bootstrapping: "bootstrapping deps…",
|
|
605120
|
+
"loading-model": "loading model…",
|
|
605121
|
+
ready: "ready",
|
|
605122
|
+
listening: "listening",
|
|
605123
|
+
paused: "paused (TTS speaking)",
|
|
605124
|
+
error: "error"
|
|
605125
|
+
};
|
|
605126
|
+
const asrMicBits = asr.micLevelDb !== null ? ` mic=${asr.micLevelDb.toFixed(1)}dB${asr.speechActive ? " ●speech" : ""}` : "";
|
|
605127
|
+
const asrLine = asr.phase !== "idle" ? `│ ├─ whisper: ${[asr.backend, asr.model].filter(Boolean).join(" ") || "starting"} — ${asrPhaseLabel[asr.phase] ?? asr.phase}${asrMicBits}` : "";
|
|
605128
|
+
const asrHeardAge = asr.lastTranscriptAt > 0 ? Math.max(0, Math.round((now2 - asr.lastTranscriptAt) / 1e3)) : null;
|
|
605129
|
+
const asrHeardLine = asr.phase !== "idle" && asr.lastTranscript ? `│ ├─ heard${asrHeardAge !== null ? ` ${asrHeardAge}s ago` : ""}: ${trimOneLine(asr.lastTranscript, 120)}` : "";
|
|
605130
|
+
const asrStatusLine = (asr.phase === "bootstrapping" || asr.phase === "loading-model" || asr.phase === "error") && asr.lastStatus ? `│ ├─ whisper status: ${trimOneLine(asr.lastStatus, 120)}` : "";
|
|
603756
605131
|
const audioBits = [
|
|
603757
605132
|
`├─ input: ${snapshot.audio.input || snapshot.config.selectedAudioInput || "none"}`,
|
|
605133
|
+
asrLine,
|
|
605134
|
+
asrStatusLine,
|
|
605135
|
+
asrHeardLine,
|
|
603758
605136
|
snapshot.audio.activity ? `│ ├─ activity: ${snapshot.audio.activity.waveform} rms=${snapshot.audio.activity.rmsDb.toFixed(1)}dB active=${Math.round(snapshot.audio.activity.activeRatio * 100)}%` : "",
|
|
603759
605137
|
audioError ? `│ ├─ error: ${trimOneLine(audioError, 140)}` : "",
|
|
603760
605138
|
transcript ? `│ ├─ asr${snapshot.audio.asrBackend ? ` (${snapshot.audio.asrBackend})` : ""}: ${trimOneLine(transcript, 140)}` : "",
|
|
603761
605139
|
snapshot.audio.intake ? `│ ├─ intake: ${snapshot.audio.intake.intendedForAgent ? "for-agent" : "ambient"} conf=${snapshot.audio.intake.confidence.toFixed(2)} action=${snapshot.audio.intake.action}` : "",
|
|
603762
605140
|
snapshot.audio.analysis ? `│ └─ ${formatAudioAnalysisDashboardSummary(snapshot.audio.analysis)}` : ""
|
|
603763
605141
|
].filter(Boolean);
|
|
603764
|
-
for (const item of audioBits.slice(0,
|
|
605142
|
+
for (const item of audioBits.slice(0, 9)) pushDashboardWrapped(lines, item, width);
|
|
603765
605143
|
}
|
|
603766
605144
|
lines.push(`╰${horizontal}╯`);
|
|
603767
605145
|
return lines;
|
|
@@ -603945,15 +605323,23 @@ function parsePactlShortDevices(raw, kind) {
|
|
|
603945
605323
|
const parts = line.split(" ");
|
|
603946
605324
|
const name10 = parts[1]?.trim();
|
|
603947
605325
|
if (!name10) continue;
|
|
605326
|
+
const spec = (parts[3] ?? "").trim();
|
|
605327
|
+
const base3 = kind === "input" ? "Pulse/PipeWire source" : "Pulse/PipeWire sink";
|
|
603948
605328
|
devices.push({
|
|
603949
605329
|
id: `pulse:${name10}`,
|
|
603950
605330
|
label: name10,
|
|
603951
|
-
detail:
|
|
605331
|
+
detail: spec ? `${base3} · ${spec}` : base3,
|
|
603952
605332
|
source: "pulse"
|
|
603953
605333
|
});
|
|
603954
605334
|
}
|
|
603955
605335
|
return devices;
|
|
603956
605336
|
}
|
|
605337
|
+
function liveDeviceChannels(device) {
|
|
605338
|
+
const match = String(device?.detail ?? "").match(/(\d+)ch\b/);
|
|
605339
|
+
if (!match) return void 0;
|
|
605340
|
+
const channels = Number(match[1]);
|
|
605341
|
+
return Number.isFinite(channels) && channels > 0 ? channels : void 0;
|
|
605342
|
+
}
|
|
603957
605343
|
function parseCameraListJson(raw) {
|
|
603958
605344
|
try {
|
|
603959
605345
|
const parsed = JSON.parse(raw);
|
|
@@ -604001,7 +605387,7 @@ function parseCameraListText(raw) {
|
|
|
604001
605387
|
}
|
|
604002
605388
|
function loadLiveSensorConfig(repoRoot) {
|
|
604003
605389
|
try {
|
|
604004
|
-
const parsed = JSON.parse(
|
|
605390
|
+
const parsed = JSON.parse(readFileSync89(liveConfigPath(repoRoot), "utf8"));
|
|
604005
605391
|
return {
|
|
604006
605392
|
...DEFAULT_CONFIG7,
|
|
604007
605393
|
...parsed,
|
|
@@ -604015,7 +605401,7 @@ function loadLiveSensorConfig(repoRoot) {
|
|
|
604015
605401
|
}
|
|
604016
605402
|
function readLiveSensorSnapshot(repoRoot) {
|
|
604017
605403
|
try {
|
|
604018
|
-
const parsed = JSON.parse(
|
|
605404
|
+
const parsed = JSON.parse(readFileSync89(liveSnapshotPath(repoRoot), "utf8"));
|
|
604019
605405
|
return parsed?.version === 1 ? parsed : null;
|
|
604020
605406
|
} catch {
|
|
604021
605407
|
return null;
|
|
@@ -604220,12 +605606,13 @@ async function discoverAudioOutputs() {
|
|
|
604220
605606
|
if (devices.length === 0) devices.push({ id: "default", label: "default speaker", detail: "system default", source: "default" });
|
|
604221
605607
|
return { devices: dedupeDevices(devices), errors };
|
|
604222
605608
|
}
|
|
604223
|
-
async function recordAudioSample(device, durationSec, outputPath3) {
|
|
605609
|
+
async function recordAudioSample(device, durationSec, outputPath3, channels) {
|
|
604224
605610
|
const duration = Math.max(0.2, Math.min(30, Number.isFinite(durationSec) ? durationSec : 1));
|
|
604225
605611
|
const ffmpegSeconds = duration < 1 ? duration.toFixed(3) : String(Math.ceil(duration));
|
|
604226
605612
|
const arecordSeconds = String(Math.max(1, Math.ceil(duration)));
|
|
604227
605613
|
if (device.startsWith("pulse:") && await commandExists2("ffmpeg", 1500)) {
|
|
604228
605614
|
const source = device.slice("pulse:".length);
|
|
605615
|
+
const channelArgs = (channels ?? 1) > 2 ? ["-af", "pan=mono|c0=c0"] : ["-ac", "1"];
|
|
604229
605616
|
await execFileText4("ffmpeg", [
|
|
604230
605617
|
"-hide_banner",
|
|
604231
605618
|
"-loglevel",
|
|
@@ -604236,8 +605623,7 @@ async function recordAudioSample(device, durationSec, outputPath3) {
|
|
|
604236
605623
|
source,
|
|
604237
605624
|
"-t",
|
|
604238
605625
|
ffmpegSeconds,
|
|
604239
|
-
|
|
604240
|
-
"1",
|
|
605626
|
+
...channelArgs,
|
|
604241
605627
|
"-ar",
|
|
604242
605628
|
"16000",
|
|
604243
605629
|
"-acodec",
|
|
@@ -604346,8 +605732,8 @@ print(json.dumps({"ok": True, "scores": scores}))
|
|
|
604346
605732
|
return null;
|
|
604347
605733
|
}
|
|
604348
605734
|
}
|
|
604349
|
-
async function tryRecordAudioDevice(device, durationSec, outputPath3) {
|
|
604350
|
-
await recordAudioSample(device, durationSec, outputPath3);
|
|
605735
|
+
async function tryRecordAudioDevice(device, durationSec, outputPath3, channels) {
|
|
605736
|
+
await recordAudioSample(device, durationSec, outputPath3, channels);
|
|
604351
605737
|
}
|
|
604352
605738
|
function hasLiveAudioSignal(activity) {
|
|
604353
605739
|
if (!activity) return false;
|
|
@@ -604381,7 +605767,8 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604381
605767
|
for (const candidate of ordered) {
|
|
604382
605768
|
attempts.push(candidate);
|
|
604383
605769
|
try {
|
|
604384
|
-
|
|
605770
|
+
const candidateChannels = liveDeviceChannels(devices.find((entry) => entry.id === candidate));
|
|
605771
|
+
await tryRecordAudioDevice(candidate, durationSec, outputPath3, candidateChannels);
|
|
604385
605772
|
const activity = readPcm16WavActivity(outputPath3);
|
|
604386
605773
|
const method = audioCaptureMethod(candidate);
|
|
604387
605774
|
const signalDetected = hasLiveAudioSignal(activity);
|
|
@@ -604393,7 +605780,7 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604393
605780
|
device: candidate,
|
|
604394
605781
|
method,
|
|
604395
605782
|
activity,
|
|
604396
|
-
bytes:
|
|
605783
|
+
bytes: readFileSync89(outputPath3),
|
|
604397
605784
|
errors: [...errors]
|
|
604398
605785
|
};
|
|
604399
605786
|
}
|
|
@@ -604403,7 +605790,7 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604403
605790
|
}
|
|
604404
605791
|
}
|
|
604405
605792
|
if (firstQuietCapture) {
|
|
604406
|
-
|
|
605793
|
+
writeFileSync58(outputPath3, firstQuietCapture.bytes);
|
|
604407
605794
|
return {
|
|
604408
605795
|
ok: true,
|
|
604409
605796
|
device: firstQuietCapture.device,
|
|
@@ -604499,6 +605886,7 @@ var init_live_sensors = __esm({
|
|
|
604499
605886
|
init_dist5();
|
|
604500
605887
|
init_async_process();
|
|
604501
605888
|
init_image_ascii_preview();
|
|
605889
|
+
init_listen();
|
|
604502
605890
|
MIN_VIDEO_INTERVAL_MS = 250;
|
|
604503
605891
|
LOW_LATENCY_VIDEO_INTERVAL_MS = 250;
|
|
604504
605892
|
MIN_AUDIO_INTERVAL_MS = 1500;
|
|
@@ -605167,7 +606555,7 @@ ${output}`).join("\n\n");
|
|
|
605167
606555
|
async previewAudioInput(device = this.config.selectedAudioInput) {
|
|
605168
606556
|
const input = String(device || "").trim() || this.resolveAudioInput();
|
|
605169
606557
|
ensureLiveDir(this.repoRoot);
|
|
605170
|
-
const recordingPath =
|
|
606558
|
+
const recordingPath = join126(liveDir(this.repoRoot), `audio-preview-${Date.now()}.wav`);
|
|
605171
606559
|
const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, 0.8, recordingPath, {
|
|
605172
606560
|
allowFallback: false
|
|
605173
606561
|
});
|
|
@@ -605216,7 +606604,7 @@ ${output}`).join("\n\n");
|
|
|
605216
606604
|
const input = this.resolveAudioInput();
|
|
605217
606605
|
try {
|
|
605218
606606
|
ensureLiveDir(this.repoRoot);
|
|
605219
|
-
const recordingPath =
|
|
606607
|
+
const recordingPath = join126(liveDir(this.repoRoot), "audio-activity.wav");
|
|
605220
606608
|
const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, AUDIO_ACTIVITY_SAMPLE_SEC, recordingPath, {
|
|
605221
606609
|
allowFallback: false,
|
|
605222
606610
|
requireSignal: false
|
|
@@ -605259,7 +606647,7 @@ ${output}`).join("\n\n");
|
|
|
605259
606647
|
}
|
|
605260
606648
|
const outputDir2 = liveDir(this.repoRoot);
|
|
605261
606649
|
ensureLiveDir(this.repoRoot);
|
|
605262
|
-
const recordingPath =
|
|
606650
|
+
const recordingPath = join126(outputDir2, `audio-${Date.now()}.wav`);
|
|
605263
606651
|
let transcript = "";
|
|
605264
606652
|
let asrBackend = "";
|
|
605265
606653
|
let analysis = "";
|
|
@@ -605571,7 +606959,7 @@ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=
|
|
|
605571
606959
|
}
|
|
605572
606960
|
persist() {
|
|
605573
606961
|
ensureLiveDir(this.repoRoot);
|
|
605574
|
-
|
|
606962
|
+
writeFileSync58(liveConfigPath(this.repoRoot), JSON.stringify(this.config, null, 2), "utf8");
|
|
605575
606963
|
this.snapshot = {
|
|
605576
606964
|
...this.snapshot,
|
|
605577
606965
|
version: 1,
|
|
@@ -605579,7 +606967,7 @@ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=
|
|
|
605579
606967
|
config: this.config,
|
|
605580
606968
|
devices: this.devices
|
|
605581
606969
|
};
|
|
605582
|
-
|
|
606970
|
+
writeFileSync58(liveSnapshotPath(this.repoRoot), JSON.stringify(this.snapshot, null, 2), "utf8");
|
|
605583
606971
|
}
|
|
605584
606972
|
};
|
|
605585
606973
|
}
|
|
@@ -605725,8 +607113,8 @@ var init_tool_adapter = __esm({
|
|
|
605725
607113
|
});
|
|
605726
607114
|
|
|
605727
607115
|
// packages/cli/src/tui/runtime-verification.ts
|
|
605728
|
-
import { existsSync as
|
|
605729
|
-
import { dirname as
|
|
607116
|
+
import { existsSync as existsSync111, readFileSync as readFileSync90, readdirSync as readdirSync36 } from "node:fs";
|
|
607117
|
+
import { dirname as dirname38, extname as extname14, join as join127, relative as relative12, resolve as resolve54 } from "node:path";
|
|
605730
607118
|
function safeRelative(root, file) {
|
|
605731
607119
|
const rel = relative12(root, file);
|
|
605732
607120
|
return rel && !rel.startsWith("..") ? rel : file;
|
|
@@ -605737,16 +607125,16 @@ function collectHtmlFiles(root, maxFiles) {
|
|
|
605737
607125
|
if (out.length >= maxFiles || depth > 8) return;
|
|
605738
607126
|
let entries;
|
|
605739
607127
|
try {
|
|
605740
|
-
entries =
|
|
607128
|
+
entries = readdirSync36(dir, { withFileTypes: true });
|
|
605741
607129
|
} catch {
|
|
605742
607130
|
return;
|
|
605743
607131
|
}
|
|
605744
607132
|
for (const entry of entries) {
|
|
605745
607133
|
if (out.length >= maxFiles) break;
|
|
605746
607134
|
if (entry.isDirectory()) {
|
|
605747
|
-
if (!SKIP_DIRS2.has(entry.name)) walk2(
|
|
607135
|
+
if (!SKIP_DIRS2.has(entry.name)) walk2(join127(dir, entry.name), depth + 1);
|
|
605748
607136
|
} else if (entry.isFile() && /\.html?$/i.test(entry.name)) {
|
|
605749
|
-
out.push(
|
|
607137
|
+
out.push(join127(dir, entry.name));
|
|
605750
607138
|
}
|
|
605751
607139
|
}
|
|
605752
607140
|
};
|
|
@@ -605771,15 +607159,15 @@ function resolveScriptSource(root, htmlFile, src2) {
|
|
|
605771
607159
|
const withoutHash = raw.split("#", 1)[0] ?? "";
|
|
605772
607160
|
const clean5 = withoutHash.split("?", 1)[0] ?? "";
|
|
605773
607161
|
if (!clean5 || !SCRIPT_EXTS.has(extname14(clean5).toLowerCase())) return null;
|
|
605774
|
-
const abs = clean5.startsWith("/") ? resolve54(root, `.${clean5}`) : resolve54(
|
|
607162
|
+
const abs = clean5.startsWith("/") ? resolve54(root, `.${clean5}`) : resolve54(dirname38(htmlFile), clean5);
|
|
605775
607163
|
const rel = relative12(root, abs);
|
|
605776
607164
|
if (rel.startsWith("..") || rel === "") return null;
|
|
605777
|
-
return
|
|
607165
|
+
return existsSync111(abs) ? abs : null;
|
|
605778
607166
|
}
|
|
605779
607167
|
function referencedScriptAssets(root, htmlFile) {
|
|
605780
607168
|
let html = "";
|
|
605781
607169
|
try {
|
|
605782
|
-
html =
|
|
607170
|
+
html = readFileSync90(htmlFile, "utf8");
|
|
605783
607171
|
} catch {
|
|
605784
607172
|
return [];
|
|
605785
607173
|
}
|
|
@@ -605809,7 +607197,7 @@ async function syntaxCheckJavaScriptModule(source) {
|
|
|
605809
607197
|
}
|
|
605810
607198
|
async function runStaticRuntimeAssetSyntaxCheck(projectRoot, options2 = {}) {
|
|
605811
607199
|
const root = resolve54(projectRoot);
|
|
605812
|
-
if (!
|
|
607200
|
+
if (!existsSync111(root)) {
|
|
605813
607201
|
return { ok: true, checkedAssets: 0, htmlFiles: 0, issues: [], skippedReason: "project root missing" };
|
|
605814
607202
|
}
|
|
605815
607203
|
const htmlFiles = collectHtmlFiles(root, options2.maxHtmlFiles ?? 80);
|
|
@@ -605822,7 +607210,7 @@ async function runStaticRuntimeAssetSyntaxCheck(projectRoot, options2 = {}) {
|
|
|
605822
607210
|
if (issueByAsset.has(assetFile)) continue;
|
|
605823
607211
|
let source = "";
|
|
605824
607212
|
try {
|
|
605825
|
-
source =
|
|
607213
|
+
source = readFileSync90(assetFile, "utf8");
|
|
605826
607214
|
} catch {
|
|
605827
607215
|
continue;
|
|
605828
607216
|
}
|
|
@@ -605895,1194 +607283,6 @@ var init_runtime_verification = __esm({
|
|
|
605895
607283
|
}
|
|
605896
607284
|
});
|
|
605897
607285
|
|
|
605898
|
-
// packages/cli/src/api/py-embed.ts
|
|
605899
|
-
var py_embed_exports = {};
|
|
605900
|
-
__export(py_embed_exports, {
|
|
605901
|
-
ensureEmbedDeps: () => ensureEmbedDeps,
|
|
605902
|
-
getVenvPython: () => getVenvPython,
|
|
605903
|
-
runEmbedAudio: () => runEmbedAudio,
|
|
605904
|
-
runEmbedImage: () => runEmbedImage,
|
|
605905
|
-
runEmbedText: () => runEmbedText,
|
|
605906
|
-
runTranscribeFile: () => runTranscribeFile
|
|
605907
|
-
});
|
|
605908
|
-
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
605909
|
-
import { existsSync as existsSync110, mkdirSync as mkdirSync68, writeFileSync as writeFileSync57 } from "node:fs";
|
|
605910
|
-
import { join as join126 } from "node:path";
|
|
605911
|
-
import { homedir as homedir38 } from "node:os";
|
|
605912
|
-
function getVenvDir() {
|
|
605913
|
-
return join126(homedir38(), ".omnius", "venv");
|
|
605914
|
-
}
|
|
605915
|
-
function getVenvPython() {
|
|
605916
|
-
const base3 = getVenvDir();
|
|
605917
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
605918
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
605919
|
-
return join126(base3, bin, exe);
|
|
605920
|
-
}
|
|
605921
|
-
function ensureEmbedDeps() {
|
|
605922
|
-
const venv = getVenvDir();
|
|
605923
|
-
let log22 = "";
|
|
605924
|
-
try {
|
|
605925
|
-
mkdirSync68(venv, { recursive: true });
|
|
605926
|
-
} catch {
|
|
605927
|
-
}
|
|
605928
|
-
if (!existsSync110(getVenvPython())) {
|
|
605929
|
-
const mk = spawnSync8("python3", ["-m", "venv", venv], { encoding: "utf8" });
|
|
605930
|
-
log22 += mk.stdout + mk.stderr;
|
|
605931
|
-
}
|
|
605932
|
-
const pip = process.platform === "win32" ? join126(venv, "Scripts", "pip.exe") : join126(venv, "bin", "pip");
|
|
605933
|
-
const up = spawnSync8(pip, ["install", "--upgrade", "pip", "setuptools", "wheel"], { encoding: "utf8", timeout: 3e5 });
|
|
605934
|
-
log22 += up.stdout + up.stderr;
|
|
605935
|
-
const asrPkgs = [
|
|
605936
|
-
"numpy==1.26.4",
|
|
605937
|
-
"soundfile",
|
|
605938
|
-
"faster-whisper",
|
|
605939
|
-
"openai-whisper",
|
|
605940
|
-
// Embedding helpers used elsewhere (kept lightweight compared to full torch stack)
|
|
605941
|
-
"Pillow"
|
|
605942
|
-
];
|
|
605943
|
-
const ins = spawnSync8(pip, ["install", "--upgrade", ...asrPkgs], { encoding: "utf8", timeout: 9e5 });
|
|
605944
|
-
log22 += ins.stdout + ins.stderr;
|
|
605945
|
-
let fullOk = true;
|
|
605946
|
-
if (process.env["OMNIUS_INSTALL_FULL_EMBED_DEPS"] === "1") {
|
|
605947
|
-
const fullPkgs = ["torch", "torchaudio", "open_clip_torch", "speechbrain"];
|
|
605948
|
-
const full = spawnSync8(pip, ["install", "--upgrade", ...fullPkgs], { encoding: "utf8", timeout: 18e5 });
|
|
605949
|
-
log22 += full.stdout + full.stderr;
|
|
605950
|
-
fullOk = full.status === 0;
|
|
605951
|
-
}
|
|
605952
|
-
const ok3 = ins.status === 0 && up.status === 0 && fullOk;
|
|
605953
|
-
return { ok: ok3, log: log22 };
|
|
605954
|
-
}
|
|
605955
|
-
function runEmbedImage(input) {
|
|
605956
|
-
const py = getVenvPython();
|
|
605957
|
-
const script = locateScript("embed-image.py");
|
|
605958
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
605959
|
-
try {
|
|
605960
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605961
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605962
|
-
} catch {
|
|
605963
|
-
}
|
|
605964
|
-
return null;
|
|
605965
|
-
}
|
|
605966
|
-
function runEmbedAudio(input) {
|
|
605967
|
-
const py = getVenvPython();
|
|
605968
|
-
const script = locateScript("embed-audio.py");
|
|
605969
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
605970
|
-
try {
|
|
605971
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605972
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605973
|
-
} catch {
|
|
605974
|
-
}
|
|
605975
|
-
return null;
|
|
605976
|
-
}
|
|
605977
|
-
function runEmbedText(text2) {
|
|
605978
|
-
const py = getVenvPython();
|
|
605979
|
-
const script = locateScript("embed-text.py");
|
|
605980
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify({ text: text2 }), encoding: "utf8" });
|
|
605981
|
-
try {
|
|
605982
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605983
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605984
|
-
} catch {
|
|
605985
|
-
}
|
|
605986
|
-
return null;
|
|
605987
|
-
}
|
|
605988
|
-
function runTranscribeFile(input) {
|
|
605989
|
-
const py = getVenvPython();
|
|
605990
|
-
const script = locateScript("transcribe-file.py");
|
|
605991
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8", timeout: 6e5 });
|
|
605992
|
-
try {
|
|
605993
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605994
|
-
if (typeof out.text === "string") return out.text;
|
|
605995
|
-
} catch {
|
|
605996
|
-
}
|
|
605997
|
-
return null;
|
|
605998
|
-
}
|
|
605999
|
-
function locateScript(name10) {
|
|
606000
|
-
const candidates = [
|
|
606001
|
-
join126(process.cwd(), "dist", "scripts", name10),
|
|
606002
|
-
join126(process.cwd(), name10),
|
|
606003
|
-
join126(process.cwd(), "scripts", name10)
|
|
606004
|
-
];
|
|
606005
|
-
for (const c9 of candidates) if (existsSync110(c9)) return c9;
|
|
606006
|
-
const fallback = join126(process.cwd(), name10);
|
|
606007
|
-
try {
|
|
606008
|
-
writeFileSync57(fallback, 'print("missing script")\n');
|
|
606009
|
-
} catch {
|
|
606010
|
-
}
|
|
606011
|
-
return fallback;
|
|
606012
|
-
}
|
|
606013
|
-
var init_py_embed = __esm({
|
|
606014
|
-
"packages/cli/src/api/py-embed.ts"() {
|
|
606015
|
-
"use strict";
|
|
606016
|
-
}
|
|
606017
|
-
});
|
|
606018
|
-
|
|
606019
|
-
// packages/cli/src/tui/listen.ts
|
|
606020
|
-
var listen_exports = {};
|
|
606021
|
-
__export(listen_exports, {
|
|
606022
|
-
ListenEngine: () => ListenEngine,
|
|
606023
|
-
ensureTranscribeCliBackground: () => ensureTranscribeCliBackground,
|
|
606024
|
-
getListenEngine: () => getListenEngine,
|
|
606025
|
-
isAudioPath: () => isAudioPath,
|
|
606026
|
-
isTranscribablePath: () => isTranscribablePath,
|
|
606027
|
-
isVideoPath: () => isVideoPath,
|
|
606028
|
-
transcribeFileViaWhisper: () => transcribeFileViaWhisper,
|
|
606029
|
-
waitForTranscribeCli: () => waitForTranscribeCli
|
|
606030
|
-
});
|
|
606031
|
-
import { spawn as spawn29 } from "node:child_process";
|
|
606032
|
-
import {
|
|
606033
|
-
existsSync as existsSync111,
|
|
606034
|
-
mkdirSync as mkdirSync69,
|
|
606035
|
-
writeFileSync as writeFileSync58,
|
|
606036
|
-
readdirSync as readdirSync36
|
|
606037
|
-
} from "node:fs";
|
|
606038
|
-
import { join as join127, dirname as dirname38 } from "node:path";
|
|
606039
|
-
import { homedir as homedir39 } from "node:os";
|
|
606040
|
-
import { fileURLToPath as fileURLToPath16 } from "node:url";
|
|
606041
|
-
import { EventEmitter as EventEmitter6 } from "node:events";
|
|
606042
|
-
import { createInterface as createInterface2 } from "node:readline";
|
|
606043
|
-
function isAudioPath(path12) {
|
|
606044
|
-
const ext = path12.toLowerCase().split(".").pop();
|
|
606045
|
-
return ext ? AUDIO_EXTENSIONS.has(`.${ext}`) : false;
|
|
606046
|
-
}
|
|
606047
|
-
function isVideoPath(path12) {
|
|
606048
|
-
const ext = path12.toLowerCase().split(".").pop();
|
|
606049
|
-
return ext ? VIDEO_EXTENSIONS2.has(`.${ext}`) : false;
|
|
606050
|
-
}
|
|
606051
|
-
function isTranscribablePath(path12) {
|
|
606052
|
-
return isAudioPath(path12) || isVideoPath(path12);
|
|
606053
|
-
}
|
|
606054
|
-
async function findMicCaptureCommand() {
|
|
606055
|
-
const platform7 = process.platform;
|
|
606056
|
-
if (platform7 === "linux") {
|
|
606057
|
-
if (await commandExists2("arecord")) {
|
|
606058
|
-
return {
|
|
606059
|
-
cmd: "arecord",
|
|
606060
|
-
args: [
|
|
606061
|
-
"-f",
|
|
606062
|
-
"S16_LE",
|
|
606063
|
-
"-r",
|
|
606064
|
-
"16000",
|
|
606065
|
-
"-c",
|
|
606066
|
-
"1",
|
|
606067
|
-
"-t",
|
|
606068
|
-
"raw",
|
|
606069
|
-
"-q",
|
|
606070
|
-
"-"
|
|
606071
|
-
]
|
|
606072
|
-
};
|
|
606073
|
-
}
|
|
606074
|
-
}
|
|
606075
|
-
if (platform7 === "darwin") {
|
|
606076
|
-
if (await commandExists2("sox")) {
|
|
606077
|
-
return {
|
|
606078
|
-
cmd: "sox",
|
|
606079
|
-
args: [
|
|
606080
|
-
"-d",
|
|
606081
|
-
"-t",
|
|
606082
|
-
"raw",
|
|
606083
|
-
"-r",
|
|
606084
|
-
"16000",
|
|
606085
|
-
"-c",
|
|
606086
|
-
"1",
|
|
606087
|
-
"-b",
|
|
606088
|
-
"16",
|
|
606089
|
-
"-e",
|
|
606090
|
-
"signed-integer",
|
|
606091
|
-
"-"
|
|
606092
|
-
]
|
|
606093
|
-
};
|
|
606094
|
-
}
|
|
606095
|
-
}
|
|
606096
|
-
if (await commandExists2("ffmpeg")) {
|
|
606097
|
-
if (platform7 === "linux") {
|
|
606098
|
-
return {
|
|
606099
|
-
cmd: "ffmpeg",
|
|
606100
|
-
args: [
|
|
606101
|
-
"-f",
|
|
606102
|
-
"pulse",
|
|
606103
|
-
"-i",
|
|
606104
|
-
"default",
|
|
606105
|
-
"-ar",
|
|
606106
|
-
"16000",
|
|
606107
|
-
"-ac",
|
|
606108
|
-
"1",
|
|
606109
|
-
"-f",
|
|
606110
|
-
"s16le",
|
|
606111
|
-
"-loglevel",
|
|
606112
|
-
"quiet",
|
|
606113
|
-
"pipe:1"
|
|
606114
|
-
]
|
|
606115
|
-
};
|
|
606116
|
-
} else if (platform7 === "darwin") {
|
|
606117
|
-
return {
|
|
606118
|
-
cmd: "ffmpeg",
|
|
606119
|
-
args: [
|
|
606120
|
-
"-f",
|
|
606121
|
-
"avfoundation",
|
|
606122
|
-
"-i",
|
|
606123
|
-
":0",
|
|
606124
|
-
"-ar",
|
|
606125
|
-
"16000",
|
|
606126
|
-
"-ac",
|
|
606127
|
-
"1",
|
|
606128
|
-
"-f",
|
|
606129
|
-
"s16le",
|
|
606130
|
-
"-loglevel",
|
|
606131
|
-
"quiet",
|
|
606132
|
-
"pipe:1"
|
|
606133
|
-
]
|
|
606134
|
-
};
|
|
606135
|
-
}
|
|
606136
|
-
}
|
|
606137
|
-
return null;
|
|
606138
|
-
}
|
|
606139
|
-
async function findTranscribeFileScript() {
|
|
606140
|
-
const thisDir = dirname38(fileURLToPath16(import.meta.url));
|
|
606141
|
-
const candidates = [
|
|
606142
|
-
join127(thisDir, "../../../../packages/execution/scripts/transcribe-file.py"),
|
|
606143
|
-
join127(thisDir, "../../../packages/execution/scripts/transcribe-file.py"),
|
|
606144
|
-
join127(thisDir, "../../execution/scripts/transcribe-file.py"),
|
|
606145
|
-
join127(thisDir, "../scripts/transcribe-file.py"),
|
|
606146
|
-
join127(thisDir, "../../scripts/transcribe-file.py")
|
|
606147
|
-
];
|
|
606148
|
-
for (const p2 of candidates) {
|
|
606149
|
-
if (existsSync111(p2)) return p2;
|
|
606150
|
-
}
|
|
606151
|
-
try {
|
|
606152
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606153
|
-
const candidates2 = [
|
|
606154
|
-
join127(globalRoot, "omnius", "dist", "scripts", "transcribe-file.py"),
|
|
606155
|
-
join127(globalRoot, "omnius", "scripts", "transcribe-file.py")
|
|
606156
|
-
];
|
|
606157
|
-
for (const p2 of candidates2) {
|
|
606158
|
-
if (existsSync111(p2)) return p2;
|
|
606159
|
-
}
|
|
606160
|
-
} catch {
|
|
606161
|
-
}
|
|
606162
|
-
return null;
|
|
606163
|
-
}
|
|
606164
|
-
async function transcribeFileViaWhisper(filePath, model) {
|
|
606165
|
-
const script = await findTranscribeFileScript();
|
|
606166
|
-
if (!script) return null;
|
|
606167
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
606168
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
606169
|
-
const venvPython2 = join127(homedir39(), ".omnius", "venv", bin, exe);
|
|
606170
|
-
if (!existsSync111(venvPython2)) return null;
|
|
606171
|
-
return new Promise((resolve76) => {
|
|
606172
|
-
const child = spawn29(venvPython2, [script], {
|
|
606173
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606174
|
-
env: process.env
|
|
606175
|
-
});
|
|
606176
|
-
let out = "";
|
|
606177
|
-
let err = "";
|
|
606178
|
-
let timer = null;
|
|
606179
|
-
const stop2 = (val) => {
|
|
606180
|
-
if (timer) clearTimeout(timer);
|
|
606181
|
-
try {
|
|
606182
|
-
child.kill("SIGTERM");
|
|
606183
|
-
} catch {
|
|
606184
|
-
}
|
|
606185
|
-
resolve76(val ? parse3(val) : null);
|
|
606186
|
-
};
|
|
606187
|
-
function parse3(raw) {
|
|
606188
|
-
try {
|
|
606189
|
-
const j = JSON.parse(raw);
|
|
606190
|
-
if (j.error) return null;
|
|
606191
|
-
if (typeof j.text !== "string") return null;
|
|
606192
|
-
return {
|
|
606193
|
-
text: j.text.trim(),
|
|
606194
|
-
duration: null,
|
|
606195
|
-
speakers: [],
|
|
606196
|
-
segments: []
|
|
606197
|
-
};
|
|
606198
|
-
} catch {
|
|
606199
|
-
return null;
|
|
606200
|
-
}
|
|
606201
|
-
}
|
|
606202
|
-
child.stdout?.on("data", (d2) => {
|
|
606203
|
-
out += d2.toString("utf-8");
|
|
606204
|
-
});
|
|
606205
|
-
child.stderr?.on("data", (d2) => {
|
|
606206
|
-
err += d2.toString("utf-8");
|
|
606207
|
-
});
|
|
606208
|
-
child.once("error", () => stop2(null));
|
|
606209
|
-
child.once("close", () => {
|
|
606210
|
-
if (out.trim()) resolve76(parse3(out));
|
|
606211
|
-
else {
|
|
606212
|
-
void err;
|
|
606213
|
-
resolve76(null);
|
|
606214
|
-
}
|
|
606215
|
-
});
|
|
606216
|
-
timer = setTimeout(() => stop2(null), 12e4);
|
|
606217
|
-
try {
|
|
606218
|
-
child.stdin?.write(JSON.stringify({ path: filePath, model }) + "\n");
|
|
606219
|
-
child.stdin?.end();
|
|
606220
|
-
} catch {
|
|
606221
|
-
stop2(null);
|
|
606222
|
-
}
|
|
606223
|
-
});
|
|
606224
|
-
}
|
|
606225
|
-
async function findLiveWhisperScript() {
|
|
606226
|
-
const thisDir = dirname38(fileURLToPath16(import.meta.url));
|
|
606227
|
-
const candidates = [
|
|
606228
|
-
join127(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
|
|
606229
|
-
join127(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
|
|
606230
|
-
join127(thisDir, "../../execution/scripts/live-whisper.py"),
|
|
606231
|
-
// npm install layout — scripts bundled alongside dist
|
|
606232
|
-
join127(thisDir, "../scripts/live-whisper.py"),
|
|
606233
|
-
join127(thisDir, "../../scripts/live-whisper.py")
|
|
606234
|
-
];
|
|
606235
|
-
for (const p2 of candidates) {
|
|
606236
|
-
if (existsSync111(p2)) return p2;
|
|
606237
|
-
}
|
|
606238
|
-
try {
|
|
606239
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606240
|
-
const candidates2 = [
|
|
606241
|
-
join127(globalRoot, "omnius", "dist", "scripts", "live-whisper.py"),
|
|
606242
|
-
join127(globalRoot, "omnius", "scripts", "live-whisper.py")
|
|
606243
|
-
];
|
|
606244
|
-
for (const p2 of candidates2) {
|
|
606245
|
-
if (existsSync111(p2)) return p2;
|
|
606246
|
-
}
|
|
606247
|
-
} catch {
|
|
606248
|
-
}
|
|
606249
|
-
const nvmBase = join127(homedir39(), ".nvm", "versions", "node");
|
|
606250
|
-
if (existsSync111(nvmBase)) {
|
|
606251
|
-
try {
|
|
606252
|
-
for (const ver of readdirSync36(nvmBase)) {
|
|
606253
|
-
const p2 = join127(
|
|
606254
|
-
nvmBase,
|
|
606255
|
-
ver,
|
|
606256
|
-
"lib",
|
|
606257
|
-
"node_modules",
|
|
606258
|
-
"omnius",
|
|
606259
|
-
"dist",
|
|
606260
|
-
"scripts",
|
|
606261
|
-
"live-whisper.py"
|
|
606262
|
-
);
|
|
606263
|
-
if (existsSync111(p2)) return p2;
|
|
606264
|
-
}
|
|
606265
|
-
} catch {
|
|
606266
|
-
}
|
|
606267
|
-
}
|
|
606268
|
-
return null;
|
|
606269
|
-
}
|
|
606270
|
-
async function ensureVenvForTranscribeCli() {
|
|
606271
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
606272
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
606273
|
-
const venvBin = join127(homedir39(), ".omnius", "venv", bin);
|
|
606274
|
-
const venvPython2 = join127(venvBin, exe);
|
|
606275
|
-
if (!existsSync111(venvPython2)) {
|
|
606276
|
-
try {
|
|
606277
|
-
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
606278
|
-
if (typeof mod3.ensureEmbedDeps === "function") mod3.ensureEmbedDeps();
|
|
606279
|
-
} catch {
|
|
606280
|
-
}
|
|
606281
|
-
}
|
|
606282
|
-
if (!existsSync111(venvPython2)) {
|
|
606283
|
-
try {
|
|
606284
|
-
mkdirSync69(join127(homedir39(), ".omnius", "venv"), { recursive: true });
|
|
606285
|
-
await execFileText4("python3", ["-m", "venv", join127(homedir39(), ".omnius", "venv")], { timeout: 6e4 });
|
|
606286
|
-
} catch {
|
|
606287
|
-
return false;
|
|
606288
|
-
}
|
|
606289
|
-
}
|
|
606290
|
-
process.env.TRANSCRIBE_PYTHON = venvPython2;
|
|
606291
|
-
const pathSep2 = process.platform === "win32" ? ";" : ":";
|
|
606292
|
-
const currentPath = process.env.PATH || "";
|
|
606293
|
-
if (!currentPath.split(pathSep2).includes(venvBin)) {
|
|
606294
|
-
process.env.PATH = `${venvBin}${pathSep2}${currentPath}`;
|
|
606295
|
-
}
|
|
606296
|
-
try {
|
|
606297
|
-
await execFileText4(venvPython2, ["-c", "import numpy"], { timeout: 1e4 });
|
|
606298
|
-
} catch {
|
|
606299
|
-
try {
|
|
606300
|
-
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "pip", "setuptools", "wheel", "numpy==1.26.4"], { timeout: 3e5 });
|
|
606301
|
-
} catch {
|
|
606302
|
-
return false;
|
|
606303
|
-
}
|
|
606304
|
-
}
|
|
606305
|
-
try {
|
|
606306
|
-
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
606307
|
-
return true;
|
|
606308
|
-
} catch {
|
|
606309
|
-
}
|
|
606310
|
-
try {
|
|
606311
|
-
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "transcribe-cli"], { timeout: 3e5 });
|
|
606312
|
-
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
606313
|
-
return true;
|
|
606314
|
-
} catch {
|
|
606315
|
-
return false;
|
|
606316
|
-
}
|
|
606317
|
-
}
|
|
606318
|
-
async function requireTranscribeCliFrom2(packageDir) {
|
|
606319
|
-
try {
|
|
606320
|
-
const { createRequire: createRequire11 } = await import("node:module");
|
|
606321
|
-
const req3 = createRequire11(import.meta.url);
|
|
606322
|
-
const distPath = join127(packageDir, "dist", "index.js");
|
|
606323
|
-
if (existsSync111(distPath)) return req3(distPath);
|
|
606324
|
-
return req3(packageDir);
|
|
606325
|
-
} catch {
|
|
606326
|
-
return null;
|
|
606327
|
-
}
|
|
606328
|
-
}
|
|
606329
|
-
async function loadManagedTranscribeCli() {
|
|
606330
|
-
const packageDir = join127(MANAGED_TRANSCRIBE_CLI_DIR2, "node_modules", "transcribe-cli");
|
|
606331
|
-
if (!existsSync111(join127(packageDir, "dist", "index.js"))) return null;
|
|
606332
|
-
return requireTranscribeCliFrom2(packageDir);
|
|
606333
|
-
}
|
|
606334
|
-
async function ensureManagedTranscribeCliNode() {
|
|
606335
|
-
if (await loadManagedTranscribeCli()) return true;
|
|
606336
|
-
if (_managedTranscribeCliInstallPromise) return _managedTranscribeCliInstallPromise;
|
|
606337
|
-
_managedTranscribeCliInstallPromise = (async () => {
|
|
606338
|
-
try {
|
|
606339
|
-
mkdirSync69(MANAGED_TRANSCRIBE_CLI_DIR2, { recursive: true });
|
|
606340
|
-
if (!existsSync111(join127(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"))) {
|
|
606341
|
-
writeFileSync58(
|
|
606342
|
-
join127(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"),
|
|
606343
|
-
JSON.stringify({ private: true, dependencies: {} }, null, 2),
|
|
606344
|
-
"utf8"
|
|
606345
|
-
);
|
|
606346
|
-
}
|
|
606347
|
-
await execFileText4("npm", ["install", "--prefix", MANAGED_TRANSCRIBE_CLI_DIR2, "transcribe-cli@^2.0.1"], {
|
|
606348
|
-
timeout: 18e4
|
|
606349
|
-
});
|
|
606350
|
-
return Boolean(await loadManagedTranscribeCli());
|
|
606351
|
-
} catch {
|
|
606352
|
-
return false;
|
|
606353
|
-
}
|
|
606354
|
-
})();
|
|
606355
|
-
return _managedTranscribeCliInstallPromise;
|
|
606356
|
-
}
|
|
606357
|
-
function ensureTranscribeCliBackground() {
|
|
606358
|
-
if (_bgInstallPromise) return _bgInstallPromise;
|
|
606359
|
-
_bgInstallPromise = (async () => {
|
|
606360
|
-
const nodeReady = await ensureManagedTranscribeCliNode();
|
|
606361
|
-
const pyReady = await ensureVenvForTranscribeCli();
|
|
606362
|
-
return nodeReady && pyReady;
|
|
606363
|
-
})();
|
|
606364
|
-
return _bgInstallPromise;
|
|
606365
|
-
}
|
|
606366
|
-
async function waitForTranscribeCli() {
|
|
606367
|
-
if (_bgInstallPromise) {
|
|
606368
|
-
return _bgInstallPromise;
|
|
606369
|
-
}
|
|
606370
|
-
return Boolean(await loadManagedTranscribeCli()) && await ensureVenvForTranscribeCli();
|
|
606371
|
-
}
|
|
606372
|
-
function getListenEngine(config) {
|
|
606373
|
-
if (!_engine) {
|
|
606374
|
-
_engine = new ListenEngine(config);
|
|
606375
|
-
}
|
|
606376
|
-
return _engine;
|
|
606377
|
-
}
|
|
606378
|
-
var MANAGED_TRANSCRIBE_CLI_DIR2, AUDIO_EXTENSIONS, VIDEO_EXTENSIONS2, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _managedTranscribeCliInstallPromise, _engine;
|
|
606379
|
-
var init_listen = __esm({
|
|
606380
|
-
"packages/cli/src/tui/listen.ts"() {
|
|
606381
|
-
"use strict";
|
|
606382
|
-
init_typed_node_events();
|
|
606383
|
-
init_async_process();
|
|
606384
|
-
MANAGED_TRANSCRIBE_CLI_DIR2 = join127(homedir39(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
|
|
606385
|
-
AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
606386
|
-
".mp3",
|
|
606387
|
-
".wav",
|
|
606388
|
-
".flac",
|
|
606389
|
-
".aac",
|
|
606390
|
-
".m4a",
|
|
606391
|
-
".ogg",
|
|
606392
|
-
".wma",
|
|
606393
|
-
".opus"
|
|
606394
|
-
]);
|
|
606395
|
-
VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
606396
|
-
".mp4",
|
|
606397
|
-
".mkv",
|
|
606398
|
-
".avi",
|
|
606399
|
-
".mov",
|
|
606400
|
-
".webm",
|
|
606401
|
-
".flv",
|
|
606402
|
-
".wmv",
|
|
606403
|
-
".m4v",
|
|
606404
|
-
".ts"
|
|
606405
|
-
]);
|
|
606406
|
-
WhisperFallbackTranscriber = class extends EventEmitter6 {
|
|
606407
|
-
constructor(model, scriptPath2) {
|
|
606408
|
-
super();
|
|
606409
|
-
this.model = model;
|
|
606410
|
-
this.scriptPath = scriptPath2;
|
|
606411
|
-
}
|
|
606412
|
-
model;
|
|
606413
|
-
scriptPath;
|
|
606414
|
-
process = null;
|
|
606415
|
-
_ready = false;
|
|
606416
|
-
get ready() {
|
|
606417
|
-
return this._ready;
|
|
606418
|
-
}
|
|
606419
|
-
async start() {
|
|
606420
|
-
let pyPath = "python3";
|
|
606421
|
-
try {
|
|
606422
|
-
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
606423
|
-
const ensure = mod3.ensureEmbedDeps;
|
|
606424
|
-
const getPy = mod3.getVenvPython;
|
|
606425
|
-
if (typeof ensure === "function") {
|
|
606426
|
-
try {
|
|
606427
|
-
const res = ensure();
|
|
606428
|
-
if (res?.log) this.emit("status", res.log.slice(-500));
|
|
606429
|
-
} catch {
|
|
606430
|
-
}
|
|
606431
|
-
}
|
|
606432
|
-
if (typeof getPy === "function") {
|
|
606433
|
-
pyPath = getPy();
|
|
606434
|
-
}
|
|
606435
|
-
} catch {
|
|
606436
|
-
}
|
|
606437
|
-
this.process = spawn29(
|
|
606438
|
-
pyPath,
|
|
606439
|
-
[
|
|
606440
|
-
this.scriptPath,
|
|
606441
|
-
"--model",
|
|
606442
|
-
this.model,
|
|
606443
|
-
"--chunk-seconds",
|
|
606444
|
-
"3",
|
|
606445
|
-
"--window-seconds",
|
|
606446
|
-
"10"
|
|
606447
|
-
],
|
|
606448
|
-
{
|
|
606449
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606450
|
-
env: { ...process.env }
|
|
606451
|
-
}
|
|
606452
|
-
);
|
|
606453
|
-
return new Promise((resolve76, reject) => {
|
|
606454
|
-
const timeout2 = setTimeout(() => {
|
|
606455
|
-
reject(
|
|
606456
|
-
new Error(
|
|
606457
|
-
"Whisper fallback: model load timeout (5 min). First run downloads the model."
|
|
606458
|
-
)
|
|
606459
|
-
);
|
|
606460
|
-
}, 3e5);
|
|
606461
|
-
const rl = createInterface2({ input: this.process.stdout });
|
|
606462
|
-
onReadlineLine(rl, (line) => {
|
|
606463
|
-
try {
|
|
606464
|
-
const evt = JSON.parse(line);
|
|
606465
|
-
switch (evt.type) {
|
|
606466
|
-
case "status":
|
|
606467
|
-
this.emit("status", evt.message);
|
|
606468
|
-
break;
|
|
606469
|
-
case "ready":
|
|
606470
|
-
this._ready = true;
|
|
606471
|
-
clearTimeout(timeout2);
|
|
606472
|
-
this.emit("ready");
|
|
606473
|
-
resolve76();
|
|
606474
|
-
break;
|
|
606475
|
-
case "transcript":
|
|
606476
|
-
this.emit("transcript", {
|
|
606477
|
-
text: evt.text,
|
|
606478
|
-
isFinal: evt.isFinal ?? false
|
|
606479
|
-
});
|
|
606480
|
-
break;
|
|
606481
|
-
case "error":
|
|
606482
|
-
this.emit("error", new Error(evt.message));
|
|
606483
|
-
break;
|
|
606484
|
-
}
|
|
606485
|
-
} catch {
|
|
606486
|
-
}
|
|
606487
|
-
});
|
|
606488
|
-
this.process.stderr?.on("data", (data) => {
|
|
606489
|
-
const text2 = data.toString().trim();
|
|
606490
|
-
if (text2) this.emit("status", text2);
|
|
606491
|
-
});
|
|
606492
|
-
onChildError(this.process, (err) => {
|
|
606493
|
-
clearTimeout(timeout2);
|
|
606494
|
-
reject(err);
|
|
606495
|
-
});
|
|
606496
|
-
onChildClose(this.process, (code8) => {
|
|
606497
|
-
if (!this._ready) {
|
|
606498
|
-
clearTimeout(timeout2);
|
|
606499
|
-
reject(
|
|
606500
|
-
new Error(`Whisper worker exited with code ${code8} before ready`)
|
|
606501
|
-
);
|
|
606502
|
-
}
|
|
606503
|
-
});
|
|
606504
|
-
});
|
|
606505
|
-
}
|
|
606506
|
-
/** Write PCM16 audio data to the worker's stdin. */
|
|
606507
|
-
write(chunk) {
|
|
606508
|
-
if (this.process?.stdin?.writable) {
|
|
606509
|
-
this.process.stdin.write(chunk);
|
|
606510
|
-
}
|
|
606511
|
-
}
|
|
606512
|
-
/** Stop the worker. */
|
|
606513
|
-
stop() {
|
|
606514
|
-
if (this.process) {
|
|
606515
|
-
try {
|
|
606516
|
-
this.process.stdin?.end();
|
|
606517
|
-
this.process.kill("SIGTERM");
|
|
606518
|
-
} catch {
|
|
606519
|
-
}
|
|
606520
|
-
this.process = null;
|
|
606521
|
-
}
|
|
606522
|
-
this._ready = false;
|
|
606523
|
-
}
|
|
606524
|
-
};
|
|
606525
|
-
ListenEngine = class extends EventEmitter6 {
|
|
606526
|
-
config;
|
|
606527
|
-
micProcess = null;
|
|
606528
|
-
liveTranscriber = null;
|
|
606529
|
-
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
606530
|
-
active = false;
|
|
606531
|
-
paused = false;
|
|
606532
|
-
// Pause state for voicechat (stops mic but keeps transcriber)
|
|
606533
|
-
silenceTimer = null;
|
|
606534
|
-
countdownInterval = null;
|
|
606535
|
-
lastTranscriptTime = 0;
|
|
606536
|
-
pendingText = "";
|
|
606537
|
-
blinkTimer = null;
|
|
606538
|
-
blinkState = false;
|
|
606539
|
-
transcribeCliAvailable = null;
|
|
606540
|
-
constructor(config) {
|
|
606541
|
-
super();
|
|
606542
|
-
this.config = {
|
|
606543
|
-
model: config?.model ?? "base",
|
|
606544
|
-
mode: config?.mode ?? "confirm",
|
|
606545
|
-
silenceTimeoutMs: config?.silenceTimeoutMs ?? 3e3
|
|
606546
|
-
};
|
|
606547
|
-
}
|
|
606548
|
-
get isActive() {
|
|
606549
|
-
return this.active;
|
|
606550
|
-
}
|
|
606551
|
-
get isBlinking() {
|
|
606552
|
-
return this.active && this.blinkState;
|
|
606553
|
-
}
|
|
606554
|
-
get isPaused() {
|
|
606555
|
-
return this.paused;
|
|
606556
|
-
}
|
|
606557
|
-
get currentModel() {
|
|
606558
|
-
return this.config.model;
|
|
606559
|
-
}
|
|
606560
|
-
get currentMode() {
|
|
606561
|
-
return this.config.mode;
|
|
606562
|
-
}
|
|
606563
|
-
get pendingTranscript() {
|
|
606564
|
-
return this.pendingText;
|
|
606565
|
-
}
|
|
606566
|
-
setModel(model) {
|
|
606567
|
-
this.config.model = model;
|
|
606568
|
-
}
|
|
606569
|
-
setMode(mode) {
|
|
606570
|
-
this.config.mode = mode;
|
|
606571
|
-
}
|
|
606572
|
-
/** Check if transcribe-cli is available. */
|
|
606573
|
-
async isAvailable() {
|
|
606574
|
-
if (this.transcribeCliAvailable !== null)
|
|
606575
|
-
return this.transcribeCliAvailable;
|
|
606576
|
-
const tc = await this.loadTranscribeCli() || (await ensureManagedTranscribeCliNode() ? await this.loadTranscribeCli() : null);
|
|
606577
|
-
const transcribeCliReady = Boolean(tc && await ensureVenvForTranscribeCli());
|
|
606578
|
-
const whisperFallbackReady = Boolean(await findLiveWhisperScript());
|
|
606579
|
-
this.transcribeCliAvailable = transcribeCliReady || whisperFallbackReady;
|
|
606580
|
-
return this.transcribeCliAvailable;
|
|
606581
|
-
}
|
|
606582
|
-
/** Load transcribe-cli — bundled as a dependency of omnius. */
|
|
606583
|
-
async loadTranscribeCli() {
|
|
606584
|
-
try {
|
|
606585
|
-
const { createRequire: createRequire11 } = await import("node:module");
|
|
606586
|
-
const req3 = createRequire11(import.meta.url);
|
|
606587
|
-
return req3("transcribe-cli");
|
|
606588
|
-
} catch {
|
|
606589
|
-
}
|
|
606590
|
-
const managed = await loadManagedTranscribeCli();
|
|
606591
|
-
if (managed) return managed;
|
|
606592
|
-
try {
|
|
606593
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606594
|
-
const tcPath = join127(globalRoot, "transcribe-cli");
|
|
606595
|
-
if (existsSync111(join127(tcPath, "dist", "index.js"))) {
|
|
606596
|
-
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
606597
|
-
if (loaded) return loaded;
|
|
606598
|
-
}
|
|
606599
|
-
} catch {
|
|
606600
|
-
}
|
|
606601
|
-
const nvmBase = join127(homedir39(), ".nvm", "versions", "node");
|
|
606602
|
-
if (existsSync111(nvmBase)) {
|
|
606603
|
-
try {
|
|
606604
|
-
const { readdirSync: readdirSync61 } = await import("node:fs");
|
|
606605
|
-
for (const ver of readdirSync61(nvmBase)) {
|
|
606606
|
-
const tcPath = join127(
|
|
606607
|
-
nvmBase,
|
|
606608
|
-
ver,
|
|
606609
|
-
"lib",
|
|
606610
|
-
"node_modules",
|
|
606611
|
-
"transcribe-cli"
|
|
606612
|
-
);
|
|
606613
|
-
if (existsSync111(join127(tcPath, "dist", "index.js"))) {
|
|
606614
|
-
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
606615
|
-
if (loaded) return loaded;
|
|
606616
|
-
}
|
|
606617
|
-
}
|
|
606618
|
-
} catch {
|
|
606619
|
-
}
|
|
606620
|
-
}
|
|
606621
|
-
return null;
|
|
606622
|
-
}
|
|
606623
|
-
/**
|
|
606624
|
-
* Start listening — captures microphone audio and feeds to TranscribeLive.
|
|
606625
|
-
*/
|
|
606626
|
-
async start() {
|
|
606627
|
-
if (this.active) return "Already listening.";
|
|
606628
|
-
const micCmd = await findMicCaptureCommand();
|
|
606629
|
-
if (!micCmd) {
|
|
606630
|
-
return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
|
|
606631
|
-
}
|
|
606632
|
-
let tc = await this.loadTranscribeCli();
|
|
606633
|
-
if (!tc) {
|
|
606634
|
-
if (_bgInstallPromise) {
|
|
606635
|
-
this.emit("info", "Waiting for transcribe-cli install...");
|
|
606636
|
-
await _bgInstallPromise;
|
|
606637
|
-
this.transcribeCliAvailable = null;
|
|
606638
|
-
tc = await this.loadTranscribeCli();
|
|
606639
|
-
}
|
|
606640
|
-
if (!tc) {
|
|
606641
|
-
try {
|
|
606642
|
-
await ensureManagedTranscribeCliNode();
|
|
606643
|
-
this.transcribeCliAvailable = null;
|
|
606644
|
-
tc = await this.loadTranscribeCli();
|
|
606645
|
-
} catch {
|
|
606646
|
-
}
|
|
606647
|
-
}
|
|
606648
|
-
}
|
|
606649
|
-
let usedFallback = false;
|
|
606650
|
-
let transcribeCliError = null;
|
|
606651
|
-
if (tc) {
|
|
606652
|
-
const TranscribeLive = tc.TranscribeLive;
|
|
606653
|
-
if (TranscribeLive) {
|
|
606654
|
-
let tcUpToDate = false;
|
|
606655
|
-
try {
|
|
606656
|
-
const tcPkgPath = join127(
|
|
606657
|
-
dirname38(__require.resolve?.("transcribe-cli/package.json") || ""),
|
|
606658
|
-
"package.json"
|
|
606659
|
-
);
|
|
606660
|
-
if (existsSync111(tcPkgPath)) {
|
|
606661
|
-
const tcPkg = JSON.parse(
|
|
606662
|
-
__require("fs").readFileSync(tcPkgPath, "utf8")
|
|
606663
|
-
);
|
|
606664
|
-
tcUpToDate = tcPkg.version && tcPkg.version >= "2.0.1";
|
|
606665
|
-
}
|
|
606666
|
-
} catch {
|
|
606667
|
-
try {
|
|
606668
|
-
const out = await execFileText4("npm", ["list", "-g", "transcribe-cli", "--depth=0", "--json"], {
|
|
606669
|
-
timeout: 1e4
|
|
606670
|
-
});
|
|
606671
|
-
const parsed = JSON.parse(out);
|
|
606672
|
-
const ver = parsed?.dependencies?.["transcribe-cli"]?.version || "";
|
|
606673
|
-
tcUpToDate = ver >= "2.0.1";
|
|
606674
|
-
} catch {
|
|
606675
|
-
}
|
|
606676
|
-
}
|
|
606677
|
-
if (!tcUpToDate) {
|
|
606678
|
-
try {
|
|
606679
|
-
await ensureManagedTranscribeCliNode();
|
|
606680
|
-
this.transcribeCliAvailable = null;
|
|
606681
|
-
const reloaded = await this.loadTranscribeCli();
|
|
606682
|
-
if (reloaded?.TranscribeLive) {
|
|
606683
|
-
tc = reloaded;
|
|
606684
|
-
}
|
|
606685
|
-
} catch {
|
|
606686
|
-
}
|
|
606687
|
-
}
|
|
606688
|
-
const venvReady = await ensureVenvForTranscribeCli();
|
|
606689
|
-
if (!venvReady) {
|
|
606690
|
-
transcribeCliError = "venv Python missing numpy (required by transcribe-cli) — using whisper fallback";
|
|
606691
|
-
} else {
|
|
606692
|
-
let caughtUncaught = null;
|
|
606693
|
-
const uncaughtHandler = (err) => {
|
|
606694
|
-
const msg = err?.message || String(err);
|
|
606695
|
-
if (msg.includes("live_worker") || msg.includes("numpy") || msg.includes("transcribe_cli") || msg.includes("ModuleNotFoundError") || msg.includes("Traceback")) {
|
|
606696
|
-
caughtUncaught = err;
|
|
606697
|
-
} else {
|
|
606698
|
-
throw err;
|
|
606699
|
-
}
|
|
606700
|
-
};
|
|
606701
|
-
process.on("uncaughtException", uncaughtHandler);
|
|
606702
|
-
try {
|
|
606703
|
-
this.liveTranscriber = new TranscribeLive({
|
|
606704
|
-
model: this.config.model,
|
|
606705
|
-
sampleRate: 16e3,
|
|
606706
|
-
channels: 1,
|
|
606707
|
-
sampleWidth: 2,
|
|
606708
|
-
chunkDuration: 3
|
|
606709
|
-
});
|
|
606710
|
-
this.liveTranscriber.on(
|
|
606711
|
-
"transcript",
|
|
606712
|
-
(evt) => {
|
|
606713
|
-
if (!evt.text.trim()) return;
|
|
606714
|
-
this.lastTranscriptTime = Date.now();
|
|
606715
|
-
this.pendingText = evt.text.trim();
|
|
606716
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
606717
|
-
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
606718
|
-
}
|
|
606719
|
-
);
|
|
606720
|
-
this.liveTranscriber.on("error", (err) => {
|
|
606721
|
-
this.emit("error", err);
|
|
606722
|
-
});
|
|
606723
|
-
await new Promise((resolve76, reject) => {
|
|
606724
|
-
const timeout2 = setTimeout(
|
|
606725
|
-
() => reject(new Error("Model load timeout (60s)")),
|
|
606726
|
-
6e4
|
|
606727
|
-
);
|
|
606728
|
-
this.liveTranscriber.on("ready", () => {
|
|
606729
|
-
clearTimeout(timeout2);
|
|
606730
|
-
resolve76();
|
|
606731
|
-
});
|
|
606732
|
-
this.liveTranscriber.on("error", (err) => {
|
|
606733
|
-
clearTimeout(timeout2);
|
|
606734
|
-
reject(err);
|
|
606735
|
-
});
|
|
606736
|
-
});
|
|
606737
|
-
if (caughtUncaught) {
|
|
606738
|
-
throw caughtUncaught;
|
|
606739
|
-
}
|
|
606740
|
-
} catch (err) {
|
|
606741
|
-
try {
|
|
606742
|
-
this.liveTranscriber?.stop();
|
|
606743
|
-
} catch {
|
|
606744
|
-
}
|
|
606745
|
-
this.liveTranscriber = null;
|
|
606746
|
-
transcribeCliError = err instanceof Error ? err.message : String(err);
|
|
606747
|
-
} finally {
|
|
606748
|
-
process.removeListener("uncaughtException", uncaughtHandler);
|
|
606749
|
-
}
|
|
606750
|
-
}
|
|
606751
|
-
}
|
|
606752
|
-
}
|
|
606753
|
-
if (!this.liveTranscriber) {
|
|
606754
|
-
const scriptPath2 = await findLiveWhisperScript();
|
|
606755
|
-
if (!scriptPath2) {
|
|
606756
|
-
const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
|
|
606757
|
-
return `No transcription backend available.${hint} live-whisper.py not found.`;
|
|
606758
|
-
}
|
|
606759
|
-
try {
|
|
606760
|
-
const fallback = new WhisperFallbackTranscriber(
|
|
606761
|
-
this.config.model,
|
|
606762
|
-
scriptPath2
|
|
606763
|
-
);
|
|
606764
|
-
usedFallback = true;
|
|
606765
|
-
fallback.on("status", (msg) => {
|
|
606766
|
-
this.emit("info", msg);
|
|
606767
|
-
});
|
|
606768
|
-
await fallback.start();
|
|
606769
|
-
fallback.on("transcript", (evt) => {
|
|
606770
|
-
if (!evt.text.trim()) return;
|
|
606771
|
-
this.lastTranscriptTime = Date.now();
|
|
606772
|
-
this.pendingText = evt.text.trim();
|
|
606773
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
606774
|
-
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
606775
|
-
});
|
|
606776
|
-
fallback.on("error", (err) => {
|
|
606777
|
-
this.emit("error", err);
|
|
606778
|
-
});
|
|
606779
|
-
this.liveTranscriber = fallback;
|
|
606780
|
-
} catch (err) {
|
|
606781
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
606782
|
-
const tcHint = transcribeCliError ? `
|
|
606783
|
-
transcribe-cli error: ${transcribeCliError}` : "";
|
|
606784
|
-
return `Failed to start live transcription: ${msg}${tcHint}`;
|
|
606785
|
-
}
|
|
606786
|
-
}
|
|
606787
|
-
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
606788
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606789
|
-
env: { ...process.env }
|
|
606790
|
-
});
|
|
606791
|
-
this.micProcess.stdout?.on("data", (chunk) => {
|
|
606792
|
-
if (this.active && !this.paused && this.liveTranscriber) {
|
|
606793
|
-
this.liveTranscriber.write(chunk);
|
|
606794
|
-
}
|
|
606795
|
-
});
|
|
606796
|
-
this.micProcess.stderr?.on("data", () => {
|
|
606797
|
-
});
|
|
606798
|
-
onChildError(this.micProcess, (err) => {
|
|
606799
|
-
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
606800
|
-
this.stop();
|
|
606801
|
-
});
|
|
606802
|
-
onChildClose(this.micProcess, () => {
|
|
606803
|
-
if (this.active) {
|
|
606804
|
-
this.stop();
|
|
606805
|
-
}
|
|
606806
|
-
});
|
|
606807
|
-
this.active = true;
|
|
606808
|
-
this.blinkState = true;
|
|
606809
|
-
this.blinkTimer = setInterval(() => {
|
|
606810
|
-
this.blinkState = !this.blinkState;
|
|
606811
|
-
this.emit("recording", this.blinkState);
|
|
606812
|
-
}, 500);
|
|
606813
|
-
this.lastTranscriptTime = Date.now();
|
|
606814
|
-
this.emit("started");
|
|
606815
|
-
this.emit("recording", true);
|
|
606816
|
-
const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
|
|
606817
|
-
const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
|
|
606818
|
-
return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
|
|
606819
|
-
}
|
|
606820
|
-
/**
|
|
606821
|
-
* Stop listening — cleanup mic, transcriber, timers.
|
|
606822
|
-
*/
|
|
606823
|
-
async stop() {
|
|
606824
|
-
if (!this.active) return "Not listening.";
|
|
606825
|
-
this.active = false;
|
|
606826
|
-
this.blinkState = false;
|
|
606827
|
-
if (this.blinkTimer) {
|
|
606828
|
-
clearInterval(this.blinkTimer);
|
|
606829
|
-
this.blinkTimer = null;
|
|
606830
|
-
}
|
|
606831
|
-
if (this.silenceTimer) {
|
|
606832
|
-
clearTimeout(this.silenceTimer);
|
|
606833
|
-
this.silenceTimer = null;
|
|
606834
|
-
}
|
|
606835
|
-
if (this.countdownInterval) {
|
|
606836
|
-
clearInterval(this.countdownInterval);
|
|
606837
|
-
this.countdownInterval = null;
|
|
606838
|
-
}
|
|
606839
|
-
if (this.micProcess) {
|
|
606840
|
-
try {
|
|
606841
|
-
this.micProcess.kill("SIGTERM");
|
|
606842
|
-
} catch {
|
|
606843
|
-
}
|
|
606844
|
-
this.micProcess = null;
|
|
606845
|
-
}
|
|
606846
|
-
if (this.liveTranscriber) {
|
|
606847
|
-
try {
|
|
606848
|
-
this.liveTranscriber.stop();
|
|
606849
|
-
} catch {
|
|
606850
|
-
}
|
|
606851
|
-
this.liveTranscriber = null;
|
|
606852
|
-
}
|
|
606853
|
-
this.emit("recording", false);
|
|
606854
|
-
this.emit("stopped");
|
|
606855
|
-
const result = this.pendingText;
|
|
606856
|
-
this.pendingText = "";
|
|
606857
|
-
return result || "Stopped listening.";
|
|
606858
|
-
}
|
|
606859
|
-
/**
|
|
606860
|
-
* Accept the current pending transcript (for confirm mode).
|
|
606861
|
-
* Returns the text to inject into the input line.
|
|
606862
|
-
*/
|
|
606863
|
-
acceptTranscript() {
|
|
606864
|
-
const text2 = this.pendingText;
|
|
606865
|
-
this.pendingText = "";
|
|
606866
|
-
return text2;
|
|
606867
|
-
}
|
|
606868
|
-
/**
|
|
606869
|
-
* Pause microphone capture (for voicechat during TTS output).
|
|
606870
|
-
* Keeps transcriber alive but stops feeding it audio.
|
|
606871
|
-
*/
|
|
606872
|
-
pause() {
|
|
606873
|
-
if (!this.active || this.paused) return;
|
|
606874
|
-
this.paused = true;
|
|
606875
|
-
if (this.blinkTimer) {
|
|
606876
|
-
clearInterval(this.blinkTimer);
|
|
606877
|
-
this.blinkTimer = null;
|
|
606878
|
-
}
|
|
606879
|
-
this.blinkState = false;
|
|
606880
|
-
if (this.micProcess) {
|
|
606881
|
-
try {
|
|
606882
|
-
this.micProcess.kill("SIGTERM");
|
|
606883
|
-
} catch {
|
|
606884
|
-
}
|
|
606885
|
-
this.micProcess = null;
|
|
606886
|
-
}
|
|
606887
|
-
this.emit("paused");
|
|
606888
|
-
this.emit("recording", false);
|
|
606889
|
-
}
|
|
606890
|
-
/**
|
|
606891
|
-
* Resume microphone capture after pause.
|
|
606892
|
-
*/
|
|
606893
|
-
async resume() {
|
|
606894
|
-
if (!this.active || !this.paused) return "Not paused.";
|
|
606895
|
-
this.paused = false;
|
|
606896
|
-
const micCmd = await findMicCaptureCommand();
|
|
606897
|
-
if (!micCmd) {
|
|
606898
|
-
return "No microphone capture tool found.";
|
|
606899
|
-
}
|
|
606900
|
-
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
606901
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606902
|
-
env: { ...process.env }
|
|
606903
|
-
});
|
|
606904
|
-
this.micProcess.stdout?.on("data", (chunk) => {
|
|
606905
|
-
if (this.active && !this.paused && this.liveTranscriber) {
|
|
606906
|
-
this.liveTranscriber.write(chunk);
|
|
606907
|
-
}
|
|
606908
|
-
});
|
|
606909
|
-
this.micProcess.stderr?.on("data", () => {
|
|
606910
|
-
});
|
|
606911
|
-
onChildError(this.micProcess, (err) => {
|
|
606912
|
-
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
606913
|
-
this.stop();
|
|
606914
|
-
});
|
|
606915
|
-
onChildClose(this.micProcess, () => {
|
|
606916
|
-
if (this.active && !this.paused) {
|
|
606917
|
-
this.stop();
|
|
606918
|
-
}
|
|
606919
|
-
});
|
|
606920
|
-
this.blinkState = true;
|
|
606921
|
-
this.blinkTimer = setInterval(() => {
|
|
606922
|
-
this.blinkState = !this.blinkState;
|
|
606923
|
-
this.emit("recording", this.blinkState);
|
|
606924
|
-
}, 500);
|
|
606925
|
-
this.emit("resumed");
|
|
606926
|
-
this.emit("recording", true);
|
|
606927
|
-
return "Resumed listening.";
|
|
606928
|
-
}
|
|
606929
|
-
/**
|
|
606930
|
-
* Create a standalone transcriber for call sessions.
|
|
606931
|
-
* No microphone capture — just an ASR engine you feed PCM chunks to.
|
|
606932
|
-
* Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
|
|
606933
|
-
* Caller is responsible for calling .stop() when done.
|
|
606934
|
-
*/
|
|
606935
|
-
async createCallTranscriber() {
|
|
606936
|
-
let tc = await this.loadTranscribeCli();
|
|
606937
|
-
if (!tc && _bgInstallPromise) {
|
|
606938
|
-
await _bgInstallPromise;
|
|
606939
|
-
this.transcribeCliAvailable = null;
|
|
606940
|
-
tc = await this.loadTranscribeCli();
|
|
606941
|
-
}
|
|
606942
|
-
if (!tc) {
|
|
606943
|
-
try {
|
|
606944
|
-
await ensureManagedTranscribeCliNode();
|
|
606945
|
-
this.transcribeCliAvailable = null;
|
|
606946
|
-
tc = await this.loadTranscribeCli();
|
|
606947
|
-
} catch {
|
|
606948
|
-
}
|
|
606949
|
-
}
|
|
606950
|
-
if (tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
|
|
606951
|
-
try {
|
|
606952
|
-
const transcriber = new tc.TranscribeLive({
|
|
606953
|
-
model: this.config.model,
|
|
606954
|
-
sampleRate: 16e3,
|
|
606955
|
-
channels: 1,
|
|
606956
|
-
sampleWidth: 2,
|
|
606957
|
-
chunkDuration: 3
|
|
606958
|
-
});
|
|
606959
|
-
await new Promise((resolve76, reject) => {
|
|
606960
|
-
const timeout2 = setTimeout(
|
|
606961
|
-
() => reject(new Error("Model load timeout (60s)")),
|
|
606962
|
-
6e4
|
|
606963
|
-
);
|
|
606964
|
-
transcriber.on("ready", () => {
|
|
606965
|
-
clearTimeout(timeout2);
|
|
606966
|
-
resolve76();
|
|
606967
|
-
});
|
|
606968
|
-
transcriber.on("error", (err) => {
|
|
606969
|
-
clearTimeout(timeout2);
|
|
606970
|
-
reject(err);
|
|
606971
|
-
});
|
|
606972
|
-
});
|
|
606973
|
-
return transcriber;
|
|
606974
|
-
} catch {
|
|
606975
|
-
}
|
|
606976
|
-
}
|
|
606977
|
-
const scriptPath2 = await findLiveWhisperScript();
|
|
606978
|
-
if (!scriptPath2) return null;
|
|
606979
|
-
try {
|
|
606980
|
-
const fallback = new WhisperFallbackTranscriber(
|
|
606981
|
-
this.config.model,
|
|
606982
|
-
scriptPath2
|
|
606983
|
-
);
|
|
606984
|
-
await fallback.start();
|
|
606985
|
-
return fallback;
|
|
606986
|
-
} catch {
|
|
606987
|
-
return null;
|
|
606988
|
-
}
|
|
606989
|
-
}
|
|
606990
|
-
/**
|
|
606991
|
-
* Transcribe a file (audio or video) using transcribe-cli.
|
|
606992
|
-
* Returns the transcription result.
|
|
606993
|
-
*/
|
|
606994
|
-
async transcribeFile(filePath, outputDir2) {
|
|
606995
|
-
let tc = await this.loadTranscribeCli();
|
|
606996
|
-
if (!tc && _bgInstallPromise) {
|
|
606997
|
-
await _bgInstallPromise;
|
|
606998
|
-
this.transcribeCliAvailable = null;
|
|
606999
|
-
tc = await this.loadTranscribeCli();
|
|
607000
|
-
}
|
|
607001
|
-
if (!tc) {
|
|
607002
|
-
try {
|
|
607003
|
-
await ensureManagedTranscribeCliNode();
|
|
607004
|
-
this.transcribeCliAvailable = null;
|
|
607005
|
-
tc = await this.loadTranscribeCli();
|
|
607006
|
-
} catch {
|
|
607007
|
-
}
|
|
607008
|
-
}
|
|
607009
|
-
await ensureVenvForTranscribeCli();
|
|
607010
|
-
let lastErr = null;
|
|
607011
|
-
if (tc) {
|
|
607012
|
-
try {
|
|
607013
|
-
const result = await tc.transcribe(filePath, {
|
|
607014
|
-
model: this.config.model,
|
|
607015
|
-
format: "json",
|
|
607016
|
-
diarize: false,
|
|
607017
|
-
wordTimestamps: false
|
|
607018
|
-
});
|
|
607019
|
-
if (outputDir2) {
|
|
607020
|
-
const { basename: basename43 } = await import("node:path");
|
|
607021
|
-
const transcriptDir = join127(outputDir2, ".omnius", "transcripts");
|
|
607022
|
-
mkdirSync69(transcriptDir, { recursive: true });
|
|
607023
|
-
const outFile = join127(transcriptDir, `${basename43(filePath)}.txt`);
|
|
607024
|
-
writeFileSync58(outFile, result.text, "utf-8");
|
|
607025
|
-
}
|
|
607026
|
-
return {
|
|
607027
|
-
text: result.text,
|
|
607028
|
-
duration: result.duration,
|
|
607029
|
-
speakers: result.speakers,
|
|
607030
|
-
segments: result.segments
|
|
607031
|
-
};
|
|
607032
|
-
} catch (err) {
|
|
607033
|
-
lastErr = err;
|
|
607034
|
-
}
|
|
607035
|
-
}
|
|
607036
|
-
const fb = await transcribeFileViaWhisper(filePath, this.config.model);
|
|
607037
|
-
if (fb) {
|
|
607038
|
-
if (outputDir2) {
|
|
607039
|
-
const { basename: basename43 } = await import("node:path");
|
|
607040
|
-
const transcriptDir = join127(outputDir2, ".omnius", "transcripts");
|
|
607041
|
-
mkdirSync69(transcriptDir, { recursive: true });
|
|
607042
|
-
const outFile = join127(transcriptDir, `${basename43(filePath)}.txt`);
|
|
607043
|
-
writeFileSync58(outFile, fb.text, "utf-8");
|
|
607044
|
-
}
|
|
607045
|
-
return fb;
|
|
607046
|
-
}
|
|
607047
|
-
if (lastErr) {
|
|
607048
|
-
this.emit(
|
|
607049
|
-
"error",
|
|
607050
|
-
lastErr instanceof Error ? lastErr : new Error(String(lastErr))
|
|
607051
|
-
);
|
|
607052
|
-
}
|
|
607053
|
-
return null;
|
|
607054
|
-
}
|
|
607055
|
-
// -------------------------------------------------------------------------
|
|
607056
|
-
// Auto-mode silence detection
|
|
607057
|
-
// -------------------------------------------------------------------------
|
|
607058
|
-
resetSilenceTimer() {
|
|
607059
|
-
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
607060
|
-
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
|
607061
|
-
const timeoutMs = this.config.silenceTimeoutMs;
|
|
607062
|
-
const startTime = Date.now();
|
|
607063
|
-
this.countdownInterval = setInterval(() => {
|
|
607064
|
-
const elapsed = Date.now() - startTime;
|
|
607065
|
-
const remaining = Math.max(0, Math.ceil((timeoutMs - elapsed) / 1e3));
|
|
607066
|
-
this.emit("countdown", remaining);
|
|
607067
|
-
if (remaining <= 0 && this.countdownInterval) {
|
|
607068
|
-
clearInterval(this.countdownInterval);
|
|
607069
|
-
this.countdownInterval = null;
|
|
607070
|
-
}
|
|
607071
|
-
}, 1e3);
|
|
607072
|
-
this.silenceTimer = setTimeout(() => {
|
|
607073
|
-
if (this.active && this.pendingText) {
|
|
607074
|
-
this.emit("countdown", 0);
|
|
607075
|
-
this.emit("transcript", this.pendingText, true);
|
|
607076
|
-
}
|
|
607077
|
-
}, timeoutMs);
|
|
607078
|
-
}
|
|
607079
|
-
};
|
|
607080
|
-
_bgInstallPromise = null;
|
|
607081
|
-
_managedTranscribeCliInstallPromise = null;
|
|
607082
|
-
_engine = null;
|
|
607083
|
-
}
|
|
607084
|
-
});
|
|
607085
|
-
|
|
607086
607286
|
// node_modules/.pnpm/ws@8.20.1/node_modules/ws/lib/constants.js
|
|
607087
607287
|
var require_constants11 = __commonJS({
|
|
607088
607288
|
"node_modules/.pnpm/ws@8.20.1/node_modules/ws/lib/constants.js"(exports, module) {
|
|
@@ -625732,6 +625932,10 @@ var init_status_bar = __esm({
|
|
|
625732
625932
|
inputStateProvider = null;
|
|
625733
625933
|
/** Whether microphone is recording (blinking indicator) */
|
|
625734
625934
|
_recording = false;
|
|
625935
|
+
/** Whether speech is currently detected on the mic (from ListenEngine metering) */
|
|
625936
|
+
_micSpeech = false;
|
|
625937
|
+
/** Current mic input level in dBFS (rounded), null when unknown */
|
|
625938
|
+
_micLevelDb = null;
|
|
625735
625939
|
/** Countdown seconds for auto-mode silence timeout (0 = not counting down) */
|
|
625736
625940
|
_countdown = 0;
|
|
625737
625941
|
_recBlink = true;
|
|
@@ -626545,6 +626749,18 @@ var init_status_bar = __esm({
|
|
|
626545
626749
|
this._countdown = seconds;
|
|
626546
626750
|
if (this.active) this.renderFooterPreserveCursor();
|
|
626547
626751
|
}
|
|
626752
|
+
/**
|
|
626753
|
+
* Update live mic activity: whether speech is currently detected and the
|
|
626754
|
+
* current input level (dBFS). Drives the REC indicator so it reflects real
|
|
626755
|
+
* whisper-bound speech instead of only a blink timer.
|
|
626756
|
+
*/
|
|
626757
|
+
setMicActivity(speech, levelDb) {
|
|
626758
|
+
const level = typeof levelDb === "number" && Number.isFinite(levelDb) ? Math.round(levelDb) : null;
|
|
626759
|
+
if (speech === this._micSpeech && level === this._micLevelDb) return;
|
|
626760
|
+
this._micSpeech = speech;
|
|
626761
|
+
this._micLevelDb = level;
|
|
626762
|
+
if (this.active && this._recording) this.renderFooterPreserveCursor();
|
|
626763
|
+
}
|
|
626548
626764
|
/** Start or stop the braille-dot processing animation on the buffer line. */
|
|
626549
626765
|
setProcessing(active) {
|
|
626550
626766
|
if (active === this._processing) return;
|
|
@@ -629222,10 +629438,19 @@ ${CONTENT_BG_SEQ}`);
|
|
|
629222
629438
|
});
|
|
629223
629439
|
}
|
|
629224
629440
|
if (this._recording) {
|
|
629225
|
-
const dot = this._recBlink ? pastel2(210, "●") : " ";
|
|
629226
629441
|
const countdown = this._countdown > 0 ? c3.dim(` ${_StatusBar.digitBar(this._countdown)}s`) : "";
|
|
629227
|
-
const
|
|
629228
|
-
|
|
629442
|
+
const countdownW = this._countdown > 0 ? 1 + `${this._countdown}s`.length : 0;
|
|
629443
|
+
let recStr;
|
|
629444
|
+
let recW;
|
|
629445
|
+
if (this._micSpeech) {
|
|
629446
|
+
recStr = pastel2(210, "● REC") + countdown;
|
|
629447
|
+
recW = 5 + countdownW;
|
|
629448
|
+
} else {
|
|
629449
|
+
const dot = this._recBlink ? pastel2(210, "●") : c3.dim("○");
|
|
629450
|
+
const levelStr = this._micLevelDb !== null ? ` ${this._micLevelDb}dB` : "";
|
|
629451
|
+
recStr = dot + c3.dim(` mic${levelStr}`) + countdown;
|
|
629452
|
+
recW = 1 + 4 + levelStr.length + countdownW;
|
|
629453
|
+
}
|
|
629229
629454
|
sections.push({
|
|
629230
629455
|
expanded: recStr,
|
|
629231
629456
|
compact: recStr,
|
|
@@ -696176,6 +696401,19 @@ function repeatingCharPenalty(s2) {
|
|
|
696176
696401
|
if (cur > maxRun) maxRun = cur;
|
|
696177
696402
|
return Math.min(1, Math.max(0, (maxRun - 3) / 10));
|
|
696178
696403
|
}
|
|
696404
|
+
function gibberishTokenPenalty(s2) {
|
|
696405
|
+
const tokens = s2.trim().split(/\s+/).filter(Boolean);
|
|
696406
|
+
if (tokens.length < 2) return 0;
|
|
696407
|
+
let penalty = 0;
|
|
696408
|
+
const unique2 = new Set(tokens.map((t2) => t2.toLowerCase()));
|
|
696409
|
+
const uniqueRatio = unique2.size / tokens.length;
|
|
696410
|
+
if (tokens.length >= 4 && uniqueRatio <= 0.5) {
|
|
696411
|
+
penalty += Math.min(1, (0.5 - uniqueRatio) * 2 + 0.5);
|
|
696412
|
+
}
|
|
696413
|
+
const mixed = tokens.filter((t2) => /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{3,}$/.test(t2) && /[A-Za-z]\d|\d[A-Za-z]/.test(t2));
|
|
696414
|
+
if (mixed.length / tokens.length >= 0.5) penalty += 0.8;
|
|
696415
|
+
return Math.min(1, penalty);
|
|
696416
|
+
}
|
|
696179
696417
|
function computeSignalFromText(text2, confidence2) {
|
|
696180
696418
|
const t2 = text2.trim();
|
|
696181
696419
|
if (!t2) return 0;
|
|
@@ -696190,6 +696428,7 @@ function computeSignalFromText(text2, confidence2) {
|
|
|
696190
696428
|
else if (wc >= 1 && alpha >= 0.3 && len >= 4) score = 0.35;
|
|
696191
696429
|
else score = 0.15;
|
|
696192
696430
|
score -= repeatingCharPenalty(t2) * 0.4;
|
|
696431
|
+
score -= gibberishTokenPenalty(t2) * 0.8;
|
|
696193
696432
|
if (typeof confidence2 === "number" && !Number.isNaN(confidence2)) {
|
|
696194
696433
|
score = 0.7 * score + 0.3 * clamp0116(confidence2);
|
|
696195
696434
|
}
|
|
@@ -696198,6 +696437,9 @@ function computeSignalFromText(text2, confidence2) {
|
|
|
696198
696437
|
function truncateForLog(s2, n2) {
|
|
696199
696438
|
return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
|
|
696200
696439
|
}
|
|
696440
|
+
function sanitizeToolName2(name10) {
|
|
696441
|
+
return name10.trim().replace(/[({\[].*$/, "").replace(/[^A-Za-z0-9_-]/g, "");
|
|
696442
|
+
}
|
|
696201
696443
|
function extractToolJson(text2) {
|
|
696202
696444
|
const lines = text2.split(/\r?\n/);
|
|
696203
696445
|
for (const line of lines) {
|
|
@@ -696206,7 +696448,8 @@ function extractToolJson(text2) {
|
|
|
696206
696448
|
try {
|
|
696207
696449
|
const obj = JSON.parse(t2);
|
|
696208
696450
|
if (typeof obj.tool === "string") {
|
|
696209
|
-
const name10 = obj.tool;
|
|
696451
|
+
const name10 = sanitizeToolName2(obj.tool);
|
|
696452
|
+
if (!name10) continue;
|
|
696210
696453
|
const args = obj.args && typeof obj.args === "object" ? obj.args : {};
|
|
696211
696454
|
return { name: name10, args };
|
|
696212
696455
|
}
|
|
@@ -696224,18 +696467,38 @@ function extractToolJsonLoose(text2) {
|
|
|
696224
696467
|
try {
|
|
696225
696468
|
const obj = JSON.parse(match[0]);
|
|
696226
696469
|
if (typeof obj.tool === "string") {
|
|
696227
|
-
const
|
|
696228
|
-
|
|
696470
|
+
const name10 = sanitizeToolName2(obj.tool);
|
|
696471
|
+
if (name10) {
|
|
696472
|
+
const args = obj.args && typeof obj.args === "object" ? obj.args : {};
|
|
696473
|
+
return { name: name10, args };
|
|
696474
|
+
}
|
|
696229
696475
|
}
|
|
696230
696476
|
} catch {
|
|
696231
696477
|
}
|
|
696232
696478
|
}
|
|
696479
|
+
const nameMatch = stripped.match(/"tool"\s*:\s*"?\s*([A-Za-z0-9_-]+)/);
|
|
696480
|
+
if (nameMatch?.[1]) {
|
|
696481
|
+
const name10 = sanitizeToolName2(nameMatch[1]);
|
|
696482
|
+
if (name10) {
|
|
696483
|
+
let args = {};
|
|
696484
|
+
const argsMatch = stripped.match(/"args"\s*:\s*(\{[\s\S]*?\})/);
|
|
696485
|
+
if (argsMatch?.[1]) {
|
|
696486
|
+
try {
|
|
696487
|
+
const parsed = JSON.parse(argsMatch[1]);
|
|
696488
|
+
if (parsed && typeof parsed === "object") args = parsed;
|
|
696489
|
+
} catch {
|
|
696490
|
+
}
|
|
696491
|
+
}
|
|
696492
|
+
return { name: name10, args };
|
|
696493
|
+
}
|
|
696494
|
+
}
|
|
696233
696495
|
return null;
|
|
696234
696496
|
}
|
|
696235
696497
|
function stripToolJsonLines(text2) {
|
|
696236
696498
|
const lines = text2.split(/\r?\n/);
|
|
696237
696499
|
const kept = lines.filter((l2) => {
|
|
696238
696500
|
const t2 = l2.trim();
|
|
696501
|
+
if (/^\{\s*"?tool"?\s*[:=]/.test(t2)) return false;
|
|
696239
696502
|
if (!t2.startsWith("{") || !t2.endsWith("}")) return true;
|
|
696240
696503
|
try {
|
|
696241
696504
|
const obj = JSON.parse(t2);
|
|
@@ -696475,12 +696738,12 @@ Rules:
|
|
|
696475
696738
|
this.active = true;
|
|
696476
696739
|
this.context = [{ role: "system", content: SYSTEM_PROMPT2 }];
|
|
696477
696740
|
if (this.toolRelay) {
|
|
696478
|
-
this.toolCatalogNote = `Available tools
|
|
696479
|
-
|
|
696480
|
-
|
|
696481
|
-
|
|
696482
|
-
|
|
696483
|
-
|
|
696741
|
+
this.toolCatalogNote = `Available tools. To call one, emit exactly one line of valid JSON and nothing else:
|
|
696742
|
+
{"tool":"voice_env","args":{}} → environment facts (cwd, os, cpu, mem)
|
|
696743
|
+
{"tool":"voice_status","args":{}} → main-agent status
|
|
696744
|
+
{"tool":"voice_list_files","args":{"dir":"."}} → list directory (bounded)
|
|
696745
|
+
{"tool":"voice_read_file","args":{"path":"<file>","max":2048}} → read file snippet
|
|
696746
|
+
{"tool":"voice_to_main","args":{"message":"<task>","start":true}} → relay/start task`;
|
|
696484
696747
|
this.context.push({ role: "system", content: this.toolCatalogNote });
|
|
696485
696748
|
}
|
|
696486
696749
|
this.turnCount = 0;
|
|
@@ -696623,7 +696886,7 @@ Rules:
|
|
|
696623
696886
|
this.setState("LISTENING");
|
|
696624
696887
|
return;
|
|
696625
696888
|
}
|
|
696626
|
-
const score = this.lastSignalScore ?? computeSignalFromText(text2);
|
|
696889
|
+
const score = Math.min(this.lastSignalScore ?? 1, computeSignalFromText(text2));
|
|
696627
696890
|
if (score < MIN_SIGNAL_SCORE || NOISE_ONLY_RE.test(text2)) {
|
|
696628
696891
|
if (this.debugSnr) this.onStatus(`Ignoring low-signal utterance (SNR:${score.toFixed(2)}): ${truncateForLog(text2, 48)}`);
|
|
696629
696892
|
this.emit("snrFiltered", { score, text: text2 });
|
|
@@ -735615,6 +735878,9 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
735615
735878
|
engine.on("recording", (active) => {
|
|
735616
735879
|
statusBar.setRecording(active);
|
|
735617
735880
|
});
|
|
735881
|
+
engine.on("level", (evt) => {
|
|
735882
|
+
statusBar.setMicActivity(evt.speechActive, evt.levelDb);
|
|
735883
|
+
});
|
|
735618
735884
|
engine.on("countdown", (seconds) => {
|
|
735619
735885
|
statusBar.setCountdown(seconds);
|
|
735620
735886
|
});
|
|
@@ -735919,6 +736185,12 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
735919
736185
|
const { VoiceChatSession: VoiceChatSession2 } = await Promise.resolve().then(() => (init_voicechat(), voicechat_exports));
|
|
735920
736186
|
const { ListenEngine: ListenEngine2 } = await Promise.resolve().then(() => (init_listen(), listen_exports));
|
|
735921
736187
|
const listenEng = new ListenEngine2();
|
|
736188
|
+
listenEng.on("recording", (active) => {
|
|
736189
|
+
statusBar.setRecording(active);
|
|
736190
|
+
});
|
|
736191
|
+
listenEng.on("level", (evt) => {
|
|
736192
|
+
statusBar.setMicActivity(evt.speechActive, evt.levelDb);
|
|
736193
|
+
});
|
|
735922
736194
|
const summaryRunner = {
|
|
735923
736195
|
injectUserMessage(content) {
|
|
735924
736196
|
if (activeTask?.runner) {
|