aura-code 0.6.3 → 0.10.1
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/README.md +47 -164
- package/dist/agent/affect.d.ts +13 -0
- package/dist/agent/affect.js +34 -0
- package/dist/agent/affect.js.map +1 -0
- package/dist/agent/compactor.d.ts +27 -4
- package/dist/agent/compactor.js +185 -54
- package/dist/agent/compactor.js.map +1 -1
- package/dist/agent/confess.d.ts +35 -0
- package/dist/agent/confess.js +296 -0
- package/dist/agent/confess.js.map +1 -0
- package/dist/agent/context.d.ts +0 -4
- package/dist/agent/context.js +0 -38
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/domain-expertise.d.ts +22 -0
- package/dist/agent/domain-expertise.js +110 -0
- package/dist/agent/domain-expertise.js.map +1 -0
- package/dist/agent/executive-queue.d.ts +26 -0
- package/dist/agent/executive-queue.js +50 -0
- package/dist/agent/executive-queue.js.map +1 -0
- package/dist/agent/generational-flush.d.ts +28 -0
- package/dist/agent/generational-flush.js +48 -0
- package/dist/agent/generational-flush.js.map +1 -0
- package/dist/agent/loop-profile.d.ts +50 -0
- package/dist/agent/loop-profile.js +88 -0
- package/dist/agent/loop-profile.js.map +1 -0
- package/dist/agent/loop.d.ts +14 -5
- package/dist/agent/loop.js +167 -39
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory-consolidate.d.ts +14 -0
- package/dist/agent/memory-consolidate.js +141 -0
- package/dist/agent/memory-consolidate.js.map +1 -0
- package/dist/agent/mixture.d.ts +33 -0
- package/dist/agent/mixture.js +138 -0
- package/dist/agent/mixture.js.map +1 -0
- package/dist/agent/spawner.js +2 -2
- package/dist/agent/spawner.js.map +1 -1
- package/dist/agent/system-prompt.d.ts +1 -1
- package/dist/agent/system-prompt.js +50 -22
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/unified-memory.d.ts +15 -0
- package/dist/agent/unified-memory.js +164 -0
- package/dist/agent/unified-memory.js.map +1 -0
- package/dist/checkpoints/engine.d.ts +44 -0
- package/dist/checkpoints/engine.js +324 -0
- package/dist/checkpoints/engine.js.map +1 -0
- package/dist/cli/command-palette.d.ts +26 -0
- package/dist/cli/command-palette.js +133 -0
- package/dist/cli/command-palette.js.map +1 -0
- package/dist/cli/context-health.d.ts +51 -0
- package/dist/cli/context-health.js +187 -0
- package/dist/cli/context-health.js.map +1 -0
- package/dist/cli/diamond.d.ts +67 -0
- package/dist/cli/diamond.js +222 -12
- package/dist/cli/diamond.js.map +1 -1
- package/dist/cli/dic.d.ts +2 -0
- package/dist/cli/dic.js +106 -0
- package/dist/cli/dic.js.map +1 -0
- package/dist/cli/diff-view.d.ts +18 -0
- package/dist/cli/diff-view.js +114 -0
- package/dist/cli/diff-view.js.map +1 -0
- package/dist/cli/display.d.ts +18 -4
- package/dist/cli/display.js +74 -151
- package/dist/cli/display.js.map +1 -1
- package/dist/cli/help-data.d.ts +7 -0
- package/dist/cli/help-data.js +80 -0
- package/dist/cli/help-data.js.map +1 -0
- package/dist/cli/index.js +1002 -852
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/markdown.d.ts +5 -0
- package/dist/cli/markdown.js +128 -0
- package/dist/cli/markdown.js.map +1 -0
- package/dist/cli/model-select.d.ts +39 -0
- package/dist/cli/model-select.js +65 -0
- package/dist/cli/model-select.js.map +1 -0
- package/dist/cli/tui.d.ts +34 -0
- package/dist/cli/tui.js +1176 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -17
- package/dist/config/defaults.js +25 -46
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/project-config.d.ts +2 -6
- package/dist/config/project-config.js +2 -2
- package/dist/config/project-config.js.map +1 -1
- package/dist/doctor/checks.d.ts +18 -0
- package/dist/doctor/checks.js +524 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/doctor/index.d.ts +4 -0
- package/dist/doctor/index.js +229 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/repair.d.ts +19 -0
- package/dist/doctor/repair.js +126 -0
- package/dist/doctor/repair.js.map +1 -0
- package/dist/doctor/types.d.ts +38 -0
- package/dist/doctor/types.js +10 -0
- package/dist/doctor/types.js.map +1 -0
- package/dist/dream/dream.d.ts +79 -59
- package/dist/dream/dream.js +324 -144
- package/dist/dream/dream.js.map +1 -1
- package/dist/dream/episode.d.ts +17 -0
- package/dist/dream/episode.js +116 -0
- package/dist/dream/episode.js.map +1 -0
- package/dist/kanban/engine.d.ts +26 -0
- package/dist/kanban/engine.js +208 -0
- package/dist/kanban/engine.js.map +1 -0
- package/dist/kanban/types.d.ts +19 -0
- package/dist/kanban/types.js +9 -1
- package/dist/kanban/types.js.map +1 -1
- package/dist/machina/render-html.js +4 -2
- package/dist/machina/render-html.js.map +1 -1
- package/dist/machina/render-terminal.js +11 -4
- package/dist/machina/render-terminal.js.map +1 -1
- package/dist/machina/repair.d.ts +23 -0
- package/dist/machina/repair.js +98 -0
- package/dist/machina/repair.js.map +1 -0
- package/dist/machina/spec.js +12 -12
- package/dist/machina/spec.js.map +1 -1
- package/dist/machina/verify.d.ts +13 -1
- package/dist/machina/verify.js +12 -2
- package/dist/machina/verify.js.map +1 -1
- package/dist/mining/extract.d.ts +34 -0
- package/dist/mining/extract.js +229 -0
- package/dist/mining/extract.js.map +1 -0
- package/dist/mining/refine.d.ts +28 -0
- package/dist/mining/refine.js +256 -0
- package/dist/mining/refine.js.map +1 -0
- package/dist/perception/extractor.js +5 -3
- package/dist/perception/extractor.js.map +1 -1
- package/dist/perception/graph-store.d.ts +0 -11
- package/dist/perception/graph-store.js +0 -33
- package/dist/perception/graph-store.js.map +1 -1
- package/dist/perception/index.d.ts +1 -1
- package/dist/perception/index.js +1 -2
- package/dist/perception/index.js.map +1 -1
- package/dist/plugins/commands.d.ts +38 -0
- package/dist/plugins/commands.js +134 -0
- package/dist/plugins/commands.js.map +1 -0
- package/dist/plugins/frontmatter.d.ts +14 -0
- package/dist/plugins/frontmatter.js +78 -0
- package/dist/plugins/frontmatter.js.map +1 -0
- package/dist/plugins/hooks.d.ts +12 -0
- package/dist/plugins/hooks.js +148 -0
- package/dist/plugins/hooks.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.js +255 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/market.d.ts +23 -0
- package/dist/plugins/market.js +281 -0
- package/dist/plugins/market.js.map +1 -0
- package/dist/plugins/types.d.ts +90 -0
- package/dist/plugins/types.js +20 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/providers/anthropic-oauth-draft.d.ts +119 -0
- package/dist/providers/anthropic-oauth-draft.js +414 -0
- package/dist/providers/anthropic-oauth-draft.js.map +1 -0
- package/dist/providers/anthropic.d.ts +4 -1
- package/dist/providers/anthropic.js +35 -7
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +43 -19
- package/dist/providers/factory.js +183 -129
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/google.js +9 -22
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/live-models.d.ts +23 -0
- package/dist/providers/live-models.js +145 -0
- package/dist/providers/live-models.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +1 -12
- package/dist/providers/openai-compatible.js +71 -108
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/resilient-factory.js +1 -1
- package/dist/providers/resilient-factory.js.map +1 -1
- package/dist/repl/queue.d.ts +22 -0
- package/dist/repl/queue.js +165 -0
- package/dist/repl/queue.js.map +1 -0
- package/dist/repl/side-channel.d.ts +23 -0
- package/dist/repl/side-channel.js +63 -0
- package/dist/repl/side-channel.js.map +1 -0
- package/dist/research/council.d.ts +2 -0
- package/dist/research/council.js +29 -4
- package/dist/research/council.js.map +1 -1
- package/dist/ruby/alternator.d.ts +11 -30
- package/dist/ruby/alternator.js +59 -38
- package/dist/ruby/alternator.js.map +1 -1
- package/dist/ruby/episode-capture.d.ts +0 -10
- package/dist/ruby/episode-capture.js +0 -33
- package/dist/ruby/episode-capture.js.map +1 -1
- package/dist/ruby/types.js +1 -1
- package/dist/ruby/types.js.map +1 -1
- package/dist/safety/path-jail.d.ts +17 -0
- package/dist/safety/path-jail.js +100 -0
- package/dist/safety/path-jail.js.map +1 -0
- package/dist/safety/permissions.d.ts +12 -21
- package/dist/safety/permissions.js +81 -216
- package/dist/safety/permissions.js.map +1 -1
- package/dist/safety/ssrf.d.ts +29 -0
- package/dist/safety/ssrf.js +185 -0
- package/dist/safety/ssrf.js.map +1 -0
- package/dist/server/index.js +64 -10
- package/dist/server/index.js.map +1 -1
- package/dist/setup/first-run.d.ts +0 -24
- package/dist/setup/first-run.js +10 -344
- package/dist/setup/first-run.js.map +1 -1
- package/dist/setup/key-store.d.ts +12 -0
- package/dist/setup/key-store.js +108 -0
- package/dist/setup/key-store.js.map +1 -0
- package/dist/setup/provider-registry.js +31 -18
- package/dist/setup/provider-registry.js.map +1 -1
- package/dist/setup/provider-test.d.ts +7 -0
- package/dist/setup/provider-test.js +73 -10
- package/dist/setup/provider-test.js.map +1 -1
- package/dist/setup/provider-wizard.d.ts +4 -1
- package/dist/setup/provider-wizard.js +92 -26
- package/dist/setup/provider-wizard.js.map +1 -1
- package/dist/setup/xiaomi.d.ts +1 -1
- package/dist/setup/xiaomi.js +3 -3
- package/dist/setup/xiaomi.js.map +1 -1
- package/dist/tools/browser.js +14 -10
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/clipboard.js +7 -1
- package/dist/tools/clipboard.js.map +1 -1
- package/dist/tools/dictate.d.ts +60 -0
- package/dist/tools/dictate.js +983 -0
- package/dist/tools/dictate.js.map +1 -0
- package/dist/tools/dictate_patched.js +1057 -0
- package/dist/tools/edit-file.js +10 -2
- package/dist/tools/edit-file.js.map +1 -1
- package/dist/tools/http-request.js +14 -4
- package/dist/tools/http-request.js.map +1 -1
- package/dist/tools/image-read.js +34 -2
- package/dist/tools/image-read.js.map +1 -1
- package/dist/tools/index.d.ts +16 -1
- package/dist/tools/index.js +59 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp.js +12 -0
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/read-file.js +10 -14
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/telegram-audio-policy.d.ts +30 -0
- package/dist/tools/telegram-audio-policy.js +49 -0
- package/dist/tools/telegram-audio-policy.js.map +1 -0
- package/dist/tools/telegram-bot.js +1136 -537
- package/dist/tools/telegram-bot.js.map +1 -1
- package/dist/tools/telegram-voice.d.ts +18 -5
- package/dist/tools/telegram-voice.js +54 -12
- package/dist/tools/telegram-voice.js.map +1 -1
- package/dist/tools/telegram.d.ts +3 -3
- package/dist/tools/telegram.js +5 -84
- package/dist/tools/telegram.js.map +1 -1
- package/dist/tools/tools.d.ts +2 -14
- package/dist/tools/tools.js +79 -144
- package/dist/tools/tools.js.map +1 -1
- package/dist/tools/web-fetch.js +39 -7
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-search.d.ts +0 -20
- package/dist/tools/web-search.js +40 -137
- package/dist/tools/web-search.js.map +1 -1
- package/dist/util/errors.js +1 -15
- package/dist/util/errors.js.map +1 -1
- package/dist/util/json-repair.d.ts +11 -0
- package/dist/util/json-repair.js +31 -0
- package/dist/util/json-repair.js.map +1 -0
- package/dist/util/rate-limiter.js +2 -8
- package/dist/util/rate-limiter.js.map +1 -1
- package/dist/viz/index.js +793 -792
- package/dist/viz/index.js.map +1 -1
- package/package.json +12 -32
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.injectText = injectText;
|
|
40
|
+
exports.listDevices = listDevices;
|
|
41
|
+
exports.getDefaultDevice = getDefaultDevice;
|
|
42
|
+
exports.dictate = dictate;
|
|
43
|
+
exports.toggleDictation = toggleDictation;
|
|
44
|
+
exports.speakText = speakText;
|
|
45
|
+
exports.synthesizeSpeech = synthesizeSpeech;
|
|
46
|
+
exports.stripWavPrefix = stripWavPrefix;
|
|
47
|
+
exports.transcribeFile = transcribeFile;
|
|
48
|
+
exports.listVoices = listVoices;
|
|
49
|
+
exports.dictationLoop = dictationLoop;
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
55
|
+
const openai_1 = __importDefault(require("openai"));
|
|
56
|
+
const https = __importStar(require("https"));
|
|
57
|
+
const SAMPLE_RATE = 16000;
|
|
58
|
+
const MIMO_BASE = 'https://api.xiaomimimo.com/v1';
|
|
59
|
+
const MIMO_VOICES = {
|
|
60
|
+
mimo_default: { gender: 'auto', lang: 'auto' },
|
|
61
|
+
"\u51b0\u7cd6": { gender: 'female', lang: 'zh' },
|
|
62
|
+
"\u8309\u8389": { gender: 'female', lang: 'zh' },
|
|
63
|
+
"\u82cf\u6253": { gender: 'male', lang: 'zh' },
|
|
64
|
+
"\u767d\u6866": { gender: 'male', lang: 'zh' },
|
|
65
|
+
Mia: { gender: 'female', lang: 'en' },
|
|
66
|
+
Chloe: { gender: 'female', lang: 'en' },
|
|
67
|
+
Milo: { gender: 'male', lang: 'en' },
|
|
68
|
+
Dean: { gender: 'male', lang: 'en' },
|
|
69
|
+
};
|
|
70
|
+
function listProviders() {
|
|
71
|
+
const providers = [];
|
|
72
|
+
// Local Parakeet (NVIDIA) — highest priority when explicitly configured
|
|
73
|
+
if (process.env.PARAKEET_BASE_URL) {
|
|
74
|
+
const key = process.env.PARAKEET_API_KEY || 'sk-local';
|
|
75
|
+
providers.push({ key, baseURL: process.env.PARAKEET_BASE_URL, name: 'Parakeet' });
|
|
76
|
+
}
|
|
77
|
+
// GLM-ASR (Z.ai) — next-gen real-time ASR, OpenAI-compatible /audio/transcriptions.
|
|
78
|
+
// OPT-IN via DIC_USE_GLM_ASR=1: it currently needs a Z.ai audio balance
|
|
79
|
+
// package (err 1113) and the plain OpenAI SDK can hang against api.z.ai, so
|
|
80
|
+
// it's off the default path until the account is funded. When enabled it's
|
|
81
|
+
// tried first and falls through to MiMo on any failure.
|
|
82
|
+
if (process.env.ZHIPU_API_KEY && process.env.DIC_USE_GLM_ASR === '1') {
|
|
83
|
+
providers.push({
|
|
84
|
+
key: process.env.ZHIPU_API_KEY,
|
|
85
|
+
baseURL: process.env.ZHIPU_BASE_URL || 'https://api.z.ai/api/paas/v4',
|
|
86
|
+
name: 'GLM-ASR',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (process.env.XIAOMI_API_KEY) {
|
|
90
|
+
const key = process.env.XIAOMI_API_KEY;
|
|
91
|
+
const baseURL = key.startsWith('tp-')
|
|
92
|
+
? (process.env.XIAOMI_BASE_URL || 'https://token-plan-sgp.xiaomimimo.com/v1')
|
|
93
|
+
: MIMO_BASE;
|
|
94
|
+
providers.push({ key, baseURL, name: 'Xiaomi MiMo' });
|
|
95
|
+
}
|
|
96
|
+
if (process.env.OPENAI_API_KEY) {
|
|
97
|
+
providers.push({ key: process.env.OPENAI_API_KEY, baseURL: 'https://api.openai.com/v1', name: 'OpenAI' });
|
|
98
|
+
}
|
|
99
|
+
if (process.env.GROQ_API_KEY) {
|
|
100
|
+
providers.push({ key: process.env.GROQ_API_KEY, baseURL: 'https://api.groq.com/openai/v1', name: 'Groq' });
|
|
101
|
+
}
|
|
102
|
+
return providers;
|
|
103
|
+
}
|
|
104
|
+
function buildClient(api) {
|
|
105
|
+
const opts = {
|
|
106
|
+
apiKey: api.key,
|
|
107
|
+
baseURL: api.baseURL,
|
|
108
|
+
// Bounded so a hung provider eventually falls through, but generous enough
|
|
109
|
+
// for TTS (audio generation is slower than a chat/transcription call).
|
|
110
|
+
timeout: 60_000,
|
|
111
|
+
maxRetries: 0,
|
|
112
|
+
// api.z.ai silently drops requests that arrive without a User-Agent
|
|
113
|
+
// (see the zai-edge memory) — always send one.
|
|
114
|
+
defaultHeaders: { 'User-Agent': 'aura-dic/1.0' },
|
|
115
|
+
// OpenAI SDK v4 uses agentkeepalive by default, which hangs on some
|
|
116
|
+
// endpoints (e.g. Xiaomi MiMo Token Plan). A plain https.Agent with
|
|
117
|
+
// keepAlive: false avoids the issue.
|
|
118
|
+
httpAgent: new https.Agent({ keepAlive: false }),
|
|
119
|
+
};
|
|
120
|
+
return new openai_1.default(opts);
|
|
121
|
+
}
|
|
122
|
+
function cleanup(tmpDir) {
|
|
123
|
+
try {
|
|
124
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
}
|
|
128
|
+
// ─── Clipboard injection ──────────────────────────────────────────────────
|
|
129
|
+
/** Best-effort clipboard copy (wl-copy on Wayland, xclip/xsel on X11). */
|
|
130
|
+
async function copyToClipboard(text) {
|
|
131
|
+
const candidates = process.env.WAYLAND_DISPLAY
|
|
132
|
+
? [['wl-copy'], ['xclip', '-selection', 'clipboard'], ['xsel', '--clipboard', '--input']]
|
|
133
|
+
: [['xclip', '-selection', 'clipboard'], ['xsel', '--clipboard', '--input'], ['wl-copy']];
|
|
134
|
+
for (const [cmd, ...args] of candidates) {
|
|
135
|
+
try {
|
|
136
|
+
(0, child_process_1.execSync)('which ' + cmd, { stdio: 'pipe' });
|
|
137
|
+
// stdout/stderr must be 'ignore': wl-copy (and xclip) fork a daemon to
|
|
138
|
+
// serve the clipboard, which inherits the pipes — 'close' then never
|
|
139
|
+
// fires. Wait on 'exit' (the parent's) instead.
|
|
140
|
+
const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
141
|
+
proc.stdin.write(text);
|
|
142
|
+
proc.stdin.end();
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
const t = setTimeout(() => reject(new Error(cmd + ' timed out')), 5000);
|
|
145
|
+
proc.on('exit', () => { clearTimeout(t); resolve(); });
|
|
146
|
+
proc.on('error', (e) => { clearTimeout(t); reject(e); });
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
catch { /* try next */ }
|
|
151
|
+
}
|
|
152
|
+
throw new Error('No clipboard tool found (install wl-clipboard, xclip, or xsel)');
|
|
153
|
+
}
|
|
154
|
+
/** Press the Enter key in the focused window (Wayland: ydotool; X11: xdotool). */
|
|
155
|
+
function pressEnter(isWayland) {
|
|
156
|
+
if (isWayland) {
|
|
157
|
+
try {
|
|
158
|
+
(0, child_process_1.execSync)('which ydotool', { stdio: 'pipe' });
|
|
159
|
+
// ydotool key: <keycode>:<1=down|0=up>. 28 = KEY_ENTER (Linux evdev).
|
|
160
|
+
(0, child_process_1.execSync)('ydotool key 28:1 28:0', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 5000 });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
catch { /* fall through */ }
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
(0, child_process_1.execSync)('which xdotool', { stdio: 'pipe' });
|
|
167
|
+
(0, child_process_1.execSync)('xdotool key --clearmodifiers Return', { stdio: ['ignore', 'ignore', 'ignore'], timeout: 5000 });
|
|
168
|
+
}
|
|
169
|
+
catch { /* Enter is best-effort — the text is already typed */ }
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Copy text to clipboard AND type it into the focused window.
|
|
173
|
+
* Types the characters directly (wtype on Wayland, xdotool type on X11)
|
|
174
|
+
* instead of simulating Ctrl+V — terminals treat Ctrl+V as a literal
|
|
175
|
+
* character, not paste, so a paste keystroke never lands in a CLI prompt.
|
|
176
|
+
* With { submit: true }, presses Enter afterward to send the prompt.
|
|
177
|
+
*/
|
|
178
|
+
async function injectText(text, opts = {}) {
|
|
179
|
+
try {
|
|
180
|
+
await copyToClipboard(text);
|
|
181
|
+
}
|
|
182
|
+
catch { /* typing is the main path */ }
|
|
183
|
+
// Newlines would submit a prompt / run a shell line mid-text — flatten them.
|
|
184
|
+
const typed = text.replace(/\s*\n\s*/g, ' ').trim();
|
|
185
|
+
if (!typed)
|
|
186
|
+
return;
|
|
187
|
+
const isWayland = !!process.env.WAYLAND_DISPLAY;
|
|
188
|
+
if (isWayland) {
|
|
189
|
+
let injected = false;
|
|
190
|
+
try {
|
|
191
|
+
(0, child_process_1.execSync)('which wtype', { stdio: 'pipe' });
|
|
192
|
+
await new Promise(r => setTimeout(r, 150));
|
|
193
|
+
// "wtype -" reads the text from stdin — no shell-quoting pitfalls.
|
|
194
|
+
(0, child_process_1.execSync)('wtype -', { input: typed, stdio: ['pipe', 'pipe', 'pipe'], timeout: 20000 });
|
|
195
|
+
injected = true;
|
|
196
|
+
}
|
|
197
|
+
catch { /* KWin rejects the virtual-keyboard protocol — try ydotool */ }
|
|
198
|
+
// ydotool types via kernel uinput — works on any Wayland compositor,
|
|
199
|
+
// but needs the ydotoold daemon and /dev/uinput access.
|
|
200
|
+
if (!injected) {
|
|
201
|
+
try {
|
|
202
|
+
(0, child_process_1.execSync)('which ydotool', { stdio: 'pipe' });
|
|
203
|
+
await new Promise(r => setTimeout(r, 150));
|
|
204
|
+
(0, child_process_1.execSync)('ydotool type --file -', { input: typed, stdio: ['pipe', 'pipe', 'pipe'], timeout: 20000 });
|
|
205
|
+
injected = true;
|
|
206
|
+
}
|
|
207
|
+
catch { /* fall through to error below */ }
|
|
208
|
+
}
|
|
209
|
+
if (!injected) {
|
|
210
|
+
// Don't fall back to xdotool here: on Wayland it "types" only into
|
|
211
|
+
// XWayland windows, so it reports success while a native terminal
|
|
212
|
+
// (Konsole, kitty…) receives nothing — a silent failure.
|
|
213
|
+
throw new Error('no working Wayland injector — this compositor rejects wtype; install ydotool + ydotoold');
|
|
214
|
+
}
|
|
215
|
+
if (opts.submit)
|
|
216
|
+
pressEnter(true);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
(0, child_process_1.execSync)('which xdotool', { stdio: 'pipe' });
|
|
221
|
+
await new Promise(r => setTimeout(r, 150));
|
|
222
|
+
(0, child_process_1.execSync)('xdotool type --clearmodifiers --file -', { input: typed, stdio: ['pipe', 'pipe', 'pipe'], timeout: 20000 });
|
|
223
|
+
if (opts.submit)
|
|
224
|
+
pressEnter(false);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
throw new Error('Cannot inject text: install xdotool (X11)');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ─── Audio level helper ────────────────────────────────────────────────────
|
|
231
|
+
function rmsLevel(pcmBuffer) {
|
|
232
|
+
if (pcmBuffer.length < 2)
|
|
233
|
+
return 0;
|
|
234
|
+
let sum = 0;
|
|
235
|
+
const samples = pcmBuffer.length / 2;
|
|
236
|
+
for (let i = 0; i < pcmBuffer.length; i += 2) {
|
|
237
|
+
const val = pcmBuffer.readInt16LE(i);
|
|
238
|
+
sum += val * val;
|
|
239
|
+
}
|
|
240
|
+
return Math.sqrt(sum / samples);
|
|
241
|
+
}
|
|
242
|
+
function pickPlayer() {
|
|
243
|
+
for (const p of ['aplay', 'paplay', 'ffplay']) {
|
|
244
|
+
try {
|
|
245
|
+
(0, child_process_1.execSync)('which ' + p, { stdio: 'pipe' });
|
|
246
|
+
return p;
|
|
247
|
+
}
|
|
248
|
+
catch { }
|
|
249
|
+
}
|
|
250
|
+
return 'aplay';
|
|
251
|
+
}
|
|
252
|
+
function playWav(wavBuffer) {
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
const player = pickPlayer();
|
|
255
|
+
const proc = (0, child_process_1.spawn)(player, [], { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
256
|
+
proc.on('error', reject);
|
|
257
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error('Player exited with code ' + code)));
|
|
258
|
+
proc.stdin.write(wavBuffer);
|
|
259
|
+
proc.stdin.end();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// ─── Audio device listing ─────────────────────────────────────────────────
|
|
263
|
+
function listDevices() {
|
|
264
|
+
console.log(chalk_1.default.hex('#cc785c').bold('\n Available Audio Input Devices\n'));
|
|
265
|
+
try {
|
|
266
|
+
const output = (0, child_process_1.execSync)('pactl list sources short', { stdio: 'pipe', encoding: 'utf8', timeout: 5000 });
|
|
267
|
+
const lines = output.trim().split('\n');
|
|
268
|
+
if (lines.length === 0 || (lines.length === 1 && !lines[0].trim())) {
|
|
269
|
+
console.log(chalk_1.default.hex('#ede0cc')(' No audio input devices found.\n'));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const line of lines) {
|
|
273
|
+
const parts = line.trim().split(/\s+/);
|
|
274
|
+
if (parts.length < 2)
|
|
275
|
+
continue;
|
|
276
|
+
const id = parts[1]; // source name
|
|
277
|
+
const state = parts[7] || '';
|
|
278
|
+
const icon = state === 'RUNNING' ? '🎤 ' : ' ';
|
|
279
|
+
console.log(` ${icon}${chalk_1.default.hex('#ede0cc')(id)}`);
|
|
280
|
+
}
|
|
281
|
+
console.log(chalk_1.default.hex('#8a7768')('\n Use: dic --device <name>\n'));
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Could not list devices (pactl not available).\n'));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function getDefaultDevice() {
|
|
288
|
+
try {
|
|
289
|
+
const output = (0, child_process_1.execSync)('pactl get-default-source 2>/dev/null || pw-record --list-targets 2>/dev/null | head -1', { stdio: 'pipe', encoding: 'utf8', timeout: 5000 });
|
|
290
|
+
return output.trim();
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return '';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function pickRecorder(deviceId) {
|
|
297
|
+
// Prefer PipeWire's pw-record (handles BT, USB headsets correctly)
|
|
298
|
+
try {
|
|
299
|
+
(0, child_process_1.execSync)('which pw-record', { stdio: 'pipe' });
|
|
300
|
+
const args = ['--rate=' + String(SAMPLE_RATE), '--format=s16', '--channels=1'];
|
|
301
|
+
if (deviceId) {
|
|
302
|
+
args.push('--target=' + deviceId);
|
|
303
|
+
}
|
|
304
|
+
return { cmd: 'pw-record', args };
|
|
305
|
+
}
|
|
306
|
+
catch { }
|
|
307
|
+
// Fallback 1: PulseAudio's parec + ffmpeg conversion
|
|
308
|
+
try {
|
|
309
|
+
(0, child_process_1.execSync)('which parec', { stdio: 'pipe' });
|
|
310
|
+
const args = ['--rate=' + String(SAMPLE_RATE), '--format=s16le', '--channels=1', '--raw'];
|
|
311
|
+
if (deviceId) {
|
|
312
|
+
args.push('--device=' + deviceId);
|
|
313
|
+
}
|
|
314
|
+
return { cmd: 'parec', args };
|
|
315
|
+
}
|
|
316
|
+
catch { }
|
|
317
|
+
// Fallback 2: ALSA arecord
|
|
318
|
+
try {
|
|
319
|
+
(0, child_process_1.execSync)('which arecord', { stdio: 'pipe' });
|
|
320
|
+
const args = ['-r', String(SAMPLE_RATE), '-f', 'S16_LE', '-c', '1', '-t', 'wav'];
|
|
321
|
+
if (deviceId) {
|
|
322
|
+
args.push('-D', deviceId);
|
|
323
|
+
}
|
|
324
|
+
return { cmd: 'arecord', args };
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Try to transcribe with one provider. Returns the transcribed text.
|
|
331
|
+
* Throws on failure; caller should catch and try next provider.
|
|
332
|
+
*/
|
|
333
|
+
async function transcribeWith(api, wavPath, printName) {
|
|
334
|
+
const client = buildClient(api);
|
|
335
|
+
if (printName) {
|
|
336
|
+
const sizeKb = (fs.statSync(wavPath).size / 1024).toFixed(1);
|
|
337
|
+
console.log(chalk_1.default.hex('#8a7768')('\n Audio: ' + sizeKb + ' KB (' + api.name + ')'));
|
|
338
|
+
console.log(chalk_1.default.hex('#cc785c')(' Transcribing\u2026\n'));
|
|
339
|
+
}
|
|
340
|
+
if (api.name === 'Xiaomi MiMo') {
|
|
341
|
+
// MiMo ASR takes inline base64 — cap it so a runaway recording doesn't
|
|
342
|
+
// blow the request (25 MB wav ≈ 13 min at 16 kHz mono, ~33 MB base64).
|
|
343
|
+
const audioBytes = fs.statSync(wavPath).size;
|
|
344
|
+
const MAX_AUDIO_BYTES = 25_000_000;
|
|
345
|
+
if (audioBytes > MAX_AUDIO_BYTES) {
|
|
346
|
+
throw new Error(`Recording too large for inline ASR (${(audioBytes / 1e6).toFixed(1)} MB > ${MAX_AUDIO_BYTES / 1e6} MB). ` +
|
|
347
|
+
'Record shorter segments or use "dic loop" for continuous dictation.');
|
|
348
|
+
}
|
|
349
|
+
const audioBase64 = fs.readFileSync(wavPath).toString('base64');
|
|
350
|
+
const response = await client.chat.completions.create({
|
|
351
|
+
model: 'mimo-v2.5-asr',
|
|
352
|
+
messages: [{
|
|
353
|
+
role: 'user',
|
|
354
|
+
content: [{
|
|
355
|
+
type: 'input_audio',
|
|
356
|
+
input_audio: { data: 'data:audio/wav;base64,' + audioBase64 },
|
|
357
|
+
}],
|
|
358
|
+
}],
|
|
359
|
+
asr_options: { language: 'auto' },
|
|
360
|
+
});
|
|
361
|
+
return (response.choices[0]?.message?.content ?? '').trim();
|
|
362
|
+
}
|
|
363
|
+
const model = api.name === 'Groq' ? 'whisper-large-v3-turbo'
|
|
364
|
+
: api.name === 'Parakeet' ? 'parakeet-tdt-0.6b'
|
|
365
|
+
: api.name === 'GLM-ASR' ? 'glm-asr-2512'
|
|
366
|
+
: 'whisper-1';
|
|
367
|
+
// GLM-ASR caps at 30s / 25 MB per Z.ai docs; guard so we fail fast (and
|
|
368
|
+
// fall through to MiMo) rather than sending an over-long clip.
|
|
369
|
+
if (api.name === 'GLM-ASR') {
|
|
370
|
+
const bytes = fs.statSync(wavPath).size;
|
|
371
|
+
if (bytes > 25_000_000)
|
|
372
|
+
throw new Error('GLM-ASR: audio > 25 MB');
|
|
373
|
+
}
|
|
374
|
+
const transcription = await client.audio.transcriptions.create({
|
|
375
|
+
file: fs.createReadStream(wavPath),
|
|
376
|
+
model,
|
|
377
|
+
response_format: 'text',
|
|
378
|
+
});
|
|
379
|
+
const text = typeof transcription === 'string'
|
|
380
|
+
? transcription
|
|
381
|
+
: transcription.text ?? String(transcription);
|
|
382
|
+
return text.trim();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Should we fall through to the next STT provider on this error?
|
|
386
|
+
*
|
|
387
|
+
* A fallback chain should be forgiving: auth failures (401), balance/resource
|
|
388
|
+
* errors (Z.ai 1113), rate limits (429), and network/connection blips all mean
|
|
389
|
+
* "this provider couldn't serve it — try the next one". The ONLY thing that
|
|
390
|
+
* should abort the whole chain is a fatal input problem (audio too large),
|
|
391
|
+
* which we mark explicitly so it isn't retried pointlessly against every
|
|
392
|
+
* provider.
|
|
393
|
+
*/
|
|
394
|
+
function shouldFallThrough(err) {
|
|
395
|
+
const msg = String(err?.message ?? err ?? '');
|
|
396
|
+
if (/audio > 25 MB|too large/i.test(msg))
|
|
397
|
+
return false; // fatal input — don't retry
|
|
398
|
+
if (err?.status === 401 || err?.status === 403 || err?.status === 429)
|
|
399
|
+
return true;
|
|
400
|
+
return /401|403|429|Invalid API Key|1113|Insufficient balance|no resource package|Connection error| ETIMEDOUT|ECONNRESET|ENOTFOUND|network|timeout|fetch failed/i.test(msg);
|
|
401
|
+
}
|
|
402
|
+
async function dictate(opts = {}) {
|
|
403
|
+
const deviceId = opts.deviceId;
|
|
404
|
+
const inject = opts.inject ?? false;
|
|
405
|
+
const providers = listProviders();
|
|
406
|
+
if (providers.length === 0) {
|
|
407
|
+
console.error(chalk_1.default.hex('#b15439')('\n No API key found. Set one of:\n' +
|
|
408
|
+
' PARAKEET_BASE_URL (local NVIDIA Parakeet, no key needed)\n' +
|
|
409
|
+
' XIAOMI_API_KEY (recommended, MiMo ASR free tier)\n' +
|
|
410
|
+
' OPENAI_API_KEY (Whisper)\n' +
|
|
411
|
+
' GROQ_API_KEY (Whisper via Groq, very fast)\n'));
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const recorderInfo = pickRecorder(deviceId);
|
|
415
|
+
if (!recorderInfo) {
|
|
416
|
+
console.error(chalk_1.default.hex('#b15439')('\n No audio recorder found. Install pw-record, parec, or arecord.\n'));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dic-'));
|
|
420
|
+
const wavPath = path.join(tmpDir, 'recording.wav');
|
|
421
|
+
// Spawn the appropriate recorder
|
|
422
|
+
let recorder;
|
|
423
|
+
let parecFfmpeg = null;
|
|
424
|
+
if (recorderInfo.cmd === 'parec') {
|
|
425
|
+
// parec outputs raw PCM (args already have --raw) → pipe through ffmpeg to create WAV
|
|
426
|
+
const ffmpeg = (0, child_process_1.spawn)('ffmpeg', [
|
|
427
|
+
'-f', 's16le', '-ar', String(SAMPLE_RATE), '-ac', '1',
|
|
428
|
+
'-i', 'pipe:0', '-y', wavPath,
|
|
429
|
+
], { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
430
|
+
recorder = (0, child_process_1.spawn)(recorderInfo.cmd, recorderInfo.args, {
|
|
431
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
432
|
+
});
|
|
433
|
+
recorder.stdout.pipe(ffmpeg.stdin);
|
|
434
|
+
parecFfmpeg = ffmpeg;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// pw-record and arecord write WAV directly
|
|
438
|
+
recorder = (0, child_process_1.spawn)(recorderInfo.cmd, [...recorderInfo.args, wavPath], {
|
|
439
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// Show device info
|
|
443
|
+
const devName = deviceId || getDefaultDevice();
|
|
444
|
+
console.log(chalk_1.default.hex('#5a9e6e')('\n \uD83C\uDFA4 Recording \u2014 speak into your microphone.'));
|
|
445
|
+
if (devName)
|
|
446
|
+
console.log(chalk_1.default.hex('#8a7768')(' Device: ' + devName));
|
|
447
|
+
console.log(chalk_1.default.hex('#8a7768')(' Press Ctrl+C to stop and transcribe.\n'));
|
|
448
|
+
await new Promise((resolve) => {
|
|
449
|
+
const onSigint = () => {
|
|
450
|
+
process.removeListener('SIGINT', onSigint);
|
|
451
|
+
recorder.kill('SIGTERM');
|
|
452
|
+
if (parecFfmpeg)
|
|
453
|
+
parecFfmpeg.kill('SIGTERM');
|
|
454
|
+
resolve();
|
|
455
|
+
};
|
|
456
|
+
process.on('SIGINT', onSigint);
|
|
457
|
+
});
|
|
458
|
+
await new Promise(r => setTimeout(r, 300));
|
|
459
|
+
if (!fs.existsSync(wavPath)) {
|
|
460
|
+
console.error(chalk_1.default.hex('#b15439')('\n No audio captured.\n'));
|
|
461
|
+
cleanup(tmpDir);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
const stat = fs.statSync(wavPath);
|
|
465
|
+
if (stat.size < 512) {
|
|
466
|
+
console.log(chalk_1.default.hex('#cc9e5c')('\n Recording too short \u2014 nothing to transcribe.\n'));
|
|
467
|
+
cleanup(tmpDir);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// Normalize audio level — boosts quiet speech for better ASR accuracy
|
|
471
|
+
const normPath = path.join(tmpDir, 'normalized.wav');
|
|
472
|
+
try {
|
|
473
|
+
(0, child_process_1.execSync)(`ffmpeg -y -i "${wavPath}" -af "dynaudnorm=p=0.9:r=0.5" -ar ${SAMPLE_RATE} -ac 1 "${normPath}" 2>/dev/null`, { stdio: 'pipe', timeout: 10000 });
|
|
474
|
+
fs.renameSync(normPath, wavPath);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
try {
|
|
478
|
+
fs.unlinkSync(normPath);
|
|
479
|
+
}
|
|
480
|
+
catch { }
|
|
481
|
+
}
|
|
482
|
+
// Try each provider in order, falling through on auth errors
|
|
483
|
+
let text = '';
|
|
484
|
+
for (let i = 0; i < providers.length; i++) {
|
|
485
|
+
const api = providers[i];
|
|
486
|
+
try {
|
|
487
|
+
text = await transcribeWith(api, wavPath, i === 0);
|
|
488
|
+
break; // success
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
if (shouldFallThrough(err) && i < providers.length - 1) {
|
|
492
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' ' + api.name + ': unavailable (key/balance), trying next provider...\n'));
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
// Last provider failed or non-recoverable error — bail out
|
|
496
|
+
console.error(chalk_1.default.hex('#b15439')('\n Transcription failed (' + api.name + '):'), String(err), '\n');
|
|
497
|
+
cleanup(tmpDir);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const cleaned = text.trim();
|
|
502
|
+
if (cleaned) {
|
|
503
|
+
console.log(chalk_1.default.hex('#5a9e6e').bold(' \u2500\u2500 Transcription \u2500\u2500'));
|
|
504
|
+
console.log(chalk_1.default.hex('#ede0cc')(' ' + cleaned));
|
|
505
|
+
console.log(chalk_1.default.hex('#5a9e6e').bold(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n'));
|
|
506
|
+
let onClipboard = false;
|
|
507
|
+
try {
|
|
508
|
+
const clipboardModule = await Promise.resolve().then(() => __importStar(require('./clipboard.js')));
|
|
509
|
+
await clipboardModule.clipboardTool({ action: 'copy', text: cleaned });
|
|
510
|
+
onClipboard = true;
|
|
511
|
+
}
|
|
512
|
+
catch { /* injection below still works without clipboard */ }
|
|
513
|
+
if (inject) {
|
|
514
|
+
try {
|
|
515
|
+
await injectText(cleaned);
|
|
516
|
+
console.log(chalk_1.default.hex('#8a7768')(' ⌨️ Typed into focused window' + (onClipboard ? ' (also on clipboard).\n' : '.\n')));
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' ⌨️ Injection failed (' + String(err) + ')' +
|
|
520
|
+
(onClipboard ? ' — text is on the clipboard, paste manually.\n' : '.\n')));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (onClipboard) {
|
|
524
|
+
console.log(chalk_1.default.hex('#8a7768')(' 📋 Copied to clipboard.\n'));
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Could not copy to clipboard — text shown above only.\n'));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Save failed recording for debugging
|
|
532
|
+
const saveDir = path.join(os.homedir(), '.aura', 'recordings');
|
|
533
|
+
const saveName = 'dic_' + new Date().toISOString().replace(/[:.]/g, '-') + '.wav';
|
|
534
|
+
const savePath = path.join(saveDir, saveName);
|
|
535
|
+
try {
|
|
536
|
+
fs.mkdirSync(saveDir, { recursive: true });
|
|
537
|
+
if (fs.existsSync(wavPath))
|
|
538
|
+
fs.copyFileSync(wavPath, savePath);
|
|
539
|
+
}
|
|
540
|
+
catch { }
|
|
541
|
+
cleanup(tmpDir);
|
|
542
|
+
if (fs.existsSync(savePath)) {
|
|
543
|
+
console.log(chalk_1.default.hex('#cc9e5c')('\n No speech detected.'));
|
|
544
|
+
console.log(chalk_1.default.hex('#8a7768')(' Recording saved for debugging: ' + savePath + '\n'));
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
console.log(chalk_1.default.hex('#cc9e5c')('\n No speech detected.\n'));
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
cleanup(tmpDir);
|
|
552
|
+
}
|
|
553
|
+
// ─── Toggle dictation (one hotkey: start → stop → transcribe → inject) ──────
|
|
554
|
+
//
|
|
555
|
+
// Designed for a global shortcut (e.g. KDE Super+Space). Press once to start
|
|
556
|
+
// recording; press again to stop, transcribe, and type the result — with Enter
|
|
557
|
+
// — into the focused window. State lives in a lock dir so the second, separate
|
|
558
|
+
// process can find and stop the first.
|
|
559
|
+
function toggleStateDir() {
|
|
560
|
+
const base = process.env.XDG_RUNTIME_DIR || os.tmpdir();
|
|
561
|
+
return path.join(base, 'dic-toggle');
|
|
562
|
+
}
|
|
563
|
+
/** Normalize a recorded wav in place and transcribe it with provider fallback. */
|
|
564
|
+
async function normalizeAndTranscribe(wavPath, providers) {
|
|
565
|
+
const normPath = wavPath.replace(/\.wav$/, '.norm.wav');
|
|
566
|
+
try {
|
|
567
|
+
(0, child_process_1.execSync)(`ffmpeg -y -i "${wavPath}" -af "dynaudnorm=p=0.9:r=0.5" -ar ${SAMPLE_RATE} -ac 1 "${normPath}" 2>/dev/null`, { stdio: 'pipe', timeout: 10000 });
|
|
568
|
+
fs.renameSync(normPath, wavPath);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
try {
|
|
572
|
+
fs.unlinkSync(normPath);
|
|
573
|
+
}
|
|
574
|
+
catch { }
|
|
575
|
+
}
|
|
576
|
+
for (let i = 0; i < providers.length; i++) {
|
|
577
|
+
try {
|
|
578
|
+
return (await transcribeWith(providers[i], wavPath, false)).trim();
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
if (shouldFallThrough(err) && i < providers.length - 1)
|
|
582
|
+
continue;
|
|
583
|
+
throw err;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return '';
|
|
587
|
+
}
|
|
588
|
+
function notify(title, body, replaceTag = 'dic-toggle') {
|
|
589
|
+
try {
|
|
590
|
+
(0, child_process_1.execSync)(`notify-send -a dic -h string:x-canonical-private-synchronous:${replaceTag} ` +
|
|
591
|
+
`${JSON.stringify(title)} ${JSON.stringify(body)}`, { stdio: 'ignore', timeout: 3000 });
|
|
592
|
+
}
|
|
593
|
+
catch { /* notifications are optional */ }
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Toggle: first call starts recording (detached recorder + state files),
|
|
597
|
+
* second call stops it and transcribes + injects. Returns when the whole
|
|
598
|
+
* cycle (or the start half) is done.
|
|
599
|
+
*/
|
|
600
|
+
async function toggleDictation(opts = {}) {
|
|
601
|
+
const stateDir = toggleStateDir();
|
|
602
|
+
const pidFile = path.join(stateDir, 'recorder.pid');
|
|
603
|
+
const wavFile = path.join(stateDir, 'recording.wav');
|
|
604
|
+
const rawFile = path.join(stateDir, 'recording.raw');
|
|
605
|
+
// ── STOP branch: a recording is already in progress ──────────────────────
|
|
606
|
+
if (fs.existsSync(pidFile)) {
|
|
607
|
+
let pid = 0;
|
|
608
|
+
try {
|
|
609
|
+
pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
610
|
+
}
|
|
611
|
+
catch { }
|
|
612
|
+
const isParec = fs.existsSync(path.join(stateDir, 'parec'));
|
|
613
|
+
// Stop the recorder; a stale pid (process already gone) is fine.
|
|
614
|
+
if (pid > 0) {
|
|
615
|
+
try {
|
|
616
|
+
process.kill(pid, 'SIGTERM');
|
|
617
|
+
}
|
|
618
|
+
catch { }
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
fs.unlinkSync(pidFile);
|
|
622
|
+
}
|
|
623
|
+
catch { }
|
|
624
|
+
await new Promise(r => setTimeout(r, 400)); // let the file flush/close
|
|
625
|
+
// parec path recorded raw PCM → wrap to wav.
|
|
626
|
+
const srcRaw = isParec ? rawFile : wavFile;
|
|
627
|
+
if (isParec && fs.existsSync(rawFile)) {
|
|
628
|
+
try {
|
|
629
|
+
(0, child_process_1.execSync)(`ffmpeg -y -f s16le -ar ${SAMPLE_RATE} -ac 1 -i "${rawFile}" "${wavFile}" 2>/dev/null`, { stdio: 'pipe', timeout: 10000 });
|
|
630
|
+
}
|
|
631
|
+
catch { }
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
fs.unlinkSync(path.join(stateDir, 'parec'));
|
|
635
|
+
}
|
|
636
|
+
catch { }
|
|
637
|
+
if (!fs.existsSync(wavFile) || fs.statSync(wavFile).size < 512) {
|
|
638
|
+
notify('🎤 dic', 'Nothing recorded.');
|
|
639
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Nothing recorded.\n'));
|
|
640
|
+
try {
|
|
641
|
+
fs.unlinkSync(rawFile);
|
|
642
|
+
}
|
|
643
|
+
catch { }
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
notify('🎤 dic', 'Transcribing…');
|
|
647
|
+
const providers = listProviders();
|
|
648
|
+
if (providers.length === 0) {
|
|
649
|
+
notify('🎤 dic', 'No STT API key set.');
|
|
650
|
+
console.error(chalk_1.default.hex('#b15439')(' No STT API key set.\n'));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
let cleaned = '';
|
|
654
|
+
try {
|
|
655
|
+
cleaned = await normalizeAndTranscribe(wavFile, providers);
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
notify('🎤 dic', 'Transcription failed.');
|
|
659
|
+
console.error(chalk_1.default.hex('#b15439')(' Transcription failed: ' + String(err) + '\n'));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
try {
|
|
664
|
+
fs.unlinkSync(wavFile);
|
|
665
|
+
}
|
|
666
|
+
catch { }
|
|
667
|
+
try {
|
|
668
|
+
fs.unlinkSync(rawFile);
|
|
669
|
+
}
|
|
670
|
+
catch { }
|
|
671
|
+
}
|
|
672
|
+
if (!cleaned) {
|
|
673
|
+
notify('🎤 dic', 'No speech detected.');
|
|
674
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' No speech detected.\n'));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
console.log(chalk_1.default.hex('#ede0cc')(' ' + cleaned));
|
|
678
|
+
try {
|
|
679
|
+
await injectText(cleaned, { submit: opts.submit ?? true });
|
|
680
|
+
notify('🎤 dic', '✓ ' + cleaned.slice(0, 80));
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
// injectText already copied to clipboard on the way in.
|
|
684
|
+
notify('🎤 dic', 'Typed failed — on clipboard.');
|
|
685
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Injection failed (' + String(err) + ') — text is on the clipboard.\n'));
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// ── START branch: no recording yet → begin one ───────────────────────────
|
|
690
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
691
|
+
const recorderInfo = pickRecorder(opts.deviceId);
|
|
692
|
+
if (!recorderInfo) {
|
|
693
|
+
notify('🎤 dic', 'No recorder (install pw-record/arecord).');
|
|
694
|
+
console.error(chalk_1.default.hex('#b15439')(' No audio recorder found.\n'));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
let child;
|
|
698
|
+
if (recorderInfo.cmd === 'parec') {
|
|
699
|
+
// Mark this as the raw-PCM path so STOP knows to wrap it.
|
|
700
|
+
fs.writeFileSync(path.join(stateDir, 'parec'), '');
|
|
701
|
+
child = (0, child_process_1.spawn)(recorderInfo.cmd, [...recorderInfo.args], {
|
|
702
|
+
stdio: ['ignore', fs.openSync(rawFile, 'w'), 'ignore'],
|
|
703
|
+
detached: true,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
child = (0, child_process_1.spawn)(recorderInfo.cmd, [...recorderInfo.args, wavFile], {
|
|
708
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
709
|
+
detached: true,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
child.unref();
|
|
713
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
714
|
+
notify('🎤 dic', 'Recording… (press again to send)');
|
|
715
|
+
console.log(chalk_1.default.hex('#5a9e6e')(' 🎤 Recording — press the hotkey again to stop & send.\n'));
|
|
716
|
+
}
|
|
717
|
+
// ─── TTS: text-to-speech via MiMo TTS ─────────────────────────────────────
|
|
718
|
+
async function speakText(text, voice) {
|
|
719
|
+
const apiKey = process.env.XIAOMI_API_KEY;
|
|
720
|
+
if (!apiKey) {
|
|
721
|
+
console.error(chalk_1.default.hex('#b15439')('\n XIAOMI_API_KEY required for MiMo TTS.\n'));
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
const baseURL = apiKey.startsWith('tp-')
|
|
725
|
+
? (process.env.XIAOMI_BASE_URL || 'https://token-plan-sgp.xiaomimimo.com/v1')
|
|
726
|
+
: MIMO_BASE;
|
|
727
|
+
const client = buildClient({ key: apiKey, baseURL });
|
|
728
|
+
const selectedVoice = voice || 'mimo_default';
|
|
729
|
+
console.log(chalk_1.default.hex('#cc785c')(' Generating speech (voice: ' + selectedVoice + ')...\n'));
|
|
730
|
+
try {
|
|
731
|
+
const response = await client.chat.completions.create({
|
|
732
|
+
model: 'mimo-v2.5-tts',
|
|
733
|
+
messages: [
|
|
734
|
+
{
|
|
735
|
+
role: 'user',
|
|
736
|
+
content: 'Read the following text clearly and naturally with appropriate expression.',
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
role: 'assistant',
|
|
740
|
+
content: text,
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
audio: { format: 'wav', voice: selectedVoice },
|
|
744
|
+
});
|
|
745
|
+
const msg = response.choices[0]?.message;
|
|
746
|
+
const audioData = msg?.audio?.data;
|
|
747
|
+
if (!audioData) {
|
|
748
|
+
throw new Error('No audio data in response. Message keys: ' + Object.keys(msg || {}).join(', '));
|
|
749
|
+
}
|
|
750
|
+
// Decode base64 and play
|
|
751
|
+
const wavBuffer = Buffer.from(audioData, 'base64');
|
|
752
|
+
console.log(chalk_1.default.hex('#8a7768')(' Playing... (' + (wavBuffer.length / 1024).toFixed(0) + ' KB)\n'));
|
|
753
|
+
await playWav(wavBuffer);
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
console.error(chalk_1.default.hex('#b15439')('\n TTS failed:'), String(err), '\n');
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Synthesize speech and return the WAV bytes (no playback, no process.exit).
|
|
762
|
+
* For programmatic callers like the Telegram bot that need the audio buffer
|
|
763
|
+
* to send as a voice message. Throws on failure rather than exiting.
|
|
764
|
+
*/
|
|
765
|
+
async function synthesizeSpeech(text, voice) {
|
|
766
|
+
const apiKey = process.env.XIAOMI_API_KEY;
|
|
767
|
+
if (!apiKey)
|
|
768
|
+
throw new Error('XIAOMI_API_KEY required for MiMo TTS');
|
|
769
|
+
const baseURL = apiKey.startsWith('tp-')
|
|
770
|
+
? (process.env.XIAOMI_BASE_URL || 'https://token-plan-sgp.xiaomimimo.com/v1')
|
|
771
|
+
: MIMO_BASE;
|
|
772
|
+
const client = buildClient({ key: apiKey, baseURL });
|
|
773
|
+
const response = await client.chat.completions.create({
|
|
774
|
+
model: 'mimo-v2.5-tts',
|
|
775
|
+
messages: [
|
|
776
|
+
{ role: 'user', content: 'Read the following text clearly and naturally with appropriate expression.' },
|
|
777
|
+
{ role: 'assistant', content: text },
|
|
778
|
+
],
|
|
779
|
+
audio: { format: 'wav', voice: voice || 'mimo_default' },
|
|
780
|
+
});
|
|
781
|
+
const audioData = response.choices[0]?.message?.audio?.data;
|
|
782
|
+
if (!audioData)
|
|
783
|
+
throw new Error('No audio data in TTS response');
|
|
784
|
+
return stripWavPrefix(Buffer.from(audioData, 'base64'));
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Strip a stray CRLF (0x0D 0x0A) prefix that the MiMo TTS API occasionally
|
|
788
|
+
* prepends before the WAV RIFF header, which corrupts the file. Only strips
|
|
789
|
+
* the exact \r\n signature to stay safe if the API ever fixes this upstream.
|
|
790
|
+
*/
|
|
791
|
+
function stripWavPrefix(buf) {
|
|
792
|
+
if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) {
|
|
793
|
+
return buf.subarray(2);
|
|
794
|
+
}
|
|
795
|
+
return buf;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Transcribe an existing audio file to text using the provider fallback chain.
|
|
799
|
+
* For programmatic callers (e.g. the Telegram bot receiving a voice note).
|
|
800
|
+
* Accepts any format ffmpeg can read; normalizes to 16 kHz mono wav first.
|
|
801
|
+
*/
|
|
802
|
+
async function transcribeFile(audioPath) {
|
|
803
|
+
const providers = listProviders();
|
|
804
|
+
if (providers.length === 0)
|
|
805
|
+
throw new Error('No STT provider configured (set XIAOMI_API_KEY, GROQ_API_KEY, …)');
|
|
806
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dic-tx-'));
|
|
807
|
+
const wavPath = path.join(tmpDir, 'in.wav');
|
|
808
|
+
try {
|
|
809
|
+
(0, child_process_1.execSync)(`ffmpeg -y -i "${audioPath}" -ar ${SAMPLE_RATE} -ac 1 "${wavPath}" 2>/dev/null`, { stdio: 'pipe', timeout: 15000 });
|
|
810
|
+
return await normalizeAndTranscribe(wavPath, providers);
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
cleanup(tmpDir);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// ─── List available TTS voices ─────────────────────────────────────────────
|
|
817
|
+
function listVoices() {
|
|
818
|
+
console.log(chalk_1.default.hex('#cc785c').bold('\n Xiaomi MiMo TTS Voices\n'));
|
|
819
|
+
console.log(chalk_1.default.hex('#4e3d30')(' ' + 'ID'.padEnd(16) + 'Gender'.padEnd(10) + 'Language'));
|
|
820
|
+
console.log(chalk_1.default.hex('#4e3d30')(' ' + '\u2500'.repeat(36)));
|
|
821
|
+
for (const [id, meta] of Object.entries(MIMO_VOICES)) {
|
|
822
|
+
const name = id === 'mimo_default' ? 'Auto (cluster default)' : id;
|
|
823
|
+
console.log(' ' + chalk_1.default.hex('#cc785c')(id.padEnd(16)) + ' ' +
|
|
824
|
+
chalk_1.default.hex('#8a7768')(meta.gender.padEnd(10)) + ' ' +
|
|
825
|
+
chalk_1.default.hex('#8a7768')(meta.lang === 'zh' ? 'Chinese' : meta.lang === 'en' ? 'English' : 'Auto'));
|
|
826
|
+
}
|
|
827
|
+
console.log(chalk_1.default.hex('#4e3d30')('\n Usage: dic speak <text> --voice <id>'));
|
|
828
|
+
console.log(chalk_1.default.hex('#4e3d30')(' Default: dic speak <text>\n'));
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Continuous dictation: record → transcribe → inject → repeat.
|
|
832
|
+
* Stops on Ctrl+C. Uses arecord/parec with raw PCM for real-time silence detection.
|
|
833
|
+
*/
|
|
834
|
+
async function dictationLoop(opts = {}) {
|
|
835
|
+
const providers = listProviders();
|
|
836
|
+
if (providers.length === 0) {
|
|
837
|
+
console.error(chalk_1.default.hex('#b15439')('\n No API key found. Set GROQ_API_KEY, OPENAI_API_KEY, or XIAOMI_API_KEY.\n'));
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
const deviceId = opts.deviceId;
|
|
841
|
+
const silenceMs = opts.silenceMs ?? 1500;
|
|
842
|
+
const maxDurationMs = opts.maxDurationMs ?? 60000;
|
|
843
|
+
const devName = deviceId || getDefaultDevice();
|
|
844
|
+
console.log(chalk_1.default.hex('#5a9e6e').bold('\n 🎙️ Dictation Loop — Continuous voice-to-text'));
|
|
845
|
+
if (devName)
|
|
846
|
+
console.log(chalk_1.default.hex('#8a7768')(' Device: ' + devName));
|
|
847
|
+
console.log(chalk_1.default.hex('#8a7768')(' Silence threshold: ' + silenceMs + 'ms'));
|
|
848
|
+
console.log(chalk_1.default.hex('#8a7768')(' Transcriptions will be injected into focused window.'));
|
|
849
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Press Ctrl+C to stop.\n'));
|
|
850
|
+
let running = true;
|
|
851
|
+
let roundNum = 0;
|
|
852
|
+
const onSigint = () => {
|
|
853
|
+
running = false;
|
|
854
|
+
console.log(chalk_1.default.hex('#cc9e5c')('\n Stopping dictation loop...\n'));
|
|
855
|
+
};
|
|
856
|
+
process.on('SIGINT', onSigint);
|
|
857
|
+
while (running) {
|
|
858
|
+
roundNum++;
|
|
859
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dic-loop-'));
|
|
860
|
+
const rawPath = path.join(tmpDir, 'recording.raw');
|
|
861
|
+
const wavPath = path.join(tmpDir, 'recording.wav');
|
|
862
|
+
try {
|
|
863
|
+
// ── Record with silence detection ──────────────────────────────
|
|
864
|
+
console.log(chalk_1.default.hex('#5a9e6e')(' ── Round ' + roundNum + ' ── Listening...\n'));
|
|
865
|
+
const recArgs = ['-r', String(SAMPLE_RATE), '-f', 'S16_LE', '-c', '1', '-t', 'raw',
|
|
866
|
+
...(deviceId ? ['-D', deviceId] : []), rawPath];
|
|
867
|
+
const recorder = (0, child_process_1.spawn)('arecord', recArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
868
|
+
let stopped = false;
|
|
869
|
+
const silenceThreshold = 400;
|
|
870
|
+
let lastSoundTime = Date.now();
|
|
871
|
+
const startTime = Date.now();
|
|
872
|
+
// Monitor the raw file for audio levels
|
|
873
|
+
const monitor = setInterval(() => {
|
|
874
|
+
if (!running || stopped)
|
|
875
|
+
return;
|
|
876
|
+
try {
|
|
877
|
+
if (!fs.existsSync(rawPath))
|
|
878
|
+
return;
|
|
879
|
+
const stat = fs.statSync(rawPath);
|
|
880
|
+
if (stat.size < SAMPLE_RATE * 2)
|
|
881
|
+
return; // need at least 1 second
|
|
882
|
+
// Read last 100ms of audio for level check
|
|
883
|
+
const chunkSize = SAMPLE_RATE * 2 * 0.1; // 100ms of s16le
|
|
884
|
+
const readStart = Math.max(0, stat.size - chunkSize);
|
|
885
|
+
const fd = fs.openSync(rawPath, 'r');
|
|
886
|
+
const buf = Buffer.alloc(Math.min(chunkSize, stat.size));
|
|
887
|
+
fs.readSync(fd, buf, 0, buf.length, readStart);
|
|
888
|
+
fs.closeSync(fd);
|
|
889
|
+
const level = rmsLevel(buf);
|
|
890
|
+
if (level > silenceThreshold) {
|
|
891
|
+
lastSoundTime = Date.now();
|
|
892
|
+
}
|
|
893
|
+
const elapsed = Date.now() - startTime;
|
|
894
|
+
const silenceElapsed = Date.now() - lastSoundTime;
|
|
895
|
+
if (silenceElapsed >= silenceMs && elapsed > 1000) {
|
|
896
|
+
// Silence detected — stop recording
|
|
897
|
+
stopped = true;
|
|
898
|
+
recorder.kill('SIGTERM');
|
|
899
|
+
}
|
|
900
|
+
else if (elapsed >= maxDurationMs) {
|
|
901
|
+
stopped = true;
|
|
902
|
+
recorder.kill('SIGTERM');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch { }
|
|
906
|
+
}, 200);
|
|
907
|
+
await new Promise((resolve) => {
|
|
908
|
+
recorder.on('close', () => resolve());
|
|
909
|
+
if (!running) {
|
|
910
|
+
recorder.kill('SIGTERM');
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
clearInterval(monitor);
|
|
914
|
+
if (!running)
|
|
915
|
+
break;
|
|
916
|
+
// ── Convert raw to WAV ─────────────────────────────────────────
|
|
917
|
+
if (!fs.existsSync(rawPath) || fs.statSync(rawPath).size < SAMPLE_RATE) {
|
|
918
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Too short, skipping...\n'));
|
|
919
|
+
cleanup(tmpDir);
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
(0, child_process_1.execSync)(`ffmpeg -y -f s16le -ar ${SAMPLE_RATE} -ac 1 -i "${rawPath}" -af "dynaudnorm=p=0.9:r=0.5" "${wavPath}"`, { stdio: 'pipe', timeout: 10000 });
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// Try without normalization
|
|
927
|
+
try {
|
|
928
|
+
(0, child_process_1.execSync)(`ffmpeg -y -f s16le -ar ${SAMPLE_RATE} -ac 1 -i "${rawPath}" "${wavPath}"`, { stdio: 'pipe', timeout: 10000 });
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' Conversion failed, skipping...\n'));
|
|
932
|
+
cleanup(tmpDir);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// ── Transcribe ─────────────────────────────────────────────────
|
|
937
|
+
let text = '';
|
|
938
|
+
for (let i = 0; i < providers.length && running; i++) {
|
|
939
|
+
try {
|
|
940
|
+
text = await transcribeWith(providers[i], wavPath, false);
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
if (shouldFallThrough(err) && i < providers.length - 1)
|
|
945
|
+
continue;
|
|
946
|
+
console.error(chalk_1.default.hex('#b15439')(' Transcribe error: ' + String(err)));
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const cleaned = text.trim();
|
|
951
|
+
if (cleaned) {
|
|
952
|
+
console.log(chalk_1.default.hex('#ede0cc')(' ' + cleaned));
|
|
953
|
+
try {
|
|
954
|
+
await injectText(cleaned);
|
|
955
|
+
console.log(chalk_1.default.hex('#5a9e6e')(' ✓ Injected\n'));
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
// Fallback to clipboard
|
|
959
|
+
try {
|
|
960
|
+
const clipboardModule = await Promise.resolve().then(() => __importStar(require('./clipboard.js')));
|
|
961
|
+
await clipboardModule.clipboardTool({ action: 'copy', text: cleaned });
|
|
962
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' 📋 Copied to clipboard (inject failed)\n'));
|
|
963
|
+
}
|
|
964
|
+
catch { }
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
console.log(chalk_1.default.hex('#cc9e5c')(' (no speech detected)\n'));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch (err) {
|
|
972
|
+
if (!running)
|
|
973
|
+
break;
|
|
974
|
+
console.error(chalk_1.default.hex('#b15439')(' Error: ' + String(err)));
|
|
975
|
+
}
|
|
976
|
+
cleanup(tmpDir);
|
|
977
|
+
if (running)
|
|
978
|
+
await new Promise(r => setTimeout(r, 300)); // brief pause between rounds
|
|
979
|
+
}
|
|
980
|
+
process.removeListener('SIGINT', onSigint);
|
|
981
|
+
console.log(chalk_1.default.hex('#5a9e6e').bold(' Dictation loop ended.\n'));
|
|
982
|
+
}
|
|
983
|
+
//# sourceMappingURL=dictate.js.map
|