omnius 1.0.417 → 1.0.418

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