omnius 1.0.417 → 1.0.419
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 +1540 -1245
- package/dist/scripts/live-whisper.py +42 -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,1397 @@ 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
|
+
async function ensureSourceCaptureVolume(source) {
|
|
603063
|
+
try {
|
|
603064
|
+
await execFileText4("pactl", ["set-source-mute", source, "0"], { timeout: 3e3 });
|
|
603065
|
+
} catch {
|
|
603066
|
+
}
|
|
603067
|
+
try {
|
|
603068
|
+
const raw = await execFileText4("pactl", ["get-source-volume", source], { timeout: 3e3 });
|
|
603069
|
+
const percents = [...raw.matchAll(/(\d+)%/g)].map((m2) => Number(m2[1])).filter((n2) => Number.isFinite(n2));
|
|
603070
|
+
const current = percents.length ? Math.max(...percents) : null;
|
|
603071
|
+
if (current !== null && current < 100) {
|
|
603072
|
+
await execFileText4("pactl", ["set-source-volume", source, "100%"], { timeout: 3e3 });
|
|
603073
|
+
}
|
|
603074
|
+
} catch {
|
|
603075
|
+
}
|
|
603076
|
+
}
|
|
603077
|
+
function micFfmpegFilter(channels) {
|
|
603078
|
+
const parts = [];
|
|
603079
|
+
if ((channels ?? 1) > 2) parts.push("pan=mono|c0=c0");
|
|
603080
|
+
parts.push("highpass=f=80");
|
|
603081
|
+
const gain2 = Number(process.env["OMNIUS_MIC_GAIN_DB"] ?? "");
|
|
603082
|
+
if (Number.isFinite(gain2) && gain2 !== 0) parts.push(`volume=${gain2}dB`);
|
|
603083
|
+
return parts.join(",");
|
|
603084
|
+
}
|
|
603085
|
+
async function findMicCaptureCommand() {
|
|
603086
|
+
const platform7 = process.platform;
|
|
603087
|
+
if (platform7 === "linux") {
|
|
603088
|
+
const resolved = await resolveMicSource();
|
|
603089
|
+
if (resolved?.alsaDevice && await commandExists2("arecord")) {
|
|
603090
|
+
return {
|
|
603091
|
+
cmd: "arecord",
|
|
603092
|
+
args: ["-D", resolved.alsaDevice, "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-q", "-"],
|
|
603093
|
+
device: resolved.alsaDevice
|
|
603094
|
+
};
|
|
603095
|
+
}
|
|
603096
|
+
if (resolved?.pulseSource && await commandExists2("ffmpeg")) {
|
|
603097
|
+
await ensureSourceCaptureVolume(resolved.pulseSource);
|
|
603098
|
+
return {
|
|
603099
|
+
cmd: "ffmpeg",
|
|
603100
|
+
args: [
|
|
603101
|
+
"-f",
|
|
603102
|
+
"pulse",
|
|
603103
|
+
"-i",
|
|
603104
|
+
resolved.pulseSource,
|
|
603105
|
+
"-af",
|
|
603106
|
+
micFfmpegFilter(resolved.channels),
|
|
603107
|
+
"-ar",
|
|
603108
|
+
"16000",
|
|
603109
|
+
"-ac",
|
|
603110
|
+
"1",
|
|
603111
|
+
"-f",
|
|
603112
|
+
"s16le",
|
|
603113
|
+
"-loglevel",
|
|
603114
|
+
"quiet",
|
|
603115
|
+
"pipe:1"
|
|
603116
|
+
],
|
|
603117
|
+
device: resolved.label
|
|
603118
|
+
};
|
|
603119
|
+
}
|
|
603120
|
+
if (await commandExists2("arecord")) {
|
|
603121
|
+
return {
|
|
603122
|
+
cmd: "arecord",
|
|
603123
|
+
args: ["-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-q", "-"],
|
|
603124
|
+
device: "default (arecord)"
|
|
603125
|
+
};
|
|
603126
|
+
}
|
|
603127
|
+
}
|
|
603128
|
+
if (platform7 === "darwin") {
|
|
603129
|
+
if (await commandExists2("sox")) {
|
|
603130
|
+
return {
|
|
603131
|
+
cmd: "sox",
|
|
603132
|
+
args: [
|
|
603133
|
+
"-d",
|
|
603134
|
+
"-t",
|
|
603135
|
+
"raw",
|
|
603136
|
+
"-r",
|
|
603137
|
+
"16000",
|
|
603138
|
+
"-c",
|
|
603139
|
+
"1",
|
|
603140
|
+
"-b",
|
|
603141
|
+
"16",
|
|
603142
|
+
"-e",
|
|
603143
|
+
"signed-integer",
|
|
603144
|
+
"-"
|
|
603145
|
+
],
|
|
603146
|
+
device: "default (sox)"
|
|
603147
|
+
};
|
|
603148
|
+
}
|
|
603149
|
+
}
|
|
603150
|
+
if (await commandExists2("ffmpeg")) {
|
|
603151
|
+
if (platform7 === "linux") {
|
|
603152
|
+
return {
|
|
603153
|
+
cmd: "ffmpeg",
|
|
603154
|
+
args: [
|
|
603155
|
+
"-f",
|
|
603156
|
+
"pulse",
|
|
603157
|
+
"-i",
|
|
603158
|
+
"default",
|
|
603159
|
+
"-ar",
|
|
603160
|
+
"16000",
|
|
603161
|
+
"-ac",
|
|
603162
|
+
"1",
|
|
603163
|
+
"-f",
|
|
603164
|
+
"s16le",
|
|
603165
|
+
"-loglevel",
|
|
603166
|
+
"quiet",
|
|
603167
|
+
"pipe:1"
|
|
603168
|
+
],
|
|
603169
|
+
device: "pulse:default"
|
|
603170
|
+
};
|
|
603171
|
+
} else if (platform7 === "darwin") {
|
|
603172
|
+
return {
|
|
603173
|
+
cmd: "ffmpeg",
|
|
603174
|
+
args: [
|
|
603175
|
+
"-f",
|
|
603176
|
+
"avfoundation",
|
|
603177
|
+
"-i",
|
|
603178
|
+
":0",
|
|
603179
|
+
"-ar",
|
|
603180
|
+
"16000",
|
|
603181
|
+
"-ac",
|
|
603182
|
+
"1",
|
|
603183
|
+
"-f",
|
|
603184
|
+
"s16le",
|
|
603185
|
+
"-loglevel",
|
|
603186
|
+
"quiet",
|
|
603187
|
+
"pipe:1"
|
|
603188
|
+
],
|
|
603189
|
+
device: "avfoundation:0"
|
|
603190
|
+
};
|
|
603191
|
+
}
|
|
603192
|
+
}
|
|
603193
|
+
return null;
|
|
603194
|
+
}
|
|
603195
|
+
async function findTranscribeFileScript() {
|
|
603196
|
+
const thisDir = dirname37(fileURLToPath16(import.meta.url));
|
|
603197
|
+
const candidates = [
|
|
603198
|
+
join125(thisDir, "../../../../packages/execution/scripts/transcribe-file.py"),
|
|
603199
|
+
join125(thisDir, "../../../packages/execution/scripts/transcribe-file.py"),
|
|
603200
|
+
join125(thisDir, "../../execution/scripts/transcribe-file.py"),
|
|
603201
|
+
join125(thisDir, "../scripts/transcribe-file.py"),
|
|
603202
|
+
join125(thisDir, "../../scripts/transcribe-file.py")
|
|
603203
|
+
];
|
|
603204
|
+
for (const p2 of candidates) {
|
|
603205
|
+
if (existsSync110(p2)) return p2;
|
|
603206
|
+
}
|
|
603207
|
+
try {
|
|
603208
|
+
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
603209
|
+
const candidates2 = [
|
|
603210
|
+
join125(globalRoot, "omnius", "dist", "scripts", "transcribe-file.py"),
|
|
603211
|
+
join125(globalRoot, "omnius", "scripts", "transcribe-file.py")
|
|
603212
|
+
];
|
|
603213
|
+
for (const p2 of candidates2) {
|
|
603214
|
+
if (existsSync110(p2)) return p2;
|
|
603215
|
+
}
|
|
603216
|
+
} catch {
|
|
603217
|
+
}
|
|
603218
|
+
return null;
|
|
603219
|
+
}
|
|
603220
|
+
async function transcribeFileViaWhisper(filePath, model) {
|
|
603221
|
+
const script = await findTranscribeFileScript();
|
|
603222
|
+
if (!script) return null;
|
|
603223
|
+
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
603224
|
+
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
603225
|
+
const venvPython2 = join125(homedir39(), ".omnius", "venv", bin, exe);
|
|
603226
|
+
if (!existsSync110(venvPython2)) return null;
|
|
603227
|
+
return new Promise((resolve76) => {
|
|
603228
|
+
const child = spawn29(venvPython2, [script], {
|
|
603229
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603230
|
+
env: process.env
|
|
603231
|
+
});
|
|
603232
|
+
let out = "";
|
|
603233
|
+
let err = "";
|
|
603234
|
+
let timer = null;
|
|
603235
|
+
const stop2 = (val) => {
|
|
603236
|
+
if (timer) clearTimeout(timer);
|
|
603237
|
+
try {
|
|
603238
|
+
child.kill("SIGTERM");
|
|
603239
|
+
} catch {
|
|
603240
|
+
}
|
|
603241
|
+
resolve76(val ? parse3(val) : null);
|
|
603242
|
+
};
|
|
603243
|
+
function parse3(raw) {
|
|
603244
|
+
try {
|
|
603245
|
+
const j = JSON.parse(raw);
|
|
603246
|
+
if (j.error) return null;
|
|
603247
|
+
if (typeof j.text !== "string") return null;
|
|
603248
|
+
return {
|
|
603249
|
+
text: j.text.trim(),
|
|
603250
|
+
duration: null,
|
|
603251
|
+
speakers: [],
|
|
603252
|
+
segments: []
|
|
603253
|
+
};
|
|
603254
|
+
} catch {
|
|
603255
|
+
return null;
|
|
603256
|
+
}
|
|
603257
|
+
}
|
|
603258
|
+
child.stdout?.on("data", (d2) => {
|
|
603259
|
+
out += d2.toString("utf-8");
|
|
603260
|
+
});
|
|
603261
|
+
child.stderr?.on("data", (d2) => {
|
|
603262
|
+
err += d2.toString("utf-8");
|
|
603263
|
+
});
|
|
603264
|
+
child.once("error", () => stop2(null));
|
|
603265
|
+
child.once("close", () => {
|
|
603266
|
+
if (out.trim()) resolve76(parse3(out));
|
|
603267
|
+
else {
|
|
603268
|
+
void err;
|
|
603269
|
+
resolve76(null);
|
|
603270
|
+
}
|
|
603271
|
+
});
|
|
603272
|
+
timer = setTimeout(() => stop2(null), 12e4);
|
|
603273
|
+
try {
|
|
603274
|
+
child.stdin?.write(JSON.stringify({ path: filePath, model }) + "\n");
|
|
603275
|
+
child.stdin?.end();
|
|
603276
|
+
} catch {
|
|
603277
|
+
stop2(null);
|
|
603278
|
+
}
|
|
603279
|
+
});
|
|
603280
|
+
}
|
|
603281
|
+
async function findLiveWhisperScript() {
|
|
603282
|
+
const thisDir = dirname37(fileURLToPath16(import.meta.url));
|
|
603283
|
+
const candidates = [
|
|
603284
|
+
join125(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
|
|
603285
|
+
join125(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
|
|
603286
|
+
join125(thisDir, "../../execution/scripts/live-whisper.py"),
|
|
603287
|
+
// npm install layout — scripts bundled alongside dist
|
|
603288
|
+
join125(thisDir, "../scripts/live-whisper.py"),
|
|
603289
|
+
join125(thisDir, "../../scripts/live-whisper.py")
|
|
603290
|
+
];
|
|
603291
|
+
for (const p2 of candidates) {
|
|
603292
|
+
if (existsSync110(p2)) return p2;
|
|
603293
|
+
}
|
|
603294
|
+
try {
|
|
603295
|
+
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
603296
|
+
const candidates2 = [
|
|
603297
|
+
join125(globalRoot, "omnius", "dist", "scripts", "live-whisper.py"),
|
|
603298
|
+
join125(globalRoot, "omnius", "scripts", "live-whisper.py")
|
|
603299
|
+
];
|
|
603300
|
+
for (const p2 of candidates2) {
|
|
603301
|
+
if (existsSync110(p2)) return p2;
|
|
603302
|
+
}
|
|
603303
|
+
} catch {
|
|
603304
|
+
}
|
|
603305
|
+
const nvmBase = join125(homedir39(), ".nvm", "versions", "node");
|
|
603306
|
+
if (existsSync110(nvmBase)) {
|
|
603307
|
+
try {
|
|
603308
|
+
for (const ver of readdirSync35(nvmBase)) {
|
|
603309
|
+
const p2 = join125(
|
|
603310
|
+
nvmBase,
|
|
603311
|
+
ver,
|
|
603312
|
+
"lib",
|
|
603313
|
+
"node_modules",
|
|
603314
|
+
"omnius",
|
|
603315
|
+
"dist",
|
|
603316
|
+
"scripts",
|
|
603317
|
+
"live-whisper.py"
|
|
603318
|
+
);
|
|
603319
|
+
if (existsSync110(p2)) return p2;
|
|
603320
|
+
}
|
|
603321
|
+
} catch {
|
|
603322
|
+
}
|
|
603323
|
+
}
|
|
603324
|
+
return null;
|
|
603325
|
+
}
|
|
603326
|
+
async function ensureVenvForTranscribeCli() {
|
|
603327
|
+
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
603328
|
+
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
603329
|
+
const venvBin = join125(homedir39(), ".omnius", "venv", bin);
|
|
603330
|
+
const venvPython2 = join125(venvBin, exe);
|
|
603331
|
+
if (!existsSync110(venvPython2)) {
|
|
603332
|
+
try {
|
|
603333
|
+
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
603334
|
+
if (typeof mod3.ensureEmbedDeps === "function") mod3.ensureEmbedDeps();
|
|
603335
|
+
} catch {
|
|
603336
|
+
}
|
|
603337
|
+
}
|
|
603338
|
+
if (!existsSync110(venvPython2)) {
|
|
603339
|
+
try {
|
|
603340
|
+
mkdirSync68(join125(homedir39(), ".omnius", "venv"), { recursive: true });
|
|
603341
|
+
await execFileText4("python3", ["-m", "venv", join125(homedir39(), ".omnius", "venv")], { timeout: 6e4 });
|
|
603342
|
+
} catch {
|
|
603343
|
+
return false;
|
|
603344
|
+
}
|
|
603345
|
+
}
|
|
603346
|
+
process.env.TRANSCRIBE_PYTHON = venvPython2;
|
|
603347
|
+
const pathSep2 = process.platform === "win32" ? ";" : ":";
|
|
603348
|
+
const currentPath = process.env.PATH || "";
|
|
603349
|
+
if (!currentPath.split(pathSep2).includes(venvBin)) {
|
|
603350
|
+
process.env.PATH = `${venvBin}${pathSep2}${currentPath}`;
|
|
603351
|
+
}
|
|
603352
|
+
try {
|
|
603353
|
+
await execFileText4(venvPython2, ["-c", "import numpy"], { timeout: 1e4 });
|
|
603354
|
+
} catch {
|
|
603355
|
+
try {
|
|
603356
|
+
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "pip", "setuptools<81", "wheel", "numpy==1.26.4"], { timeout: 3e5 });
|
|
603357
|
+
} catch {
|
|
603358
|
+
return false;
|
|
603359
|
+
}
|
|
603360
|
+
}
|
|
603361
|
+
try {
|
|
603362
|
+
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
603363
|
+
return true;
|
|
603364
|
+
} catch {
|
|
603365
|
+
}
|
|
603366
|
+
try {
|
|
603367
|
+
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "transcribe-cli"], { timeout: 3e5 });
|
|
603368
|
+
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
603369
|
+
return true;
|
|
603370
|
+
} catch {
|
|
603371
|
+
return false;
|
|
603372
|
+
}
|
|
603373
|
+
}
|
|
603374
|
+
async function requireTranscribeCliFrom2(packageDir) {
|
|
603375
|
+
try {
|
|
603376
|
+
const { createRequire: createRequire11 } = await import("node:module");
|
|
603377
|
+
const req3 = createRequire11(import.meta.url);
|
|
603378
|
+
const distPath = join125(packageDir, "dist", "index.js");
|
|
603379
|
+
if (existsSync110(distPath)) return req3(distPath);
|
|
603380
|
+
return req3(packageDir);
|
|
603381
|
+
} catch {
|
|
603382
|
+
return null;
|
|
603383
|
+
}
|
|
603384
|
+
}
|
|
603385
|
+
async function loadManagedTranscribeCli() {
|
|
603386
|
+
const packageDir = join125(MANAGED_TRANSCRIBE_CLI_DIR2, "node_modules", "transcribe-cli");
|
|
603387
|
+
if (!existsSync110(join125(packageDir, "dist", "index.js"))) return null;
|
|
603388
|
+
return requireTranscribeCliFrom2(packageDir);
|
|
603389
|
+
}
|
|
603390
|
+
async function ensureManagedTranscribeCliNode() {
|
|
603391
|
+
if (await loadManagedTranscribeCli()) return true;
|
|
603392
|
+
if (_managedTranscribeCliInstallPromise) return _managedTranscribeCliInstallPromise;
|
|
603393
|
+
_managedTranscribeCliInstallPromise = (async () => {
|
|
603394
|
+
try {
|
|
603395
|
+
mkdirSync68(MANAGED_TRANSCRIBE_CLI_DIR2, { recursive: true });
|
|
603396
|
+
if (!existsSync110(join125(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"))) {
|
|
603397
|
+
writeFileSync57(
|
|
603398
|
+
join125(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"),
|
|
603399
|
+
JSON.stringify({ private: true, dependencies: {} }, null, 2),
|
|
603400
|
+
"utf8"
|
|
603401
|
+
);
|
|
603402
|
+
}
|
|
603403
|
+
await execFileText4("npm", ["install", "--prefix", MANAGED_TRANSCRIBE_CLI_DIR2, "transcribe-cli@^2.0.1"], {
|
|
603404
|
+
timeout: 18e4
|
|
603405
|
+
});
|
|
603406
|
+
return Boolean(await loadManagedTranscribeCli());
|
|
603407
|
+
} catch {
|
|
603408
|
+
return false;
|
|
603409
|
+
}
|
|
603410
|
+
})();
|
|
603411
|
+
return _managedTranscribeCliInstallPromise;
|
|
603412
|
+
}
|
|
603413
|
+
function ensureTranscribeCliBackground() {
|
|
603414
|
+
if (_bgInstallPromise) return _bgInstallPromise;
|
|
603415
|
+
_bgInstallPromise = (async () => {
|
|
603416
|
+
const nodeReady = await ensureManagedTranscribeCliNode();
|
|
603417
|
+
const pyReady = await ensureVenvForTranscribeCli();
|
|
603418
|
+
return nodeReady && pyReady;
|
|
603419
|
+
})();
|
|
603420
|
+
return _bgInstallPromise;
|
|
603421
|
+
}
|
|
603422
|
+
async function waitForTranscribeCli() {
|
|
603423
|
+
if (_bgInstallPromise) {
|
|
603424
|
+
return _bgInstallPromise;
|
|
603425
|
+
}
|
|
603426
|
+
return Boolean(await loadManagedTranscribeCli()) && await ensureVenvForTranscribeCli();
|
|
603427
|
+
}
|
|
603428
|
+
function getListenEngine(config) {
|
|
603429
|
+
if (!_engine) {
|
|
603430
|
+
_engine = new ListenEngine(config);
|
|
603431
|
+
}
|
|
603432
|
+
return _engine;
|
|
603433
|
+
}
|
|
603434
|
+
var MANAGED_TRANSCRIBE_CLI_DIR2, AUDIO_EXTENSIONS, VIDEO_EXTENSIONS2, listenLiveState, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _managedTranscribeCliInstallPromise, _engine;
|
|
603435
|
+
var init_listen = __esm({
|
|
603436
|
+
"packages/cli/src/tui/listen.ts"() {
|
|
603437
|
+
"use strict";
|
|
603438
|
+
init_typed_node_events();
|
|
603439
|
+
init_async_process();
|
|
603440
|
+
MANAGED_TRANSCRIBE_CLI_DIR2 = join125(homedir39(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
|
|
603441
|
+
AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
603442
|
+
".mp3",
|
|
603443
|
+
".wav",
|
|
603444
|
+
".flac",
|
|
603445
|
+
".aac",
|
|
603446
|
+
".m4a",
|
|
603447
|
+
".ogg",
|
|
603448
|
+
".wma",
|
|
603449
|
+
".opus"
|
|
603450
|
+
]);
|
|
603451
|
+
VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
603452
|
+
".mp4",
|
|
603453
|
+
".mkv",
|
|
603454
|
+
".avi",
|
|
603455
|
+
".mov",
|
|
603456
|
+
".webm",
|
|
603457
|
+
".flv",
|
|
603458
|
+
".wmv",
|
|
603459
|
+
".m4v",
|
|
603460
|
+
".ts"
|
|
603461
|
+
]);
|
|
603462
|
+
listenLiveState = {
|
|
603463
|
+
phase: "idle",
|
|
603464
|
+
backend: "",
|
|
603465
|
+
model: "",
|
|
603466
|
+
micDevice: "",
|
|
603467
|
+
micLevelDb: null,
|
|
603468
|
+
noiseFloorDb: null,
|
|
603469
|
+
speechActive: false,
|
|
603470
|
+
lastTranscript: "",
|
|
603471
|
+
lastTranscriptAt: 0,
|
|
603472
|
+
lastStatus: "",
|
|
603473
|
+
updatedAt: 0
|
|
603474
|
+
};
|
|
603475
|
+
WhisperFallbackTranscriber = class extends EventEmitter6 {
|
|
603476
|
+
constructor(model, scriptPath2) {
|
|
603477
|
+
super();
|
|
603478
|
+
this.model = model;
|
|
603479
|
+
this.scriptPath = scriptPath2;
|
|
603480
|
+
}
|
|
603481
|
+
model;
|
|
603482
|
+
scriptPath;
|
|
603483
|
+
process = null;
|
|
603484
|
+
_ready = false;
|
|
603485
|
+
get ready() {
|
|
603486
|
+
return this._ready;
|
|
603487
|
+
}
|
|
603488
|
+
async start() {
|
|
603489
|
+
let pyPath = "python3";
|
|
603490
|
+
try {
|
|
603491
|
+
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
603492
|
+
const ensure = mod3.ensureEmbedDeps;
|
|
603493
|
+
const getPy = mod3.getVenvPython;
|
|
603494
|
+
if (typeof ensure === "function") {
|
|
603495
|
+
try {
|
|
603496
|
+
const res = ensure();
|
|
603497
|
+
if (res?.log) this.emit("status", res.log.slice(-500));
|
|
603498
|
+
} catch {
|
|
603499
|
+
}
|
|
603500
|
+
}
|
|
603501
|
+
if (typeof getPy === "function") {
|
|
603502
|
+
pyPath = getPy();
|
|
603503
|
+
}
|
|
603504
|
+
} catch {
|
|
603505
|
+
}
|
|
603506
|
+
updateListenLiveState({
|
|
603507
|
+
phase: "loading-model",
|
|
603508
|
+
backend: "openai-whisper",
|
|
603509
|
+
model: this.model,
|
|
603510
|
+
lastStatus: `loading whisper ${this.model} model...`
|
|
603511
|
+
});
|
|
603512
|
+
this.process = spawn29(
|
|
603513
|
+
pyPath,
|
|
603514
|
+
[
|
|
603515
|
+
this.scriptPath,
|
|
603516
|
+
"--model",
|
|
603517
|
+
this.model,
|
|
603518
|
+
"--chunk-seconds",
|
|
603519
|
+
"3",
|
|
603520
|
+
"--window-seconds",
|
|
603521
|
+
"10"
|
|
603522
|
+
],
|
|
603523
|
+
{
|
|
603524
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603525
|
+
env: { ...process.env }
|
|
603526
|
+
}
|
|
603527
|
+
);
|
|
603528
|
+
return new Promise((resolve76, reject) => {
|
|
603529
|
+
const timeout2 = setTimeout(() => {
|
|
603530
|
+
reject(
|
|
603531
|
+
new Error(
|
|
603532
|
+
"Whisper fallback: model load timeout (5 min). First run downloads the model."
|
|
603533
|
+
)
|
|
603534
|
+
);
|
|
603535
|
+
}, 3e5);
|
|
603536
|
+
const rl = createInterface2({ input: this.process.stdout });
|
|
603537
|
+
onReadlineLine(rl, (line) => {
|
|
603538
|
+
try {
|
|
603539
|
+
const evt = JSON.parse(line);
|
|
603540
|
+
switch (evt.type) {
|
|
603541
|
+
case "status":
|
|
603542
|
+
updateListenLiveState({ lastStatus: String(evt.message ?? "").slice(0, 160) });
|
|
603543
|
+
this.emit("status", evt.message);
|
|
603544
|
+
break;
|
|
603545
|
+
case "ready":
|
|
603546
|
+
this._ready = true;
|
|
603547
|
+
clearTimeout(timeout2);
|
|
603548
|
+
updateListenLiveState({ phase: "ready", lastStatus: "whisper model loaded" });
|
|
603549
|
+
this.emit("ready");
|
|
603550
|
+
resolve76();
|
|
603551
|
+
break;
|
|
603552
|
+
case "transcript":
|
|
603553
|
+
this.emit("transcript", {
|
|
603554
|
+
text: evt.text,
|
|
603555
|
+
isFinal: evt.isFinal ?? false
|
|
603556
|
+
});
|
|
603557
|
+
break;
|
|
603558
|
+
case "error":
|
|
603559
|
+
this.emit("error", new Error(evt.message));
|
|
603560
|
+
break;
|
|
603561
|
+
}
|
|
603562
|
+
} catch {
|
|
603563
|
+
}
|
|
603564
|
+
});
|
|
603565
|
+
this.process.stderr?.on("data", (data) => {
|
|
603566
|
+
const text2 = data.toString().trim();
|
|
603567
|
+
if (text2) this.emit("status", text2);
|
|
603568
|
+
});
|
|
603569
|
+
onChildError(this.process, (err) => {
|
|
603570
|
+
clearTimeout(timeout2);
|
|
603571
|
+
reject(err);
|
|
603572
|
+
});
|
|
603573
|
+
onChildClose(this.process, (code8) => {
|
|
603574
|
+
if (!this._ready) {
|
|
603575
|
+
clearTimeout(timeout2);
|
|
603576
|
+
reject(
|
|
603577
|
+
new Error(`Whisper worker exited with code ${code8} before ready`)
|
|
603578
|
+
);
|
|
603579
|
+
}
|
|
603580
|
+
});
|
|
603581
|
+
});
|
|
603582
|
+
}
|
|
603583
|
+
/** Write PCM16 audio data to the worker's stdin. */
|
|
603584
|
+
write(chunk) {
|
|
603585
|
+
if (this.process?.stdin?.writable) {
|
|
603586
|
+
this.process.stdin.write(chunk);
|
|
603587
|
+
}
|
|
603588
|
+
}
|
|
603589
|
+
/** Stop the worker. */
|
|
603590
|
+
stop() {
|
|
603591
|
+
if (this.process) {
|
|
603592
|
+
try {
|
|
603593
|
+
this.process.stdin?.end();
|
|
603594
|
+
this.process.kill("SIGTERM");
|
|
603595
|
+
} catch {
|
|
603596
|
+
}
|
|
603597
|
+
this.process = null;
|
|
603598
|
+
}
|
|
603599
|
+
this._ready = false;
|
|
603600
|
+
}
|
|
603601
|
+
};
|
|
603602
|
+
ListenEngine = class extends EventEmitter6 {
|
|
603603
|
+
config;
|
|
603604
|
+
micProcess = null;
|
|
603605
|
+
liveTranscriber = null;
|
|
603606
|
+
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
603607
|
+
active = false;
|
|
603608
|
+
paused = false;
|
|
603609
|
+
// Pause state for voicechat (stops mic but keeps transcriber)
|
|
603610
|
+
silenceTimer = null;
|
|
603611
|
+
countdownInterval = null;
|
|
603612
|
+
lastTranscriptTime = 0;
|
|
603613
|
+
pendingText = "";
|
|
603614
|
+
blinkTimer = null;
|
|
603615
|
+
blinkState = false;
|
|
603616
|
+
transcribeCliAvailable = null;
|
|
603617
|
+
constructor(config) {
|
|
603618
|
+
super();
|
|
603619
|
+
this.config = {
|
|
603620
|
+
model: config?.model ?? "base",
|
|
603621
|
+
mode: config?.mode ?? "confirm",
|
|
603622
|
+
silenceTimeoutMs: config?.silenceTimeoutMs ?? 3e3
|
|
603623
|
+
};
|
|
603624
|
+
}
|
|
603625
|
+
get isActive() {
|
|
603626
|
+
return this.active;
|
|
603627
|
+
}
|
|
603628
|
+
get isBlinking() {
|
|
603629
|
+
return this.active && this.blinkState;
|
|
603630
|
+
}
|
|
603631
|
+
get isPaused() {
|
|
603632
|
+
return this.paused;
|
|
603633
|
+
}
|
|
603634
|
+
get currentModel() {
|
|
603635
|
+
return this.config.model;
|
|
603636
|
+
}
|
|
603637
|
+
get currentMode() {
|
|
603638
|
+
return this.config.mode;
|
|
603639
|
+
}
|
|
603640
|
+
get pendingTranscript() {
|
|
603641
|
+
return this.pendingText;
|
|
603642
|
+
}
|
|
603643
|
+
setModel(model) {
|
|
603644
|
+
this.config.model = model;
|
|
603645
|
+
}
|
|
603646
|
+
setMode(mode) {
|
|
603647
|
+
this.config.mode = mode;
|
|
603648
|
+
}
|
|
603649
|
+
// Mic level metering state (dBFS)
|
|
603650
|
+
micLevelDb = null;
|
|
603651
|
+
micNoiseFloorDb = null;
|
|
603652
|
+
micSpeechActive = false;
|
|
603653
|
+
lastLevelEmit = 0;
|
|
603654
|
+
/**
|
|
603655
|
+
* Spawn the mic capture process and wire PCM piping + level metering.
|
|
603656
|
+
* Shared by start() and resume() so both paths meter identically.
|
|
603657
|
+
*/
|
|
603658
|
+
spawnMicProcess(micCmd) {
|
|
603659
|
+
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
603660
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
603661
|
+
env: { ...process.env }
|
|
603662
|
+
});
|
|
603663
|
+
updateListenLiveState({ micDevice: micCmd.device });
|
|
603664
|
+
this.micProcess.stdout?.on("data", (chunk) => {
|
|
603665
|
+
this.meterMicChunk(chunk);
|
|
603666
|
+
if (this.active && !this.paused && this.liveTranscriber) {
|
|
603667
|
+
this.liveTranscriber.write(chunk);
|
|
603668
|
+
}
|
|
603669
|
+
});
|
|
603670
|
+
this.micProcess.stderr?.on("data", () => {
|
|
603671
|
+
});
|
|
603672
|
+
onChildError(this.micProcess, (err) => {
|
|
603673
|
+
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
603674
|
+
this.stop();
|
|
603675
|
+
});
|
|
603676
|
+
onChildClose(this.micProcess, () => {
|
|
603677
|
+
if (this.active && !this.paused) {
|
|
603678
|
+
this.stop();
|
|
603679
|
+
}
|
|
603680
|
+
});
|
|
603681
|
+
}
|
|
603682
|
+
/**
|
|
603683
|
+
* Compute the PCM16 chunk level (dBFS), track an adaptive noise floor, and
|
|
603684
|
+
* flag speech when the level rises meaningfully above it. Drives the status
|
|
603685
|
+
* bar mic indicator and the /live dashboard ASR line.
|
|
603686
|
+
*/
|
|
603687
|
+
meterMicChunk(chunk) {
|
|
603688
|
+
const samples = Math.floor(chunk.length / 2);
|
|
603689
|
+
if (samples <= 0) return;
|
|
603690
|
+
let sumSquares = 0;
|
|
603691
|
+
const stride = samples > 4096 ? 4 : 1;
|
|
603692
|
+
let counted = 0;
|
|
603693
|
+
for (let i2 = 0; i2 + 1 < chunk.length; i2 += 2 * stride) {
|
|
603694
|
+
const v = chunk.readInt16LE(i2) / 32768;
|
|
603695
|
+
sumSquares += v * v;
|
|
603696
|
+
counted++;
|
|
603697
|
+
}
|
|
603698
|
+
if (counted === 0) return;
|
|
603699
|
+
const rms = Math.sqrt(sumSquares / counted);
|
|
603700
|
+
const levelDb = 20 * Math.log10(Math.max(1e-6, rms));
|
|
603701
|
+
this.micLevelDb = this.micLevelDb === null ? levelDb : levelDb > this.micLevelDb ? 0.6 * levelDb + 0.4 * this.micLevelDb : 0.2 * levelDb + 0.8 * this.micLevelDb;
|
|
603702
|
+
if (this.micNoiseFloorDb === null) this.micNoiseFloorDb = levelDb;
|
|
603703
|
+
else if (levelDb < this.micNoiseFloorDb) this.micNoiseFloorDb = 0.3 * levelDb + 0.7 * this.micNoiseFloorDb;
|
|
603704
|
+
else this.micNoiseFloorDb = 0.02 * levelDb + 0.98 * this.micNoiseFloorDb;
|
|
603705
|
+
const floor = this.micNoiseFloorDb;
|
|
603706
|
+
const speech = this.micLevelDb > Math.max(-58, floor + 6);
|
|
603707
|
+
const changed = speech !== this.micSpeechActive;
|
|
603708
|
+
this.micSpeechActive = speech;
|
|
603709
|
+
const now2 = Date.now();
|
|
603710
|
+
if (changed || now2 - this.lastLevelEmit >= 300) {
|
|
603711
|
+
this.lastLevelEmit = now2;
|
|
603712
|
+
updateListenLiveState({
|
|
603713
|
+
micLevelDb: Number(this.micLevelDb.toFixed(1)),
|
|
603714
|
+
noiseFloorDb: Number(floor.toFixed(1)),
|
|
603715
|
+
speechActive: speech
|
|
603716
|
+
});
|
|
603717
|
+
this.emit("level", {
|
|
603718
|
+
levelDb: this.micLevelDb,
|
|
603719
|
+
noiseFloorDb: floor,
|
|
603720
|
+
speechActive: speech
|
|
603721
|
+
});
|
|
603722
|
+
}
|
|
603723
|
+
}
|
|
603724
|
+
/** Check if transcribe-cli is available. */
|
|
603725
|
+
async isAvailable() {
|
|
603726
|
+
if (this.transcribeCliAvailable !== null)
|
|
603727
|
+
return this.transcribeCliAvailable;
|
|
603728
|
+
const tc = await this.loadTranscribeCli() || (await ensureManagedTranscribeCliNode() ? await this.loadTranscribeCli() : null);
|
|
603729
|
+
const transcribeCliReady = Boolean(tc && await ensureVenvForTranscribeCli());
|
|
603730
|
+
const whisperFallbackReady = Boolean(await findLiveWhisperScript());
|
|
603731
|
+
this.transcribeCliAvailable = transcribeCliReady || whisperFallbackReady;
|
|
603732
|
+
return this.transcribeCliAvailable;
|
|
603733
|
+
}
|
|
603734
|
+
/** Load transcribe-cli — bundled as a dependency of omnius. */
|
|
603735
|
+
async loadTranscribeCli() {
|
|
603736
|
+
try {
|
|
603737
|
+
const { createRequire: createRequire11 } = await import("node:module");
|
|
603738
|
+
const req3 = createRequire11(import.meta.url);
|
|
603739
|
+
return req3("transcribe-cli");
|
|
603740
|
+
} catch {
|
|
603741
|
+
}
|
|
603742
|
+
const managed = await loadManagedTranscribeCli();
|
|
603743
|
+
if (managed) return managed;
|
|
603744
|
+
try {
|
|
603745
|
+
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
603746
|
+
const tcPath = join125(globalRoot, "transcribe-cli");
|
|
603747
|
+
if (existsSync110(join125(tcPath, "dist", "index.js"))) {
|
|
603748
|
+
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
603749
|
+
if (loaded) return loaded;
|
|
603750
|
+
}
|
|
603751
|
+
} catch {
|
|
603752
|
+
}
|
|
603753
|
+
const nvmBase = join125(homedir39(), ".nvm", "versions", "node");
|
|
603754
|
+
if (existsSync110(nvmBase)) {
|
|
603755
|
+
try {
|
|
603756
|
+
const { readdirSync: readdirSync61 } = await import("node:fs");
|
|
603757
|
+
for (const ver of readdirSync61(nvmBase)) {
|
|
603758
|
+
const tcPath = join125(
|
|
603759
|
+
nvmBase,
|
|
603760
|
+
ver,
|
|
603761
|
+
"lib",
|
|
603762
|
+
"node_modules",
|
|
603763
|
+
"transcribe-cli"
|
|
603764
|
+
);
|
|
603765
|
+
if (existsSync110(join125(tcPath, "dist", "index.js"))) {
|
|
603766
|
+
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
603767
|
+
if (loaded) return loaded;
|
|
603768
|
+
}
|
|
603769
|
+
}
|
|
603770
|
+
} catch {
|
|
603771
|
+
}
|
|
603772
|
+
}
|
|
603773
|
+
return null;
|
|
603774
|
+
}
|
|
603775
|
+
/**
|
|
603776
|
+
* Start listening — captures microphone audio and feeds to TranscribeLive.
|
|
603777
|
+
*/
|
|
603778
|
+
async start() {
|
|
603779
|
+
if (this.active) return "Already listening.";
|
|
603780
|
+
updateListenLiveState({
|
|
603781
|
+
phase: "bootstrapping",
|
|
603782
|
+
model: this.config.model,
|
|
603783
|
+
backend: "",
|
|
603784
|
+
lastStatus: "starting ASR backend...",
|
|
603785
|
+
speechActive: false
|
|
603786
|
+
});
|
|
603787
|
+
const micCmd = await findMicCaptureCommand();
|
|
603788
|
+
if (!micCmd) {
|
|
603789
|
+
updateListenLiveState({ phase: "error", lastStatus: "no mic capture tool (arecord/sox/ffmpeg)" });
|
|
603790
|
+
return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
|
|
603791
|
+
}
|
|
603792
|
+
let tc = await this.loadTranscribeCli();
|
|
603793
|
+
if (!tc) {
|
|
603794
|
+
if (_bgInstallPromise) {
|
|
603795
|
+
this.emit("info", "Waiting for transcribe-cli install...");
|
|
603796
|
+
await _bgInstallPromise;
|
|
603797
|
+
this.transcribeCliAvailable = null;
|
|
603798
|
+
tc = await this.loadTranscribeCli();
|
|
603799
|
+
}
|
|
603800
|
+
if (!tc) {
|
|
603801
|
+
try {
|
|
603802
|
+
await ensureManagedTranscribeCliNode();
|
|
603803
|
+
this.transcribeCliAvailable = null;
|
|
603804
|
+
tc = await this.loadTranscribeCli();
|
|
603805
|
+
} catch {
|
|
603806
|
+
}
|
|
603807
|
+
}
|
|
603808
|
+
}
|
|
603809
|
+
let usedFallback = false;
|
|
603810
|
+
let transcribeCliError = null;
|
|
603811
|
+
if (tc) {
|
|
603812
|
+
const TranscribeLive = tc.TranscribeLive;
|
|
603813
|
+
if (TranscribeLive) {
|
|
603814
|
+
let tcUpToDate = false;
|
|
603815
|
+
try {
|
|
603816
|
+
const tcPkgPath = join125(
|
|
603817
|
+
dirname37(__require.resolve?.("transcribe-cli/package.json") || ""),
|
|
603818
|
+
"package.json"
|
|
603819
|
+
);
|
|
603820
|
+
if (existsSync110(tcPkgPath)) {
|
|
603821
|
+
const tcPkg = JSON.parse(
|
|
603822
|
+
__require("fs").readFileSync(tcPkgPath, "utf8")
|
|
603823
|
+
);
|
|
603824
|
+
tcUpToDate = tcPkg.version && tcPkg.version >= "2.0.1";
|
|
603825
|
+
}
|
|
603826
|
+
} catch {
|
|
603827
|
+
try {
|
|
603828
|
+
const out = await execFileText4("npm", ["list", "-g", "transcribe-cli", "--depth=0", "--json"], {
|
|
603829
|
+
timeout: 1e4
|
|
603830
|
+
});
|
|
603831
|
+
const parsed = JSON.parse(out);
|
|
603832
|
+
const ver = parsed?.dependencies?.["transcribe-cli"]?.version || "";
|
|
603833
|
+
tcUpToDate = ver >= "2.0.1";
|
|
603834
|
+
} catch {
|
|
603835
|
+
}
|
|
603836
|
+
}
|
|
603837
|
+
if (!tcUpToDate) {
|
|
603838
|
+
try {
|
|
603839
|
+
await ensureManagedTranscribeCliNode();
|
|
603840
|
+
this.transcribeCliAvailable = null;
|
|
603841
|
+
const reloaded = await this.loadTranscribeCli();
|
|
603842
|
+
if (reloaded?.TranscribeLive) {
|
|
603843
|
+
tc = reloaded;
|
|
603844
|
+
}
|
|
603845
|
+
} catch {
|
|
603846
|
+
}
|
|
603847
|
+
}
|
|
603848
|
+
const venvReady = await ensureVenvForTranscribeCli();
|
|
603849
|
+
if (!venvReady) {
|
|
603850
|
+
transcribeCliError = "venv Python missing numpy (required by transcribe-cli) — using whisper fallback";
|
|
603851
|
+
} else {
|
|
603852
|
+
let caughtUncaught = null;
|
|
603853
|
+
const uncaughtHandler = (err) => {
|
|
603854
|
+
const msg = err?.message || String(err);
|
|
603855
|
+
if (msg.includes("live_worker") || msg.includes("numpy") || msg.includes("transcribe_cli") || msg.includes("ModuleNotFoundError") || msg.includes("Traceback")) {
|
|
603856
|
+
caughtUncaught = err;
|
|
603857
|
+
} else {
|
|
603858
|
+
throw err;
|
|
603859
|
+
}
|
|
603860
|
+
};
|
|
603861
|
+
process.on("uncaughtException", uncaughtHandler);
|
|
603862
|
+
try {
|
|
603863
|
+
this.liveTranscriber = new TranscribeLive({
|
|
603864
|
+
model: this.config.model,
|
|
603865
|
+
sampleRate: 16e3,
|
|
603866
|
+
channels: 1,
|
|
603867
|
+
sampleWidth: 2,
|
|
603868
|
+
chunkDuration: 3
|
|
603869
|
+
});
|
|
603870
|
+
this.liveTranscriber.on(
|
|
603871
|
+
"transcript",
|
|
603872
|
+
(evt) => {
|
|
603873
|
+
if (!evt.text.trim()) return;
|
|
603874
|
+
this.lastTranscriptTime = Date.now();
|
|
603875
|
+
this.pendingText = evt.text.trim();
|
|
603876
|
+
updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
|
|
603877
|
+
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
603878
|
+
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
603879
|
+
}
|
|
603880
|
+
);
|
|
603881
|
+
this.liveTranscriber.on("error", (err) => {
|
|
603882
|
+
this.emit("error", err);
|
|
603883
|
+
});
|
|
603884
|
+
await new Promise((resolve76, reject) => {
|
|
603885
|
+
const timeout2 = setTimeout(
|
|
603886
|
+
() => reject(new Error("Model load timeout (60s)")),
|
|
603887
|
+
6e4
|
|
603888
|
+
);
|
|
603889
|
+
this.liveTranscriber.on("ready", () => {
|
|
603890
|
+
clearTimeout(timeout2);
|
|
603891
|
+
resolve76();
|
|
603892
|
+
});
|
|
603893
|
+
this.liveTranscriber.on("error", (err) => {
|
|
603894
|
+
clearTimeout(timeout2);
|
|
603895
|
+
reject(err);
|
|
603896
|
+
});
|
|
603897
|
+
});
|
|
603898
|
+
if (caughtUncaught) {
|
|
603899
|
+
throw caughtUncaught;
|
|
603900
|
+
}
|
|
603901
|
+
} catch (err) {
|
|
603902
|
+
try {
|
|
603903
|
+
this.liveTranscriber?.stop();
|
|
603904
|
+
} catch {
|
|
603905
|
+
}
|
|
603906
|
+
this.liveTranscriber = null;
|
|
603907
|
+
transcribeCliError = err instanceof Error ? err.message : String(err);
|
|
603908
|
+
} finally {
|
|
603909
|
+
process.removeListener("uncaughtException", uncaughtHandler);
|
|
603910
|
+
}
|
|
603911
|
+
}
|
|
603912
|
+
}
|
|
603913
|
+
}
|
|
603914
|
+
if (!this.liveTranscriber) {
|
|
603915
|
+
const scriptPath2 = await findLiveWhisperScript();
|
|
603916
|
+
if (!scriptPath2) {
|
|
603917
|
+
const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
|
|
603918
|
+
return `No transcription backend available.${hint} live-whisper.py not found.`;
|
|
603919
|
+
}
|
|
603920
|
+
try {
|
|
603921
|
+
const fallback = new WhisperFallbackTranscriber(
|
|
603922
|
+
this.config.model,
|
|
603923
|
+
scriptPath2
|
|
603924
|
+
);
|
|
603925
|
+
usedFallback = true;
|
|
603926
|
+
fallback.on("status", (msg) => {
|
|
603927
|
+
this.emit("info", msg);
|
|
603928
|
+
});
|
|
603929
|
+
await fallback.start();
|
|
603930
|
+
fallback.on("transcript", (evt) => {
|
|
603931
|
+
if (!evt.text.trim()) return;
|
|
603932
|
+
this.lastTranscriptTime = Date.now();
|
|
603933
|
+
this.pendingText = evt.text.trim();
|
|
603934
|
+
updateListenLiveState({ lastTranscript: this.pendingText, lastTranscriptAt: this.lastTranscriptTime });
|
|
603935
|
+
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
603936
|
+
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
603937
|
+
});
|
|
603938
|
+
fallback.on("error", (err) => {
|
|
603939
|
+
this.emit("error", err);
|
|
603940
|
+
});
|
|
603941
|
+
this.liveTranscriber = fallback;
|
|
603942
|
+
} catch (err) {
|
|
603943
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
603944
|
+
const tcHint = transcribeCliError ? `
|
|
603945
|
+
transcribe-cli error: ${transcribeCliError}` : "";
|
|
603946
|
+
updateListenLiveState({ phase: "error", lastStatus: msg.slice(0, 160) });
|
|
603947
|
+
return `Failed to start live transcription: ${msg}${tcHint}`;
|
|
603948
|
+
}
|
|
603949
|
+
}
|
|
603950
|
+
this.spawnMicProcess(micCmd);
|
|
603951
|
+
this.active = true;
|
|
603952
|
+
this.blinkState = true;
|
|
603953
|
+
this.blinkTimer = setInterval(() => {
|
|
603954
|
+
this.blinkState = !this.blinkState;
|
|
603955
|
+
this.emit("recording", this.blinkState);
|
|
603956
|
+
}, 500);
|
|
603957
|
+
this.lastTranscriptTime = Date.now();
|
|
603958
|
+
this.emit("started");
|
|
603959
|
+
this.emit("recording", true);
|
|
603960
|
+
const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
|
|
603961
|
+
const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
|
|
603962
|
+
updateListenLiveState({
|
|
603963
|
+
phase: "listening",
|
|
603964
|
+
backend,
|
|
603965
|
+
model: this.config.model,
|
|
603966
|
+
lastStatus: `listening (${modeDesc})`
|
|
603967
|
+
});
|
|
603968
|
+
return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
|
|
603969
|
+
}
|
|
603970
|
+
/**
|
|
603971
|
+
* Stop listening — cleanup mic, transcriber, timers.
|
|
603972
|
+
*/
|
|
603973
|
+
async stop() {
|
|
603974
|
+
if (!this.active) return "Not listening.";
|
|
603975
|
+
this.active = false;
|
|
603976
|
+
this.blinkState = false;
|
|
603977
|
+
updateListenLiveState({ phase: "idle", speechActive: false, micLevelDb: null, lastStatus: "stopped" });
|
|
603978
|
+
if (this.blinkTimer) {
|
|
603979
|
+
clearInterval(this.blinkTimer);
|
|
603980
|
+
this.blinkTimer = null;
|
|
603981
|
+
}
|
|
603982
|
+
if (this.silenceTimer) {
|
|
603983
|
+
clearTimeout(this.silenceTimer);
|
|
603984
|
+
this.silenceTimer = null;
|
|
603985
|
+
}
|
|
603986
|
+
if (this.countdownInterval) {
|
|
603987
|
+
clearInterval(this.countdownInterval);
|
|
603988
|
+
this.countdownInterval = null;
|
|
603989
|
+
}
|
|
603990
|
+
if (this.micProcess) {
|
|
603991
|
+
try {
|
|
603992
|
+
this.micProcess.kill("SIGTERM");
|
|
603993
|
+
} catch {
|
|
603994
|
+
}
|
|
603995
|
+
this.micProcess = null;
|
|
603996
|
+
}
|
|
603997
|
+
if (this.liveTranscriber) {
|
|
603998
|
+
try {
|
|
603999
|
+
this.liveTranscriber.stop();
|
|
604000
|
+
} catch {
|
|
604001
|
+
}
|
|
604002
|
+
this.liveTranscriber = null;
|
|
604003
|
+
}
|
|
604004
|
+
this.emit("recording", false);
|
|
604005
|
+
this.emit("stopped");
|
|
604006
|
+
const result = this.pendingText;
|
|
604007
|
+
this.pendingText = "";
|
|
604008
|
+
return result || "Stopped listening.";
|
|
604009
|
+
}
|
|
604010
|
+
/**
|
|
604011
|
+
* Accept the current pending transcript (for confirm mode).
|
|
604012
|
+
* Returns the text to inject into the input line.
|
|
604013
|
+
*/
|
|
604014
|
+
acceptTranscript() {
|
|
604015
|
+
const text2 = this.pendingText;
|
|
604016
|
+
this.pendingText = "";
|
|
604017
|
+
return text2;
|
|
604018
|
+
}
|
|
604019
|
+
/**
|
|
604020
|
+
* Pause microphone capture (for voicechat during TTS output).
|
|
604021
|
+
* Keeps transcriber alive but stops feeding it audio.
|
|
604022
|
+
*/
|
|
604023
|
+
pause() {
|
|
604024
|
+
if (!this.active || this.paused) return;
|
|
604025
|
+
this.paused = true;
|
|
604026
|
+
if (this.blinkTimer) {
|
|
604027
|
+
clearInterval(this.blinkTimer);
|
|
604028
|
+
this.blinkTimer = null;
|
|
604029
|
+
}
|
|
604030
|
+
this.blinkState = false;
|
|
604031
|
+
if (this.micProcess) {
|
|
604032
|
+
try {
|
|
604033
|
+
this.micProcess.kill("SIGTERM");
|
|
604034
|
+
} catch {
|
|
604035
|
+
}
|
|
604036
|
+
this.micProcess = null;
|
|
604037
|
+
}
|
|
604038
|
+
updateListenLiveState({ phase: "paused", speechActive: false, lastStatus: "mic paused (TTS speaking)" });
|
|
604039
|
+
this.emit("paused");
|
|
604040
|
+
this.emit("recording", false);
|
|
604041
|
+
}
|
|
604042
|
+
/**
|
|
604043
|
+
* Resume microphone capture after pause.
|
|
604044
|
+
*/
|
|
604045
|
+
async resume() {
|
|
604046
|
+
if (!this.active || !this.paused) return "Not paused.";
|
|
604047
|
+
this.paused = false;
|
|
604048
|
+
const micCmd = await findMicCaptureCommand();
|
|
604049
|
+
if (!micCmd) {
|
|
604050
|
+
return "No microphone capture tool found.";
|
|
604051
|
+
}
|
|
604052
|
+
this.spawnMicProcess(micCmd);
|
|
604053
|
+
this.blinkState = true;
|
|
604054
|
+
this.blinkTimer = setInterval(() => {
|
|
604055
|
+
this.blinkState = !this.blinkState;
|
|
604056
|
+
this.emit("recording", this.blinkState);
|
|
604057
|
+
}, 500);
|
|
604058
|
+
updateListenLiveState({ phase: "listening", lastStatus: "listening" });
|
|
604059
|
+
this.emit("resumed");
|
|
604060
|
+
this.emit("recording", true);
|
|
604061
|
+
return "Resumed listening.";
|
|
604062
|
+
}
|
|
604063
|
+
/**
|
|
604064
|
+
* Create a standalone transcriber for call sessions.
|
|
604065
|
+
* No microphone capture — just an ASR engine you feed PCM chunks to.
|
|
604066
|
+
* Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
|
|
604067
|
+
* Caller is responsible for calling .stop() when done.
|
|
604068
|
+
*/
|
|
604069
|
+
async createCallTranscriber() {
|
|
604070
|
+
let tc = await this.loadTranscribeCli();
|
|
604071
|
+
if (!tc && _bgInstallPromise) {
|
|
604072
|
+
await _bgInstallPromise;
|
|
604073
|
+
this.transcribeCliAvailable = null;
|
|
604074
|
+
tc = await this.loadTranscribeCli();
|
|
604075
|
+
}
|
|
604076
|
+
if (!tc) {
|
|
604077
|
+
try {
|
|
604078
|
+
await ensureManagedTranscribeCliNode();
|
|
604079
|
+
this.transcribeCliAvailable = null;
|
|
604080
|
+
tc = await this.loadTranscribeCli();
|
|
604081
|
+
} catch {
|
|
604082
|
+
}
|
|
604083
|
+
}
|
|
604084
|
+
if (tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
|
|
604085
|
+
try {
|
|
604086
|
+
const transcriber = new tc.TranscribeLive({
|
|
604087
|
+
model: this.config.model,
|
|
604088
|
+
sampleRate: 16e3,
|
|
604089
|
+
channels: 1,
|
|
604090
|
+
sampleWidth: 2,
|
|
604091
|
+
chunkDuration: 3
|
|
604092
|
+
});
|
|
604093
|
+
await new Promise((resolve76, reject) => {
|
|
604094
|
+
const timeout2 = setTimeout(
|
|
604095
|
+
() => reject(new Error("Model load timeout (60s)")),
|
|
604096
|
+
6e4
|
|
604097
|
+
);
|
|
604098
|
+
transcriber.on("ready", () => {
|
|
604099
|
+
clearTimeout(timeout2);
|
|
604100
|
+
resolve76();
|
|
604101
|
+
});
|
|
604102
|
+
transcriber.on("error", (err) => {
|
|
604103
|
+
clearTimeout(timeout2);
|
|
604104
|
+
reject(err);
|
|
604105
|
+
});
|
|
604106
|
+
});
|
|
604107
|
+
return transcriber;
|
|
604108
|
+
} catch {
|
|
604109
|
+
}
|
|
604110
|
+
}
|
|
604111
|
+
const scriptPath2 = await findLiveWhisperScript();
|
|
604112
|
+
if (!scriptPath2) return null;
|
|
604113
|
+
try {
|
|
604114
|
+
const fallback = new WhisperFallbackTranscriber(
|
|
604115
|
+
this.config.model,
|
|
604116
|
+
scriptPath2
|
|
604117
|
+
);
|
|
604118
|
+
await fallback.start();
|
|
604119
|
+
return fallback;
|
|
604120
|
+
} catch {
|
|
604121
|
+
return null;
|
|
604122
|
+
}
|
|
604123
|
+
}
|
|
604124
|
+
/**
|
|
604125
|
+
* Transcribe a file (audio or video) using transcribe-cli.
|
|
604126
|
+
* Returns the transcription result.
|
|
604127
|
+
*/
|
|
604128
|
+
async transcribeFile(filePath, outputDir2) {
|
|
604129
|
+
let tc = await this.loadTranscribeCli();
|
|
604130
|
+
if (!tc && _bgInstallPromise) {
|
|
604131
|
+
await _bgInstallPromise;
|
|
604132
|
+
this.transcribeCliAvailable = null;
|
|
604133
|
+
tc = await this.loadTranscribeCli();
|
|
604134
|
+
}
|
|
604135
|
+
if (!tc) {
|
|
604136
|
+
try {
|
|
604137
|
+
await ensureManagedTranscribeCliNode();
|
|
604138
|
+
this.transcribeCliAvailable = null;
|
|
604139
|
+
tc = await this.loadTranscribeCli();
|
|
604140
|
+
} catch {
|
|
604141
|
+
}
|
|
604142
|
+
}
|
|
604143
|
+
await ensureVenvForTranscribeCli();
|
|
604144
|
+
let lastErr = null;
|
|
604145
|
+
if (tc) {
|
|
604146
|
+
try {
|
|
604147
|
+
const result = await tc.transcribe(filePath, {
|
|
604148
|
+
model: this.config.model,
|
|
604149
|
+
format: "json",
|
|
604150
|
+
diarize: false,
|
|
604151
|
+
wordTimestamps: false
|
|
604152
|
+
});
|
|
604153
|
+
if (outputDir2) {
|
|
604154
|
+
const { basename: basename43 } = await import("node:path");
|
|
604155
|
+
const transcriptDir = join125(outputDir2, ".omnius", "transcripts");
|
|
604156
|
+
mkdirSync68(transcriptDir, { recursive: true });
|
|
604157
|
+
const outFile = join125(transcriptDir, `${basename43(filePath)}.txt`);
|
|
604158
|
+
writeFileSync57(outFile, result.text, "utf-8");
|
|
604159
|
+
}
|
|
604160
|
+
return {
|
|
604161
|
+
text: result.text,
|
|
604162
|
+
duration: result.duration,
|
|
604163
|
+
speakers: result.speakers,
|
|
604164
|
+
segments: result.segments
|
|
604165
|
+
};
|
|
604166
|
+
} catch (err) {
|
|
604167
|
+
lastErr = err;
|
|
604168
|
+
}
|
|
604169
|
+
}
|
|
604170
|
+
const fb = await transcribeFileViaWhisper(filePath, this.config.model);
|
|
604171
|
+
if (fb) {
|
|
604172
|
+
if (outputDir2) {
|
|
604173
|
+
const { basename: basename43 } = await import("node:path");
|
|
604174
|
+
const transcriptDir = join125(outputDir2, ".omnius", "transcripts");
|
|
604175
|
+
mkdirSync68(transcriptDir, { recursive: true });
|
|
604176
|
+
const outFile = join125(transcriptDir, `${basename43(filePath)}.txt`);
|
|
604177
|
+
writeFileSync57(outFile, fb.text, "utf-8");
|
|
604178
|
+
}
|
|
604179
|
+
return fb;
|
|
604180
|
+
}
|
|
604181
|
+
if (lastErr) {
|
|
604182
|
+
this.emit(
|
|
604183
|
+
"error",
|
|
604184
|
+
lastErr instanceof Error ? lastErr : new Error(String(lastErr))
|
|
604185
|
+
);
|
|
604186
|
+
}
|
|
604187
|
+
return null;
|
|
604188
|
+
}
|
|
604189
|
+
// -------------------------------------------------------------------------
|
|
604190
|
+
// Auto-mode silence detection
|
|
604191
|
+
// -------------------------------------------------------------------------
|
|
604192
|
+
resetSilenceTimer() {
|
|
604193
|
+
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
604194
|
+
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
|
604195
|
+
const timeoutMs = this.config.silenceTimeoutMs;
|
|
604196
|
+
const startTime = Date.now();
|
|
604197
|
+
this.countdownInterval = setInterval(() => {
|
|
604198
|
+
const elapsed = Date.now() - startTime;
|
|
604199
|
+
const remaining = Math.max(0, Math.ceil((timeoutMs - elapsed) / 1e3));
|
|
604200
|
+
this.emit("countdown", remaining);
|
|
604201
|
+
if (remaining <= 0 && this.countdownInterval) {
|
|
604202
|
+
clearInterval(this.countdownInterval);
|
|
604203
|
+
this.countdownInterval = null;
|
|
604204
|
+
}
|
|
604205
|
+
}, 1e3);
|
|
604206
|
+
this.silenceTimer = setTimeout(() => {
|
|
604207
|
+
if (this.active && this.pendingText) {
|
|
604208
|
+
this.emit("countdown", 0);
|
|
604209
|
+
this.emit("transcript", this.pendingText, true);
|
|
604210
|
+
}
|
|
604211
|
+
}, timeoutMs);
|
|
604212
|
+
}
|
|
604213
|
+
};
|
|
604214
|
+
_bgInstallPromise = null;
|
|
604215
|
+
_managedTranscribeCliInstallPromise = null;
|
|
604216
|
+
_engine = null;
|
|
604217
|
+
}
|
|
604218
|
+
});
|
|
604219
|
+
|
|
602843
604220
|
// packages/cli/src/tui/live-sensors.ts
|
|
602844
|
-
import { mkdirSync as
|
|
602845
|
-
import { basename as basename23, join as
|
|
604221
|
+
import { mkdirSync as mkdirSync69, readFileSync as readFileSync89, writeFileSync as writeFileSync58 } from "node:fs";
|
|
604222
|
+
import { basename as basename23, join as join126, relative as relative11 } from "node:path";
|
|
602846
604223
|
function liveDir(repoRoot) {
|
|
602847
|
-
return
|
|
604224
|
+
return join126(repoRoot, ".omnius", "live");
|
|
602848
604225
|
}
|
|
602849
604226
|
function liveConfigPath(repoRoot) {
|
|
602850
|
-
return
|
|
604227
|
+
return join126(liveDir(repoRoot), "config.json");
|
|
602851
604228
|
}
|
|
602852
604229
|
function liveSnapshotPath(repoRoot) {
|
|
602853
|
-
return
|
|
604230
|
+
return join126(liveDir(repoRoot), "latest.json");
|
|
602854
604231
|
}
|
|
602855
604232
|
function ensureLiveDir(repoRoot) {
|
|
602856
|
-
|
|
604233
|
+
mkdirSync69(liveDir(repoRoot), { recursive: true });
|
|
602857
604234
|
}
|
|
602858
604235
|
function nowIso3(ms) {
|
|
602859
604236
|
return new Date(ms).toISOString();
|
|
@@ -603175,7 +604552,7 @@ function sanitizeLiveAudioError(value2) {
|
|
|
603175
604552
|
}
|
|
603176
604553
|
function readPcm16WavActivity(filePath, bins = 36) {
|
|
603177
604554
|
try {
|
|
603178
|
-
const buf =
|
|
604555
|
+
const buf = readFileSync89(filePath);
|
|
603179
604556
|
if (buf.length < 44 || buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WAVE") return void 0;
|
|
603180
604557
|
let offset = 12;
|
|
603181
604558
|
let channels = 1;
|
|
@@ -603214,7 +604591,7 @@ function readPcm16WavActivity(filePath, bins = 36) {
|
|
|
603214
604591
|
const abs = Math.abs(sample);
|
|
603215
604592
|
peak = Math.max(peak, abs);
|
|
603216
604593
|
sumSquares += sample * sample;
|
|
603217
|
-
if (abs >
|
|
604594
|
+
if (abs > 8e-3) active++;
|
|
603218
604595
|
const b = Math.min(bucketSquares.length - 1, Math.floor(i2 / frames * bucketSquares.length));
|
|
603219
604596
|
bucketSquares[b] += sample * sample;
|
|
603220
604597
|
bucketCounts[b] += 1;
|
|
@@ -603753,15 +605130,32 @@ function formatLiveDashboardFromSnapshot(snapshot, opts = {}) {
|
|
|
603753
605130
|
const transcriptFailure = isAudioFailureText(snapshot.audio.transcript) ? snapshot.audio.transcript : "";
|
|
603754
605131
|
const transcript = transcriptFailure ? "" : snapshot.audio.transcript;
|
|
603755
605132
|
const audioError = sanitizeLiveAudioError([snapshot.audio.error, transcriptFailure].filter(Boolean).join(" | "));
|
|
605133
|
+
const asr = getListenLiveState();
|
|
605134
|
+
const asrPhaseLabel = {
|
|
605135
|
+
bootstrapping: "bootstrapping deps…",
|
|
605136
|
+
"loading-model": "loading model…",
|
|
605137
|
+
ready: "ready",
|
|
605138
|
+
listening: "listening",
|
|
605139
|
+
paused: "paused (TTS speaking)",
|
|
605140
|
+
error: "error"
|
|
605141
|
+
};
|
|
605142
|
+
const asrMicBits = asr.micLevelDb !== null ? ` mic=${asr.micLevelDb.toFixed(1)}dB${asr.speechActive ? " ●speech" : ""}` : "";
|
|
605143
|
+
const asrLine = asr.phase !== "idle" ? `│ ├─ whisper: ${[asr.backend, asr.model].filter(Boolean).join(" ") || "starting"} — ${asrPhaseLabel[asr.phase] ?? asr.phase}${asrMicBits}` : "";
|
|
605144
|
+
const asrHeardAge = asr.lastTranscriptAt > 0 ? Math.max(0, Math.round((now2 - asr.lastTranscriptAt) / 1e3)) : null;
|
|
605145
|
+
const asrHeardLine = asr.phase !== "idle" && asr.lastTranscript ? `│ ├─ heard${asrHeardAge !== null ? ` ${asrHeardAge}s ago` : ""}: ${trimOneLine(asr.lastTranscript, 120)}` : "";
|
|
605146
|
+
const asrStatusLine = (asr.phase === "bootstrapping" || asr.phase === "loading-model" || asr.phase === "error") && asr.lastStatus ? `│ ├─ whisper status: ${trimOneLine(asr.lastStatus, 120)}` : "";
|
|
603756
605147
|
const audioBits = [
|
|
603757
605148
|
`├─ input: ${snapshot.audio.input || snapshot.config.selectedAudioInput || "none"}`,
|
|
605149
|
+
asrLine,
|
|
605150
|
+
asrStatusLine,
|
|
605151
|
+
asrHeardLine,
|
|
603758
605152
|
snapshot.audio.activity ? `│ ├─ activity: ${snapshot.audio.activity.waveform} rms=${snapshot.audio.activity.rmsDb.toFixed(1)}dB active=${Math.round(snapshot.audio.activity.activeRatio * 100)}%` : "",
|
|
603759
605153
|
audioError ? `│ ├─ error: ${trimOneLine(audioError, 140)}` : "",
|
|
603760
605154
|
transcript ? `│ ├─ asr${snapshot.audio.asrBackend ? ` (${snapshot.audio.asrBackend})` : ""}: ${trimOneLine(transcript, 140)}` : "",
|
|
603761
605155
|
snapshot.audio.intake ? `│ ├─ intake: ${snapshot.audio.intake.intendedForAgent ? "for-agent" : "ambient"} conf=${snapshot.audio.intake.confidence.toFixed(2)} action=${snapshot.audio.intake.action}` : "",
|
|
603762
605156
|
snapshot.audio.analysis ? `│ └─ ${formatAudioAnalysisDashboardSummary(snapshot.audio.analysis)}` : ""
|
|
603763
605157
|
].filter(Boolean);
|
|
603764
|
-
for (const item of audioBits.slice(0,
|
|
605158
|
+
for (const item of audioBits.slice(0, 9)) pushDashboardWrapped(lines, item, width);
|
|
603765
605159
|
}
|
|
603766
605160
|
lines.push(`╰${horizontal}╯`);
|
|
603767
605161
|
return lines;
|
|
@@ -603945,15 +605339,23 @@ function parsePactlShortDevices(raw, kind) {
|
|
|
603945
605339
|
const parts = line.split(" ");
|
|
603946
605340
|
const name10 = parts[1]?.trim();
|
|
603947
605341
|
if (!name10) continue;
|
|
605342
|
+
const spec = (parts[3] ?? "").trim();
|
|
605343
|
+
const base3 = kind === "input" ? "Pulse/PipeWire source" : "Pulse/PipeWire sink";
|
|
603948
605344
|
devices.push({
|
|
603949
605345
|
id: `pulse:${name10}`,
|
|
603950
605346
|
label: name10,
|
|
603951
|
-
detail:
|
|
605347
|
+
detail: spec ? `${base3} · ${spec}` : base3,
|
|
603952
605348
|
source: "pulse"
|
|
603953
605349
|
});
|
|
603954
605350
|
}
|
|
603955
605351
|
return devices;
|
|
603956
605352
|
}
|
|
605353
|
+
function liveDeviceChannels(device) {
|
|
605354
|
+
const match = String(device?.detail ?? "").match(/(\d+)ch\b/);
|
|
605355
|
+
if (!match) return void 0;
|
|
605356
|
+
const channels = Number(match[1]);
|
|
605357
|
+
return Number.isFinite(channels) && channels > 0 ? channels : void 0;
|
|
605358
|
+
}
|
|
603957
605359
|
function parseCameraListJson(raw) {
|
|
603958
605360
|
try {
|
|
603959
605361
|
const parsed = JSON.parse(raw);
|
|
@@ -604001,7 +605403,7 @@ function parseCameraListText(raw) {
|
|
|
604001
605403
|
}
|
|
604002
605404
|
function loadLiveSensorConfig(repoRoot) {
|
|
604003
605405
|
try {
|
|
604004
|
-
const parsed = JSON.parse(
|
|
605406
|
+
const parsed = JSON.parse(readFileSync89(liveConfigPath(repoRoot), "utf8"));
|
|
604005
605407
|
return {
|
|
604006
605408
|
...DEFAULT_CONFIG7,
|
|
604007
605409
|
...parsed,
|
|
@@ -604015,7 +605417,7 @@ function loadLiveSensorConfig(repoRoot) {
|
|
|
604015
605417
|
}
|
|
604016
605418
|
function readLiveSensorSnapshot(repoRoot) {
|
|
604017
605419
|
try {
|
|
604018
|
-
const parsed = JSON.parse(
|
|
605420
|
+
const parsed = JSON.parse(readFileSync89(liveSnapshotPath(repoRoot), "utf8"));
|
|
604019
605421
|
return parsed?.version === 1 ? parsed : null;
|
|
604020
605422
|
} catch {
|
|
604021
605423
|
return null;
|
|
@@ -604025,16 +605427,22 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
|
|
|
604025
605427
|
if (!snapshot) return "";
|
|
604026
605428
|
const now2 = opts.now ?? Date.now();
|
|
604027
605429
|
const maxAgeMs = opts.maxAgeMs ?? 9e4;
|
|
605430
|
+
const toolCapable = opts.toolCapable !== false;
|
|
604028
605431
|
const lines = [];
|
|
604029
605432
|
const activeVideo = snapshot.config.videoEnabled || Boolean(snapshot.video);
|
|
604030
605433
|
const activeAudio = snapshot.config.audioEnabled || snapshot.config.audioOutputEnabled || Boolean(snapshot.audio);
|
|
604031
605434
|
if (!activeVideo && !activeAudio) return "";
|
|
604032
605435
|
lines.push("<live-sensor-context>");
|
|
604033
605436
|
lines.push("## LIVE SENSOR STATUS");
|
|
604034
|
-
|
|
604035
|
-
|
|
604036
|
-
|
|
604037
|
-
|
|
605437
|
+
if (toolCapable) {
|
|
605438
|
+
lines.push("Live sensor context is operator-enabled ambient input. Treat it as current perceptual evidence for this turn; if it is stale, missing, or insufficient, use camera/audio tools to refresh before answering what you see or hear.");
|
|
605439
|
+
lines.push('Live world-model rule: when the user asks what is around, who is present, what changed, what is heard, or asks a deictic question like "what do you see", treat the live camera/audio fields as first-class context. Be curious and tool-capable: refresh stale sensors, inspect frame paths, query vision/CLIP/visual_memory/audio tools, and combine metadata into a concise answer. Do not rely only on summaries when the task requires current perception.');
|
|
605440
|
+
lines.push("Live speech rule: in low-latency voice/live modes, short spoken replies are appropriate for direct address, identity clarification, or important visual/audio triggers. Do not narrate every frame.");
|
|
605441
|
+
lines.push("Identity rule: never invent names for observed people. If a person is unknown and identity matters, ask the user or the person for their name, then store the association with visual_memory(action='enroll', image='<face crop>', name='<name>') when a face crop is available, or multimodal_memory(action='meet', person_name='<name>') when live voice/name context is available.");
|
|
605442
|
+
} else {
|
|
605443
|
+
lines.push("This is your current live perception: what the camera sees and what is heard right now. Answer questions about sight/sound directly from these fields.");
|
|
605444
|
+
lines.push("If the data below is stale or missing, say you cannot see/hear clearly right now. Never invent observations or names for people. To have the main agent investigate further, relay a request with the voice_to_main tool.");
|
|
605445
|
+
}
|
|
604038
605446
|
lines.push(`Snapshot file: ${liveSnapshotPath(snapshot.repoRoot)}`);
|
|
604039
605447
|
lines.push(`Streams: video=${snapshot.config.videoEnabled ? "on" : "off"} infer=${snapshot.config.inferEnabled ? "on" : "off"} clip=${snapshot.config.clipEnabled ? "on" : "off"} audio=${snapshot.config.audioEnabled ? "on" : "off"} asr=${snapshot.config.asrEnabled ? "on" : "off"} sounds=${snapshot.config.audioAnalysisEnabled ? "on" : "off"} output-monitor=${snapshot.config.audioOutputEnabled ? "on" : "off"}`);
|
|
604040
605448
|
if (snapshot.config.selectedCamera) {
|
|
@@ -604177,8 +605585,8 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
|
|
|
604177
605585
|
... live sensor context truncated ...
|
|
604178
605586
|
</live-sensor-context>` : text2;
|
|
604179
605587
|
}
|
|
604180
|
-
function formatLiveSensorContext(repoRoot) {
|
|
604181
|
-
return formatLiveSensorContextFromSnapshot(readLiveSensorSnapshot(repoRoot));
|
|
605588
|
+
function formatLiveSensorContext(repoRoot, opts = {}) {
|
|
605589
|
+
return formatLiveSensorContextFromSnapshot(readLiveSensorSnapshot(repoRoot), opts);
|
|
604182
605590
|
}
|
|
604183
605591
|
function isLiveSensorSnapshotActive(snapshot) {
|
|
604184
605592
|
const cfg = snapshot?.config;
|
|
@@ -604220,12 +605628,13 @@ async function discoverAudioOutputs() {
|
|
|
604220
605628
|
if (devices.length === 0) devices.push({ id: "default", label: "default speaker", detail: "system default", source: "default" });
|
|
604221
605629
|
return { devices: dedupeDevices(devices), errors };
|
|
604222
605630
|
}
|
|
604223
|
-
async function recordAudioSample(device, durationSec, outputPath3) {
|
|
605631
|
+
async function recordAudioSample(device, durationSec, outputPath3, channels) {
|
|
604224
605632
|
const duration = Math.max(0.2, Math.min(30, Number.isFinite(durationSec) ? durationSec : 1));
|
|
604225
605633
|
const ffmpegSeconds = duration < 1 ? duration.toFixed(3) : String(Math.ceil(duration));
|
|
604226
605634
|
const arecordSeconds = String(Math.max(1, Math.ceil(duration)));
|
|
604227
605635
|
if (device.startsWith("pulse:") && await commandExists2("ffmpeg", 1500)) {
|
|
604228
605636
|
const source = device.slice("pulse:".length);
|
|
605637
|
+
const channelArgs = (channels ?? 1) > 2 ? ["-af", "pan=mono|c0=c0"] : ["-ac", "1"];
|
|
604229
605638
|
await execFileText4("ffmpeg", [
|
|
604230
605639
|
"-hide_banner",
|
|
604231
605640
|
"-loglevel",
|
|
@@ -604236,8 +605645,7 @@ async function recordAudioSample(device, durationSec, outputPath3) {
|
|
|
604236
605645
|
source,
|
|
604237
605646
|
"-t",
|
|
604238
605647
|
ffmpegSeconds,
|
|
604239
|
-
|
|
604240
|
-
"1",
|
|
605648
|
+
...channelArgs,
|
|
604241
605649
|
"-ar",
|
|
604242
605650
|
"16000",
|
|
604243
605651
|
"-acodec",
|
|
@@ -604346,12 +605754,12 @@ print(json.dumps({"ok": True, "scores": scores}))
|
|
|
604346
605754
|
return null;
|
|
604347
605755
|
}
|
|
604348
605756
|
}
|
|
604349
|
-
async function tryRecordAudioDevice(device, durationSec, outputPath3) {
|
|
604350
|
-
await recordAudioSample(device, durationSec, outputPath3);
|
|
605757
|
+
async function tryRecordAudioDevice(device, durationSec, outputPath3, channels) {
|
|
605758
|
+
await recordAudioSample(device, durationSec, outputPath3, channels);
|
|
604351
605759
|
}
|
|
604352
605760
|
function hasLiveAudioSignal(activity) {
|
|
604353
605761
|
if (!activity) return false;
|
|
604354
|
-
return activity.peak >= 0.
|
|
605762
|
+
return activity.peak >= 0.012 || activity.rmsDb > -55 || activity.activeRatio >= 0.015;
|
|
604355
605763
|
}
|
|
604356
605764
|
function audioCaptureMethod(device) {
|
|
604357
605765
|
return device.startsWith("pulse:") ? "pulse/ffmpeg" : "arecord";
|
|
@@ -604381,7 +605789,8 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604381
605789
|
for (const candidate of ordered) {
|
|
604382
605790
|
attempts.push(candidate);
|
|
604383
605791
|
try {
|
|
604384
|
-
|
|
605792
|
+
const candidateChannels = liveDeviceChannels(devices.find((entry) => entry.id === candidate));
|
|
605793
|
+
await tryRecordAudioDevice(candidate, durationSec, outputPath3, candidateChannels);
|
|
604385
605794
|
const activity = readPcm16WavActivity(outputPath3);
|
|
604386
605795
|
const method = audioCaptureMethod(candidate);
|
|
604387
605796
|
const signalDetected = hasLiveAudioSignal(activity);
|
|
@@ -604393,7 +605802,7 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604393
605802
|
device: candidate,
|
|
604394
605803
|
method,
|
|
604395
605804
|
activity,
|
|
604396
|
-
bytes:
|
|
605805
|
+
bytes: readFileSync89(outputPath3),
|
|
604397
605806
|
errors: [...errors]
|
|
604398
605807
|
};
|
|
604399
605808
|
}
|
|
@@ -604403,7 +605812,7 @@ async function recordAudioSampleWithRecovery(preferred, devices, durationSec, ou
|
|
|
604403
605812
|
}
|
|
604404
605813
|
}
|
|
604405
605814
|
if (firstQuietCapture) {
|
|
604406
|
-
|
|
605815
|
+
writeFileSync58(outputPath3, firstQuietCapture.bytes);
|
|
604407
605816
|
return {
|
|
604408
605817
|
ok: true,
|
|
604409
605818
|
device: firstQuietCapture.device,
|
|
@@ -604499,6 +605908,7 @@ var init_live_sensors = __esm({
|
|
|
604499
605908
|
init_dist5();
|
|
604500
605909
|
init_async_process();
|
|
604501
605910
|
init_image_ascii_preview();
|
|
605911
|
+
init_listen();
|
|
604502
605912
|
MIN_VIDEO_INTERVAL_MS = 250;
|
|
604503
605913
|
LOW_LATENCY_VIDEO_INTERVAL_MS = 250;
|
|
604504
605914
|
MIN_AUDIO_INTERVAL_MS = 1500;
|
|
@@ -605167,7 +606577,7 @@ ${output}`).join("\n\n");
|
|
|
605167
606577
|
async previewAudioInput(device = this.config.selectedAudioInput) {
|
|
605168
606578
|
const input = String(device || "").trim() || this.resolveAudioInput();
|
|
605169
606579
|
ensureLiveDir(this.repoRoot);
|
|
605170
|
-
const recordingPath =
|
|
606580
|
+
const recordingPath = join126(liveDir(this.repoRoot), `audio-preview-${Date.now()}.wav`);
|
|
605171
606581
|
const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, 0.8, recordingPath, {
|
|
605172
606582
|
allowFallback: false
|
|
605173
606583
|
});
|
|
@@ -605216,7 +606626,7 @@ ${output}`).join("\n\n");
|
|
|
605216
606626
|
const input = this.resolveAudioInput();
|
|
605217
606627
|
try {
|
|
605218
606628
|
ensureLiveDir(this.repoRoot);
|
|
605219
|
-
const recordingPath =
|
|
606629
|
+
const recordingPath = join126(liveDir(this.repoRoot), "audio-activity.wav");
|
|
605220
606630
|
const capture = await recordAudioSampleWithRecovery(input, this.devices.audioInputs, AUDIO_ACTIVITY_SAMPLE_SEC, recordingPath, {
|
|
605221
606631
|
allowFallback: false,
|
|
605222
606632
|
requireSignal: false
|
|
@@ -605259,7 +606669,7 @@ ${output}`).join("\n\n");
|
|
|
605259
606669
|
}
|
|
605260
606670
|
const outputDir2 = liveDir(this.repoRoot);
|
|
605261
606671
|
ensureLiveDir(this.repoRoot);
|
|
605262
|
-
const recordingPath =
|
|
606672
|
+
const recordingPath = join126(outputDir2, `audio-${Date.now()}.wav`);
|
|
605263
606673
|
let transcript = "";
|
|
605264
606674
|
let asrBackend = "";
|
|
605265
606675
|
let analysis = "";
|
|
@@ -605571,7 +606981,7 @@ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=
|
|
|
605571
606981
|
}
|
|
605572
606982
|
persist() {
|
|
605573
606983
|
ensureLiveDir(this.repoRoot);
|
|
605574
|
-
|
|
606984
|
+
writeFileSync58(liveConfigPath(this.repoRoot), JSON.stringify(this.config, null, 2), "utf8");
|
|
605575
606985
|
this.snapshot = {
|
|
605576
606986
|
...this.snapshot,
|
|
605577
606987
|
version: 1,
|
|
@@ -605579,7 +606989,7 @@ reason=${decision2.reason} confidence=${decision2.confidence.toFixed(2)} source=
|
|
|
605579
606989
|
config: this.config,
|
|
605580
606990
|
devices: this.devices
|
|
605581
606991
|
};
|
|
605582
|
-
|
|
606992
|
+
writeFileSync58(liveSnapshotPath(this.repoRoot), JSON.stringify(this.snapshot, null, 2), "utf8");
|
|
605583
606993
|
}
|
|
605584
606994
|
};
|
|
605585
606995
|
}
|
|
@@ -605725,8 +607135,8 @@ var init_tool_adapter = __esm({
|
|
|
605725
607135
|
});
|
|
605726
607136
|
|
|
605727
607137
|
// packages/cli/src/tui/runtime-verification.ts
|
|
605728
|
-
import { existsSync as
|
|
605729
|
-
import { dirname as
|
|
607138
|
+
import { existsSync as existsSync111, readFileSync as readFileSync90, readdirSync as readdirSync36 } from "node:fs";
|
|
607139
|
+
import { dirname as dirname38, extname as extname14, join as join127, relative as relative12, resolve as resolve54 } from "node:path";
|
|
605730
607140
|
function safeRelative(root, file) {
|
|
605731
607141
|
const rel = relative12(root, file);
|
|
605732
607142
|
return rel && !rel.startsWith("..") ? rel : file;
|
|
@@ -605737,16 +607147,16 @@ function collectHtmlFiles(root, maxFiles) {
|
|
|
605737
607147
|
if (out.length >= maxFiles || depth > 8) return;
|
|
605738
607148
|
let entries;
|
|
605739
607149
|
try {
|
|
605740
|
-
entries =
|
|
607150
|
+
entries = readdirSync36(dir, { withFileTypes: true });
|
|
605741
607151
|
} catch {
|
|
605742
607152
|
return;
|
|
605743
607153
|
}
|
|
605744
607154
|
for (const entry of entries) {
|
|
605745
607155
|
if (out.length >= maxFiles) break;
|
|
605746
607156
|
if (entry.isDirectory()) {
|
|
605747
|
-
if (!SKIP_DIRS2.has(entry.name)) walk2(
|
|
607157
|
+
if (!SKIP_DIRS2.has(entry.name)) walk2(join127(dir, entry.name), depth + 1);
|
|
605748
607158
|
} else if (entry.isFile() && /\.html?$/i.test(entry.name)) {
|
|
605749
|
-
out.push(
|
|
607159
|
+
out.push(join127(dir, entry.name));
|
|
605750
607160
|
}
|
|
605751
607161
|
}
|
|
605752
607162
|
};
|
|
@@ -605771,15 +607181,15 @@ function resolveScriptSource(root, htmlFile, src2) {
|
|
|
605771
607181
|
const withoutHash = raw.split("#", 1)[0] ?? "";
|
|
605772
607182
|
const clean5 = withoutHash.split("?", 1)[0] ?? "";
|
|
605773
607183
|
if (!clean5 || !SCRIPT_EXTS.has(extname14(clean5).toLowerCase())) return null;
|
|
605774
|
-
const abs = clean5.startsWith("/") ? resolve54(root, `.${clean5}`) : resolve54(
|
|
607184
|
+
const abs = clean5.startsWith("/") ? resolve54(root, `.${clean5}`) : resolve54(dirname38(htmlFile), clean5);
|
|
605775
607185
|
const rel = relative12(root, abs);
|
|
605776
607186
|
if (rel.startsWith("..") || rel === "") return null;
|
|
605777
|
-
return
|
|
607187
|
+
return existsSync111(abs) ? abs : null;
|
|
605778
607188
|
}
|
|
605779
607189
|
function referencedScriptAssets(root, htmlFile) {
|
|
605780
607190
|
let html = "";
|
|
605781
607191
|
try {
|
|
605782
|
-
html =
|
|
607192
|
+
html = readFileSync90(htmlFile, "utf8");
|
|
605783
607193
|
} catch {
|
|
605784
607194
|
return [];
|
|
605785
607195
|
}
|
|
@@ -605809,7 +607219,7 @@ async function syntaxCheckJavaScriptModule(source) {
|
|
|
605809
607219
|
}
|
|
605810
607220
|
async function runStaticRuntimeAssetSyntaxCheck(projectRoot, options2 = {}) {
|
|
605811
607221
|
const root = resolve54(projectRoot);
|
|
605812
|
-
if (!
|
|
607222
|
+
if (!existsSync111(root)) {
|
|
605813
607223
|
return { ok: true, checkedAssets: 0, htmlFiles: 0, issues: [], skippedReason: "project root missing" };
|
|
605814
607224
|
}
|
|
605815
607225
|
const htmlFiles = collectHtmlFiles(root, options2.maxHtmlFiles ?? 80);
|
|
@@ -605822,7 +607232,7 @@ async function runStaticRuntimeAssetSyntaxCheck(projectRoot, options2 = {}) {
|
|
|
605822
607232
|
if (issueByAsset.has(assetFile)) continue;
|
|
605823
607233
|
let source = "";
|
|
605824
607234
|
try {
|
|
605825
|
-
source =
|
|
607235
|
+
source = readFileSync90(assetFile, "utf8");
|
|
605826
607236
|
} catch {
|
|
605827
607237
|
continue;
|
|
605828
607238
|
}
|
|
@@ -605895,1194 +607305,6 @@ var init_runtime_verification = __esm({
|
|
|
605895
607305
|
}
|
|
605896
607306
|
});
|
|
605897
607307
|
|
|
605898
|
-
// packages/cli/src/api/py-embed.ts
|
|
605899
|
-
var py_embed_exports = {};
|
|
605900
|
-
__export(py_embed_exports, {
|
|
605901
|
-
ensureEmbedDeps: () => ensureEmbedDeps,
|
|
605902
|
-
getVenvPython: () => getVenvPython,
|
|
605903
|
-
runEmbedAudio: () => runEmbedAudio,
|
|
605904
|
-
runEmbedImage: () => runEmbedImage,
|
|
605905
|
-
runEmbedText: () => runEmbedText,
|
|
605906
|
-
runTranscribeFile: () => runTranscribeFile
|
|
605907
|
-
});
|
|
605908
|
-
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
605909
|
-
import { existsSync as existsSync110, mkdirSync as mkdirSync68, writeFileSync as writeFileSync57 } from "node:fs";
|
|
605910
|
-
import { join as join126 } from "node:path";
|
|
605911
|
-
import { homedir as homedir38 } from "node:os";
|
|
605912
|
-
function getVenvDir() {
|
|
605913
|
-
return join126(homedir38(), ".omnius", "venv");
|
|
605914
|
-
}
|
|
605915
|
-
function getVenvPython() {
|
|
605916
|
-
const base3 = getVenvDir();
|
|
605917
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
605918
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
605919
|
-
return join126(base3, bin, exe);
|
|
605920
|
-
}
|
|
605921
|
-
function ensureEmbedDeps() {
|
|
605922
|
-
const venv = getVenvDir();
|
|
605923
|
-
let log22 = "";
|
|
605924
|
-
try {
|
|
605925
|
-
mkdirSync68(venv, { recursive: true });
|
|
605926
|
-
} catch {
|
|
605927
|
-
}
|
|
605928
|
-
if (!existsSync110(getVenvPython())) {
|
|
605929
|
-
const mk = spawnSync8("python3", ["-m", "venv", venv], { encoding: "utf8" });
|
|
605930
|
-
log22 += mk.stdout + mk.stderr;
|
|
605931
|
-
}
|
|
605932
|
-
const pip = process.platform === "win32" ? join126(venv, "Scripts", "pip.exe") : join126(venv, "bin", "pip");
|
|
605933
|
-
const up = spawnSync8(pip, ["install", "--upgrade", "pip", "setuptools", "wheel"], { encoding: "utf8", timeout: 3e5 });
|
|
605934
|
-
log22 += up.stdout + up.stderr;
|
|
605935
|
-
const asrPkgs = [
|
|
605936
|
-
"numpy==1.26.4",
|
|
605937
|
-
"soundfile",
|
|
605938
|
-
"faster-whisper",
|
|
605939
|
-
"openai-whisper",
|
|
605940
|
-
// Embedding helpers used elsewhere (kept lightweight compared to full torch stack)
|
|
605941
|
-
"Pillow"
|
|
605942
|
-
];
|
|
605943
|
-
const ins = spawnSync8(pip, ["install", "--upgrade", ...asrPkgs], { encoding: "utf8", timeout: 9e5 });
|
|
605944
|
-
log22 += ins.stdout + ins.stderr;
|
|
605945
|
-
let fullOk = true;
|
|
605946
|
-
if (process.env["OMNIUS_INSTALL_FULL_EMBED_DEPS"] === "1") {
|
|
605947
|
-
const fullPkgs = ["torch", "torchaudio", "open_clip_torch", "speechbrain"];
|
|
605948
|
-
const full = spawnSync8(pip, ["install", "--upgrade", ...fullPkgs], { encoding: "utf8", timeout: 18e5 });
|
|
605949
|
-
log22 += full.stdout + full.stderr;
|
|
605950
|
-
fullOk = full.status === 0;
|
|
605951
|
-
}
|
|
605952
|
-
const ok3 = ins.status === 0 && up.status === 0 && fullOk;
|
|
605953
|
-
return { ok: ok3, log: log22 };
|
|
605954
|
-
}
|
|
605955
|
-
function runEmbedImage(input) {
|
|
605956
|
-
const py = getVenvPython();
|
|
605957
|
-
const script = locateScript("embed-image.py");
|
|
605958
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
605959
|
-
try {
|
|
605960
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605961
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605962
|
-
} catch {
|
|
605963
|
-
}
|
|
605964
|
-
return null;
|
|
605965
|
-
}
|
|
605966
|
-
function runEmbedAudio(input) {
|
|
605967
|
-
const py = getVenvPython();
|
|
605968
|
-
const script = locateScript("embed-audio.py");
|
|
605969
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8" });
|
|
605970
|
-
try {
|
|
605971
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605972
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605973
|
-
} catch {
|
|
605974
|
-
}
|
|
605975
|
-
return null;
|
|
605976
|
-
}
|
|
605977
|
-
function runEmbedText(text2) {
|
|
605978
|
-
const py = getVenvPython();
|
|
605979
|
-
const script = locateScript("embed-text.py");
|
|
605980
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify({ text: text2 }), encoding: "utf8" });
|
|
605981
|
-
try {
|
|
605982
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605983
|
-
if (Array.isArray(out.embedding)) return new Float32Array(out.embedding);
|
|
605984
|
-
} catch {
|
|
605985
|
-
}
|
|
605986
|
-
return null;
|
|
605987
|
-
}
|
|
605988
|
-
function runTranscribeFile(input) {
|
|
605989
|
-
const py = getVenvPython();
|
|
605990
|
-
const script = locateScript("transcribe-file.py");
|
|
605991
|
-
const p2 = spawnSync8(py, [script], { input: JSON.stringify(input), encoding: "utf8", timeout: 6e5 });
|
|
605992
|
-
try {
|
|
605993
|
-
const out = JSON.parse(p2.stdout || "{}");
|
|
605994
|
-
if (typeof out.text === "string") return out.text;
|
|
605995
|
-
} catch {
|
|
605996
|
-
}
|
|
605997
|
-
return null;
|
|
605998
|
-
}
|
|
605999
|
-
function locateScript(name10) {
|
|
606000
|
-
const candidates = [
|
|
606001
|
-
join126(process.cwd(), "dist", "scripts", name10),
|
|
606002
|
-
join126(process.cwd(), name10),
|
|
606003
|
-
join126(process.cwd(), "scripts", name10)
|
|
606004
|
-
];
|
|
606005
|
-
for (const c9 of candidates) if (existsSync110(c9)) return c9;
|
|
606006
|
-
const fallback = join126(process.cwd(), name10);
|
|
606007
|
-
try {
|
|
606008
|
-
writeFileSync57(fallback, 'print("missing script")\n');
|
|
606009
|
-
} catch {
|
|
606010
|
-
}
|
|
606011
|
-
return fallback;
|
|
606012
|
-
}
|
|
606013
|
-
var init_py_embed = __esm({
|
|
606014
|
-
"packages/cli/src/api/py-embed.ts"() {
|
|
606015
|
-
"use strict";
|
|
606016
|
-
}
|
|
606017
|
-
});
|
|
606018
|
-
|
|
606019
|
-
// packages/cli/src/tui/listen.ts
|
|
606020
|
-
var listen_exports = {};
|
|
606021
|
-
__export(listen_exports, {
|
|
606022
|
-
ListenEngine: () => ListenEngine,
|
|
606023
|
-
ensureTranscribeCliBackground: () => ensureTranscribeCliBackground,
|
|
606024
|
-
getListenEngine: () => getListenEngine,
|
|
606025
|
-
isAudioPath: () => isAudioPath,
|
|
606026
|
-
isTranscribablePath: () => isTranscribablePath,
|
|
606027
|
-
isVideoPath: () => isVideoPath,
|
|
606028
|
-
transcribeFileViaWhisper: () => transcribeFileViaWhisper,
|
|
606029
|
-
waitForTranscribeCli: () => waitForTranscribeCli
|
|
606030
|
-
});
|
|
606031
|
-
import { spawn as spawn29 } from "node:child_process";
|
|
606032
|
-
import {
|
|
606033
|
-
existsSync as existsSync111,
|
|
606034
|
-
mkdirSync as mkdirSync69,
|
|
606035
|
-
writeFileSync as writeFileSync58,
|
|
606036
|
-
readdirSync as readdirSync36
|
|
606037
|
-
} from "node:fs";
|
|
606038
|
-
import { join as join127, dirname as dirname38 } from "node:path";
|
|
606039
|
-
import { homedir as homedir39 } from "node:os";
|
|
606040
|
-
import { fileURLToPath as fileURLToPath16 } from "node:url";
|
|
606041
|
-
import { EventEmitter as EventEmitter6 } from "node:events";
|
|
606042
|
-
import { createInterface as createInterface2 } from "node:readline";
|
|
606043
|
-
function isAudioPath(path12) {
|
|
606044
|
-
const ext = path12.toLowerCase().split(".").pop();
|
|
606045
|
-
return ext ? AUDIO_EXTENSIONS.has(`.${ext}`) : false;
|
|
606046
|
-
}
|
|
606047
|
-
function isVideoPath(path12) {
|
|
606048
|
-
const ext = path12.toLowerCase().split(".").pop();
|
|
606049
|
-
return ext ? VIDEO_EXTENSIONS2.has(`.${ext}`) : false;
|
|
606050
|
-
}
|
|
606051
|
-
function isTranscribablePath(path12) {
|
|
606052
|
-
return isAudioPath(path12) || isVideoPath(path12);
|
|
606053
|
-
}
|
|
606054
|
-
async function findMicCaptureCommand() {
|
|
606055
|
-
const platform7 = process.platform;
|
|
606056
|
-
if (platform7 === "linux") {
|
|
606057
|
-
if (await commandExists2("arecord")) {
|
|
606058
|
-
return {
|
|
606059
|
-
cmd: "arecord",
|
|
606060
|
-
args: [
|
|
606061
|
-
"-f",
|
|
606062
|
-
"S16_LE",
|
|
606063
|
-
"-r",
|
|
606064
|
-
"16000",
|
|
606065
|
-
"-c",
|
|
606066
|
-
"1",
|
|
606067
|
-
"-t",
|
|
606068
|
-
"raw",
|
|
606069
|
-
"-q",
|
|
606070
|
-
"-"
|
|
606071
|
-
]
|
|
606072
|
-
};
|
|
606073
|
-
}
|
|
606074
|
-
}
|
|
606075
|
-
if (platform7 === "darwin") {
|
|
606076
|
-
if (await commandExists2("sox")) {
|
|
606077
|
-
return {
|
|
606078
|
-
cmd: "sox",
|
|
606079
|
-
args: [
|
|
606080
|
-
"-d",
|
|
606081
|
-
"-t",
|
|
606082
|
-
"raw",
|
|
606083
|
-
"-r",
|
|
606084
|
-
"16000",
|
|
606085
|
-
"-c",
|
|
606086
|
-
"1",
|
|
606087
|
-
"-b",
|
|
606088
|
-
"16",
|
|
606089
|
-
"-e",
|
|
606090
|
-
"signed-integer",
|
|
606091
|
-
"-"
|
|
606092
|
-
]
|
|
606093
|
-
};
|
|
606094
|
-
}
|
|
606095
|
-
}
|
|
606096
|
-
if (await commandExists2("ffmpeg")) {
|
|
606097
|
-
if (platform7 === "linux") {
|
|
606098
|
-
return {
|
|
606099
|
-
cmd: "ffmpeg",
|
|
606100
|
-
args: [
|
|
606101
|
-
"-f",
|
|
606102
|
-
"pulse",
|
|
606103
|
-
"-i",
|
|
606104
|
-
"default",
|
|
606105
|
-
"-ar",
|
|
606106
|
-
"16000",
|
|
606107
|
-
"-ac",
|
|
606108
|
-
"1",
|
|
606109
|
-
"-f",
|
|
606110
|
-
"s16le",
|
|
606111
|
-
"-loglevel",
|
|
606112
|
-
"quiet",
|
|
606113
|
-
"pipe:1"
|
|
606114
|
-
]
|
|
606115
|
-
};
|
|
606116
|
-
} else if (platform7 === "darwin") {
|
|
606117
|
-
return {
|
|
606118
|
-
cmd: "ffmpeg",
|
|
606119
|
-
args: [
|
|
606120
|
-
"-f",
|
|
606121
|
-
"avfoundation",
|
|
606122
|
-
"-i",
|
|
606123
|
-
":0",
|
|
606124
|
-
"-ar",
|
|
606125
|
-
"16000",
|
|
606126
|
-
"-ac",
|
|
606127
|
-
"1",
|
|
606128
|
-
"-f",
|
|
606129
|
-
"s16le",
|
|
606130
|
-
"-loglevel",
|
|
606131
|
-
"quiet",
|
|
606132
|
-
"pipe:1"
|
|
606133
|
-
]
|
|
606134
|
-
};
|
|
606135
|
-
}
|
|
606136
|
-
}
|
|
606137
|
-
return null;
|
|
606138
|
-
}
|
|
606139
|
-
async function findTranscribeFileScript() {
|
|
606140
|
-
const thisDir = dirname38(fileURLToPath16(import.meta.url));
|
|
606141
|
-
const candidates = [
|
|
606142
|
-
join127(thisDir, "../../../../packages/execution/scripts/transcribe-file.py"),
|
|
606143
|
-
join127(thisDir, "../../../packages/execution/scripts/transcribe-file.py"),
|
|
606144
|
-
join127(thisDir, "../../execution/scripts/transcribe-file.py"),
|
|
606145
|
-
join127(thisDir, "../scripts/transcribe-file.py"),
|
|
606146
|
-
join127(thisDir, "../../scripts/transcribe-file.py")
|
|
606147
|
-
];
|
|
606148
|
-
for (const p2 of candidates) {
|
|
606149
|
-
if (existsSync111(p2)) return p2;
|
|
606150
|
-
}
|
|
606151
|
-
try {
|
|
606152
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606153
|
-
const candidates2 = [
|
|
606154
|
-
join127(globalRoot, "omnius", "dist", "scripts", "transcribe-file.py"),
|
|
606155
|
-
join127(globalRoot, "omnius", "scripts", "transcribe-file.py")
|
|
606156
|
-
];
|
|
606157
|
-
for (const p2 of candidates2) {
|
|
606158
|
-
if (existsSync111(p2)) return p2;
|
|
606159
|
-
}
|
|
606160
|
-
} catch {
|
|
606161
|
-
}
|
|
606162
|
-
return null;
|
|
606163
|
-
}
|
|
606164
|
-
async function transcribeFileViaWhisper(filePath, model) {
|
|
606165
|
-
const script = await findTranscribeFileScript();
|
|
606166
|
-
if (!script) return null;
|
|
606167
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
606168
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
606169
|
-
const venvPython2 = join127(homedir39(), ".omnius", "venv", bin, exe);
|
|
606170
|
-
if (!existsSync111(venvPython2)) return null;
|
|
606171
|
-
return new Promise((resolve76) => {
|
|
606172
|
-
const child = spawn29(venvPython2, [script], {
|
|
606173
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606174
|
-
env: process.env
|
|
606175
|
-
});
|
|
606176
|
-
let out = "";
|
|
606177
|
-
let err = "";
|
|
606178
|
-
let timer = null;
|
|
606179
|
-
const stop2 = (val) => {
|
|
606180
|
-
if (timer) clearTimeout(timer);
|
|
606181
|
-
try {
|
|
606182
|
-
child.kill("SIGTERM");
|
|
606183
|
-
} catch {
|
|
606184
|
-
}
|
|
606185
|
-
resolve76(val ? parse3(val) : null);
|
|
606186
|
-
};
|
|
606187
|
-
function parse3(raw) {
|
|
606188
|
-
try {
|
|
606189
|
-
const j = JSON.parse(raw);
|
|
606190
|
-
if (j.error) return null;
|
|
606191
|
-
if (typeof j.text !== "string") return null;
|
|
606192
|
-
return {
|
|
606193
|
-
text: j.text.trim(),
|
|
606194
|
-
duration: null,
|
|
606195
|
-
speakers: [],
|
|
606196
|
-
segments: []
|
|
606197
|
-
};
|
|
606198
|
-
} catch {
|
|
606199
|
-
return null;
|
|
606200
|
-
}
|
|
606201
|
-
}
|
|
606202
|
-
child.stdout?.on("data", (d2) => {
|
|
606203
|
-
out += d2.toString("utf-8");
|
|
606204
|
-
});
|
|
606205
|
-
child.stderr?.on("data", (d2) => {
|
|
606206
|
-
err += d2.toString("utf-8");
|
|
606207
|
-
});
|
|
606208
|
-
child.once("error", () => stop2(null));
|
|
606209
|
-
child.once("close", () => {
|
|
606210
|
-
if (out.trim()) resolve76(parse3(out));
|
|
606211
|
-
else {
|
|
606212
|
-
void err;
|
|
606213
|
-
resolve76(null);
|
|
606214
|
-
}
|
|
606215
|
-
});
|
|
606216
|
-
timer = setTimeout(() => stop2(null), 12e4);
|
|
606217
|
-
try {
|
|
606218
|
-
child.stdin?.write(JSON.stringify({ path: filePath, model }) + "\n");
|
|
606219
|
-
child.stdin?.end();
|
|
606220
|
-
} catch {
|
|
606221
|
-
stop2(null);
|
|
606222
|
-
}
|
|
606223
|
-
});
|
|
606224
|
-
}
|
|
606225
|
-
async function findLiveWhisperScript() {
|
|
606226
|
-
const thisDir = dirname38(fileURLToPath16(import.meta.url));
|
|
606227
|
-
const candidates = [
|
|
606228
|
-
join127(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
|
|
606229
|
-
join127(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
|
|
606230
|
-
join127(thisDir, "../../execution/scripts/live-whisper.py"),
|
|
606231
|
-
// npm install layout — scripts bundled alongside dist
|
|
606232
|
-
join127(thisDir, "../scripts/live-whisper.py"),
|
|
606233
|
-
join127(thisDir, "../../scripts/live-whisper.py")
|
|
606234
|
-
];
|
|
606235
|
-
for (const p2 of candidates) {
|
|
606236
|
-
if (existsSync111(p2)) return p2;
|
|
606237
|
-
}
|
|
606238
|
-
try {
|
|
606239
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606240
|
-
const candidates2 = [
|
|
606241
|
-
join127(globalRoot, "omnius", "dist", "scripts", "live-whisper.py"),
|
|
606242
|
-
join127(globalRoot, "omnius", "scripts", "live-whisper.py")
|
|
606243
|
-
];
|
|
606244
|
-
for (const p2 of candidates2) {
|
|
606245
|
-
if (existsSync111(p2)) return p2;
|
|
606246
|
-
}
|
|
606247
|
-
} catch {
|
|
606248
|
-
}
|
|
606249
|
-
const nvmBase = join127(homedir39(), ".nvm", "versions", "node");
|
|
606250
|
-
if (existsSync111(nvmBase)) {
|
|
606251
|
-
try {
|
|
606252
|
-
for (const ver of readdirSync36(nvmBase)) {
|
|
606253
|
-
const p2 = join127(
|
|
606254
|
-
nvmBase,
|
|
606255
|
-
ver,
|
|
606256
|
-
"lib",
|
|
606257
|
-
"node_modules",
|
|
606258
|
-
"omnius",
|
|
606259
|
-
"dist",
|
|
606260
|
-
"scripts",
|
|
606261
|
-
"live-whisper.py"
|
|
606262
|
-
);
|
|
606263
|
-
if (existsSync111(p2)) return p2;
|
|
606264
|
-
}
|
|
606265
|
-
} catch {
|
|
606266
|
-
}
|
|
606267
|
-
}
|
|
606268
|
-
return null;
|
|
606269
|
-
}
|
|
606270
|
-
async function ensureVenvForTranscribeCli() {
|
|
606271
|
-
const bin = process.platform === "win32" ? "Scripts" : "bin";
|
|
606272
|
-
const exe = process.platform === "win32" ? "python.exe" : "python3";
|
|
606273
|
-
const venvBin = join127(homedir39(), ".omnius", "venv", bin);
|
|
606274
|
-
const venvPython2 = join127(venvBin, exe);
|
|
606275
|
-
if (!existsSync111(venvPython2)) {
|
|
606276
|
-
try {
|
|
606277
|
-
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
606278
|
-
if (typeof mod3.ensureEmbedDeps === "function") mod3.ensureEmbedDeps();
|
|
606279
|
-
} catch {
|
|
606280
|
-
}
|
|
606281
|
-
}
|
|
606282
|
-
if (!existsSync111(venvPython2)) {
|
|
606283
|
-
try {
|
|
606284
|
-
mkdirSync69(join127(homedir39(), ".omnius", "venv"), { recursive: true });
|
|
606285
|
-
await execFileText4("python3", ["-m", "venv", join127(homedir39(), ".omnius", "venv")], { timeout: 6e4 });
|
|
606286
|
-
} catch {
|
|
606287
|
-
return false;
|
|
606288
|
-
}
|
|
606289
|
-
}
|
|
606290
|
-
process.env.TRANSCRIBE_PYTHON = venvPython2;
|
|
606291
|
-
const pathSep2 = process.platform === "win32" ? ";" : ":";
|
|
606292
|
-
const currentPath = process.env.PATH || "";
|
|
606293
|
-
if (!currentPath.split(pathSep2).includes(venvBin)) {
|
|
606294
|
-
process.env.PATH = `${venvBin}${pathSep2}${currentPath}`;
|
|
606295
|
-
}
|
|
606296
|
-
try {
|
|
606297
|
-
await execFileText4(venvPython2, ["-c", "import numpy"], { timeout: 1e4 });
|
|
606298
|
-
} catch {
|
|
606299
|
-
try {
|
|
606300
|
-
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "pip", "setuptools", "wheel", "numpy==1.26.4"], { timeout: 3e5 });
|
|
606301
|
-
} catch {
|
|
606302
|
-
return false;
|
|
606303
|
-
}
|
|
606304
|
-
}
|
|
606305
|
-
try {
|
|
606306
|
-
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
606307
|
-
return true;
|
|
606308
|
-
} catch {
|
|
606309
|
-
}
|
|
606310
|
-
try {
|
|
606311
|
-
await execFileText4(venvPython2, ["-m", "pip", "install", "-U", "transcribe-cli"], { timeout: 3e5 });
|
|
606312
|
-
await execFileText4(venvPython2, ["-c", "import transcribe_cli"], { timeout: 1e4 });
|
|
606313
|
-
return true;
|
|
606314
|
-
} catch {
|
|
606315
|
-
return false;
|
|
606316
|
-
}
|
|
606317
|
-
}
|
|
606318
|
-
async function requireTranscribeCliFrom2(packageDir) {
|
|
606319
|
-
try {
|
|
606320
|
-
const { createRequire: createRequire11 } = await import("node:module");
|
|
606321
|
-
const req3 = createRequire11(import.meta.url);
|
|
606322
|
-
const distPath = join127(packageDir, "dist", "index.js");
|
|
606323
|
-
if (existsSync111(distPath)) return req3(distPath);
|
|
606324
|
-
return req3(packageDir);
|
|
606325
|
-
} catch {
|
|
606326
|
-
return null;
|
|
606327
|
-
}
|
|
606328
|
-
}
|
|
606329
|
-
async function loadManagedTranscribeCli() {
|
|
606330
|
-
const packageDir = join127(MANAGED_TRANSCRIBE_CLI_DIR2, "node_modules", "transcribe-cli");
|
|
606331
|
-
if (!existsSync111(join127(packageDir, "dist", "index.js"))) return null;
|
|
606332
|
-
return requireTranscribeCliFrom2(packageDir);
|
|
606333
|
-
}
|
|
606334
|
-
async function ensureManagedTranscribeCliNode() {
|
|
606335
|
-
if (await loadManagedTranscribeCli()) return true;
|
|
606336
|
-
if (_managedTranscribeCliInstallPromise) return _managedTranscribeCliInstallPromise;
|
|
606337
|
-
_managedTranscribeCliInstallPromise = (async () => {
|
|
606338
|
-
try {
|
|
606339
|
-
mkdirSync69(MANAGED_TRANSCRIBE_CLI_DIR2, { recursive: true });
|
|
606340
|
-
if (!existsSync111(join127(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"))) {
|
|
606341
|
-
writeFileSync58(
|
|
606342
|
-
join127(MANAGED_TRANSCRIBE_CLI_DIR2, "package.json"),
|
|
606343
|
-
JSON.stringify({ private: true, dependencies: {} }, null, 2),
|
|
606344
|
-
"utf8"
|
|
606345
|
-
);
|
|
606346
|
-
}
|
|
606347
|
-
await execFileText4("npm", ["install", "--prefix", MANAGED_TRANSCRIBE_CLI_DIR2, "transcribe-cli@^2.0.1"], {
|
|
606348
|
-
timeout: 18e4
|
|
606349
|
-
});
|
|
606350
|
-
return Boolean(await loadManagedTranscribeCli());
|
|
606351
|
-
} catch {
|
|
606352
|
-
return false;
|
|
606353
|
-
}
|
|
606354
|
-
})();
|
|
606355
|
-
return _managedTranscribeCliInstallPromise;
|
|
606356
|
-
}
|
|
606357
|
-
function ensureTranscribeCliBackground() {
|
|
606358
|
-
if (_bgInstallPromise) return _bgInstallPromise;
|
|
606359
|
-
_bgInstallPromise = (async () => {
|
|
606360
|
-
const nodeReady = await ensureManagedTranscribeCliNode();
|
|
606361
|
-
const pyReady = await ensureVenvForTranscribeCli();
|
|
606362
|
-
return nodeReady && pyReady;
|
|
606363
|
-
})();
|
|
606364
|
-
return _bgInstallPromise;
|
|
606365
|
-
}
|
|
606366
|
-
async function waitForTranscribeCli() {
|
|
606367
|
-
if (_bgInstallPromise) {
|
|
606368
|
-
return _bgInstallPromise;
|
|
606369
|
-
}
|
|
606370
|
-
return Boolean(await loadManagedTranscribeCli()) && await ensureVenvForTranscribeCli();
|
|
606371
|
-
}
|
|
606372
|
-
function getListenEngine(config) {
|
|
606373
|
-
if (!_engine) {
|
|
606374
|
-
_engine = new ListenEngine(config);
|
|
606375
|
-
}
|
|
606376
|
-
return _engine;
|
|
606377
|
-
}
|
|
606378
|
-
var MANAGED_TRANSCRIBE_CLI_DIR2, AUDIO_EXTENSIONS, VIDEO_EXTENSIONS2, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _managedTranscribeCliInstallPromise, _engine;
|
|
606379
|
-
var init_listen = __esm({
|
|
606380
|
-
"packages/cli/src/tui/listen.ts"() {
|
|
606381
|
-
"use strict";
|
|
606382
|
-
init_typed_node_events();
|
|
606383
|
-
init_async_process();
|
|
606384
|
-
MANAGED_TRANSCRIBE_CLI_DIR2 = join127(homedir39(), ".omnius", "runtimes", "asr", "transcribe-cli-node");
|
|
606385
|
-
AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
606386
|
-
".mp3",
|
|
606387
|
-
".wav",
|
|
606388
|
-
".flac",
|
|
606389
|
-
".aac",
|
|
606390
|
-
".m4a",
|
|
606391
|
-
".ogg",
|
|
606392
|
-
".wma",
|
|
606393
|
-
".opus"
|
|
606394
|
-
]);
|
|
606395
|
-
VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
606396
|
-
".mp4",
|
|
606397
|
-
".mkv",
|
|
606398
|
-
".avi",
|
|
606399
|
-
".mov",
|
|
606400
|
-
".webm",
|
|
606401
|
-
".flv",
|
|
606402
|
-
".wmv",
|
|
606403
|
-
".m4v",
|
|
606404
|
-
".ts"
|
|
606405
|
-
]);
|
|
606406
|
-
WhisperFallbackTranscriber = class extends EventEmitter6 {
|
|
606407
|
-
constructor(model, scriptPath2) {
|
|
606408
|
-
super();
|
|
606409
|
-
this.model = model;
|
|
606410
|
-
this.scriptPath = scriptPath2;
|
|
606411
|
-
}
|
|
606412
|
-
model;
|
|
606413
|
-
scriptPath;
|
|
606414
|
-
process = null;
|
|
606415
|
-
_ready = false;
|
|
606416
|
-
get ready() {
|
|
606417
|
-
return this._ready;
|
|
606418
|
-
}
|
|
606419
|
-
async start() {
|
|
606420
|
-
let pyPath = "python3";
|
|
606421
|
-
try {
|
|
606422
|
-
const mod3 = await Promise.resolve().then(() => (init_py_embed(), py_embed_exports));
|
|
606423
|
-
const ensure = mod3.ensureEmbedDeps;
|
|
606424
|
-
const getPy = mod3.getVenvPython;
|
|
606425
|
-
if (typeof ensure === "function") {
|
|
606426
|
-
try {
|
|
606427
|
-
const res = ensure();
|
|
606428
|
-
if (res?.log) this.emit("status", res.log.slice(-500));
|
|
606429
|
-
} catch {
|
|
606430
|
-
}
|
|
606431
|
-
}
|
|
606432
|
-
if (typeof getPy === "function") {
|
|
606433
|
-
pyPath = getPy();
|
|
606434
|
-
}
|
|
606435
|
-
} catch {
|
|
606436
|
-
}
|
|
606437
|
-
this.process = spawn29(
|
|
606438
|
-
pyPath,
|
|
606439
|
-
[
|
|
606440
|
-
this.scriptPath,
|
|
606441
|
-
"--model",
|
|
606442
|
-
this.model,
|
|
606443
|
-
"--chunk-seconds",
|
|
606444
|
-
"3",
|
|
606445
|
-
"--window-seconds",
|
|
606446
|
-
"10"
|
|
606447
|
-
],
|
|
606448
|
-
{
|
|
606449
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606450
|
-
env: { ...process.env }
|
|
606451
|
-
}
|
|
606452
|
-
);
|
|
606453
|
-
return new Promise((resolve76, reject) => {
|
|
606454
|
-
const timeout2 = setTimeout(() => {
|
|
606455
|
-
reject(
|
|
606456
|
-
new Error(
|
|
606457
|
-
"Whisper fallback: model load timeout (5 min). First run downloads the model."
|
|
606458
|
-
)
|
|
606459
|
-
);
|
|
606460
|
-
}, 3e5);
|
|
606461
|
-
const rl = createInterface2({ input: this.process.stdout });
|
|
606462
|
-
onReadlineLine(rl, (line) => {
|
|
606463
|
-
try {
|
|
606464
|
-
const evt = JSON.parse(line);
|
|
606465
|
-
switch (evt.type) {
|
|
606466
|
-
case "status":
|
|
606467
|
-
this.emit("status", evt.message);
|
|
606468
|
-
break;
|
|
606469
|
-
case "ready":
|
|
606470
|
-
this._ready = true;
|
|
606471
|
-
clearTimeout(timeout2);
|
|
606472
|
-
this.emit("ready");
|
|
606473
|
-
resolve76();
|
|
606474
|
-
break;
|
|
606475
|
-
case "transcript":
|
|
606476
|
-
this.emit("transcript", {
|
|
606477
|
-
text: evt.text,
|
|
606478
|
-
isFinal: evt.isFinal ?? false
|
|
606479
|
-
});
|
|
606480
|
-
break;
|
|
606481
|
-
case "error":
|
|
606482
|
-
this.emit("error", new Error(evt.message));
|
|
606483
|
-
break;
|
|
606484
|
-
}
|
|
606485
|
-
} catch {
|
|
606486
|
-
}
|
|
606487
|
-
});
|
|
606488
|
-
this.process.stderr?.on("data", (data) => {
|
|
606489
|
-
const text2 = data.toString().trim();
|
|
606490
|
-
if (text2) this.emit("status", text2);
|
|
606491
|
-
});
|
|
606492
|
-
onChildError(this.process, (err) => {
|
|
606493
|
-
clearTimeout(timeout2);
|
|
606494
|
-
reject(err);
|
|
606495
|
-
});
|
|
606496
|
-
onChildClose(this.process, (code8) => {
|
|
606497
|
-
if (!this._ready) {
|
|
606498
|
-
clearTimeout(timeout2);
|
|
606499
|
-
reject(
|
|
606500
|
-
new Error(`Whisper worker exited with code ${code8} before ready`)
|
|
606501
|
-
);
|
|
606502
|
-
}
|
|
606503
|
-
});
|
|
606504
|
-
});
|
|
606505
|
-
}
|
|
606506
|
-
/** Write PCM16 audio data to the worker's stdin. */
|
|
606507
|
-
write(chunk) {
|
|
606508
|
-
if (this.process?.stdin?.writable) {
|
|
606509
|
-
this.process.stdin.write(chunk);
|
|
606510
|
-
}
|
|
606511
|
-
}
|
|
606512
|
-
/** Stop the worker. */
|
|
606513
|
-
stop() {
|
|
606514
|
-
if (this.process) {
|
|
606515
|
-
try {
|
|
606516
|
-
this.process.stdin?.end();
|
|
606517
|
-
this.process.kill("SIGTERM");
|
|
606518
|
-
} catch {
|
|
606519
|
-
}
|
|
606520
|
-
this.process = null;
|
|
606521
|
-
}
|
|
606522
|
-
this._ready = false;
|
|
606523
|
-
}
|
|
606524
|
-
};
|
|
606525
|
-
ListenEngine = class extends EventEmitter6 {
|
|
606526
|
-
config;
|
|
606527
|
-
micProcess = null;
|
|
606528
|
-
liveTranscriber = null;
|
|
606529
|
-
// TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
|
|
606530
|
-
active = false;
|
|
606531
|
-
paused = false;
|
|
606532
|
-
// Pause state for voicechat (stops mic but keeps transcriber)
|
|
606533
|
-
silenceTimer = null;
|
|
606534
|
-
countdownInterval = null;
|
|
606535
|
-
lastTranscriptTime = 0;
|
|
606536
|
-
pendingText = "";
|
|
606537
|
-
blinkTimer = null;
|
|
606538
|
-
blinkState = false;
|
|
606539
|
-
transcribeCliAvailable = null;
|
|
606540
|
-
constructor(config) {
|
|
606541
|
-
super();
|
|
606542
|
-
this.config = {
|
|
606543
|
-
model: config?.model ?? "base",
|
|
606544
|
-
mode: config?.mode ?? "confirm",
|
|
606545
|
-
silenceTimeoutMs: config?.silenceTimeoutMs ?? 3e3
|
|
606546
|
-
};
|
|
606547
|
-
}
|
|
606548
|
-
get isActive() {
|
|
606549
|
-
return this.active;
|
|
606550
|
-
}
|
|
606551
|
-
get isBlinking() {
|
|
606552
|
-
return this.active && this.blinkState;
|
|
606553
|
-
}
|
|
606554
|
-
get isPaused() {
|
|
606555
|
-
return this.paused;
|
|
606556
|
-
}
|
|
606557
|
-
get currentModel() {
|
|
606558
|
-
return this.config.model;
|
|
606559
|
-
}
|
|
606560
|
-
get currentMode() {
|
|
606561
|
-
return this.config.mode;
|
|
606562
|
-
}
|
|
606563
|
-
get pendingTranscript() {
|
|
606564
|
-
return this.pendingText;
|
|
606565
|
-
}
|
|
606566
|
-
setModel(model) {
|
|
606567
|
-
this.config.model = model;
|
|
606568
|
-
}
|
|
606569
|
-
setMode(mode) {
|
|
606570
|
-
this.config.mode = mode;
|
|
606571
|
-
}
|
|
606572
|
-
/** Check if transcribe-cli is available. */
|
|
606573
|
-
async isAvailable() {
|
|
606574
|
-
if (this.transcribeCliAvailable !== null)
|
|
606575
|
-
return this.transcribeCliAvailable;
|
|
606576
|
-
const tc = await this.loadTranscribeCli() || (await ensureManagedTranscribeCliNode() ? await this.loadTranscribeCli() : null);
|
|
606577
|
-
const transcribeCliReady = Boolean(tc && await ensureVenvForTranscribeCli());
|
|
606578
|
-
const whisperFallbackReady = Boolean(await findLiveWhisperScript());
|
|
606579
|
-
this.transcribeCliAvailable = transcribeCliReady || whisperFallbackReady;
|
|
606580
|
-
return this.transcribeCliAvailable;
|
|
606581
|
-
}
|
|
606582
|
-
/** Load transcribe-cli — bundled as a dependency of omnius. */
|
|
606583
|
-
async loadTranscribeCli() {
|
|
606584
|
-
try {
|
|
606585
|
-
const { createRequire: createRequire11 } = await import("node:module");
|
|
606586
|
-
const req3 = createRequire11(import.meta.url);
|
|
606587
|
-
return req3("transcribe-cli");
|
|
606588
|
-
} catch {
|
|
606589
|
-
}
|
|
606590
|
-
const managed = await loadManagedTranscribeCli();
|
|
606591
|
-
if (managed) return managed;
|
|
606592
|
-
try {
|
|
606593
|
-
const globalRoot = (await execFileText4("npm", ["root", "-g"], { timeout: 5e3 })).trim();
|
|
606594
|
-
const tcPath = join127(globalRoot, "transcribe-cli");
|
|
606595
|
-
if (existsSync111(join127(tcPath, "dist", "index.js"))) {
|
|
606596
|
-
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
606597
|
-
if (loaded) return loaded;
|
|
606598
|
-
}
|
|
606599
|
-
} catch {
|
|
606600
|
-
}
|
|
606601
|
-
const nvmBase = join127(homedir39(), ".nvm", "versions", "node");
|
|
606602
|
-
if (existsSync111(nvmBase)) {
|
|
606603
|
-
try {
|
|
606604
|
-
const { readdirSync: readdirSync61 } = await import("node:fs");
|
|
606605
|
-
for (const ver of readdirSync61(nvmBase)) {
|
|
606606
|
-
const tcPath = join127(
|
|
606607
|
-
nvmBase,
|
|
606608
|
-
ver,
|
|
606609
|
-
"lib",
|
|
606610
|
-
"node_modules",
|
|
606611
|
-
"transcribe-cli"
|
|
606612
|
-
);
|
|
606613
|
-
if (existsSync111(join127(tcPath, "dist", "index.js"))) {
|
|
606614
|
-
const loaded = await requireTranscribeCliFrom2(tcPath);
|
|
606615
|
-
if (loaded) return loaded;
|
|
606616
|
-
}
|
|
606617
|
-
}
|
|
606618
|
-
} catch {
|
|
606619
|
-
}
|
|
606620
|
-
}
|
|
606621
|
-
return null;
|
|
606622
|
-
}
|
|
606623
|
-
/**
|
|
606624
|
-
* Start listening — captures microphone audio and feeds to TranscribeLive.
|
|
606625
|
-
*/
|
|
606626
|
-
async start() {
|
|
606627
|
-
if (this.active) return "Already listening.";
|
|
606628
|
-
const micCmd = await findMicCaptureCommand();
|
|
606629
|
-
if (!micCmd) {
|
|
606630
|
-
return "No microphone capture tool found. Install arecord (Linux), sox (macOS), or ffmpeg.";
|
|
606631
|
-
}
|
|
606632
|
-
let tc = await this.loadTranscribeCli();
|
|
606633
|
-
if (!tc) {
|
|
606634
|
-
if (_bgInstallPromise) {
|
|
606635
|
-
this.emit("info", "Waiting for transcribe-cli install...");
|
|
606636
|
-
await _bgInstallPromise;
|
|
606637
|
-
this.transcribeCliAvailable = null;
|
|
606638
|
-
tc = await this.loadTranscribeCli();
|
|
606639
|
-
}
|
|
606640
|
-
if (!tc) {
|
|
606641
|
-
try {
|
|
606642
|
-
await ensureManagedTranscribeCliNode();
|
|
606643
|
-
this.transcribeCliAvailable = null;
|
|
606644
|
-
tc = await this.loadTranscribeCli();
|
|
606645
|
-
} catch {
|
|
606646
|
-
}
|
|
606647
|
-
}
|
|
606648
|
-
}
|
|
606649
|
-
let usedFallback = false;
|
|
606650
|
-
let transcribeCliError = null;
|
|
606651
|
-
if (tc) {
|
|
606652
|
-
const TranscribeLive = tc.TranscribeLive;
|
|
606653
|
-
if (TranscribeLive) {
|
|
606654
|
-
let tcUpToDate = false;
|
|
606655
|
-
try {
|
|
606656
|
-
const tcPkgPath = join127(
|
|
606657
|
-
dirname38(__require.resolve?.("transcribe-cli/package.json") || ""),
|
|
606658
|
-
"package.json"
|
|
606659
|
-
);
|
|
606660
|
-
if (existsSync111(tcPkgPath)) {
|
|
606661
|
-
const tcPkg = JSON.parse(
|
|
606662
|
-
__require("fs").readFileSync(tcPkgPath, "utf8")
|
|
606663
|
-
);
|
|
606664
|
-
tcUpToDate = tcPkg.version && tcPkg.version >= "2.0.1";
|
|
606665
|
-
}
|
|
606666
|
-
} catch {
|
|
606667
|
-
try {
|
|
606668
|
-
const out = await execFileText4("npm", ["list", "-g", "transcribe-cli", "--depth=0", "--json"], {
|
|
606669
|
-
timeout: 1e4
|
|
606670
|
-
});
|
|
606671
|
-
const parsed = JSON.parse(out);
|
|
606672
|
-
const ver = parsed?.dependencies?.["transcribe-cli"]?.version || "";
|
|
606673
|
-
tcUpToDate = ver >= "2.0.1";
|
|
606674
|
-
} catch {
|
|
606675
|
-
}
|
|
606676
|
-
}
|
|
606677
|
-
if (!tcUpToDate) {
|
|
606678
|
-
try {
|
|
606679
|
-
await ensureManagedTranscribeCliNode();
|
|
606680
|
-
this.transcribeCliAvailable = null;
|
|
606681
|
-
const reloaded = await this.loadTranscribeCli();
|
|
606682
|
-
if (reloaded?.TranscribeLive) {
|
|
606683
|
-
tc = reloaded;
|
|
606684
|
-
}
|
|
606685
|
-
} catch {
|
|
606686
|
-
}
|
|
606687
|
-
}
|
|
606688
|
-
const venvReady = await ensureVenvForTranscribeCli();
|
|
606689
|
-
if (!venvReady) {
|
|
606690
|
-
transcribeCliError = "venv Python missing numpy (required by transcribe-cli) — using whisper fallback";
|
|
606691
|
-
} else {
|
|
606692
|
-
let caughtUncaught = null;
|
|
606693
|
-
const uncaughtHandler = (err) => {
|
|
606694
|
-
const msg = err?.message || String(err);
|
|
606695
|
-
if (msg.includes("live_worker") || msg.includes("numpy") || msg.includes("transcribe_cli") || msg.includes("ModuleNotFoundError") || msg.includes("Traceback")) {
|
|
606696
|
-
caughtUncaught = err;
|
|
606697
|
-
} else {
|
|
606698
|
-
throw err;
|
|
606699
|
-
}
|
|
606700
|
-
};
|
|
606701
|
-
process.on("uncaughtException", uncaughtHandler);
|
|
606702
|
-
try {
|
|
606703
|
-
this.liveTranscriber = new TranscribeLive({
|
|
606704
|
-
model: this.config.model,
|
|
606705
|
-
sampleRate: 16e3,
|
|
606706
|
-
channels: 1,
|
|
606707
|
-
sampleWidth: 2,
|
|
606708
|
-
chunkDuration: 3
|
|
606709
|
-
});
|
|
606710
|
-
this.liveTranscriber.on(
|
|
606711
|
-
"transcript",
|
|
606712
|
-
(evt) => {
|
|
606713
|
-
if (!evt.text.trim()) return;
|
|
606714
|
-
this.lastTranscriptTime = Date.now();
|
|
606715
|
-
this.pendingText = evt.text.trim();
|
|
606716
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
606717
|
-
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
606718
|
-
}
|
|
606719
|
-
);
|
|
606720
|
-
this.liveTranscriber.on("error", (err) => {
|
|
606721
|
-
this.emit("error", err);
|
|
606722
|
-
});
|
|
606723
|
-
await new Promise((resolve76, reject) => {
|
|
606724
|
-
const timeout2 = setTimeout(
|
|
606725
|
-
() => reject(new Error("Model load timeout (60s)")),
|
|
606726
|
-
6e4
|
|
606727
|
-
);
|
|
606728
|
-
this.liveTranscriber.on("ready", () => {
|
|
606729
|
-
clearTimeout(timeout2);
|
|
606730
|
-
resolve76();
|
|
606731
|
-
});
|
|
606732
|
-
this.liveTranscriber.on("error", (err) => {
|
|
606733
|
-
clearTimeout(timeout2);
|
|
606734
|
-
reject(err);
|
|
606735
|
-
});
|
|
606736
|
-
});
|
|
606737
|
-
if (caughtUncaught) {
|
|
606738
|
-
throw caughtUncaught;
|
|
606739
|
-
}
|
|
606740
|
-
} catch (err) {
|
|
606741
|
-
try {
|
|
606742
|
-
this.liveTranscriber?.stop();
|
|
606743
|
-
} catch {
|
|
606744
|
-
}
|
|
606745
|
-
this.liveTranscriber = null;
|
|
606746
|
-
transcribeCliError = err instanceof Error ? err.message : String(err);
|
|
606747
|
-
} finally {
|
|
606748
|
-
process.removeListener("uncaughtException", uncaughtHandler);
|
|
606749
|
-
}
|
|
606750
|
-
}
|
|
606751
|
-
}
|
|
606752
|
-
}
|
|
606753
|
-
if (!this.liveTranscriber) {
|
|
606754
|
-
const scriptPath2 = await findLiveWhisperScript();
|
|
606755
|
-
if (!scriptPath2) {
|
|
606756
|
-
const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
|
|
606757
|
-
return `No transcription backend available.${hint} live-whisper.py not found.`;
|
|
606758
|
-
}
|
|
606759
|
-
try {
|
|
606760
|
-
const fallback = new WhisperFallbackTranscriber(
|
|
606761
|
-
this.config.model,
|
|
606762
|
-
scriptPath2
|
|
606763
|
-
);
|
|
606764
|
-
usedFallback = true;
|
|
606765
|
-
fallback.on("status", (msg) => {
|
|
606766
|
-
this.emit("info", msg);
|
|
606767
|
-
});
|
|
606768
|
-
await fallback.start();
|
|
606769
|
-
fallback.on("transcript", (evt) => {
|
|
606770
|
-
if (!evt.text.trim()) return;
|
|
606771
|
-
this.lastTranscriptTime = Date.now();
|
|
606772
|
-
this.pendingText = evt.text.trim();
|
|
606773
|
-
this.emit("transcript", this.pendingText, evt.isFinal);
|
|
606774
|
-
if (this.config.mode === "auto") this.resetSilenceTimer();
|
|
606775
|
-
});
|
|
606776
|
-
fallback.on("error", (err) => {
|
|
606777
|
-
this.emit("error", err);
|
|
606778
|
-
});
|
|
606779
|
-
this.liveTranscriber = fallback;
|
|
606780
|
-
} catch (err) {
|
|
606781
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
606782
|
-
const tcHint = transcribeCliError ? `
|
|
606783
|
-
transcribe-cli error: ${transcribeCliError}` : "";
|
|
606784
|
-
return `Failed to start live transcription: ${msg}${tcHint}`;
|
|
606785
|
-
}
|
|
606786
|
-
}
|
|
606787
|
-
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
606788
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606789
|
-
env: { ...process.env }
|
|
606790
|
-
});
|
|
606791
|
-
this.micProcess.stdout?.on("data", (chunk) => {
|
|
606792
|
-
if (this.active && !this.paused && this.liveTranscriber) {
|
|
606793
|
-
this.liveTranscriber.write(chunk);
|
|
606794
|
-
}
|
|
606795
|
-
});
|
|
606796
|
-
this.micProcess.stderr?.on("data", () => {
|
|
606797
|
-
});
|
|
606798
|
-
onChildError(this.micProcess, (err) => {
|
|
606799
|
-
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
606800
|
-
this.stop();
|
|
606801
|
-
});
|
|
606802
|
-
onChildClose(this.micProcess, () => {
|
|
606803
|
-
if (this.active) {
|
|
606804
|
-
this.stop();
|
|
606805
|
-
}
|
|
606806
|
-
});
|
|
606807
|
-
this.active = true;
|
|
606808
|
-
this.blinkState = true;
|
|
606809
|
-
this.blinkTimer = setInterval(() => {
|
|
606810
|
-
this.blinkState = !this.blinkState;
|
|
606811
|
-
this.emit("recording", this.blinkState);
|
|
606812
|
-
}, 500);
|
|
606813
|
-
this.lastTranscriptTime = Date.now();
|
|
606814
|
-
this.emit("started");
|
|
606815
|
-
this.emit("recording", true);
|
|
606816
|
-
const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
|
|
606817
|
-
const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
|
|
606818
|
-
return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
|
|
606819
|
-
}
|
|
606820
|
-
/**
|
|
606821
|
-
* Stop listening — cleanup mic, transcriber, timers.
|
|
606822
|
-
*/
|
|
606823
|
-
async stop() {
|
|
606824
|
-
if (!this.active) return "Not listening.";
|
|
606825
|
-
this.active = false;
|
|
606826
|
-
this.blinkState = false;
|
|
606827
|
-
if (this.blinkTimer) {
|
|
606828
|
-
clearInterval(this.blinkTimer);
|
|
606829
|
-
this.blinkTimer = null;
|
|
606830
|
-
}
|
|
606831
|
-
if (this.silenceTimer) {
|
|
606832
|
-
clearTimeout(this.silenceTimer);
|
|
606833
|
-
this.silenceTimer = null;
|
|
606834
|
-
}
|
|
606835
|
-
if (this.countdownInterval) {
|
|
606836
|
-
clearInterval(this.countdownInterval);
|
|
606837
|
-
this.countdownInterval = null;
|
|
606838
|
-
}
|
|
606839
|
-
if (this.micProcess) {
|
|
606840
|
-
try {
|
|
606841
|
-
this.micProcess.kill("SIGTERM");
|
|
606842
|
-
} catch {
|
|
606843
|
-
}
|
|
606844
|
-
this.micProcess = null;
|
|
606845
|
-
}
|
|
606846
|
-
if (this.liveTranscriber) {
|
|
606847
|
-
try {
|
|
606848
|
-
this.liveTranscriber.stop();
|
|
606849
|
-
} catch {
|
|
606850
|
-
}
|
|
606851
|
-
this.liveTranscriber = null;
|
|
606852
|
-
}
|
|
606853
|
-
this.emit("recording", false);
|
|
606854
|
-
this.emit("stopped");
|
|
606855
|
-
const result = this.pendingText;
|
|
606856
|
-
this.pendingText = "";
|
|
606857
|
-
return result || "Stopped listening.";
|
|
606858
|
-
}
|
|
606859
|
-
/**
|
|
606860
|
-
* Accept the current pending transcript (for confirm mode).
|
|
606861
|
-
* Returns the text to inject into the input line.
|
|
606862
|
-
*/
|
|
606863
|
-
acceptTranscript() {
|
|
606864
|
-
const text2 = this.pendingText;
|
|
606865
|
-
this.pendingText = "";
|
|
606866
|
-
return text2;
|
|
606867
|
-
}
|
|
606868
|
-
/**
|
|
606869
|
-
* Pause microphone capture (for voicechat during TTS output).
|
|
606870
|
-
* Keeps transcriber alive but stops feeding it audio.
|
|
606871
|
-
*/
|
|
606872
|
-
pause() {
|
|
606873
|
-
if (!this.active || this.paused) return;
|
|
606874
|
-
this.paused = true;
|
|
606875
|
-
if (this.blinkTimer) {
|
|
606876
|
-
clearInterval(this.blinkTimer);
|
|
606877
|
-
this.blinkTimer = null;
|
|
606878
|
-
}
|
|
606879
|
-
this.blinkState = false;
|
|
606880
|
-
if (this.micProcess) {
|
|
606881
|
-
try {
|
|
606882
|
-
this.micProcess.kill("SIGTERM");
|
|
606883
|
-
} catch {
|
|
606884
|
-
}
|
|
606885
|
-
this.micProcess = null;
|
|
606886
|
-
}
|
|
606887
|
-
this.emit("paused");
|
|
606888
|
-
this.emit("recording", false);
|
|
606889
|
-
}
|
|
606890
|
-
/**
|
|
606891
|
-
* Resume microphone capture after pause.
|
|
606892
|
-
*/
|
|
606893
|
-
async resume() {
|
|
606894
|
-
if (!this.active || !this.paused) return "Not paused.";
|
|
606895
|
-
this.paused = false;
|
|
606896
|
-
const micCmd = await findMicCaptureCommand();
|
|
606897
|
-
if (!micCmd) {
|
|
606898
|
-
return "No microphone capture tool found.";
|
|
606899
|
-
}
|
|
606900
|
-
this.micProcess = spawn29(micCmd.cmd, micCmd.args, {
|
|
606901
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
606902
|
-
env: { ...process.env }
|
|
606903
|
-
});
|
|
606904
|
-
this.micProcess.stdout?.on("data", (chunk) => {
|
|
606905
|
-
if (this.active && !this.paused && this.liveTranscriber) {
|
|
606906
|
-
this.liveTranscriber.write(chunk);
|
|
606907
|
-
}
|
|
606908
|
-
});
|
|
606909
|
-
this.micProcess.stderr?.on("data", () => {
|
|
606910
|
-
});
|
|
606911
|
-
onChildError(this.micProcess, (err) => {
|
|
606912
|
-
this.emit("error", new Error(`Mic capture failed: ${err.message}`));
|
|
606913
|
-
this.stop();
|
|
606914
|
-
});
|
|
606915
|
-
onChildClose(this.micProcess, () => {
|
|
606916
|
-
if (this.active && !this.paused) {
|
|
606917
|
-
this.stop();
|
|
606918
|
-
}
|
|
606919
|
-
});
|
|
606920
|
-
this.blinkState = true;
|
|
606921
|
-
this.blinkTimer = setInterval(() => {
|
|
606922
|
-
this.blinkState = !this.blinkState;
|
|
606923
|
-
this.emit("recording", this.blinkState);
|
|
606924
|
-
}, 500);
|
|
606925
|
-
this.emit("resumed");
|
|
606926
|
-
this.emit("recording", true);
|
|
606927
|
-
return "Resumed listening.";
|
|
606928
|
-
}
|
|
606929
|
-
/**
|
|
606930
|
-
* Create a standalone transcriber for call sessions.
|
|
606931
|
-
* No microphone capture — just an ASR engine you feed PCM chunks to.
|
|
606932
|
-
* Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
|
|
606933
|
-
* Caller is responsible for calling .stop() when done.
|
|
606934
|
-
*/
|
|
606935
|
-
async createCallTranscriber() {
|
|
606936
|
-
let tc = await this.loadTranscribeCli();
|
|
606937
|
-
if (!tc && _bgInstallPromise) {
|
|
606938
|
-
await _bgInstallPromise;
|
|
606939
|
-
this.transcribeCliAvailable = null;
|
|
606940
|
-
tc = await this.loadTranscribeCli();
|
|
606941
|
-
}
|
|
606942
|
-
if (!tc) {
|
|
606943
|
-
try {
|
|
606944
|
-
await ensureManagedTranscribeCliNode();
|
|
606945
|
-
this.transcribeCliAvailable = null;
|
|
606946
|
-
tc = await this.loadTranscribeCli();
|
|
606947
|
-
} catch {
|
|
606948
|
-
}
|
|
606949
|
-
}
|
|
606950
|
-
if (tc?.TranscribeLive && await ensureVenvForTranscribeCli()) {
|
|
606951
|
-
try {
|
|
606952
|
-
const transcriber = new tc.TranscribeLive({
|
|
606953
|
-
model: this.config.model,
|
|
606954
|
-
sampleRate: 16e3,
|
|
606955
|
-
channels: 1,
|
|
606956
|
-
sampleWidth: 2,
|
|
606957
|
-
chunkDuration: 3
|
|
606958
|
-
});
|
|
606959
|
-
await new Promise((resolve76, reject) => {
|
|
606960
|
-
const timeout2 = setTimeout(
|
|
606961
|
-
() => reject(new Error("Model load timeout (60s)")),
|
|
606962
|
-
6e4
|
|
606963
|
-
);
|
|
606964
|
-
transcriber.on("ready", () => {
|
|
606965
|
-
clearTimeout(timeout2);
|
|
606966
|
-
resolve76();
|
|
606967
|
-
});
|
|
606968
|
-
transcriber.on("error", (err) => {
|
|
606969
|
-
clearTimeout(timeout2);
|
|
606970
|
-
reject(err);
|
|
606971
|
-
});
|
|
606972
|
-
});
|
|
606973
|
-
return transcriber;
|
|
606974
|
-
} catch {
|
|
606975
|
-
}
|
|
606976
|
-
}
|
|
606977
|
-
const scriptPath2 = await findLiveWhisperScript();
|
|
606978
|
-
if (!scriptPath2) return null;
|
|
606979
|
-
try {
|
|
606980
|
-
const fallback = new WhisperFallbackTranscriber(
|
|
606981
|
-
this.config.model,
|
|
606982
|
-
scriptPath2
|
|
606983
|
-
);
|
|
606984
|
-
await fallback.start();
|
|
606985
|
-
return fallback;
|
|
606986
|
-
} catch {
|
|
606987
|
-
return null;
|
|
606988
|
-
}
|
|
606989
|
-
}
|
|
606990
|
-
/**
|
|
606991
|
-
* Transcribe a file (audio or video) using transcribe-cli.
|
|
606992
|
-
* Returns the transcription result.
|
|
606993
|
-
*/
|
|
606994
|
-
async transcribeFile(filePath, outputDir2) {
|
|
606995
|
-
let tc = await this.loadTranscribeCli();
|
|
606996
|
-
if (!tc && _bgInstallPromise) {
|
|
606997
|
-
await _bgInstallPromise;
|
|
606998
|
-
this.transcribeCliAvailable = null;
|
|
606999
|
-
tc = await this.loadTranscribeCli();
|
|
607000
|
-
}
|
|
607001
|
-
if (!tc) {
|
|
607002
|
-
try {
|
|
607003
|
-
await ensureManagedTranscribeCliNode();
|
|
607004
|
-
this.transcribeCliAvailable = null;
|
|
607005
|
-
tc = await this.loadTranscribeCli();
|
|
607006
|
-
} catch {
|
|
607007
|
-
}
|
|
607008
|
-
}
|
|
607009
|
-
await ensureVenvForTranscribeCli();
|
|
607010
|
-
let lastErr = null;
|
|
607011
|
-
if (tc) {
|
|
607012
|
-
try {
|
|
607013
|
-
const result = await tc.transcribe(filePath, {
|
|
607014
|
-
model: this.config.model,
|
|
607015
|
-
format: "json",
|
|
607016
|
-
diarize: false,
|
|
607017
|
-
wordTimestamps: false
|
|
607018
|
-
});
|
|
607019
|
-
if (outputDir2) {
|
|
607020
|
-
const { basename: basename43 } = await import("node:path");
|
|
607021
|
-
const transcriptDir = join127(outputDir2, ".omnius", "transcripts");
|
|
607022
|
-
mkdirSync69(transcriptDir, { recursive: true });
|
|
607023
|
-
const outFile = join127(transcriptDir, `${basename43(filePath)}.txt`);
|
|
607024
|
-
writeFileSync58(outFile, result.text, "utf-8");
|
|
607025
|
-
}
|
|
607026
|
-
return {
|
|
607027
|
-
text: result.text,
|
|
607028
|
-
duration: result.duration,
|
|
607029
|
-
speakers: result.speakers,
|
|
607030
|
-
segments: result.segments
|
|
607031
|
-
};
|
|
607032
|
-
} catch (err) {
|
|
607033
|
-
lastErr = err;
|
|
607034
|
-
}
|
|
607035
|
-
}
|
|
607036
|
-
const fb = await transcribeFileViaWhisper(filePath, this.config.model);
|
|
607037
|
-
if (fb) {
|
|
607038
|
-
if (outputDir2) {
|
|
607039
|
-
const { basename: basename43 } = await import("node:path");
|
|
607040
|
-
const transcriptDir = join127(outputDir2, ".omnius", "transcripts");
|
|
607041
|
-
mkdirSync69(transcriptDir, { recursive: true });
|
|
607042
|
-
const outFile = join127(transcriptDir, `${basename43(filePath)}.txt`);
|
|
607043
|
-
writeFileSync58(outFile, fb.text, "utf-8");
|
|
607044
|
-
}
|
|
607045
|
-
return fb;
|
|
607046
|
-
}
|
|
607047
|
-
if (lastErr) {
|
|
607048
|
-
this.emit(
|
|
607049
|
-
"error",
|
|
607050
|
-
lastErr instanceof Error ? lastErr : new Error(String(lastErr))
|
|
607051
|
-
);
|
|
607052
|
-
}
|
|
607053
|
-
return null;
|
|
607054
|
-
}
|
|
607055
|
-
// -------------------------------------------------------------------------
|
|
607056
|
-
// Auto-mode silence detection
|
|
607057
|
-
// -------------------------------------------------------------------------
|
|
607058
|
-
resetSilenceTimer() {
|
|
607059
|
-
if (this.silenceTimer) clearTimeout(this.silenceTimer);
|
|
607060
|
-
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
|
607061
|
-
const timeoutMs = this.config.silenceTimeoutMs;
|
|
607062
|
-
const startTime = Date.now();
|
|
607063
|
-
this.countdownInterval = setInterval(() => {
|
|
607064
|
-
const elapsed = Date.now() - startTime;
|
|
607065
|
-
const remaining = Math.max(0, Math.ceil((timeoutMs - elapsed) / 1e3));
|
|
607066
|
-
this.emit("countdown", remaining);
|
|
607067
|
-
if (remaining <= 0 && this.countdownInterval) {
|
|
607068
|
-
clearInterval(this.countdownInterval);
|
|
607069
|
-
this.countdownInterval = null;
|
|
607070
|
-
}
|
|
607071
|
-
}, 1e3);
|
|
607072
|
-
this.silenceTimer = setTimeout(() => {
|
|
607073
|
-
if (this.active && this.pendingText) {
|
|
607074
|
-
this.emit("countdown", 0);
|
|
607075
|
-
this.emit("transcript", this.pendingText, true);
|
|
607076
|
-
}
|
|
607077
|
-
}, timeoutMs);
|
|
607078
|
-
}
|
|
607079
|
-
};
|
|
607080
|
-
_bgInstallPromise = null;
|
|
607081
|
-
_managedTranscribeCliInstallPromise = null;
|
|
607082
|
-
_engine = null;
|
|
607083
|
-
}
|
|
607084
|
-
});
|
|
607085
|
-
|
|
607086
607308
|
// node_modules/.pnpm/ws@8.20.1/node_modules/ws/lib/constants.js
|
|
607087
607309
|
var require_constants11 = __commonJS({
|
|
607088
607310
|
"node_modules/.pnpm/ws@8.20.1/node_modules/ws/lib/constants.js"(exports, module) {
|
|
@@ -625732,6 +625954,10 @@ var init_status_bar = __esm({
|
|
|
625732
625954
|
inputStateProvider = null;
|
|
625733
625955
|
/** Whether microphone is recording (blinking indicator) */
|
|
625734
625956
|
_recording = false;
|
|
625957
|
+
/** Whether speech is currently detected on the mic (from ListenEngine metering) */
|
|
625958
|
+
_micSpeech = false;
|
|
625959
|
+
/** Current mic input level in dBFS (rounded), null when unknown */
|
|
625960
|
+
_micLevelDb = null;
|
|
625735
625961
|
/** Countdown seconds for auto-mode silence timeout (0 = not counting down) */
|
|
625736
625962
|
_countdown = 0;
|
|
625737
625963
|
_recBlink = true;
|
|
@@ -626545,6 +626771,18 @@ var init_status_bar = __esm({
|
|
|
626545
626771
|
this._countdown = seconds;
|
|
626546
626772
|
if (this.active) this.renderFooterPreserveCursor();
|
|
626547
626773
|
}
|
|
626774
|
+
/**
|
|
626775
|
+
* Update live mic activity: whether speech is currently detected and the
|
|
626776
|
+
* current input level (dBFS). Drives the REC indicator so it reflects real
|
|
626777
|
+
* whisper-bound speech instead of only a blink timer.
|
|
626778
|
+
*/
|
|
626779
|
+
setMicActivity(speech, levelDb) {
|
|
626780
|
+
const level = typeof levelDb === "number" && Number.isFinite(levelDb) ? Math.round(levelDb) : null;
|
|
626781
|
+
if (speech === this._micSpeech && level === this._micLevelDb) return;
|
|
626782
|
+
this._micSpeech = speech;
|
|
626783
|
+
this._micLevelDb = level;
|
|
626784
|
+
if (this.active && this._recording) this.renderFooterPreserveCursor();
|
|
626785
|
+
}
|
|
626548
626786
|
/** Start or stop the braille-dot processing animation on the buffer line. */
|
|
626549
626787
|
setProcessing(active) {
|
|
626550
626788
|
if (active === this._processing) return;
|
|
@@ -629222,10 +629460,19 @@ ${CONTENT_BG_SEQ}`);
|
|
|
629222
629460
|
});
|
|
629223
629461
|
}
|
|
629224
629462
|
if (this._recording) {
|
|
629225
|
-
const dot = this._recBlink ? pastel2(210, "●") : " ";
|
|
629226
629463
|
const countdown = this._countdown > 0 ? c3.dim(` ${_StatusBar.digitBar(this._countdown)}s`) : "";
|
|
629227
|
-
const
|
|
629228
|
-
|
|
629464
|
+
const countdownW = this._countdown > 0 ? 1 + `${this._countdown}s`.length : 0;
|
|
629465
|
+
let recStr;
|
|
629466
|
+
let recW;
|
|
629467
|
+
if (this._micSpeech) {
|
|
629468
|
+
recStr = pastel2(210, "● REC") + countdown;
|
|
629469
|
+
recW = 5 + countdownW;
|
|
629470
|
+
} else {
|
|
629471
|
+
const dot = this._recBlink ? pastel2(210, "●") : c3.dim("○");
|
|
629472
|
+
const levelStr = this._micLevelDb !== null ? ` ${this._micLevelDb}dB` : "";
|
|
629473
|
+
recStr = dot + c3.dim(` mic${levelStr}`) + countdown;
|
|
629474
|
+
recW = 1 + 4 + levelStr.length + countdownW;
|
|
629475
|
+
}
|
|
629229
629476
|
sections.push({
|
|
629230
629477
|
expanded: recStr,
|
|
629231
629478
|
compact: recStr,
|
|
@@ -696176,6 +696423,19 @@ function repeatingCharPenalty(s2) {
|
|
|
696176
696423
|
if (cur > maxRun) maxRun = cur;
|
|
696177
696424
|
return Math.min(1, Math.max(0, (maxRun - 3) / 10));
|
|
696178
696425
|
}
|
|
696426
|
+
function gibberishTokenPenalty(s2) {
|
|
696427
|
+
const tokens = s2.trim().split(/\s+/).filter(Boolean);
|
|
696428
|
+
if (tokens.length < 2) return 0;
|
|
696429
|
+
let penalty = 0;
|
|
696430
|
+
const unique2 = new Set(tokens.map((t2) => t2.toLowerCase()));
|
|
696431
|
+
const uniqueRatio = unique2.size / tokens.length;
|
|
696432
|
+
if (tokens.length >= 4 && uniqueRatio <= 0.5) {
|
|
696433
|
+
penalty += Math.min(1, (0.5 - uniqueRatio) * 2 + 0.5);
|
|
696434
|
+
}
|
|
696435
|
+
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));
|
|
696436
|
+
if (mixed.length / tokens.length >= 0.5) penalty += 0.8;
|
|
696437
|
+
return Math.min(1, penalty);
|
|
696438
|
+
}
|
|
696179
696439
|
function computeSignalFromText(text2, confidence2) {
|
|
696180
696440
|
const t2 = text2.trim();
|
|
696181
696441
|
if (!t2) return 0;
|
|
@@ -696190,6 +696450,7 @@ function computeSignalFromText(text2, confidence2) {
|
|
|
696190
696450
|
else if (wc >= 1 && alpha >= 0.3 && len >= 4) score = 0.35;
|
|
696191
696451
|
else score = 0.15;
|
|
696192
696452
|
score -= repeatingCharPenalty(t2) * 0.4;
|
|
696453
|
+
score -= gibberishTokenPenalty(t2) * 0.8;
|
|
696193
696454
|
if (typeof confidence2 === "number" && !Number.isNaN(confidence2)) {
|
|
696194
696455
|
score = 0.7 * score + 0.3 * clamp0116(confidence2);
|
|
696195
696456
|
}
|
|
@@ -696198,6 +696459,9 @@ function computeSignalFromText(text2, confidence2) {
|
|
|
696198
696459
|
function truncateForLog(s2, n2) {
|
|
696199
696460
|
return s2.length <= n2 ? s2 : s2.slice(0, n2 - 1) + "…";
|
|
696200
696461
|
}
|
|
696462
|
+
function sanitizeToolName2(name10) {
|
|
696463
|
+
return name10.trim().replace(/[({\[].*$/, "").replace(/[^A-Za-z0-9_-]/g, "");
|
|
696464
|
+
}
|
|
696201
696465
|
function extractToolJson(text2) {
|
|
696202
696466
|
const lines = text2.split(/\r?\n/);
|
|
696203
696467
|
for (const line of lines) {
|
|
@@ -696206,7 +696470,8 @@ function extractToolJson(text2) {
|
|
|
696206
696470
|
try {
|
|
696207
696471
|
const obj = JSON.parse(t2);
|
|
696208
696472
|
if (typeof obj.tool === "string") {
|
|
696209
|
-
const name10 = obj.tool;
|
|
696473
|
+
const name10 = sanitizeToolName2(obj.tool);
|
|
696474
|
+
if (!name10) continue;
|
|
696210
696475
|
const args = obj.args && typeof obj.args === "object" ? obj.args : {};
|
|
696211
696476
|
return { name: name10, args };
|
|
696212
696477
|
}
|
|
@@ -696224,18 +696489,38 @@ function extractToolJsonLoose(text2) {
|
|
|
696224
696489
|
try {
|
|
696225
696490
|
const obj = JSON.parse(match[0]);
|
|
696226
696491
|
if (typeof obj.tool === "string") {
|
|
696227
|
-
const
|
|
696228
|
-
|
|
696492
|
+
const name10 = sanitizeToolName2(obj.tool);
|
|
696493
|
+
if (name10) {
|
|
696494
|
+
const args = obj.args && typeof obj.args === "object" ? obj.args : {};
|
|
696495
|
+
return { name: name10, args };
|
|
696496
|
+
}
|
|
696229
696497
|
}
|
|
696230
696498
|
} catch {
|
|
696231
696499
|
}
|
|
696232
696500
|
}
|
|
696501
|
+
const nameMatch = stripped.match(/"tool"\s*:\s*"?\s*([A-Za-z0-9_-]+)/);
|
|
696502
|
+
if (nameMatch?.[1]) {
|
|
696503
|
+
const name10 = sanitizeToolName2(nameMatch[1]);
|
|
696504
|
+
if (name10) {
|
|
696505
|
+
let args = {};
|
|
696506
|
+
const argsMatch = stripped.match(/"args"\s*:\s*(\{[\s\S]*?\})/);
|
|
696507
|
+
if (argsMatch?.[1]) {
|
|
696508
|
+
try {
|
|
696509
|
+
const parsed = JSON.parse(argsMatch[1]);
|
|
696510
|
+
if (parsed && typeof parsed === "object") args = parsed;
|
|
696511
|
+
} catch {
|
|
696512
|
+
}
|
|
696513
|
+
}
|
|
696514
|
+
return { name: name10, args };
|
|
696515
|
+
}
|
|
696516
|
+
}
|
|
696233
696517
|
return null;
|
|
696234
696518
|
}
|
|
696235
696519
|
function stripToolJsonLines(text2) {
|
|
696236
696520
|
const lines = text2.split(/\r?\n/);
|
|
696237
696521
|
const kept = lines.filter((l2) => {
|
|
696238
696522
|
const t2 = l2.trim();
|
|
696523
|
+
if (/^\{\s*"?tool"?\s*[:=]/.test(t2)) return false;
|
|
696239
696524
|
if (!t2.startsWith("{") || !t2.endsWith("}")) return true;
|
|
696240
696525
|
try {
|
|
696241
696526
|
const obj = JSON.parse(t2);
|
|
@@ -696374,6 +696659,7 @@ var init_voicechat = __esm({
|
|
|
696374
696659
|
SYSTEM_PROMPT2 = `You are a voice assistant having a live spoken conversation. Keep responses extremely brief — 1-2 sentences max. You're speaking aloud, not writing. Be conversational, direct, and helpful. Don't use markdown or formatting — just natural speech.
|
|
696375
696660
|
|
|
696376
696661
|
Rules:
|
|
696662
|
+
- Live perception: each turn you may receive a read-only "Context snapshot" system message containing what the camera currently sees (objects, people, classifications, recent visual events) and what is currently heard (sound scene, recent sounds, transcripts). Treat it as your own live sight and hearing. When the user asks what you see, who is there, or what you hear, answer directly from that snapshot. If the snapshot is missing or stale, say you can't see/hear clearly right now — do not invent observations.
|
|
696377
696663
|
- Never invent environment facts (cwd, OS, specs, repo state). If you need a precise fact from the main agent, request a tool by outputting on a single line EXACTLY one JSON object: {"tool": string, "args": object} and nothing else. Then wait for the tool result before answering.
|
|
696378
696664
|
- You may also request to relay a user task to the main agent by emitting {"tool":"voice_to_main","args":{"message":"...","start":true}}.
|
|
696379
696665
|
- Prefer tools for factual queries; otherwise, answer directly with a short reply.
|
|
@@ -696475,12 +696761,12 @@ Rules:
|
|
|
696475
696761
|
this.active = true;
|
|
696476
696762
|
this.context = [{ role: "system", content: SYSTEM_PROMPT2 }];
|
|
696477
696763
|
if (this.toolRelay) {
|
|
696478
|
-
this.toolCatalogNote = `Available tools
|
|
696479
|
-
|
|
696480
|
-
|
|
696481
|
-
|
|
696482
|
-
|
|
696483
|
-
|
|
696764
|
+
this.toolCatalogNote = `Available tools. To call one, emit exactly one line of valid JSON and nothing else:
|
|
696765
|
+
{"tool":"voice_env","args":{}} → environment facts (cwd, os, cpu, mem)
|
|
696766
|
+
{"tool":"voice_status","args":{}} → main-agent status
|
|
696767
|
+
{"tool":"voice_list_files","args":{"dir":"."}} → list directory (bounded)
|
|
696768
|
+
{"tool":"voice_read_file","args":{"path":"<file>","max":2048}} → read file snippet
|
|
696769
|
+
{"tool":"voice_to_main","args":{"message":"<task>","start":true}} → relay/start task`;
|
|
696484
696770
|
this.context.push({ role: "system", content: this.toolCatalogNote });
|
|
696485
696771
|
}
|
|
696486
696772
|
this.turnCount = 0;
|
|
@@ -696623,7 +696909,7 @@ Rules:
|
|
|
696623
696909
|
this.setState("LISTENING");
|
|
696624
696910
|
return;
|
|
696625
696911
|
}
|
|
696626
|
-
const score = this.lastSignalScore ?? computeSignalFromText(text2);
|
|
696912
|
+
const score = Math.min(this.lastSignalScore ?? 1, computeSignalFromText(text2));
|
|
696627
696913
|
if (score < MIN_SIGNAL_SCORE || NOISE_ONLY_RE.test(text2)) {
|
|
696628
696914
|
if (this.debugSnr) this.onStatus(`Ignoring low-signal utterance (SNR:${score.toFixed(2)}): ${truncateForLog(text2, 48)}`);
|
|
696629
696915
|
this.emit("snrFiltered", { score, text: text2 });
|
|
@@ -735615,6 +735901,9 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
735615
735901
|
engine.on("recording", (active) => {
|
|
735616
735902
|
statusBar.setRecording(active);
|
|
735617
735903
|
});
|
|
735904
|
+
engine.on("level", (evt) => {
|
|
735905
|
+
statusBar.setMicActivity(evt.speechActive, evt.levelDb);
|
|
735906
|
+
});
|
|
735618
735907
|
engine.on("countdown", (seconds) => {
|
|
735619
735908
|
statusBar.setCountdown(seconds);
|
|
735620
735909
|
});
|
|
@@ -735919,6 +736208,12 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
735919
736208
|
const { VoiceChatSession: VoiceChatSession2 } = await Promise.resolve().then(() => (init_voicechat(), voicechat_exports));
|
|
735920
736209
|
const { ListenEngine: ListenEngine2 } = await Promise.resolve().then(() => (init_listen(), listen_exports));
|
|
735921
736210
|
const listenEng = new ListenEngine2();
|
|
736211
|
+
listenEng.on("recording", (active) => {
|
|
736212
|
+
statusBar.setRecording(active);
|
|
736213
|
+
});
|
|
736214
|
+
listenEng.on("level", (evt) => {
|
|
736215
|
+
statusBar.setMicActivity(evt.speechActive, evt.levelDb);
|
|
736216
|
+
});
|
|
735922
736217
|
const summaryRunner = {
|
|
735923
736218
|
injectUserMessage(content) {
|
|
735924
736219
|
if (activeTask?.runner) {
|
|
@@ -736071,7 +736366,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
736071
736366
|
} else {
|
|
736072
736367
|
parts.push("active: no");
|
|
736073
736368
|
}
|
|
736074
|
-
const liveContext = formatLiveSensorContext(repoRoot);
|
|
736369
|
+
const liveContext = formatLiveSensorContext(repoRoot, { toolCapable: false, maxChars: 7e3 });
|
|
736075
736370
|
if (liveContext) parts.push(liveContext);
|
|
736076
736371
|
return parts.join("\n");
|
|
736077
736372
|
}
|