omnius 1.0.416 → 1.0.418

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