omnius 1.0.417 → 1.0.419

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