omnius 1.0.416 → 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 +1670 -1263
- 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();
|
|
@@ -603131,25 +604492,29 @@ function liveDashboardRotationActionFromClick(snapshot, lineText, col) {
|
|
|
603131
604492
|
}
|
|
603132
604493
|
function pushDashboardWrapped(lines, value2, width) {
|
|
603133
604494
|
const contentWidth = Math.max(10, width - 2);
|
|
603134
|
-
const
|
|
604495
|
+
const normalized = stripDashboardAnsi(value2).replace(/\s+/g, " ").trimEnd();
|
|
604496
|
+
const leading = normalized.match(/^ */)?.[0] ?? "";
|
|
604497
|
+
const words = normalized.slice(leading.length).trimStart().split(" ").filter(Boolean);
|
|
603135
604498
|
if (words.length === 0) {
|
|
603136
604499
|
lines.push(`│${" ".repeat(contentWidth)}│`);
|
|
603137
604500
|
return;
|
|
603138
604501
|
}
|
|
603139
|
-
let current =
|
|
604502
|
+
let current = leading;
|
|
603140
604503
|
for (const word2 of words) {
|
|
603141
|
-
if (
|
|
603142
|
-
|
|
604504
|
+
if (current === leading) {
|
|
604505
|
+
const available = Math.max(1, contentWidth - leading.length);
|
|
604506
|
+
current = `${leading}${word2.length > available ? word2.slice(0, available) : word2}`;
|
|
603143
604507
|
continue;
|
|
603144
604508
|
}
|
|
603145
604509
|
if (current.length + 1 + word2.length <= contentWidth) {
|
|
603146
604510
|
current += ` ${word2}`;
|
|
603147
604511
|
} else {
|
|
603148
604512
|
lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
|
|
603149
|
-
|
|
604513
|
+
const available = Math.max(1, contentWidth - leading.length);
|
|
604514
|
+
current = `${leading}${word2.length > available ? word2.slice(0, available) : word2}`;
|
|
603150
604515
|
}
|
|
603151
604516
|
}
|
|
603152
|
-
if (current) lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
|
|
604517
|
+
if (current.trim()) lines.push(`│${truncateCell(` ${current}`, contentWidth)}│`);
|
|
603153
604518
|
}
|
|
603154
604519
|
function isAudioFailureText(value2) {
|
|
603155
604520
|
const text2 = String(value2 ?? "").trim();
|
|
@@ -603159,6 +604524,9 @@ function isAudioFailureText(value2) {
|
|
|
603159
604524
|
function sanitizeLiveAudioError(value2) {
|
|
603160
604525
|
const text2 = String(value2 ?? "").trim();
|
|
603161
604526
|
if (!text2) return "";
|
|
604527
|
+
if (/No live input signal detected after probing \d+ audio source/i.test(text2)) {
|
|
604528
|
+
return "No clear live input signal on selected audio input.";
|
|
604529
|
+
}
|
|
603162
604530
|
if (/transcribe-cli module:\s*No module named ['"]?transcribe_cli/i.test(text2) || /No module named ['"]?transcribe_cli/i.test(text2)) {
|
|
603163
604531
|
const parts = text2.split(/\s*\|\s*/g).map((part) => part.trim()).filter((part) => part && !/transcribe-cli module:\s*No module named ['"]?transcribe_cli/i.test(part) && !/No module named ['"]?transcribe_cli/i.test(part));
|
|
603164
604532
|
const preserved = parts.join(" | ").trim();
|
|
@@ -603168,7 +604536,7 @@ function sanitizeLiveAudioError(value2) {
|
|
|
603168
604536
|
}
|
|
603169
604537
|
function readPcm16WavActivity(filePath, bins = 36) {
|
|
603170
604538
|
try {
|
|
603171
|
-
const buf =
|
|
604539
|
+
const buf = readFileSync89(filePath);
|
|
603172
604540
|
if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") return void 0;
|
|
603173
604541
|
let offset = 12;
|
|
603174
604542
|
let channels = 1;
|
|
@@ -603541,6 +604909,104 @@ function cameraClassificationsSummary(camera, maxChars = 220) {
|
|
|
603541
604909
|
});
|
|
603542
604910
|
return trimOneLine(items.join("; "), maxChars);
|
|
603543
604911
|
}
|
|
604912
|
+
function cameraClassPriority(kind) {
|
|
604913
|
+
if (kind === "face" || kind === "identity") return 0;
|
|
604914
|
+
if (kind === "segment") return 1;
|
|
604915
|
+
if (kind === "object") return 2;
|
|
604916
|
+
if (kind === "track") return 3;
|
|
604917
|
+
if (kind === "clip") return 4;
|
|
604918
|
+
return 5;
|
|
604919
|
+
}
|
|
604920
|
+
function groupedDashboardClassifications(camera, kinds) {
|
|
604921
|
+
const wanted = new Set(kinds);
|
|
604922
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
604923
|
+
for (const item of camera.classifications ?? []) {
|
|
604924
|
+
if (!wanted.has(item.kind)) continue;
|
|
604925
|
+
const label = trimOneLine(item.label || item.kind, 64) || item.kind;
|
|
604926
|
+
const key = `${item.kind}:${label.toLowerCase()}`;
|
|
604927
|
+
const existing = grouped.get(key);
|
|
604928
|
+
const count = Math.max(1, item.count);
|
|
604929
|
+
if (!existing) {
|
|
604930
|
+
grouped.set(key, {
|
|
604931
|
+
kind: item.kind,
|
|
604932
|
+
label,
|
|
604933
|
+
count,
|
|
604934
|
+
confidence: item.confidence
|
|
604935
|
+
});
|
|
604936
|
+
continue;
|
|
604937
|
+
}
|
|
604938
|
+
existing.count += count;
|
|
604939
|
+
if (item.confidence !== void 0) {
|
|
604940
|
+
existing.confidence = existing.confidence === void 0 ? item.confidence : Math.max(existing.confidence, item.confidence);
|
|
604941
|
+
}
|
|
604942
|
+
}
|
|
604943
|
+
return [...grouped.values()].sort((a2, b) => {
|
|
604944
|
+
const priority = cameraClassPriority(a2.kind) - cameraClassPriority(b.kind);
|
|
604945
|
+
if (priority !== 0) return priority;
|
|
604946
|
+
return (b.confidence ?? 0) - (a2.confidence ?? 0) || b.count - a2.count || a2.label.localeCompare(b.label);
|
|
604947
|
+
});
|
|
604948
|
+
}
|
|
604949
|
+
function formatDashboardClassList(groups, maxItems = 6) {
|
|
604950
|
+
return groups.slice(0, maxItems).map((group) => {
|
|
604951
|
+
const count = group.count > 1 ? ` x${group.count}` : "";
|
|
604952
|
+
const confidence2 = group.confidence !== void 0 ? ` ${group.confidence.toFixed(2)}` : "";
|
|
604953
|
+
return `${group.label}${count}${confidence2}`;
|
|
604954
|
+
}).join("; ");
|
|
604955
|
+
}
|
|
604956
|
+
function cleanCameraSummaryForDashboard(value2) {
|
|
604957
|
+
const raw = String(value2 ?? "").trim();
|
|
604958
|
+
if (!raw) return "";
|
|
604959
|
+
const withoutClip = raw.split(/\s+\|\s+clip:/i)[0] ?? raw;
|
|
604960
|
+
return trimOneLine(
|
|
604961
|
+
withoutClip.replace(/\b(?:mask|crop)=\S+/gi, "").replace(/\bbbox=\[[^\]]*\]/gi, "").replace(/\s{2,}/g, " "),
|
|
604962
|
+
220
|
|
604963
|
+
);
|
|
604964
|
+
}
|
|
604965
|
+
function formatClipDashboardSummary(value2) {
|
|
604966
|
+
const text2 = String(value2 ?? "").trim();
|
|
604967
|
+
if (!text2) return "";
|
|
604968
|
+
const clipText = text2.match(/\bclip:\s*([\s\S]+)$/i)?.[1] ?? text2;
|
|
604969
|
+
const parsed = parseJsonObjectFromText(clipText);
|
|
604970
|
+
if (!parsed) return /\bclip:\s*/i.test(text2) ? `clip: ${trimOneLine(clipText, 120)}` : "";
|
|
604971
|
+
const matches = Array.isArray(parsed["matches"]) ? parsed["matches"] : [];
|
|
604972
|
+
if (matches.length > 0) {
|
|
604973
|
+
const labels = matches.slice(0, 4).map((match) => {
|
|
604974
|
+
const label = trimOneLine(match["label"] ?? match["name"] ?? match["title"] ?? match["id"] ?? "match", 48);
|
|
604975
|
+
const score = boundedConfidence(match["score"] ?? match["similarity"] ?? match["confidence"]);
|
|
604976
|
+
return `${label || "match"}${score !== void 0 ? ` ${score.toFixed(2)}` : ""}`;
|
|
604977
|
+
});
|
|
604978
|
+
return `clip: ${labels.join("; ")}`;
|
|
604979
|
+
}
|
|
604980
|
+
const recognized = Math.max(0, Number(parsed["recognized_count"] ?? 0));
|
|
604981
|
+
const extra = parsed["extra_labels"];
|
|
604982
|
+
if (Array.isArray(extra) && extra.length > 0) {
|
|
604983
|
+
return `clip: extra labels ${extra.slice(0, 4).map((entry) => trimOneLine(entry, 36)).join("; ")}`;
|
|
604984
|
+
}
|
|
604985
|
+
if (typeof extra === "string" && extra.trim()) return `clip: ${trimOneLine(extra, 120)}`;
|
|
604986
|
+
return recognized > 0 ? `clip: ${recognized} recognized match(es)` : "clip: no visual-memory matches";
|
|
604987
|
+
}
|
|
604988
|
+
function formatAudioAnalysisDashboardSummary(value2) {
|
|
604989
|
+
const text2 = String(value2 ?? "").trim();
|
|
604990
|
+
if (!text2) return "";
|
|
604991
|
+
const parsed = parseJsonObjectFromText(text2);
|
|
604992
|
+
const classifications = Array.isArray(parsed?.["classifications"]) ? parsed["classifications"] : [];
|
|
604993
|
+
if (classifications.length > 0) {
|
|
604994
|
+
return `sound classes: ${classifications.slice(0, 4).map((entry) => {
|
|
604995
|
+
const label = trimOneLine(entry["class"] ?? entry["label"] ?? entry["name"] ?? "sound", 44);
|
|
604996
|
+
const score = boundedConfidence(entry["score"] ?? entry["confidence"]);
|
|
604997
|
+
return `${label || "sound"}${score !== void 0 ? ` ${score.toFixed(2)}` : ""}`;
|
|
604998
|
+
}).join("; ")}`;
|
|
604999
|
+
}
|
|
605000
|
+
return `sound: ${trimOneLine(text2.replace(/\{[\s\S]*$/, ""), 140) || trimOneLine(text2, 140)}`;
|
|
605001
|
+
}
|
|
605002
|
+
function formatCameraRotationDashboard(camera) {
|
|
605003
|
+
const orientation = camera.orientation;
|
|
605004
|
+
const rotation = orientation?.rotation ?? camera.rotation ?? 0;
|
|
605005
|
+
const base3 = formatCameraRotation(rotation);
|
|
605006
|
+
if (!orientation) return base3;
|
|
605007
|
+
const confidence2 = orientation.confidence !== void 0 ? ` ${orientation.confidence.toFixed(2)}` : "";
|
|
605008
|
+
return `${base3} (${orientation.source}${confidence2})`;
|
|
605009
|
+
}
|
|
603544
605010
|
function cameraPaneBadge(camera) {
|
|
603545
605011
|
const classifications = camera.classifications ?? [];
|
|
603546
605012
|
const faceCount = classifications.filter((item) => item.kind === "face" || item.kind === "identity").reduce((sum, item) => sum + Math.max(1, item.count), 0);
|
|
@@ -603559,7 +605025,44 @@ function cameraPaneBadge(camera) {
|
|
|
603559
605025
|
}
|
|
603560
605026
|
function cameraSnapshotSummary(camera) {
|
|
603561
605027
|
if (camera.error) return `error: ${trimOneLine(camera.error, 160)}`;
|
|
603562
|
-
return camera.summary || (camera.classifications?.length ? `classifications: ${cameraClassificationsSummary(camera)}` : "") || summarizeInference(camera.inference);
|
|
605028
|
+
return cleanCameraSummaryForDashboard(camera.summary) || (camera.classifications?.length ? `classifications: ${cameraClassificationsSummary(camera)}` : "") || summarizeInference(camera.inference);
|
|
605029
|
+
}
|
|
605030
|
+
function cameraDashboardObservationLines(camera, now2, isLast) {
|
|
605031
|
+
const age = Math.max(0, Math.round((now2 - camera.updatedAt) / 1e3));
|
|
605032
|
+
const root = isLast ? "└─" : "├─";
|
|
605033
|
+
const stem = isLast ? " " : "│ ";
|
|
605034
|
+
const lines = [
|
|
605035
|
+
`${root} ${compactSourceId(camera.source)} age=${age}s rotation=${formatCameraRotationDashboard(camera)}`
|
|
605036
|
+
];
|
|
605037
|
+
if (camera.error) {
|
|
605038
|
+
lines.push(`${stem}└─ error: ${trimOneLine(camera.error, 180)}`);
|
|
605039
|
+
return lines;
|
|
605040
|
+
}
|
|
605041
|
+
const faces = formatDashboardClassList(groupedDashboardClassifications(camera, ["face", "identity"]), 4);
|
|
605042
|
+
const segments = formatDashboardClassList(groupedDashboardClassifications(camera, ["segment"]), 6);
|
|
605043
|
+
const objects = formatDashboardClassList(groupedDashboardClassifications(camera, ["object"]), 6);
|
|
605044
|
+
const tracks = formatDashboardClassList(groupedDashboardClassifications(camera, ["track"]), 4);
|
|
605045
|
+
const triggers = formatDashboardClassList(groupedDashboardClassifications(camera, ["trigger"]), 3);
|
|
605046
|
+
const clip3 = formatClipDashboardSummary(camera.summary) || formatDashboardClassList(groupedDashboardClassifications(camera, ["clip"]), 4);
|
|
605047
|
+
const summary = cameraSnapshotSummary(camera);
|
|
605048
|
+
const includeSummary = summary && !summary.startsWith("classifications:") && !(objects && /^objects:/i.test(summary)) && !(segments && /^segments?:/i.test(summary));
|
|
605049
|
+
const detailLines = [
|
|
605050
|
+
faces ? `faces: ${faces}` : "",
|
|
605051
|
+
segments ? `segments: ${segments}` : "",
|
|
605052
|
+
objects ? `objects: ${objects}` : "",
|
|
605053
|
+
tracks ? `tracks: ${tracks}` : "",
|
|
605054
|
+
triggers ? `triggers: ${triggers}` : "",
|
|
605055
|
+
includeSummary ? summary : "",
|
|
605056
|
+
clip3,
|
|
605057
|
+
camera.framePath ? `frame: ${camera.displayPath || camera.framePath}` : "",
|
|
605058
|
+
camera.orientation?.evidence ? `orientation: ${trimOneLine(camera.orientation.evidence, 120)}` : ""
|
|
605059
|
+
].filter(Boolean);
|
|
605060
|
+
const last2 = Math.max(0, detailLines.length - 1);
|
|
605061
|
+
detailLines.forEach((detail, index) => {
|
|
605062
|
+
lines.push(`${stem}${index === last2 ? "└─" : "├─"} ${detail}`);
|
|
605063
|
+
});
|
|
605064
|
+
if (detailLines.length === 0) lines.push(`${stem}└─ preview pending`);
|
|
605065
|
+
return lines;
|
|
603563
605066
|
}
|
|
603564
605067
|
function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
|
|
603565
605068
|
const width = Math.max(60, opts.width ?? 100);
|
|
@@ -603599,35 +605102,44 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
|
|
|
603599
605102
|
lines.push(`├${horizontal}┤`);
|
|
603600
605103
|
lines.push(`│${truncateCell(" camera observations", width - 2)}│`);
|
|
603601
605104
|
}
|
|
603602
|
-
|
|
603603
|
-
const
|
|
603604
|
-
const
|
|
603605
|
-
const classifications = cameraClassificationsSummary(camera);
|
|
603606
|
-
const detailLines = [
|
|
603607
|
-
`${compactSourceId(camera.source)} age=${age}s rotation=${rotation}`,
|
|
603608
|
-
classifications ? `classifications: ${classifications}` : "",
|
|
603609
|
-
cameraSnapshotSummary(camera),
|
|
603610
|
-
camera.framePath ? `frame: ${camera.displayPath || camera.framePath}` : "",
|
|
603611
|
-
camera.orientation?.evidence ? `orientation: ${trimOneLine(camera.orientation.evidence, 120)}` : ""
|
|
603612
|
-
].filter(Boolean);
|
|
603613
|
-
for (const detail of detailLines.length ? detailLines : ["preview pending"]) {
|
|
605105
|
+
cameras.forEach((camera, index) => {
|
|
605106
|
+
const detailLines = cameraDashboardObservationLines(camera, now2, index === cameras.length - 1);
|
|
605107
|
+
for (const detail of detailLines) {
|
|
603614
605108
|
pushDashboardWrapped(lines, detail, width);
|
|
603615
605109
|
}
|
|
603616
|
-
}
|
|
605110
|
+
});
|
|
603617
605111
|
if (snapshot.audio) {
|
|
603618
605112
|
lines.push(`├${horizontal}┤`);
|
|
605113
|
+
lines.push(`│${truncateCell(" audio monitor", width - 2)}│`);
|
|
603619
605114
|
const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
|
|
603620
605115
|
const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
|
|
603621
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)}` : "";
|
|
603622
605131
|
const audioBits = [
|
|
603623
|
-
|
|
603624
|
-
|
|
603625
|
-
|
|
603626
|
-
|
|
603627
|
-
snapshot.audio.
|
|
603628
|
-
|
|
605132
|
+
`├─ input: ${snapshot.audio.input || snapshot.config.selectedAudioInput || "none"}`,
|
|
605133
|
+
asrLine,
|
|
605134
|
+
asrStatusLine,
|
|
605135
|
+
asrHeardLine,
|
|
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)}%` : "",
|
|
605137
|
+
audioError ? `│ ├─ error: ${trimOneLine(audioError, 140)}` : "",
|
|
605138
|
+
transcript ? `│ ├─ asr${snapshot.audio.asrBackend ? ` (${snapshot.audio.asrBackend})` : ""}: ${trimOneLine(transcript, 140)}` : "",
|
|
605139
|
+
snapshot.audio.intake ? `│ ├─ intake: ${snapshot.audio.intake.intendedForAgent ? "for-agent" : "ambient"} conf=${snapshot.audio.intake.confidence.toFixed(2)} action=${snapshot.audio.intake.action}` : "",
|
|
605140
|
+
snapshot.audio.analysis ? `│ └─ ${formatAudioAnalysisDashboardSummary(snapshot.audio.analysis)}` : ""
|
|
603629
605141
|
].filter(Boolean);
|
|
603630
|
-
for (const item of audioBits.slice(0,
|
|
605142
|
+
for (const item of audioBits.slice(0, 9)) pushDashboardWrapped(lines, item, width);
|
|
603631
605143
|
}
|
|
603632
605144
|
lines.push(`╰${horizontal}╯`);
|
|
603633
605145
|
return lines;
|
|
@@ -603811,15 +605323,23 @@ function parsePactlShortDevices(raw, kind) {
|
|
|
603811
605323
|
const parts = line.split(" ");
|
|
603812
605324
|
const name10 = parts[1]?.trim();
|
|
603813
605325
|
if (!name10) continue;
|
|
605326
|
+
const spec = (parts[3] ?? "").trim();
|
|
605327
|
+
const base3 = kind === "input" ? "Pulse/PipeWire source" : "Pulse/PipeWire sink";
|
|
603814
605328
|
devices.push({
|
|
603815
605329
|
id: `pulse:${name10}`,
|
|
603816
605330
|
label: name10,
|
|
603817
|
-
detail:
|
|
605331
|
+
detail: spec ? `${base3} · ${spec}` : base3,
|
|
603818
605332
|
source: "pulse"
|
|
603819
605333
|
});
|
|
603820
605334
|
}
|
|
603821
605335
|
return devices;
|
|
603822
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
|
+
}
|
|
603823
605343
|
function parseCameraListJson(raw) {
|
|
603824
605344
|
try {
|
|
603825
605345
|
const parsed = JSON.parse(raw);
|
|
@@ -603867,7 +605387,7 @@ function parseCameraListText(raw) {
|
|
|
603867
605387
|
}
|
|
603868
605388
|
function loadLiveSensorConfig(repoRoot) {
|
|
603869
605389
|
try {
|
|
603870
|
-
const parsed = JSON.parse(
|
|
605390
|
+
const parsed = JSON.parse(readFileSync89(liveConfigPath(repoRoot), "utf8"));
|
|
603871
605391
|
return {
|
|
603872
605392
|
...DEFAULT_CONFIG7,
|
|
603873
605393
|
...parsed,
|
|
@@ -603881,7 +605401,7 @@ function loadLiveSensorConfig(repoRoot) {
|
|
|
603881
605401
|
}
|
|
603882
605402
|
function readLiveSensorSnapshot(repoRoot) {
|
|
603883
605403
|
try {
|
|
603884
|
-
const parsed = JSON.parse(
|
|
605404
|
+
const parsed = JSON.parse(readFileSync89(liveSnapshotPath(repoRoot), "utf8"));
|
|
603885
605405
|
return parsed?.version === 1 ? parsed : null;
|
|
603886
605406
|
} catch {
|
|
603887
605407
|
return null;
|
|
@@ -604086,12 +605606,13 @@ async function discoverAudioOutputs() {
|
|
|
604086
605606
|
if (devices.length === 0) devices.push({ id: "default", label: "default speaker", detail: "system default", source: "default" });
|
|
604087
605607
|
return { devices: dedupeDevices(devices), errors };
|
|
604088
605608
|
}
|
|
604089
|
-
async function recordAudioSample(device, durationSec, outputPath3) {
|
|
605609
|
+
async function recordAudioSample(device, durationSec, outputPath3, channels) {
|
|
604090
605610
|
const duration = Math.max(0.2, Math.min(30, Number.isFinite(durationSec) ? durationSec : 1));
|
|
604091
605611
|
const ffmpegSeconds = duration < 1 ? duration.toFixed(3) : String(Math.ceil(duration));
|
|
604092
605612
|
const arecordSeconds = String(Math.max(1, Math.ceil(duration)));
|
|
604093
605613
|
if (device.startsWith("pulse:") && await commandExists2("ffmpeg", 1500)) {
|
|
604094
605614
|
const source = device.slice("pulse:".length);
|
|
605615
|
+
const channelArgs = (channels ?? 1) > 2 ? ["-af", "pan=mono|c0=c0"] : ["-ac", "1"];
|
|
604095
605616
|
await execFileText4("ffmpeg", [
|
|
604096
605617
|
"-hide_banner",
|
|
604097
605618
|
"-loglevel",
|
|
@@ -604102,8 +605623,7 @@ async function recordAudioSample(device, durationSec, outputPath3) {
|
|
|
604102
605623
|
source,
|
|
604103
605624
|
"-t",
|
|
604104
605625
|
ffmpegSeconds,
|
|
604105
|
-
|
|
604106
|
-
"1",
|
|
605626
|
+
...channelArgs,
|
|
604107
605627
|
"-ar",
|
|
604108
605628
|
"16000",
|
|
604109
605629
|
"-acodec",
|
|
@@ -604212,8 +605732,8 @@ print(json.dumps({"ok": True, "scores": scores}))
|
|
|
604212
605732
|
return null;
|
|
604213
605733
|
}
|
|
604214
605734
|
}
|
|
604215
|
-
async function tryRecordAudioDevice(device, durationSec, outputPath3) {
|
|
604216
|
-
await recordAudioSample(device, durationSec, outputPath3);
|
|
605735
|
+
async function tryRecordAudioDevice(device, durationSec, outputPath3, channels) {
|
|
605736
|
+
await recordAudioSample(device, durationSec, outputPath3, channels);
|
|
604217
605737
|
}
|
|
604218
605738
|
function hasLiveAudioSignal(activity) {
|
|
604219
605739
|
if (!activity) return false;
|
|
@@ -604247,7 +605767,8 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604247
605767
|
for (const candidate of ordered) {
|
|
604248
605768
|
attempts.push(candidate);
|
|
604249
605769
|
try {
|
|
604250
|
-
|
|
605770
|
+
const candidateChannels = liveDeviceChannels(devices.find((entry) => entry.id === candidate));
|
|
605771
|
+
await tryRecordAudioDevice(candidate, durationSec, outputPath3, candidateChannels);
|
|
604251
605772
|
const activity = readPcm16WavActivity(outputPath3);
|
|
604252
605773
|
const method = audioCaptureMethod(candidate);
|
|
604253
605774
|
const signalDetected = hasLiveAudioSignal(activity);
|
|
@@ -604259,7 +605780,7 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604259
605780
|
device: candidate,
|
|
604260
605781
|
method,
|
|
604261
605782
|
activity,
|
|
604262
|
-
bytes:
|
|
605783
|
+
bytes: readFileSync89(outputPath3),
|
|
604263
605784
|
errors: [...errors]
|
|
604264
605785
|
};
|
|
604265
605786
|
}
|
|
@@ -604269,7 +605790,7 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604269
605790
|
}
|
|
604270
605791
|
}
|
|
604271
605792
|
if (firstQuietCapture) {
|
|
604272
|
-
|
|
605793
|
+
writeFileSync58(outputPath3, firstQuietCapture.bytes);
|
|
604273
605794
|
return {
|
|
604274
605795
|
ok: true,
|
|
604275
605796
|
device: firstQuietCapture.device,
|
|
@@ -604365,6 +605886,7 @@ var init_live_sensors = __esm({
|
|
|
604365
605886
|
init_dist5();
|
|
604366
605887
|
init_async_process();
|
|
604367
605888
|
init_image_ascii_preview();
|
|
605889
|
+
init_listen();
|
|
604368
605890
|
MIN_VIDEO_INTERVAL_MS = 250;
|
|
604369
605891
|
LOW_LATENCY_VIDEO_INTERVAL_MS = 250;
|
|
604370
605892
|
MIN_AUDIO_INTERVAL_MS = 1500;
|
|
@@ -604977,7 +606499,8 @@ var init_live_sensors = __esm({
|
|
|
604977
606499
|
clipSummary = await this.recognizeFrameObjects(preview.framePath, source, sampledAt, { emit: false });
|
|
604978
606500
|
const current = this.snapshot.cameras?.[source];
|
|
604979
606501
|
if (current) {
|
|
604980
|
-
|
|
606502
|
+
const clipLine = formatClipDashboardSummary(clipSummary);
|
|
606503
|
+
current.summary = [cleanCameraSummaryForDashboard(current.summary), clipLine].filter(Boolean).join(" | ");
|
|
604981
606504
|
current.classifications = mergeCameraClassifications(
|
|
604982
606505
|
current.classifications,
|
|
604983
606506
|
classificationsFromClipSummary(clipSummary, sampledAt, preview.framePath)
|
|
@@ -605032,7 +606555,7 @@ ${output}`).join("\n\n");
|
|
|
605032
606555
|
async previewAudioInput(device = this.config.selectedAudioInput) {
|
|
605033
606556
|
const input = String(device || "").trim() || this.resolveAudioInput();
|
|
605034
606557
|
ensureLiveDir(this.repoRoot);
|
|
605035
|
-
const recordingPath =
|
|
606558
|
+
const recordingPath = join126(liveDir(this.repoRoot), `audio-preview-${Date.now()}.wav`);
|
|
605036
606559
|
const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, 0.8, recordingPath, {
|
|
605037
606560
|
allowFallback: false
|
|
605038
606561
|
});
|
|
@@ -605081,7 +606604,7 @@ ${output}`).join("\n\n");
|
|
|
605081
606604
|
const input = this.resolveAudioInput();
|
|
605082
606605
|
try {
|
|
605083
606606
|
ensureLiveDir(this.repoRoot);
|
|
605084
|
-
const recordingPath =
|
|
606607
|
+
const recordingPath = join126(liveDir(this.repoRoot), "audio-activity.wav");
|
|
605085
606608
|
const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, AUDIO_ACTIVITY_SAMPLE_SEC, recordingPath, {
|
|
605086
606609
|
allowFallback: false,
|
|
605087
606610
|
requireSignal: false
|
|
@@ -605124,7 +606647,7 @@ ${output}`).join("\n\n");
|
|
|
605124
606647
|
}
|
|
605125
606648
|
const outputDir2 = liveDir(this.repoRoot);
|
|
605126
606649
|
ensureLiveDir(this.repoRoot);
|
|
605127
|
-
const recordingPath =
|
|
606650
|
+
const recordingPath = join126(outputDir2, `audio-${Date.now()}.wav`);
|
|
605128
606651
|
let transcript = "";
|
|
605129
606652
|
let asrBackend = "";
|
|
605130
606653
|
let analysis = "";
|
|
@@ -605436,7 +606959,7 @@ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=
|
|
|
605436
606959
|
}
|
|
605437
606960
|
persist() {
|
|
605438
606961
|
ensureLiveDir(this.repoRoot);
|
|
605439
|
-
|
|
606962
|
+
writeFileSync58(liveConfigPath(this.repoRoot), JSON.stringify(this.config, null, 2), "utf8");
|
|
605440
606963
|
this.snapshot = {
|
|
605441
606964
|
...this.snapshot,
|
|
605442
606965
|
version: 1,
|
|
@@ -605444,7 +606967,7 @@ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=
|
|
|
605444
606967
|
config: this.config,
|
|
605445
606968
|
devices: this.devices
|
|
605446
606969
|
};
|
|
605447
|
-
|
|
606970
|
+
writeFileSync58(liveSnapshotPath(this.repoRoot), JSON.stringify(this.snapshot, null, 2), "utf8");
|
|
605448
606971
|
}
|
|
605449
606972
|
};
|
|
605450
606973
|
}
|
|
@@ -605590,8 +607113,8 @@ var init_tool_adapter = __esm({
|
|
|
605590
607113
|
});
|
|
605591
607114
|
|
|
605592
607115
|
// packages/cli/src/tui/runtime-verification.ts
|
|
605593
|
-
import { existsSync as
|
|
605594
|
-
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";
|
|
605595
607118
|
function safeRelative(root, file) {
|
|
605596
607119
|
const rel = relative12(root, file);
|
|
605597
607120
|
return rel && !rel.startsWith("..") ? rel : file;
|
|
@@ -605602,16 +607125,16 @@ function collectHtmlFiles(root, maxFiles) {
|
|
|
605602
607125
|
if (out.length >= maxFiles || depth > 8) return;
|
|
605603
607126
|
let entries;
|
|
605604
607127
|
try {
|
|
605605
|
-
entries =
|
|
607128
|
+
entries = readdirSync36(dir, { withFileTypes: true });
|
|
605606
607129
|
} catch {
|
|
605607
607130
|
return;
|
|
605608
607131
|
}
|
|
605609
607132
|
for (const entry of entries) {
|
|
605610
607133
|
if (out.length >= maxFiles) break;
|
|
605611
607134
|
if (entry.isDirectory()) {
|
|
605612
|
-
if (!SKIP_DIRS2.has(entry.name)) walk2(
|
|
607135
|
+
if (!SKIP_DIRS2.has(entry.name)) walk2(join127(dir, entry.name), depth + 1);
|
|
605613
607136
|
} else if (entry.isFile() && /\.html?$/i.test(entry.name)) {
|
|
605614
|
-
out.push(
|
|
607137
|
+
out.push(join127(dir, entry.name));
|
|
605615
607138
|
}
|
|
605616
607139
|
}
|
|
605617
607140
|
};
|
|
@@ -605636,15 +607159,15 @@ function resolveScriptSource(root, htmlFile, src2) {
|
|
|
605636
607159
|
const withoutHash = raw.split("#", 1)[0] ?? "";
|
|
605637
607160
|
const clean5 = withoutHash.split("?", 1)[0] ?? "";
|
|
605638
607161
|
if (!clean5 || !SCRIPT_EXTS.has(extname14(clean5).toLowerCase())) return null;
|
|
605639
|
-
const abs = clean5.startsWith("/") ? resolve54(root, `.${clean5}`) : resolve54(
|
|
607162
|
+
const abs = clean5.startsWith("/") ? resolve54(root, `.${clean5}`) : resolve54(dirname38(htmlFile), clean5);
|
|
605640
607163
|
const rel = relative12(root, abs);
|
|
605641
607164
|
if (rel.startsWith("..") || rel === "") return null;
|
|
605642
|
-
return
|
|
607165
|
+
return existsSync111(abs) ? abs : null;
|
|
605643
607166
|
}
|
|
605644
607167
|
function referencedScriptAssets(root, htmlFile) {
|
|
605645
607168
|
let html = "";
|
|
605646
607169
|
try {
|
|
605647
|
-
html =
|
|
607170
|
+
html = readFileSync90(htmlFile, "utf8");
|
|
605648
607171
|
} catch {
|
|
605649
607172
|
return [];
|
|
605650
607173
|
}
|
|
@@ -605674,7 +607197,7 @@ async function syntaxCheckJavaScriptModule(source) {
|
|
|
605674
607197
|
}
|
|
605675
607198
|
async function runStaticRuntimeAssetSyntaxCheck(projectRoot, options2 = {}) {
|
|
605676
607199
|
const root = resolve54(projectRoot);
|
|
605677
|
-
if (!
|
|
607200
|
+
if (!existsSync111(root)) {
|
|
605678
607201
|
return { ok: true, checkedAssets: 0, htmlFiles: 0, issues: [], skippedReason: "project root missing" };
|
|
605679
607202
|
}
|
|
605680
607203
|
const htmlFiles = collectHtmlFiles(root, options2.maxHtmlFiles ?? 80);
|
|
@@ -605687,7 +607210,7 @@ async function runStaticRuntimeAssetSyntaxCheck(projectRoot, options2 = {}) {
|
|
|
605687
607210
|
if (issueByAsset.has(assetFile)) continue;
|
|
605688
607211
|
let source = "";
|
|
605689
607212
|
try {
|
|
605690
|
-
source =
|
|
607213
|
+
source = readFileSync90(assetFile, "utf8");
|
|
605691
607214
|
} catch {
|
|
605692
607215
|
continue;
|
|
605693
607216
|
}
|
|
@@ -605760,1194 +607283,6 @@ var init_runtime_verification = __esm({
|
|
|
605760
607283
|
}
|
|
605761
607284
|
});
|
|
605762
607285
|
|
|
605763
|
-
// packages/cli/src/api/py-embed.ts
|
|
605764
|
-
var py_embed_exports = {};
|
|
605765
|
-
__export(py_embed_exports, {
|
|
605766
|
-
ensureEmbedDeps: () => ensureEmbedDeps,
|
|
605767
|
-
getVenvPython: () => getVenvPython,
|
|
605768
|
-
runEmbedAudio: () => runEmbedAudio,
|
|
605769
|
-
runEmbedImage: () => runEmbedImage,
|
|
605770
|
-
runEmbedText: () => runEmbedText,
|
|
605771
|
-
runTranscribeFile: () => runTranscribeFile
|
|
605772
|
-
});
|
|
605773
|
-
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
605774
|
-
import { existsSync as existsSync110, mkdirSync as mkdirSync68, writeFileSync as writeFileSync57 } from "node:fs";
|
|
605775
|
-
import { join as join126 } from "node:path";
|
|
605776
|
-
import { homedir as homedir38 } from "node:os";
|
|
605777
|
-
function getVenvDir() {
|
|
605778
|
-
return join126(homedir38(), ".omnius", "venv");
|
|
605779
|
-
}
|
|
605780
|
-
function getVenvPython() {
|
|
605781
|
-
const base3 = getVenvDir();
|
|
605782
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
605783
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
605784
|
-
return join126(base3, bin, exe);
|
|
605785
|
-
}
|
|
605786
|
-
function ensureEmbedDeps() {
|
|
605787
|
-
const venv = getVenvDir();
|
|
605788
|
-
let log22 = "";
|
|
605789
|
-
try {
|
|
605790
|
-
mkdirSync68(venv, { recursive: true });
|
|
605791
|
-
} catch {
|
|
605792
|
-
}
|
|
605793
|
-
if (!existsSync110(getVenvPython())) {
|
|
605794
|
-
const mk = spawnSync8("python3", ["-m", "venv", venv], { encoding: "utf8" });
|
|
605795
|
-
log22 += mk.stdout + mk.stderr;
|
|
605796
|
-
}
|
|
605797
|
-
const pip = process.platform === "win32" ? join126(venv, "Scripts", "pip.exe") : join126(venv, "bin", "pip");
|
|
605798
|
-
const up = spawnSync8(pip, ["install", "--upgrade", "pip", "setuptools", "wheel"], { encoding: "utf8", timeout: 3e5 });
|
|
605799
|
-
log22 += up.stdout + up.stderr;
|
|
605800
|
-
const asrPkgs = [
|
|
605801
|
-
"numpy==1.26.4",
|
|
605802
|
-
"soundfile",
|
|
605803
|
-
"faster-whisper",
|
|
605804
|
-
"openai-whisper",
|
|
605805
|
-
// Embedding helpers used elsewhere (kept lightweight compared to full torch stack)
|
|
605806
|
-
"Pillow"
|
|
605807
|
-
];
|
|
605808
|
-
const ins = spawnSync8(pip, ["install", "--upgrade", ...asrPkgs], { encoding: "utf8", timeout: 9e5 });
|
|
605809
|
-
log22 += ins.stdout + ins.stderr;
|
|
605810
|
-
let fullOk = true;
|
|
605811
|
-
if (process.env["OMNIUS_INSTALL_FULL_EMBED_DEPS"] === "1") {
|
|
605812
|
-
const fullPkgs = ["torch", "torchaudio", "open_clip_torch", "speechbrain"];
|
|
605813
|
-
const full = spawnSync8(pip, ["install", "--upgrade", ...fullPkgs], { encoding: "utf8", timeout: 18e5 });
|
|
605814
|
-
log22 += full.stdout + full.stderr;
|
|
605815
|
-
fullOk = full.status === 0;
|
|
605816
|
-
}
|
|
605817
|
-
const ok3 = ins.status === 0 && up.status === 0 && fullOk;
|
|
605818
|
-
return { ok: ok3, log: log22 };
|
|
605819
|
-
}
|
|
605820
|
-
function runEmbedImage(input) {
|
|
605821
|
-
const py = getVenvPython();
|
|
605822
|
-
const script = locateScript("embed-image.py");
|
|
605823
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
605824
|
-
try {
|
|
605825
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605826
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605827
|
-
} catch {
|
|
605828
|
-
}
|
|
605829
|
-
return null;
|
|
605830
|
-
}
|
|
605831
|
-
function runEmbedAudio(input) {
|
|
605832
|
-
const py = getVenvPython();
|
|
605833
|
-
const script = locateScript("embed-audio.py");
|
|
605834
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
605835
|
-
try {
|
|
605836
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605837
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605838
|
-
} catch {
|
|
605839
|
-
}
|
|
605840
|
-
return null;
|
|
605841
|
-
}
|
|
605842
|
-
function runEmbedText(text2) {
|
|
605843
|
-
const py = getVenvPython();
|
|
605844
|
-
const script = locateScript("embed-text.py");
|
|
605845
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify({ text: text2 }), encoding: "utf8" });
|
|
605846
|
-
try {
|
|
605847
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605848
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605849
|
-
} catch {
|
|
605850
|
-
}
|
|
605851
|
-
return null;
|
|
605852
|
-
}
|
|
605853
|
-
function runTranscribeFile(input) {
|
|
605854
|
-
const py = getVenvPython();
|
|
605855
|
-
const script = locateScript("transcribe-file.py");
|
|
605856
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8", timeout: 6e5 });
|
|
605857
|
-
try {
|
|
605858
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605859
|
-
if (typeof out.text === "string") return out.text;
|
|
605860
|
-
} catch {
|
|
605861
|
-
}
|
|
605862
|
-
return null;
|
|
605863
|
-
}
|
|
605864
|
-
function locateScript(name10) {
|
|
605865
|
-
const candidates = [
|
|
605866
|
-
join126(process.cwd(), "dist", "scripts", name10),
|
|
605867
|
-
join126(process.cwd(), name10),
|
|
605868
|
-
join126(process.cwd(), "scripts", name10)
|
|
605869
|
-
];
|
|
605870
|
-
for (const c9 of candidates) if (existsSync110(c9)) return c9;
|
|
605871
|
-
const fallback = join126(process.cwd(), name10);
|
|
605872
|
-
try {
|
|
605873
|
-
writeFileSync57(fallback, 'print("missing script")\n');
|
|
605874
|
-
} catch {
|
|
605875
|
-
}
|
|
605876
|
-
return fallback;
|
|
605877
|
-
}
|
|
605878
|
-
var init_py_embed = __esm({
|
|
605879
|
-
"packages/cli/src/api/py-embed.ts"() {
|
|
605880
|
-
"use strict";
|
|
605881
|
-
}
|
|
605882
|
-
});
|
|
605883
|
-
|
|
605884
|
-
// packages/cli/src/tui/listen.ts
|
|
605885
|
-
var listen_exports = {};
|
|
605886
|
-
__export(listen_exports, {
|
|
605887
|
-
ListenEngine: () => ListenEngine,
|
|
605888
|
-
ensureTranscribeCliBackground: () => ensureTranscribeCliBackground,
|
|
605889
|
-
getListenEngine: () => getListenEngine,
|
|
605890
|
-
isAudioPath: () => isAudioPath,
|
|
605891
|
-
isTranscribablePath: () => isTranscribablePath,
|
|
605892
|
-
isVideoPath: () => isVideoPath,
|
|
605893
|
-
transcribeFileViaWhisper: () => transcribeFileViaWhisper,
|
|
605894
|
-
waitForTranscribeCli: () => waitForTranscribeCli
|
|
605895
|
-
});
|
|
605896
|
-
import { spawn as spawn29 } from "node:child_process";
|
|
605897
|
-
import {
|
|
605898
|
-
existsSync as existsSync111,
|
|
605899
|
-
mkdirSync as mkdirSync69,
|
|
605900
|
-
writeFileSync as writeFileSync58,
|
|
605901
|
-
readdirSync as readdirSync36
|
|
605902
|
-
} from "node:fs";
|
|
605903
|
-
import { join as join127, dirname as dirname38 } from "node:path";
|
|
605904
|
-
import { homedir as homedir39 } from "node:os";
|
|
605905
|
-
import { fileURLToPath as fileURLToPath16 } from "node:url";
|
|
605906
|
-
import { EventEmitter as EventEmitter6 } from "node:events";
|
|
605907
|
-
import { createInterface as createInterface2 } from "node:readline";
|
|
605908
|
-
function isAudioPath(path12) {
|
|
605909
|
-
const ext = path12.toLowerCase().split(".").pop();
|
|
605910
|
-
return ext ? AUDIO_EXTENSIONS.has(`.${ext}`) : false;
|
|
605911
|
-
}
|
|
605912
|
-
function isVideoPath(path12) {
|
|
605913
|
-
const ext = path12.toLowerCase().split(".").pop();
|
|
605914
|
-
return ext ? VIDEO_EXTENSIONS2.has(`.${ext}`) : false;
|
|
605915
|
-
}
|
|
605916
|
-
function isTranscribablePath(path12) {
|
|
605917
|
-
return isAudioPath(path12) || isVideoPath(path12);
|
|
605918
|
-
}
|
|
605919
|
-
async function findMicCaptureCommand() {
|
|
605920
|
-
const platform7 = process.platform;
|
|
605921
|
-
if (platform7 === "linux") {
|
|
605922
|
-
if (await commandExists2("arecord")) {
|
|
605923
|
-
return {
|
|
605924
|
-
cmd: "arecord",
|
|
605925
|
-
args: [
|
|
605926
|
-
"-f",
|
|
605927
|
-
"S16_LE",
|
|
605928
|
-
"-r",
|
|
605929
|
-
"16000",
|
|
605930
|
-
"-c",
|
|
605931
|
-
"1",
|
|
605932
|
-
"-t",
|
|
605933
|
-
"raw",
|
|
605934
|
-
"-q",
|
|
605935
|
-
"-"
|
|
605936
|
-
]
|
|
605937
|
-
};
|
|
605938
|
-
}
|
|
605939
|
-
}
|
|
605940
|
-
if (platform7 === "darwin") {
|
|
605941
|
-
if (await commandExists2("sox")) {
|
|
605942
|
-
return {
|
|
605943
|
-
cmd: "sox",
|
|
605944
|
-
args: [
|
|
605945
|
-
"-d",
|
|
605946
|
-
"-t",
|
|
605947
|
-
"raw",
|
|
605948
|
-
"-r",
|
|
605949
|
-
"16000",
|
|
605950
|
-
"-c",
|
|
605951
|
-
"1",
|
|
605952
|
-
"-b",
|
|
605953
|
-
"16",
|
|
605954
|
-
"-e",
|
|
605955
|
-
"signed-integer",
|
|
605956
|
-
"-"
|
|
605957
|
-
]
|
|
605958
|
-
};
|
|
605959
|
-
}
|
|
605960
|
-
}
|
|
605961
|
-
if (await commandExists2("ffmpeg")) {
|
|
605962
|
-
if (platform7 === "linux") {
|
|
605963
|
-
return {
|
|
605964
|
-
cmd: "ffmpeg",
|
|
605965
|
-
args: [
|
|
605966
|
-
"-f",
|
|
605967
|
-
"pulse",
|
|
605968
|
-
"-i",
|
|
605969
|
-
"default",
|
|
605970
|
-
"-ar",
|
|
605971
|
-
"16000",
|
|
605972
|
-
"-ac",
|
|
605973
|
-
"1",
|
|
605974
|
-
"-f",
|
|
605975
|
-
"s16le",
|
|
605976
|
-
"-loglevel",
|
|
605977
|
-
"quiet",
|
|
605978
|
-
"pipe:1"
|
|
605979
|
-
]
|
|
605980
|
-
};
|
|
605981
|
-
} else if (platform7 === "darwin") {
|
|
605982
|
-
return {
|
|
605983
|
-
cmd: "ffmpeg",
|
|
605984
|
-
args: [
|
|
605985
|
-
"-f",
|
|
605986
|
-
"avfoundation",
|
|
605987
|
-
"-i",
|
|
605988
|
-
":0",
|
|
605989
|
-
"-ar",
|
|
605990
|
-
"16000",
|
|
605991
|
-
"-ac",
|
|
605992
|
-
"1",
|
|
605993
|
-
"-f",
|
|
605994
|
-
"s16le",
|
|
605995
|
-
"-loglevel",
|
|
605996
|
-
"quiet",
|
|
605997
|
-
"pipe:1"
|
|
605998
|
-
]
|
|
605999
|
-
};
|
|
606000
|
-
}
|
|
606001
|
-
}
|
|
606002
|
-
return null;
|
|
606003
|
-
}
|
|
606004
|
-
async function findTranscribeFileScript() {
|
|
606005
|
-
const thisDir = dirname38(fileURLToPath16(import.meta.url));
|
|
606006
|
-
const candidates = [
|
|
606007
|
-
join127(thisDir, "../../../../packages/execution/scripts/transcribe-file.py"),
|
|
606008
|
-
join127(thisDir, "../../../packages/execution/scripts/transcribe-file.py"),
|
|
606009
|
-
join127(thisDir, "../../execution/scripts/transcribe-file.py"),
|
|
606010
|
-
join127(thisDir, "../scripts/transcribe-file.py"),
|
|
606011
|
-
join127(thisDir, "../../scripts/transcribe-file.py")
|
|
606012
|
-
];
|
|
606013
|
-
for (const p2 of candidates) {
|
|
606014
|
-
if (existsSync111(p2)) return p2;
|
|
606015
|
-
}
|
|
606016
|
-
try {
|
|
606017
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606018
|
-
const candidates2 = [
|
|
606019
|
-
join127(globalRoot, "omnius", "dist", "scripts", "transcribe-file.py"),
|
|
606020
|
-
join127(globalRoot, "omnius", "scripts", "transcribe-file.py")
|
|
606021
|
-
];
|
|
606022
|
-
for (const p2 of candidates2) {
|
|
606023
|
-
if (existsSync111(p2)) return p2;
|
|
606024
|
-
}
|
|
606025
|
-
} catch {
|
|
606026
|
-
}
|
|
606027
|
-
return null;
|
|
606028
|
-
}
|
|
606029
|
-
async function transcribeFileViaWhisper(filePath, model) {
|
|
606030
|
-
const script = await findTranscribeFileScript();
|
|
606031
|
-
if (!script) return null;
|
|
606032
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
606033
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
606034
|
-
const venvPython2 = join127(homedir39(), ".omnius", "venv", bin, exe);
|
|
606035
|
-
if (!existsSync111(venvPython2)) return null;
|
|
606036
|
-
return new Promise((resolve76) => {
|
|
606037
|
-
const child = spawn29(venvPython2, [script], {
|
|
606038
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606039
|
-
env: process.env
|
|
606040
|
-
});
|
|
606041
|
-
let out = "";
|
|
606042
|
-
let err = "";
|
|
606043
|
-
let timer = null;
|
|
606044
|
-
const stop2 = (val) => {
|
|
606045
|
-
if (timer) clearTimeout(timer);
|
|
606046
|
-
try {
|
|
606047
|
-
child.kill("SIGTERM");
|
|
606048
|
-
} catch {
|
|
606049
|
-
}
|
|
606050
|
-
resolve76(val ? parse3(val) : null);
|
|
606051
|
-
};
|
|
606052
|
-
function parse3(raw) {
|
|
606053
|
-
try {
|
|
606054
|
-
const j = JSON.parse(raw);
|
|
606055
|
-
if (j.error) return null;
|
|
606056
|
-
if (typeof j.text !== "string") return null;
|
|
606057
|
-
return {
|
|
606058
|
-
text: j.text.trim(),
|
|
606059
|
-
duration: null,
|
|
606060
|
-
speakers: [],
|
|
606061
|
-
segments: []
|
|
606062
|
-
};
|
|
606063
|
-
} catch {
|
|
606064
|
-
return null;
|
|
606065
|
-
}
|
|
606066
|
-
}
|
|
606067
|
-
child.stdout?.on("data", (d2) => {
|
|
606068
|
-
out += d2.toString("utf-8");
|
|
606069
|
-
});
|
|
606070
|
-
child.stderr?.on("data", (d2) => {
|
|
606071
|
-
err += d2.toString("utf-8");
|
|
606072
|
-
});
|
|
606073
|
-
child.once("error", () => stop2(null));
|
|
606074
|
-
child.once("close", () => {
|
|
606075
|
-
if (out.trim()) resolve76(parse3(out));
|
|
606076
|
-
else {
|
|
606077
|
-
void err;
|
|
606078
|
-
resolve76(null);
|
|
606079
|
-
}
|
|
606080
|
-
});
|
|
606081
|
-
timer = setTimeout(() => stop2(null), 12e4);
|
|
606082
|
-
try {
|
|
606083
|
-
child.stdin?.write(JSON.stringify({ path: filePath, model }) + "\n");
|
|
606084
|
-
child.stdin?.end();
|
|
606085
|
-
} catch {
|
|
606086
|
-
stop2(null);
|
|
606087
|
-
}
|
|
606088
|
-
});
|
|
606089
|
-
}
|
|
606090
|
-
async function findLiveWhisperScript() {
|
|
606091
|
-
const thisDir = dirname38(fileURLToPath16(import.meta.url));
|
|
606092
|
-
const candidates = [
|
|
606093
|
-
join127(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
|
|
606094
|
-
join127(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
|
|
606095
|
-
join127(thisDir, "../../execution/scripts/live-whisper.py"),
|
|
606096
|
-
// npm install layout — scripts bundled alongside dist
|
|
606097
|
-
join127(thisDir, "../scripts/live-whisper.py"),
|
|
606098
|
-
join127(thisDir, "../../scripts/live-whisper.py")
|
|
606099
|
-
];
|
|
606100
|
-
for (const p2 of candidates) {
|
|
606101
|
-
if (existsSync111(p2)) return p2;
|
|
606102
|
-
}
|
|
606103
|
-
try {
|
|
606104
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606105
|
-
const candidates2 = [
|
|
606106
|
-
join127(globalRoot, "omnius", "dist", "scripts", "live-whisper.py"),
|
|
606107
|
-
join127(globalRoot, "omnius", "scripts", "live-whisper.py")
|
|
606108
|
-
];
|
|
606109
|
-
for (const p2 of candidates2) {
|
|
606110
|
-
if (existsSync111(p2)) return p2;
|
|
606111
|
-
}
|
|
606112
|
-
} catch {
|
|
606113
|
-
}
|
|
606114
|
-
const nvmBase = join127(homedir39(), ".nvm", "versions", "node");
|
|
606115
|
-
if (existsSync111(nvmBase)) {
|
|
606116
|
-
try {
|
|
606117
|
-
for (const ver of readdirSync36(nvmBase)) {
|
|
606118
|
-
const p2 = join127(
|
|
606119
|
-
nvmBase,
|
|
606120
|
-
ver,
|
|
606121
|
-
"lib",
|
|
606122
|
-
"node_modules",
|
|
606123
|
-
"omnius",
|
|
606124
|
-
"dist",
|
|
606125
|
-
"scripts",
|
|
606126
|
-
"live-whisper.py"
|
|
606127
|
-
);
|
|
606128
|
-
if (existsSync111(p2)) return p2;
|
|
606129
|
-
}
|
|
606130
|
-
} catch {
|
|
606131
|
-
}
|
|
606132
|
-
}
|
|
606133
|
-
return null;
|
|
606134
|
-
}
|
|
606135
|
-
async function ensureVenvForTranscribeCli() {
|
|
606136
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
606137
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
606138
|
-
const venvBin = join127(homedir39(), ".omnius", "venv", bin);
|
|
606139
|
-
const venvPython2 = join127(venvBin, exe);
|
|
606140
|
-
if (!existsSync111(venvPython2)) {
|
|
606141
|
-
try {
|
|
606142
|
-
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
606143
|
-
if (typeof mod3.ensureEmbedDeps === "function") mod3.ensureEmbedDeps();
|
|
606144
|
-
} catch {
|
|
606145
|
-
}
|
|
606146
|
-
}
|
|
606147
|
-
if (!existsSync111(venvPython2)) {
|
|
606148
|
-
try {
|
|
606149
|
-
mkdirSync69(join127(homedir39(), ".omnius", "venv"), { recursive: true });
|
|
606150
|
-
await execFileText4("python3", ["-m", "venv", join127(homedir39(), ".omnius", "venv")], { timeout: 6e4 });
|
|
606151
|
-
} catch {
|
|
606152
|
-
return false;
|
|
606153
|
-
}
|
|
606154
|
-
}
|
|
606155
|
-
process.env.TRANSCRIBE_PYTHON = venvPython2;
|
|
606156
|
-
const pathSep2 = process.platform === "win32" ? ";" : ":";
|
|
606157
|
-
const currentPath = process.env.PATH || "";
|
|
606158
|
-
if (!currentPath.split(pathSep2).includes(venvBin)) {
|
|
606159
|
-
process.env.PATH = `${venvBin}${pathSep2}${currentPath}`;
|
|
606160
|
-
}
|
|
606161
|
-
try {
|
|
606162
|
-
await execFileText4(venvPython2, ["-c", "import numpy"], { timeout: 1e4 });
|
|
606163
|
-
} catch {
|
|
606164
|
-
try {
|
|
606165
|
-
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "pip", "setuptools", "wheel", "numpy==1.26.4"], { timeout: 3e5 });
|
|
606166
|
-
} catch {
|
|
606167
|
-
return false;
|
|
606168
|
-
}
|
|
606169
|
-
}
|
|
606170
|
-
try {
|
|
606171
|
-
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
606172
|
-
return true;
|
|
606173
|
-
} catch {
|
|
606174
|
-
}
|
|
606175
|
-
try {
|
|
606176
|
-
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "transcribe-cli"], { timeout: 3e5 });
|
|
606177
|
-
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
606178
|
-
return true;
|
|
606179
|
-
} catch {
|
|
606180
|
-
return false;
|
|
606181
|
-
}
|
|
606182
|
-
}
|
|
606183
|
-
async function requireTranscribeCliFrom2(packageDir) {
|
|
606184
|
-
try {
|
|
606185
|
-
const { createRequire: createRequire11 } = await import("node:module");
|
|
606186
|
-
const req3 = createRequire11(import.meta.url);
|
|
606187
|
-
const distPath = join127(packageDir, "dist", "index.js");
|
|
606188
|
-
if (existsSync111(distPath)) return req3(distPath);
|
|
606189
|
-
return req3(packageDir);
|
|
606190
|
-
} catch {
|
|
606191
|
-
return null;
|
|
606192
|
-
}
|
|
606193
|
-
}
|
|
606194
|
-
async function loadManagedTranscribeCli() {
|
|
606195
|
-
const packageDir = join127(MANAGED_TRANSCRIBE_CLI_DIR2, "node_modules", "transcribe-cli");
|
|
606196
|
-
if (!existsSync111(join127(packageDir, "dist", "index.js"))) return null;
|
|
606197
|
-
return requireTranscribeCliFrom2(packageDir);
|
|
606198
|
-
}
|
|
606199
|
-
async function ensureManagedTranscribeCliNode() {
|
|
606200
|
-
if (await loadManagedTranscribeCli()) return true;
|
|
606201
|
-
if (_managedTranscribeCliInstallPromise) return _managedTranscribeCliInstallPromise;
|
|
606202
|
-
_managedTranscribeCliInstallPromise = (async () => {
|
|
606203
|
-
try {
|
|
606204
|
-
mkdirSync69(MANAGED_TRANSCRIBE_CLI_DIR2, { recursive: true });
|
|
606205
|
-
if (!existsSync111(join127(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"))) {
|
|
606206
|
-
writeFileSync58(
|
|
606207
|
-
join127(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"),
|
|
606208
|
-
JSON.stringify({ private: true, dependencies: {} }, null, 2),
|
|
606209
|
-
"utf8"
|
|
606210
|
-
);
|
|
606211
|
-
}
|
|
606212
|
-
await execFileText4("npm", ["install", "--prefix", MANAGED_TRANSCRIBE_CLI_DIR2, "transcribe-cli@^2.0.1"], {
|
|
606213
|
-
timeout: 18e4
|
|
606214
|
-
});
|
|
606215
|
-
return Boolean(await loadManagedTranscribeCli());
|
|
606216
|
-
} catch {
|
|
606217
|
-
return false;
|
|
606218
|
-
}
|
|
606219
|
-
})();
|
|
606220
|
-
return _managedTranscribeCliInstallPromise;
|
|
606221
|
-
}
|
|
606222
|
-
function ensureTranscribeCliBackground() {
|
|
606223
|
-
if (_bgInstallPromise) return _bgInstallPromise;
|
|
606224
|
-
_bgInstallPromise = (async () => {
|
|
606225
|
-
const nodeReady = await ensureManagedTranscribeCliNode();
|
|
606226
|
-
const pyReady = await ensureVenvForTranscribeCli();
|
|
606227
|
-
return nodeReady && pyReady;
|
|
606228
|
-
})();
|
|
606229
|
-
return _bgInstallPromise;
|
|
606230
|
-
}
|
|
606231
|
-
async function waitForTranscribeCli() {
|
|
606232
|
-
if (_bgInstallPromise) {
|
|
606233
|
-
return _bgInstallPromise;
|
|
606234
|
-
}
|
|
606235
|
-
return Boolean(await loadManagedTranscribeCli()) && await ensureVenvForTranscribeCli();
|
|
606236
|
-
}
|
|
606237
|
-
function getListenEngine(config) {
|
|
606238
|
-
if (!_engine) {
|
|
606239
|
-
_engine = new ListenEngine(config);
|
|
606240
|
-
}
|
|
606241
|
-
return _engine;
|
|
606242
|
-
}
|
|
606243
|
-
var MANAGED_TRANSCRIBE_CLI_DIR2, AUDIO_EXTENSIONS, VIDEO_EXTENSIONS2, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _managedTranscribeCliInstallPromise, _engine;
|
|
606244
|
-
var init_listen = __esm({
|
|
606245
|
-
"packages/cli/src/tui/listen.ts"() {
|
|
606246
|
-
"use strict";
|
|
606247
|
-
init_typed_node_events();
|
|
606248
|
-
init_async_process();
|
|
606249
|
-
MANAGED_TRANSCRIBE_CLI_DIR2 = join127(homedir39(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
|
|
606250
|
-
AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
606251
|
-
".mp3",
|
|
606252
|
-
".wav",
|
|
606253
|
-
".flac",
|
|
606254
|
-
".aac",
|
|
606255
|
-
".m4a",
|
|
606256
|
-
".ogg",
|
|
606257
|
-
".wma",
|
|
606258
|
-
".opus"
|
|
606259
|
-
]);
|
|
606260
|
-
VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
606261
|
-
".mp4",
|
|
606262
|
-
".mkv",
|
|
606263
|
-
".avi",
|
|
606264
|
-
".mov",
|
|
606265
|
-
".webm",
|
|
606266
|
-
".flv",
|
|
606267
|
-
".wmv",
|
|
606268
|
-
".m4v",
|
|
606269
|
-
".ts"
|
|
606270
|
-
]);
|
|
606271
|
-
WhisperFallbackTranscriber = class extends EventEmitter6 {
|
|
606272
|
-
constructor(model, scriptPath2) {
|
|
606273
|
-
super();
|
|
606274
|
-
this.model = model;
|
|
606275
|
-
this.scriptPath = scriptPath2;
|
|
606276
|
-
}
|
|
606277
|
-
model;
|
|
606278
|
-
scriptPath;
|
|
606279
|
-
process = null;
|
|
606280
|
-
_ready = false;
|
|
606281
|
-
get ready() {
|
|
606282
|
-
return this._ready;
|
|
606283
|
-
}
|
|
606284
|
-
async start() {
|
|
606285
|
-
let pyPath = "python3";
|
|
606286
|
-
try {
|
|
606287
|
-
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
606288
|
-
const ensure = mod3.ensureEmbedDeps;
|
|
606289
|
-
const getPy = mod3.getVenvPython;
|
|
606290
|
-
if (typeof ensure === "function") {
|
|
606291
|
-
try {
|
|
606292
|
-
const res = ensure();
|
|
606293
|
-
if (res?.log) this.emit("status", res.log.slice(-500));
|
|
606294
|
-
} catch {
|
|
606295
|
-
}
|
|
606296
|
-
}
|
|
606297
|
-
if (typeof getPy === "function") {
|
|
606298
|
-
pyPath = getPy();
|
|
606299
|
-
}
|
|
606300
|
-
} catch {
|
|
606301
|
-
}
|
|
606302
|
-
this.process = spawn29(
|
|
606303
|
-
pyPath,
|
|
606304
|
-
[
|
|
606305
|
-
this.scriptPath,
|
|
606306
|
-
"--model",
|
|
606307
|
-
this.model,
|
|
606308
|
-
"--chunk-seconds",
|
|
606309
|
-
"3",
|
|
606310
|
-
"--window-seconds",
|
|
606311
|
-
"10"
|
|
606312
|
-
],
|
|
606313
|
-
{
|
|
606314
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606315
|
-
env: { ...process.env }
|
|
606316
|
-
}
|
|
606317
|
-
);
|
|
606318
|
-
return new Promise((resolve76, reject) => {
|
|
606319
|
-
const timeout2 = setTimeout(() => {
|
|
606320
|
-
reject(
|
|
606321
|
-
new Error(
|
|
606322
|
-
"Whisper fallback: model load timeout (5 min). First run downloads the model."
|
|
606323
|
-
)
|
|
606324
|
-
);
|
|
606325
|
-
}, 3e5);
|
|
606326
|
-
const rl = createInterface2({ input: this.process.stdout });
|
|
606327
|
-
onReadlineLine(rl, (line) => {
|
|
606328
|
-
try {
|
|
606329
|
-
const evt = JSON.parse(line);
|
|
606330
|
-
switch (evt.type) {
|
|
606331
|
-
case "status":
|
|
606332
|
-
this.emit("status", evt.message);
|
|
606333
|
-
break;
|
|
606334
|
-
case "ready":
|
|
606335
|
-
this._ready = true;
|
|
606336
|
-
clearTimeout(timeout2);
|
|
606337
|
-
this.emit("ready");
|
|
606338
|
-
resolve76();
|
|
606339
|
-
break;
|
|
606340
|
-
case "transcript":
|
|
606341
|
-
this.emit("transcript", {
|
|
606342
|
-
text: evt.text,
|
|
606343
|
-
isFinal: evt.isFinal ?? false
|
|
606344
|
-
});
|
|
606345
|
-
break;
|
|
606346
|
-
case "error":
|
|
606347
|
-
this.emit("error", new Error(evt.message));
|
|
606348
|
-
break;
|
|
606349
|
-
}
|
|
606350
|
-
} catch {
|
|
606351
|
-
}
|
|
606352
|
-
});
|
|
606353
|
-
this.process.stderr?.on("data", (data) => {
|
|
606354
|
-
const text2 = data.toString().trim();
|
|
606355
|
-
if (text2) this.emit("status", text2);
|
|
606356
|
-
});
|
|
606357
|
-
onChildError(this.process, (err) => {
|
|
606358
|
-
clearTimeout(timeout2);
|
|
606359
|
-
reject(err);
|
|
606360
|
-
});
|
|
606361
|
-
onChildClose(this.process, (code8) => {
|
|
606362
|
-
if (!this._ready) {
|
|
606363
|
-
clearTimeout(timeout2);
|
|
606364
|
-
reject(
|
|
606365
|
-
new Error(`Whisper worker exited with code ${code8} before ready`)
|
|
606366
|
-
);
|
|
606367
|
-
}
|
|
606368
|
-
});
|
|
606369
|
-
});
|
|
606370
|
-
}
|
|
606371
|
-
/** Write PCM16 audio data to the worker's stdin. */
|
|
606372
|
-
write(chunk) {
|
|
606373
|
-
if (this.process?.stdin?.writable) {
|
|
606374
|
-
this.process.stdin.write(chunk);
|
|
606375
|
-
}
|
|
606376
|
-
}
|
|
606377
|
-
/** Stop the worker. */
|
|
606378
|
-
stop() {
|
|
606379
|
-
if (this.process) {
|
|
606380
|
-
try {
|
|
606381
|
-
this.process.stdin?.end();
|
|
606382
|
-
this.process.kill("SIGTERM");
|
|
606383
|
-
} catch {
|
|
606384
|
-
}
|
|
606385
|
-
this.process = null;
|
|
606386
|
-
}
|
|
606387
|
-
this._ready = false;
|
|
606388
|
-
}
|
|
606389
|
-
};
|
|
606390
|
-
ListenEngine = class extends EventEmitter6 {
|
|
606391
|
-
config;
|
|
606392
|
-
micProcess = null;
|
|
606393
|
-
liveTranscriber = null;
|
|
606394
|
-
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
606395
|
-
active = false;
|
|
606396
|
-
paused = false;
|
|
606397
|
-
// Pause state for voicechat (stops mic but keeps transcriber)
|
|
606398
|
-
silenceTimer = null;
|
|
606399
|
-
countdownInterval = null;
|
|
606400
|
-
lastTranscriptTime = 0;
|
|
606401
|
-
pendingText = "";
|
|
606402
|
-
blinkTimer = null;
|
|
606403
|
-
blinkState = false;
|
|
606404
|
-
transcribeCliAvailable = null;
|
|
606405
|
-
constructor(config) {
|
|
606406
|
-
super();
|
|
606407
|
-
this.config = {
|
|
606408
|
-
model: config?.model ?? "base",
|
|
606409
|
-
mode: config?.mode ?? "confirm",
|
|
606410
|
-
silenceTimeoutMs: config?.silenceTimeoutMs ?? 3e3
|
|
606411
|
-
};
|
|
606412
|
-
}
|
|
606413
|
-
get isActive() {
|
|
606414
|
-
return this.active;
|
|
606415
|
-
}
|
|
606416
|
-
get isBlinking() {
|
|
606417
|
-
return this.active && this.blinkState;
|
|
606418
|
-
}
|
|
606419
|
-
get isPaused() {
|
|
606420
|
-
return this.paused;
|
|
606421
|
-
}
|
|
606422
|
-
get currentModel() {
|
|
606423
|
-
return this.config.model;
|
|
606424
|
-
}
|
|
606425
|
-
get currentMode() {
|
|
606426
|
-
return this.config.mode;
|
|
606427
|
-
}
|
|
606428
|
-
get pendingTranscript() {
|
|
606429
|
-
return this.pendingText;
|
|
606430
|
-
}
|
|
606431
|
-
setModel(model) {
|
|
606432
|
-
this.config.model = model;
|
|
606433
|
-
}
|
|
606434
|
-
setMode(mode) {
|
|
606435
|
-
this.config.mode = mode;
|
|
606436
|
-
}
|
|
606437
|
-
/** Check if transcribe-cli is available. */
|
|
606438
|
-
async isAvailable() {
|
|
606439
|
-
if (this.transcribeCliAvailable !== null)
|
|
606440
|
-
return this.transcribeCliAvailable;
|
|
606441
|
-
const tc = await this.loadTranscribeCli() || (await ensureManagedTranscribeCliNode() ? await this.loadTranscribeCli() : null);
|
|
606442
|
-
const transcribeCliReady = Boolean(tc && await ensureVenvForTranscribeCli());
|
|
606443
|
-
const whisperFallbackReady = Boolean(await findLiveWhisperScript());
|
|
606444
|
-
this.transcribeCliAvailable = transcribeCliReady || whisperFallbackReady;
|
|
606445
|
-
return this.transcribeCliAvailable;
|
|
606446
|
-
}
|
|
606447
|
-
/** Load transcribe-cli — bundled as a dependency of omnius. */
|
|
606448
|
-
async loadTranscribeCli() {
|
|
606449
|
-
try {
|
|
606450
|
-
const { createRequire: createRequire11 } = await import("node:module");
|
|
606451
|
-
const req3 = createRequire11(import.meta.url);
|
|
606452
|
-
return req3("transcribe-cli");
|
|
606453
|
-
} catch {
|
|
606454
|
-
}
|
|
606455
|
-
const managed = await loadManagedTranscribeCli();
|
|
606456
|
-
if (managed) return managed;
|
|
606457
|
-
try {
|
|
606458
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606459
|
-
const tcPath = join127(globalRoot, "transcribe-cli");
|
|
606460
|
-
if (existsSync111(join127(tcPath, "dist", "index.js"))) {
|
|
606461
|
-
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
606462
|
-
if (loaded) return loaded;
|
|
606463
|
-
}
|
|
606464
|
-
} catch {
|
|
606465
|
-
}
|
|
606466
|
-
const nvmBase = join127(homedir39(), ".nvm", "versions", "node");
|
|
606467
|
-
if (existsSync111(nvmBase)) {
|
|
606468
|
-
try {
|
|
606469
|
-
const { readdirSync: readdirSync61 } = await import("node:fs");
|
|
606470
|
-
for (const ver of readdirSync61(nvmBase)) {
|
|
606471
|
-
const tcPath = join127(
|
|
606472
|
-
nvmBase,
|
|
606473
|
-
ver,
|
|
606474
|
-
"lib",
|
|
606475
|
-
"node_modules",
|
|
606476
|
-
"transcribe-cli"
|
|
606477
|
-
);
|
|
606478
|
-
if (existsSync111(join127(tcPath, "dist", "index.js"))) {
|
|
606479
|
-
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
606480
|
-
if (loaded) return loaded;
|
|
606481
|
-
}
|
|
606482
|
-
}
|
|
606483
|
-
} catch {
|
|
606484
|
-
}
|
|
606485
|
-
}
|
|
606486
|
-
return null;
|
|
606487
|
-
}
|
|
606488
|
-
/**
|
|
606489
|
-
* Start listening — captures microphone audio and feeds to TranscribeLive.
|
|
606490
|
-
*/
|
|
606491
|
-
async start() {
|
|
606492
|
-
if (this.active) return "Already listening.";
|
|
606493
|
-
const micCmd = await findMicCaptureCommand();
|
|
606494
|
-
if (!micCmd) {
|
|
606495
|
-
return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
|
|
606496
|
-
}
|
|
606497
|
-
let tc = await this.loadTranscribeCli();
|
|
606498
|
-
if (!tc) {
|
|
606499
|
-
if (_bgInstallPromise) {
|
|
606500
|
-
this.emit("info", "Waiting for transcribe-cli install...");
|
|
606501
|
-
await _bgInstallPromise;
|
|
606502
|
-
this.transcribeCliAvailable = null;
|
|
606503
|
-
tc = await this.loadTranscribeCli();
|
|
606504
|
-
}
|
|
606505
|
-
if (!tc) {
|
|
606506
|
-
try {
|
|
606507
|
-
await ensureManagedTranscribeCliNode();
|
|
606508
|
-
this.transcribeCliAvailable = null;
|
|
606509
|
-
tc = await this.loadTranscribeCli();
|
|
606510
|
-
} catch {
|
|
606511
|
-
}
|
|
606512
|
-
}
|
|
606513
|
-
}
|
|
606514
|
-
let usedFallback = false;
|
|
606515
|
-
let transcribeCliError = null;
|
|
606516
|
-
if (tc) {
|
|
606517
|
-
const TranscribeLive = tc.TranscribeLive;
|
|
606518
|
-
if (TranscribeLive) {
|
|
606519
|
-
let tcUpToDate = false;
|
|
606520
|
-
try {
|
|
606521
|
-
const tcPkgPath = join127(
|
|
606522
|
-
dirname38(__require.resolve?.("transcribe-cli/package.json") || ""),
|
|
606523
|
-
"package.json"
|
|
606524
|
-
);
|
|
606525
|
-
if (existsSync111(tcPkgPath)) {
|
|
606526
|
-
const tcPkg = JSON.parse(
|
|
606527
|
-
__require("fs").readFileSync(tcPkgPath, "utf8")
|
|
606528
|
-
);
|
|
606529
|
-
tcUpToDate = tcPkg.version && tcPkg.version >= "2.0.1";
|
|
606530
|
-
}
|
|
606531
|
-
} catch {
|
|
606532
|
-
try {
|
|
606533
|
-
const out = await execFileText4("npm", ["list", "-g", "transcribe-cli", "--depth=0", "--json"], {
|
|
606534
|
-
timeout: 1e4
|
|
606535
|
-
});
|
|
606536
|
-
const parsed = JSON.parse(out);
|
|
606537
|
-
const ver = parsed?.dependencies?.["transcribe-cli"]?.version || "";
|
|
606538
|
-
tcUpToDate = ver >= "2.0.1";
|
|
606539
|
-
} catch {
|
|
606540
|
-
}
|
|
606541
|
-
}
|
|
606542
|
-
if (!tcUpToDate) {
|
|
606543
|
-
try {
|
|
606544
|
-
await ensureManagedTranscribeCliNode();
|
|
606545
|
-
this.transcribeCliAvailable = null;
|
|
606546
|
-
const reloaded = await this.loadTranscribeCli();
|
|
606547
|
-
if (reloaded?.TranscribeLive) {
|
|
606548
|
-
tc = reloaded;
|
|
606549
|
-
}
|
|
606550
|
-
} catch {
|
|
606551
|
-
}
|
|
606552
|
-
}
|
|
606553
|
-
const venvReady = await ensureVenvForTranscribeCli();
|
|
606554
|
-
if (!venvReady) {
|
|
606555
|
-
transcribeCliError = "venv Python missing numpy (required by transcribe-cli) — using whisper fallback";
|
|
606556
|
-
} else {
|
|
606557
|
-
let caughtUncaught = null;
|
|
606558
|
-
const uncaughtHandler = (err) => {
|
|
606559
|
-
const msg = err?.message || String(err);
|
|
606560
|
-
if (msg.includes("live_worker") || msg.includes("numpy") || msg.includes("transcribe_cli") || msg.includes("ModuleNotFoundError") || msg.includes("Traceback")) {
|
|
606561
|
-
caughtUncaught = err;
|
|
606562
|
-
} else {
|
|
606563
|
-
throw err;
|
|
606564
|
-
}
|
|
606565
|
-
};
|
|
606566
|
-
process.on("uncaughtException", uncaughtHandler);
|
|
606567
|
-
try {
|
|
606568
|
-
this.liveTranscriber = new TranscribeLive({
|
|
606569
|
-
model: this.config.model,
|
|
606570
|
-
sampleRate: 16e3,
|
|
606571
|
-
channels: 1,
|
|
606572
|
-
sampleWidth: 2,
|
|
606573
|
-
chunkDuration: 3
|
|
606574
|
-
});
|
|
606575
|
-
this.liveTranscriber.on(
|
|
606576
|
-
"transcript",
|
|
606577
|
-
(evt) => {
|
|
606578
|
-
if (!evt.text.trim()) return;
|
|
606579
|
-
this.lastTranscriptTime = Date.now();
|
|
606580
|
-
this.pendingText = evt.text.trim();
|
|
606581
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
606582
|
-
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
606583
|
-
}
|
|
606584
|
-
);
|
|
606585
|
-
this.liveTranscriber.on("error", (err) => {
|
|
606586
|
-
this.emit("error", err);
|
|
606587
|
-
});
|
|
606588
|
-
await new Promise((resolve76, reject) => {
|
|
606589
|
-
const timeout2 = setTimeout(
|
|
606590
|
-
() => reject(new Error("Model load timeout (60s)")),
|
|
606591
|
-
6e4
|
|
606592
|
-
);
|
|
606593
|
-
this.liveTranscriber.on("ready", () => {
|
|
606594
|
-
clearTimeout(timeout2);
|
|
606595
|
-
resolve76();
|
|
606596
|
-
});
|
|
606597
|
-
this.liveTranscriber.on("error", (err) => {
|
|
606598
|
-
clearTimeout(timeout2);
|
|
606599
|
-
reject(err);
|
|
606600
|
-
});
|
|
606601
|
-
});
|
|
606602
|
-
if (caughtUncaught) {
|
|
606603
|
-
throw caughtUncaught;
|
|
606604
|
-
}
|
|
606605
|
-
} catch (err) {
|
|
606606
|
-
try {
|
|
606607
|
-
this.liveTranscriber?.stop();
|
|
606608
|
-
} catch {
|
|
606609
|
-
}
|
|
606610
|
-
this.liveTranscriber = null;
|
|
606611
|
-
transcribeCliError = err instanceof Error ? err.message : String(err);
|
|
606612
|
-
} finally {
|
|
606613
|
-
process.removeListener("uncaughtException", uncaughtHandler);
|
|
606614
|
-
}
|
|
606615
|
-
}
|
|
606616
|
-
}
|
|
606617
|
-
}
|
|
606618
|
-
if (!this.liveTranscriber) {
|
|
606619
|
-
const scriptPath2 = await findLiveWhisperScript();
|
|
606620
|
-
if (!scriptPath2) {
|
|
606621
|
-
const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
|
|
606622
|
-
return `No transcription backend available.${hint} live-whisper.py not found.`;
|
|
606623
|
-
}
|
|
606624
|
-
try {
|
|
606625
|
-
const fallback = new WhisperFallbackTranscriber(
|
|
606626
|
-
this.config.model,
|
|
606627
|
-
scriptPath2
|
|
606628
|
-
);
|
|
606629
|
-
usedFallback = true;
|
|
606630
|
-
fallback.on("status", (msg) => {
|
|
606631
|
-
this.emit("info", msg);
|
|
606632
|
-
});
|
|
606633
|
-
await fallback.start();
|
|
606634
|
-
fallback.on("transcript", (evt) => {
|
|
606635
|
-
if (!evt.text.trim()) return;
|
|
606636
|
-
this.lastTranscriptTime = Date.now();
|
|
606637
|
-
this.pendingText = evt.text.trim();
|
|
606638
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
606639
|
-
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
606640
|
-
});
|
|
606641
|
-
fallback.on("error", (err) => {
|
|
606642
|
-
this.emit("error", err);
|
|
606643
|
-
});
|
|
606644
|
-
this.liveTranscriber = fallback;
|
|
606645
|
-
} catch (err) {
|
|
606646
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
606647
|
-
const tcHint = transcribeCliError ? `
|
|
606648
|
-
transcribe-cli error: ${transcribeCliError}` : "";
|
|
606649
|
-
return `Failed to start live transcription: ${msg}${tcHint}`;
|
|
606650
|
-
}
|
|
606651
|
-
}
|
|
606652
|
-
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
606653
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606654
|
-
env: { ...process.env }
|
|
606655
|
-
});
|
|
606656
|
-
this.micProcess.stdout?.on("data", (chunk) => {
|
|
606657
|
-
if (this.active && !this.paused && this.liveTranscriber) {
|
|
606658
|
-
this.liveTranscriber.write(chunk);
|
|
606659
|
-
}
|
|
606660
|
-
});
|
|
606661
|
-
this.micProcess.stderr?.on("data", () => {
|
|
606662
|
-
});
|
|
606663
|
-
onChildError(this.micProcess, (err) => {
|
|
606664
|
-
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
606665
|
-
this.stop();
|
|
606666
|
-
});
|
|
606667
|
-
onChildClose(this.micProcess, () => {
|
|
606668
|
-
if (this.active) {
|
|
606669
|
-
this.stop();
|
|
606670
|
-
}
|
|
606671
|
-
});
|
|
606672
|
-
this.active = true;
|
|
606673
|
-
this.blinkState = true;
|
|
606674
|
-
this.blinkTimer = setInterval(() => {
|
|
606675
|
-
this.blinkState = !this.blinkState;
|
|
606676
|
-
this.emit("recording", this.blinkState);
|
|
606677
|
-
}, 500);
|
|
606678
|
-
this.lastTranscriptTime = Date.now();
|
|
606679
|
-
this.emit("started");
|
|
606680
|
-
this.emit("recording", true);
|
|
606681
|
-
const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
|
|
606682
|
-
const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
|
|
606683
|
-
return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
|
|
606684
|
-
}
|
|
606685
|
-
/**
|
|
606686
|
-
* Stop listening — cleanup mic, transcriber, timers.
|
|
606687
|
-
*/
|
|
606688
|
-
async stop() {
|
|
606689
|
-
if (!this.active) return "Not listening.";
|
|
606690
|
-
this.active = false;
|
|
606691
|
-
this.blinkState = false;
|
|
606692
|
-
if (this.blinkTimer) {
|
|
606693
|
-
clearInterval(this.blinkTimer);
|
|
606694
|
-
this.blinkTimer = null;
|
|
606695
|
-
}
|
|
606696
|
-
if (this.silenceTimer) {
|
|
606697
|
-
clearTimeout(this.silenceTimer);
|
|
606698
|
-
this.silenceTimer = null;
|
|
606699
|
-
}
|
|
606700
|
-
if (this.countdownInterval) {
|
|
606701
|
-
clearInterval(this.countdownInterval);
|
|
606702
|
-
this.countdownInterval = null;
|
|
606703
|
-
}
|
|
606704
|
-
if (this.micProcess) {
|
|
606705
|
-
try {
|
|
606706
|
-
this.micProcess.kill("SIGTERM");
|
|
606707
|
-
} catch {
|
|
606708
|
-
}
|
|
606709
|
-
this.micProcess = null;
|
|
606710
|
-
}
|
|
606711
|
-
if (this.liveTranscriber) {
|
|
606712
|
-
try {
|
|
606713
|
-
this.liveTranscriber.stop();
|
|
606714
|
-
} catch {
|
|
606715
|
-
}
|
|
606716
|
-
this.liveTranscriber = null;
|
|
606717
|
-
}
|
|
606718
|
-
this.emit("recording", false);
|
|
606719
|
-
this.emit("stopped");
|
|
606720
|
-
const result = this.pendingText;
|
|
606721
|
-
this.pendingText = "";
|
|
606722
|
-
return result || "Stopped listening.";
|
|
606723
|
-
}
|
|
606724
|
-
/**
|
|
606725
|
-
* Accept the current pending transcript (for confirm mode).
|
|
606726
|
-
* Returns the text to inject into the input line.
|
|
606727
|
-
*/
|
|
606728
|
-
acceptTranscript() {
|
|
606729
|
-
const text2 = this.pendingText;
|
|
606730
|
-
this.pendingText = "";
|
|
606731
|
-
return text2;
|
|
606732
|
-
}
|
|
606733
|
-
/**
|
|
606734
|
-
* Pause microphone capture (for voicechat during TTS output).
|
|
606735
|
-
* Keeps transcriber alive but stops feeding it audio.
|
|
606736
|
-
*/
|
|
606737
|
-
pause() {
|
|
606738
|
-
if (!this.active || this.paused) return;
|
|
606739
|
-
this.paused = true;
|
|
606740
|
-
if (this.blinkTimer) {
|
|
606741
|
-
clearInterval(this.blinkTimer);
|
|
606742
|
-
this.blinkTimer = null;
|
|
606743
|
-
}
|
|
606744
|
-
this.blinkState = false;
|
|
606745
|
-
if (this.micProcess) {
|
|
606746
|
-
try {
|
|
606747
|
-
this.micProcess.kill("SIGTERM");
|
|
606748
|
-
} catch {
|
|
606749
|
-
}
|
|
606750
|
-
this.micProcess = null;
|
|
606751
|
-
}
|
|
606752
|
-
this.emit("paused");
|
|
606753
|
-
this.emit("recording", false);
|
|
606754
|
-
}
|
|
606755
|
-
/**
|
|
606756
|
-
* Resume microphone capture after pause.
|
|
606757
|
-
*/
|
|
606758
|
-
async resume() {
|
|
606759
|
-
if (!this.active || !this.paused) return "Not paused.";
|
|
606760
|
-
this.paused = false;
|
|
606761
|
-
const micCmd = await findMicCaptureCommand();
|
|
606762
|
-
if (!micCmd) {
|
|
606763
|
-
return "No microphone capture tool found.";
|
|
606764
|
-
}
|
|
606765
|
-
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
606766
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606767
|
-
env: { ...process.env }
|
|
606768
|
-
});
|
|
606769
|
-
this.micProcess.stdout?.on("data", (chunk) => {
|
|
606770
|
-
if (this.active && !this.paused && this.liveTranscriber) {
|
|
606771
|
-
this.liveTranscriber.write(chunk);
|
|
606772
|
-
}
|
|
606773
|
-
});
|
|
606774
|
-
this.micProcess.stderr?.on("data", () => {
|
|
606775
|
-
});
|
|
606776
|
-
onChildError(this.micProcess, (err) => {
|
|
606777
|
-
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
606778
|
-
this.stop();
|
|
606779
|
-
});
|
|
606780
|
-
onChildClose(this.micProcess, () => {
|
|
606781
|
-
if (this.active && !this.paused) {
|
|
606782
|
-
this.stop();
|
|
606783
|
-
}
|
|
606784
|
-
});
|
|
606785
|
-
this.blinkState = true;
|
|
606786
|
-
this.blinkTimer = setInterval(() => {
|
|
606787
|
-
this.blinkState = !this.blinkState;
|
|
606788
|
-
this.emit("recording", this.blinkState);
|
|
606789
|
-
}, 500);
|
|
606790
|
-
this.emit("resumed");
|
|
606791
|
-
this.emit("recording", true);
|
|
606792
|
-
return "Resumed listening.";
|
|
606793
|
-
}
|
|
606794
|
-
/**
|
|
606795
|
-
* Create a standalone transcriber for call sessions.
|
|
606796
|
-
* No microphone capture — just an ASR engine you feed PCM chunks to.
|
|
606797
|
-
* Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
|
|
606798
|
-
* Caller is responsible for calling .stop() when done.
|
|
606799
|
-
*/
|
|
606800
|
-
async createCallTranscriber() {
|
|
606801
|
-
let tc = await this.loadTranscribeCli();
|
|
606802
|
-
if (!tc && _bgInstallPromise) {
|
|
606803
|
-
await _bgInstallPromise;
|
|
606804
|
-
this.transcribeCliAvailable = null;
|
|
606805
|
-
tc = await this.loadTranscribeCli();
|
|
606806
|
-
}
|
|
606807
|
-
if (!tc) {
|
|
606808
|
-
try {
|
|
606809
|
-
await ensureManagedTranscribeCliNode();
|
|
606810
|
-
this.transcribeCliAvailable = null;
|
|
606811
|
-
tc = await this.loadTranscribeCli();
|
|
606812
|
-
} catch {
|
|
606813
|
-
}
|
|
606814
|
-
}
|
|
606815
|
-
if (tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
|
|
606816
|
-
try {
|
|
606817
|
-
const transcriber = new tc.TranscribeLive({
|
|
606818
|
-
model: this.config.model,
|
|
606819
|
-
sampleRate: 16e3,
|
|
606820
|
-
channels: 1,
|
|
606821
|
-
sampleWidth: 2,
|
|
606822
|
-
chunkDuration: 3
|
|
606823
|
-
});
|
|
606824
|
-
await new Promise((resolve76, reject) => {
|
|
606825
|
-
const timeout2 = setTimeout(
|
|
606826
|
-
() => reject(new Error("Model load timeout (60s)")),
|
|
606827
|
-
6e4
|
|
606828
|
-
);
|
|
606829
|
-
transcriber.on("ready", () => {
|
|
606830
|
-
clearTimeout(timeout2);
|
|
606831
|
-
resolve76();
|
|
606832
|
-
});
|
|
606833
|
-
transcriber.on("error", (err) => {
|
|
606834
|
-
clearTimeout(timeout2);
|
|
606835
|
-
reject(err);
|
|
606836
|
-
});
|
|
606837
|
-
});
|
|
606838
|
-
return transcriber;
|
|
606839
|
-
} catch {
|
|
606840
|
-
}
|
|
606841
|
-
}
|
|
606842
|
-
const scriptPath2 = await findLiveWhisperScript();
|
|
606843
|
-
if (!scriptPath2) return null;
|
|
606844
|
-
try {
|
|
606845
|
-
const fallback = new WhisperFallbackTranscriber(
|
|
606846
|
-
this.config.model,
|
|
606847
|
-
scriptPath2
|
|
606848
|
-
);
|
|
606849
|
-
await fallback.start();
|
|
606850
|
-
return fallback;
|
|
606851
|
-
} catch {
|
|
606852
|
-
return null;
|
|
606853
|
-
}
|
|
606854
|
-
}
|
|
606855
|
-
/**
|
|
606856
|
-
* Transcribe a file (audio or video) using transcribe-cli.
|
|
606857
|
-
* Returns the transcription result.
|
|
606858
|
-
*/
|
|
606859
|
-
async transcribeFile(filePath, outputDir2) {
|
|
606860
|
-
let tc = await this.loadTranscribeCli();
|
|
606861
|
-
if (!tc && _bgInstallPromise) {
|
|
606862
|
-
await _bgInstallPromise;
|
|
606863
|
-
this.transcribeCliAvailable = null;
|
|
606864
|
-
tc = await this.loadTranscribeCli();
|
|
606865
|
-
}
|
|
606866
|
-
if (!tc) {
|
|
606867
|
-
try {
|
|
606868
|
-
await ensureManagedTranscribeCliNode();
|
|
606869
|
-
this.transcribeCliAvailable = null;
|
|
606870
|
-
tc = await this.loadTranscribeCli();
|
|
606871
|
-
} catch {
|
|
606872
|
-
}
|
|
606873
|
-
}
|
|
606874
|
-
await ensureVenvForTranscribeCli();
|
|
606875
|
-
let lastErr = null;
|
|
606876
|
-
if (tc) {
|
|
606877
|
-
try {
|
|
606878
|
-
const result = await tc.transcribe(filePath, {
|
|
606879
|
-
model: this.config.model,
|
|
606880
|
-
format: "json",
|
|
606881
|
-
diarize: false,
|
|
606882
|
-
wordTimestamps: false
|
|
606883
|
-
});
|
|
606884
|
-
if (outputDir2) {
|
|
606885
|
-
const { basename: basename43 } = await import("node:path");
|
|
606886
|
-
const transcriptDir = join127(outputDir2, ".omnius", "transcripts");
|
|
606887
|
-
mkdirSync69(transcriptDir, { recursive: true });
|
|
606888
|
-
const outFile = join127(transcriptDir, `${basename43(filePath)}.txt`);
|
|
606889
|
-
writeFileSync58(outFile, result.text, "utf-8");
|
|
606890
|
-
}
|
|
606891
|
-
return {
|
|
606892
|
-
text: result.text,
|
|
606893
|
-
duration: result.duration,
|
|
606894
|
-
speakers: result.speakers,
|
|
606895
|
-
segments: result.segments
|
|
606896
|
-
};
|
|
606897
|
-
} catch (err) {
|
|
606898
|
-
lastErr = err;
|
|
606899
|
-
}
|
|
606900
|
-
}
|
|
606901
|
-
const fb = await transcribeFileViaWhisper(filePath, this.config.model);
|
|
606902
|
-
if (fb) {
|
|
606903
|
-
if (outputDir2) {
|
|
606904
|
-
const { basename: basename43 } = await import("node:path");
|
|
606905
|
-
const transcriptDir = join127(outputDir2, ".omnius", "transcripts");
|
|
606906
|
-
mkdirSync69(transcriptDir, { recursive: true });
|
|
606907
|
-
const outFile = join127(transcriptDir, `${basename43(filePath)}.txt`);
|
|
606908
|
-
writeFileSync58(outFile, fb.text, "utf-8");
|
|
606909
|
-
}
|
|
606910
|
-
return fb;
|
|
606911
|
-
}
|
|
606912
|
-
if (lastErr) {
|
|
606913
|
-
this.emit(
|
|
606914
|
-
"error",
|
|
606915
|
-
lastErr instanceof Error ? lastErr : new Error(String(lastErr))
|
|
606916
|
-
);
|
|
606917
|
-
}
|
|
606918
|
-
return null;
|
|
606919
|
-
}
|
|
606920
|
-
// -------------------------------------------------------------------------
|
|
606921
|
-
// Auto-mode silence detection
|
|
606922
|
-
// -------------------------------------------------------------------------
|
|
606923
|
-
resetSilenceTimer() {
|
|
606924
|
-
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
606925
|
-
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
|
606926
|
-
const timeoutMs = this.config.silenceTimeoutMs;
|
|
606927
|
-
const startTime = Date.now();
|
|
606928
|
-
this.countdownInterval = setInterval(() => {
|
|
606929
|
-
const elapsed = Date.now() - startTime;
|
|
606930
|
-
const remaining = Math.max(0, Math.ceil((timeoutMs - elapsed) / 1e3));
|
|
606931
|
-
this.emit("countdown", remaining);
|
|
606932
|
-
if (remaining <= 0 && this.countdownInterval) {
|
|
606933
|
-
clearInterval(this.countdownInterval);
|
|
606934
|
-
this.countdownInterval = null;
|
|
606935
|
-
}
|
|
606936
|
-
}, 1e3);
|
|
606937
|
-
this.silenceTimer = setTimeout(() => {
|
|
606938
|
-
if (this.active && this.pendingText) {
|
|
606939
|
-
this.emit("countdown", 0);
|
|
606940
|
-
this.emit("transcript", this.pendingText, true);
|
|
606941
|
-
}
|
|
606942
|
-
}, timeoutMs);
|
|
606943
|
-
}
|
|
606944
|
-
};
|
|
606945
|
-
_bgInstallPromise = null;
|
|
606946
|
-
_managedTranscribeCliInstallPromise = null;
|
|
606947
|
-
_engine = null;
|
|
606948
|
-
}
|
|
606949
|
-
});
|
|
606950
|
-
|
|
606951
607286
|
// node_modules/.pnpm/ws@8.20.1/node_modules/ws/lib/constants.js
|
|
606952
607287
|
var require_constants11 = __commonJS({
|
|
606953
607288
|
"node_modules/.pnpm/ws@8.20.1/node_modules/ws/lib/constants.js"(exports, module) {
|
|
@@ -625597,6 +625932,10 @@ var init_status_bar = __esm({
|
|
|
625597
625932
|
inputStateProvider = null;
|
|
625598
625933
|
/** Whether microphone is recording (blinking indicator) */
|
|
625599
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;
|
|
625600
625939
|
/** Countdown seconds for auto-mode silence timeout (0 = not counting down) */
|
|
625601
625940
|
_countdown = 0;
|
|
625602
625941
|
_recBlink = true;
|
|
@@ -626410,6 +626749,18 @@ var init_status_bar = __esm({
|
|
|
626410
626749
|
this._countdown = seconds;
|
|
626411
626750
|
if (this.active) this.renderFooterPreserveCursor();
|
|
626412
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
|
+
}
|
|
626413
626764
|
/** Start or stop the braille-dot processing animation on the buffer line. */
|
|
626414
626765
|
setProcessing(active) {
|
|
626415
626766
|
if (active === this._processing) return;
|
|
@@ -629087,10 +629438,19 @@ ${CONTENT_BG_SEQ}`);
|
|
|
629087
629438
|
});
|
|
629088
629439
|
}
|
|
629089
629440
|
if (this._recording) {
|
|
629090
|
-
const dot = this._recBlink ? pastel2(210, "●") : " ";
|
|
629091
629441
|
const countdown = this._countdown > 0 ? c3.dim(` ${_StatusBar.digitBar(this._countdown)}s`) : "";
|
|
629092
|
-
const
|
|
629093
|
-
|
|
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
|
+
}
|
|
629094
629454
|
sections.push({
|
|
629095
629455
|
expanded: recStr,
|
|
629096
629456
|
compact: recStr,
|
|
@@ -696041,6 +696401,19 @@ function repeatingCharPenalty(s2) {
|
|
|
696041
696401
|
if (cur > maxRun) maxRun = cur;
|
|
696042
696402
|
return Math.min(1, Math.max(0, (maxRun - 3) / 10));
|
|
696043
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
|
+
}
|
|
696044
696417
|
function computeSignalFromText(text2, confidence2) {
|
|
696045
696418
|
const t2 = text2.trim();
|
|
696046
696419
|
if (!t2) return 0;
|
|
@@ -696055,6 +696428,7 @@ function computeSignalFromText(text2, confidence2) {
|
|
|
696055
696428
|
else if (wc >= 1 && alpha >= 0.3 && len >= 4) score = 0.35;
|
|
696056
696429
|
else score = 0.15;
|
|
696057
696430
|
score -= repeatingCharPenalty(t2) * 0.4;
|
|
696431
|
+
score -= gibberishTokenPenalty(t2) * 0.8;
|
|
696058
696432
|
if (typeof confidence2 === "number" && !Number.isNaN(confidence2)) {
|
|
696059
696433
|
score = 0.7 * score + 0.3 * clamp0116(confidence2);
|
|
696060
696434
|
}
|
|
@@ -696063,6 +696437,9 @@ function computeSignalFromText(text2, confidence2) {
|
|
|
696063
696437
|
function truncateForLog(s2, n2) {
|
|
696064
696438
|
return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
|
|
696065
696439
|
}
|
|
696440
|
+
function sanitizeToolName2(name10) {
|
|
696441
|
+
return name10.trim().replace(/[({\[].*$/, "").replace(/[^A-Za-z0-9_-]/g, "");
|
|
696442
|
+
}
|
|
696066
696443
|
function extractToolJson(text2) {
|
|
696067
696444
|
const lines = text2.split(/\r?\n/);
|
|
696068
696445
|
for (const line of lines) {
|
|
@@ -696071,7 +696448,8 @@ function extractToolJson(text2) {
|
|
|
696071
696448
|
try {
|
|
696072
696449
|
const obj = JSON.parse(t2);
|
|
696073
696450
|
if (typeof obj.tool === "string") {
|
|
696074
|
-
const name10 = obj.tool;
|
|
696451
|
+
const name10 = sanitizeToolName2(obj.tool);
|
|
696452
|
+
if (!name10) continue;
|
|
696075
696453
|
const args = obj.args && typeof obj.args === "object" ? obj.args : {};
|
|
696076
696454
|
return { name: name10, args };
|
|
696077
696455
|
}
|
|
@@ -696089,18 +696467,38 @@ function extractToolJsonLoose(text2) {
|
|
|
696089
696467
|
try {
|
|
696090
696468
|
const obj = JSON.parse(match[0]);
|
|
696091
696469
|
if (typeof obj.tool === "string") {
|
|
696092
|
-
const
|
|
696093
|
-
|
|
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
|
+
}
|
|
696094
696475
|
}
|
|
696095
696476
|
} catch {
|
|
696096
696477
|
}
|
|
696097
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
|
+
}
|
|
696098
696495
|
return null;
|
|
696099
696496
|
}
|
|
696100
696497
|
function stripToolJsonLines(text2) {
|
|
696101
696498
|
const lines = text2.split(/\r?\n/);
|
|
696102
696499
|
const kept = lines.filter((l2) => {
|
|
696103
696500
|
const t2 = l2.trim();
|
|
696501
|
+
if (/^\{\s*"?tool"?\s*[:=]/.test(t2)) return false;
|
|
696104
696502
|
if (!t2.startsWith("{") || !t2.endsWith("}")) return true;
|
|
696105
696503
|
try {
|
|
696106
696504
|
const obj = JSON.parse(t2);
|
|
@@ -696340,12 +696738,12 @@ Rules:
|
|
|
696340
696738
|
this.active = true;
|
|
696341
696739
|
this.context = [{ role: "system", content: SYSTEM_PROMPT2 }];
|
|
696342
696740
|
if (this.toolRelay) {
|
|
696343
|
-
this.toolCatalogNote = `Available tools
|
|
696344
|
-
|
|
696345
|
-
|
|
696346
|
-
|
|
696347
|
-
|
|
696348
|
-
|
|
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`;
|
|
696349
696747
|
this.context.push({ role: "system", content: this.toolCatalogNote });
|
|
696350
696748
|
}
|
|
696351
696749
|
this.turnCount = 0;
|
|
@@ -696488,7 +696886,7 @@ Rules:
|
|
|
696488
696886
|
this.setState("LISTENING");
|
|
696489
696887
|
return;
|
|
696490
696888
|
}
|
|
696491
|
-
const score = this.lastSignalScore ?? computeSignalFromText(text2);
|
|
696889
|
+
const score = Math.min(this.lastSignalScore ?? 1, computeSignalFromText(text2));
|
|
696492
696890
|
if (score < MIN_SIGNAL_SCORE || NOISE_ONLY_RE.test(text2)) {
|
|
696493
696891
|
if (this.debugSnr) this.onStatus(`Ignoring low-signal utterance (SNR:${score.toFixed(2)}): ${truncateForLog(text2, 48)}`);
|
|
696494
696892
|
this.emit("snrFiltered", { score, text: text2 });
|
|
@@ -735480,6 +735878,9 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
735480
735878
|
engine.on("recording", (active) => {
|
|
735481
735879
|
statusBar.setRecording(active);
|
|
735482
735880
|
});
|
|
735881
|
+
engine.on("level", (evt) => {
|
|
735882
|
+
statusBar.setMicActivity(evt.speechActive, evt.levelDb);
|
|
735883
|
+
});
|
|
735483
735884
|
engine.on("countdown", (seconds) => {
|
|
735484
735885
|
statusBar.setCountdown(seconds);
|
|
735485
735886
|
});
|
|
@@ -735784,6 +736185,12 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
735784
736185
|
const { VoiceChatSession: VoiceChatSession2 } = await Promise.resolve().then(() => (init_voicechat(), voicechat_exports));
|
|
735785
736186
|
const { ListenEngine: ListenEngine2 } = await Promise.resolve().then(() => (init_listen(), listen_exports));
|
|
735786
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
|
+
});
|
|
735787
736194
|
const summaryRunner = {
|
|
735788
736195
|
injectUserMessage(content) {
|
|
735789
736196
|
if (activeTask?.runner) {
|