dexterm-cli 0.1.1 → 0.1.3
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/packages/agents/src/bus.js +57 -6
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
- package/dist/packages/cli/src/agent-cmd.js +107 -10
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +66 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +21 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +66 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +596 -49
- package/dist/packages/cli/src/mcp-cmd.js +321 -0
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +69 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +194 -0
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +3 -2
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +4 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/sources/dexscreener.js +14 -0
- package/dist/packages/data/src/sources/pumpportal.js +6 -1
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +179 -5
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +3 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +12 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/voice/src/controller.js +57 -4
- package/dist/packages/voice/src/doctor.js +96 -11
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +55 -8
- package/dist/packages/voice/src/index.js +9 -3
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/stt/whisper.js +14 -4
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +449 -0
- package/dist/packages/voice/src/tts/say.js +72 -35
- package/dist/packages/voice/src/tts/voices.js +209 -0
- package/package.json +5 -9
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
// listen() is called — so the on-screen indicator and the real mic state can never disagree.
|
|
6
6
|
import { route, isSafe } from "./router.js";
|
|
7
7
|
import { spokenSummary } from "./summary.js";
|
|
8
|
+
import { speakable } from "./speakable.js";
|
|
9
|
+
import { isBannedVoice } from "./tts/voices.js";
|
|
10
|
+
import { DEFAULT_RATE } from "./tts/say.js";
|
|
11
|
+
/** Live rate-nudge bounds and step (task-1411, the < / > keys). Clamped so a voice stays intelligible. */
|
|
12
|
+
export const RATE_MIN = 120;
|
|
13
|
+
export const RATE_MAX = 320;
|
|
14
|
+
export const RATE_STEP = 15;
|
|
8
15
|
export class VoiceController {
|
|
9
16
|
stt;
|
|
10
17
|
tts;
|
|
@@ -15,6 +22,7 @@ export class VoiceController {
|
|
|
15
22
|
muted;
|
|
16
23
|
voice;
|
|
17
24
|
rate;
|
|
25
|
+
pitch;
|
|
18
26
|
voices = [];
|
|
19
27
|
lastTranscript = null;
|
|
20
28
|
lastSay = null;
|
|
@@ -42,7 +50,8 @@ export class VoiceController {
|
|
|
42
50
|
this.tts = opts.tts;
|
|
43
51
|
this.muted = opts.muted ?? false;
|
|
44
52
|
this.voice = opts.voice ?? null;
|
|
45
|
-
this.rate = opts.rate ??
|
|
53
|
+
this.rate = opts.rate ?? DEFAULT_RATE;
|
|
54
|
+
this.pitch = opts.pitch;
|
|
46
55
|
this.now = opts.now ?? (() => Date.now());
|
|
47
56
|
this.maxListenMs = opts.maxListenMs ?? 15_000;
|
|
48
57
|
this.setTimer = opts.setTimer ?? ((fn, ms) => { const h = setTimeout(fn, ms); h.unref?.(); return h; });
|
|
@@ -71,12 +80,16 @@ export class VoiceController {
|
|
|
71
80
|
get isListening() { return this.mode === 'listening'; }
|
|
72
81
|
/** The mic is live OR about to be (listen() in flight): the states a key must cancel. */
|
|
73
82
|
get micEngaged() { return this.mode === 'listening' || this.starting; }
|
|
74
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Load the voice list lazily (used by cycleVoice and the status line). Novelty voices on the ban-list
|
|
85
|
+
* are filtered OUT so the user can never cycle into "Zarvox" / "Bad News" and so a null starting voice
|
|
86
|
+
* is seeded to a safe one, never the machine's (possibly novelty) system voice (task-1393).
|
|
87
|
+
*/
|
|
75
88
|
async loadVoices() {
|
|
76
89
|
if (!this.tts)
|
|
77
90
|
return;
|
|
78
91
|
try {
|
|
79
|
-
this.voices = await this.tts.listVoices();
|
|
92
|
+
this.voices = (await this.tts.listVoices()).filter((n) => !isBannedVoice(n));
|
|
80
93
|
if (!this.voice && this.voices.length)
|
|
81
94
|
this.voice = this.voices[0];
|
|
82
95
|
}
|
|
@@ -92,6 +105,12 @@ export class VoiceController {
|
|
|
92
105
|
this.message = this.stt ? null : 'voice-in unavailable on this machine — type questions; replies are still spoken';
|
|
93
106
|
this.setMode('idle');
|
|
94
107
|
void this.loadVoices();
|
|
108
|
+
// task-1416: pre-load the TTS engine (Piper loads its model into a standby process) so the first
|
|
109
|
+
// reply starts speaking quickly. A no-op for engines that stream from spawn (`say`).
|
|
110
|
+
try {
|
|
111
|
+
this.tts?.warmup?.(this.rate);
|
|
112
|
+
}
|
|
113
|
+
catch { /* warm-up is best-effort; never break enabling voice */ }
|
|
95
114
|
}
|
|
96
115
|
disable() {
|
|
97
116
|
if (!this.enabled)
|
|
@@ -100,6 +119,10 @@ export class VoiceController {
|
|
|
100
119
|
this.stopSpeaking();
|
|
101
120
|
this.enabled = false;
|
|
102
121
|
this.pendingLatency = null;
|
|
122
|
+
try {
|
|
123
|
+
this.tts?.dispose?.();
|
|
124
|
+
}
|
|
125
|
+
catch { /* best-effort cleanup */ }
|
|
103
126
|
this.setMode('off');
|
|
104
127
|
}
|
|
105
128
|
toggle() { this.enabled ? this.disable() : this.enable(); }
|
|
@@ -272,6 +295,9 @@ export class VoiceController {
|
|
|
272
295
|
}
|
|
273
296
|
/** Low-level speak: stops any current reply, then reads `text`. Muted → skip audio but update state. */
|
|
274
297
|
speak(text) {
|
|
298
|
+
// Speak like a trader (task-1411): no digit-by-digit tiny prices, big numbers in human words,
|
|
299
|
+
// natural percents. Applied to EVERY spoken line so an ack or an analyst answer never reads raw.
|
|
300
|
+
text = speakable(text);
|
|
275
301
|
this.lastSay = text;
|
|
276
302
|
if (!this.tts || this.muted) {
|
|
277
303
|
// Still resolve the latency sample (the "spoken word" is just suppressed) so the table is complete.
|
|
@@ -285,6 +311,7 @@ export class VoiceController {
|
|
|
285
311
|
const handle = this.tts.speak(text, {
|
|
286
312
|
voice: this.voice ?? undefined,
|
|
287
313
|
rate: this.rate,
|
|
314
|
+
pitch: this.pitch,
|
|
288
315
|
onFirstAudio: () => this.recordFirstAudio(),
|
|
289
316
|
});
|
|
290
317
|
this.speaking = handle;
|
|
@@ -362,7 +389,7 @@ export class VoiceController {
|
|
|
362
389
|
this.stopSpeaking(); this.notify(); }
|
|
363
390
|
toggleMute() { this.setMuted(!this.muted); }
|
|
364
391
|
get isMuted() { return this.muted; }
|
|
365
|
-
/** Cycle to the next installed voice
|
|
392
|
+
/** Cycle to the next installed (safe) voice and persist the choice. No-op when there are none. */
|
|
366
393
|
cycleVoice() {
|
|
367
394
|
if (!this.voices.length) {
|
|
368
395
|
this.notify();
|
|
@@ -370,10 +397,36 @@ export class VoiceController {
|
|
|
370
397
|
}
|
|
371
398
|
const i = this.voice ? this.voices.indexOf(this.voice) : -1;
|
|
372
399
|
this.voice = this.voices[(i + 1) % this.voices.length];
|
|
400
|
+
try {
|
|
401
|
+
this.opts.onVoiceChange?.(this.voice);
|
|
402
|
+
}
|
|
403
|
+
catch { /* persistence is best-effort; never break the UI */ }
|
|
373
404
|
this.notify();
|
|
374
405
|
}
|
|
375
406
|
currentVoice() { return this.voice; }
|
|
376
407
|
availableVoices() { return this.voices.slice(); }
|
|
408
|
+
/** The current speaking rate (words-per-minute-ish), for the status line / help. */
|
|
409
|
+
currentRate() { return this.rate; }
|
|
410
|
+
/**
|
|
411
|
+
* Nudge the speaking rate by `steps` × RATE_STEP (— for slower, + for faster), clamp it, persist it,
|
|
412
|
+
* and surface it on the status line (task-1411: the < / > keys). Takes effect on the next reply; it
|
|
413
|
+
* does not restart a line already in flight. A no-op when the rate is already at the clamp.
|
|
414
|
+
*/
|
|
415
|
+
nudgeRate(steps) {
|
|
416
|
+
const next = Math.max(RATE_MIN, Math.min(RATE_MAX, this.rate + steps * RATE_STEP));
|
|
417
|
+
if (next === this.rate) {
|
|
418
|
+
this.message = `voice rate ${this.rate} wpm (${next <= RATE_MIN ? 'slowest' : 'fastest'})`;
|
|
419
|
+
this.notify();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.rate = next;
|
|
423
|
+
this.message = `voice rate ${this.rate} wpm`;
|
|
424
|
+
try {
|
|
425
|
+
this.opts.onRateChange?.(this.rate);
|
|
426
|
+
}
|
|
427
|
+
catch { /* persistence is best-effort; never break the UI */ }
|
|
428
|
+
this.notify();
|
|
429
|
+
}
|
|
377
430
|
/** A compact latency report for the handoff / `--latency` demo. */
|
|
378
431
|
report() {
|
|
379
432
|
const paths = ['instant', 'analyst'];
|
|
@@ -7,22 +7,107 @@
|
|
|
7
7
|
export function voiceDoctor(deps) {
|
|
8
8
|
const checks = [];
|
|
9
9
|
if (deps.platform !== 'darwin') {
|
|
10
|
+
const win = deps.platform === 'win32';
|
|
11
|
+
// Speech OUT off macOS: Piper (neural, preferred) → espeak-ng (baseline) → warn (task-1408).
|
|
12
|
+
// NOTE: `apt install piper` on Debian is an UNRELATED gaming-mouse tool — never suggest it; the
|
|
13
|
+
// Piper TTS binary comes from the rhasspy/piper prebuilt release, the voice from `dexterm voice download`.
|
|
14
|
+
const piperRelease = 'github.com/rhasspy/piper/releases';
|
|
15
|
+
const whisperRelease = 'github.com/ggml-org/whisper.cpp/releases';
|
|
16
|
+
const piperReady = !!deps.piperPresent && !!deps.piperVoicePresent && !!deps.playerName;
|
|
17
|
+
// What Piper is still missing (for an upgrade hint / the no-voice fix).
|
|
18
|
+
const piperBits = [];
|
|
19
|
+
if (!deps.piperPresent)
|
|
20
|
+
piperBits.push(`the Piper binary (prebuilt from ${piperRelease} into \`~/.dexterm/piper/\`, NOT \`apt install piper\`)`);
|
|
21
|
+
if (!deps.piperVoicePresent)
|
|
22
|
+
piperBits.push('the voice model (`dexterm voice download piper-voice` — asks first, checksum-verified)');
|
|
23
|
+
if (!deps.playerName)
|
|
24
|
+
piperBits.push(win ? 'an audio player (ffplay from ffmpeg)' : 'an audio player (`sudo apt install alsa-utils` for aplay, or pulseaudio-utils for paplay)');
|
|
25
|
+
if (piperReady) {
|
|
26
|
+
checks.push({ name: 'Voice out (Piper)', status: 'pass', detail: `Piper neural voice ready — en_GB-alan-medium, plays via ${deps.playerName} (local, no key)` });
|
|
27
|
+
}
|
|
28
|
+
else if (deps.espeakPresent) {
|
|
29
|
+
checks.push({
|
|
30
|
+
name: 'Voice out (espeak-ng)', status: 'pass',
|
|
31
|
+
detail: `espeak-ng ready — British male default (local, no key). For a higher-quality neural voice, add Piper: ${piperBits.join('; ')}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
checks.push({
|
|
36
|
+
name: 'Voice out', status: 'warn',
|
|
37
|
+
detail: `spoken replies are off on ${deps.platform} until a speech engine is installed; the analyst still answers in text`,
|
|
38
|
+
fix: win
|
|
39
|
+
? 'Install espeak-ng (`choco install espeak` or the espeak-ng release). For a neural voice, add Piper (prebuilt from '
|
|
40
|
+
+ `${piperRelease}) + \`dexterm voice download piper-voice\` + ffplay.`
|
|
41
|
+
: 'Fastest: `sudo apt install espeak-ng`. Better (neural): install the Piper prebuilt binary from '
|
|
42
|
+
+ `${piperRelease} into \`~/.dexterm/piper/\`, run \`dexterm voice download piper-voice\`, and \`sudo apt install alsa-utils\` for a player. `
|
|
43
|
+
+ 'Do NOT `apt install piper` — on Debian that is an unrelated mouse tool.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Speech IN — whisper.cpp (the default off macOS). Not in Debian: use the prebuilt release.
|
|
47
|
+
if (deps.resolvedStt === 'whisper' || (deps.whisperBinPresent && deps.whisperModelPresent && deps.micToolPresent)) {
|
|
48
|
+
checks.push({ name: 'Voice in (whisper)', status: 'pass', detail: 'whisper.cpp base.en + a mic tool (local, no key)' });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const bits = [];
|
|
52
|
+
if (!deps.whisperBinPresent)
|
|
53
|
+
bits.push('no whisper-cli');
|
|
54
|
+
else if (!deps.whisperModelPresent)
|
|
55
|
+
bits.push('no base.en model');
|
|
56
|
+
if (!deps.micToolPresent)
|
|
57
|
+
bits.push('no mic tool (arecord/sox/ffmpeg)');
|
|
58
|
+
checks.push({
|
|
59
|
+
name: 'Voice in', status: 'warn',
|
|
60
|
+
detail: `speech-to-text is off (${bits.join(', ') || 'whisper not set up'}); you can still type questions — voice degrades to text`,
|
|
61
|
+
fix: `Install whisper.cpp (not in Debian — use the prebuilt release from ${whisperRelease}) + a mic tool `
|
|
62
|
+
+ '(`sudo apt install alsa-utils` for arecord, or sox), then `dexterm voice download whisper` (asks first, checksum-verified).',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (win) {
|
|
66
|
+
checks.push({
|
|
67
|
+
name: 'Windows voice', status: 'warn',
|
|
68
|
+
detail: 'espeak-ng (or Piper) is the supported Windows speech-out path; native SAPI is not wired in this build',
|
|
69
|
+
fix: 'Use espeak-ng or Piper on Windows for now. (SAPI support is a later enhancement.)',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return checks;
|
|
73
|
+
}
|
|
74
|
+
// Speech OUT — Piper neural (the default when set up, task-1416) → macOS `say` (fallback) → text.
|
|
75
|
+
const macPiperReady = !!deps.piperPresent && !!deps.piperVoicePresent && !!deps.playerName;
|
|
76
|
+
if (macPiperReady) {
|
|
10
77
|
checks.push({
|
|
11
|
-
name: 'Voice
|
|
12
|
-
detail: `voice
|
|
13
|
-
fix: 'Use dexterm on macOS for voice, or just use the keyboard — every voice action has a key.',
|
|
78
|
+
name: 'Voice out (Piper)', status: 'pass',
|
|
79
|
+
detail: `neural British voice is the default — en_GB-alan-medium, plays via ${deps.playerName} (local, no key); fallback: macOS \`say\``,
|
|
14
80
|
});
|
|
15
|
-
return checks;
|
|
16
81
|
}
|
|
17
|
-
// Speech OUT — macOS say.
|
|
18
82
|
if (deps.sayAvailable) {
|
|
19
|
-
|
|
83
|
+
const role = macPiperReady ? 'the fallback if Piper is removed' : 'the default — press the voice key, then cycle the voice';
|
|
84
|
+
checks.push({ name: macPiperReady ? 'Voice out (say, fallback)' : 'Voice out (say)', status: 'pass', detail: `macOS \`say\` ready · ${deps.voiceCount} voice${deps.voiceCount === 1 ? '' : 's'} · ${role}` });
|
|
85
|
+
// Which `say` voice speaks, and why — so a machine can never silently use a novelty/odd system voice (task-1393).
|
|
86
|
+
const voiceLabel = macPiperReady ? 'Fallback voice' : 'Default voice';
|
|
87
|
+
if (deps.resolvedVoice) {
|
|
88
|
+
checks.push({ name: voiceLabel, status: 'pass', detail: `${deps.resolvedVoiceReason ?? deps.resolvedVoice} · , changes it` });
|
|
89
|
+
}
|
|
90
|
+
else if (deps.resolvedVoice === null) {
|
|
91
|
+
checks.push({
|
|
92
|
+
name: voiceLabel, status: 'warn',
|
|
93
|
+
detail: deps.resolvedVoiceReason ?? 'no safe voice installed — spoken replies are off (text still works)',
|
|
94
|
+
fix: 'Install a British male voice in System Settings → Accessibility → Spoken Content → System Voice → Manage Voices (Daniel, English (UK)); the Enhanced/Premium download sounds best.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Offer the higher-quality neural voice when it is not set up yet (never silent; the user decides).
|
|
98
|
+
if (!macPiperReady) {
|
|
99
|
+
checks.push({
|
|
100
|
+
name: 'Neural voice (Piper)', status: 'warn',
|
|
101
|
+
detail: 'a higher-quality neural British voice is available but not set up; `say` is speaking for now',
|
|
102
|
+
fix: 'Run `dexterm voice setup` to install it (asks first; a private Python runtime + a ~61 MB checksum-verified model; runs offline after, `dexterm voice uninstall` removes it).',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
20
105
|
}
|
|
21
|
-
else {
|
|
106
|
+
else if (!macPiperReady) {
|
|
22
107
|
checks.push({
|
|
23
|
-
name: 'Voice out
|
|
108
|
+
name: 'Voice out', status: 'warn',
|
|
24
109
|
detail: 'macOS `say` not found; spoken replies are off, the analyst still answers in text',
|
|
25
|
-
fix: '`say` ships with macOS — if it is missing, reinstall the Command Line Tools (`xcode-select --install`).',
|
|
110
|
+
fix: '`say` ships with macOS — if it is missing, reinstall the Command Line Tools (`xcode-select --install`). Or set up the neural voice: `dexterm voice setup`.',
|
|
26
111
|
});
|
|
27
112
|
}
|
|
28
113
|
// Speech IN — Apple Speech (preferred) or whisper (fallback).
|
|
@@ -47,8 +132,8 @@ export function voiceDoctor(deps) {
|
|
|
47
132
|
name: 'Voice in', status: 'warn',
|
|
48
133
|
detail: `speech-to-text is off (${bits.join(', ')}); you can still type questions — voice degrades to text`,
|
|
49
134
|
fix: 'Fastest free path: install Xcode command line tools for Apple Speech (`xcode-select --install`). '
|
|
50
|
-
+ 'Or whisper: `brew install whisper-cpp sox`, then download
|
|
51
|
-
+ '`~/.dexterm/models/ggml-base.en.bin`
|
|
135
|
+
+ 'Or whisper: `brew install whisper-cpp sox`, then `dexterm voice download whisper` (asks first, checksum-verified, writes '
|
|
136
|
+
+ '`~/.dexterm/models/ggml-base.en.bin`). Never use large-v3-turbo on a CPU-only Mac — it is unusably slow.',
|
|
52
137
|
});
|
|
53
138
|
}
|
|
54
139
|
// Mic-capture tool note (only relevant to the whisper path; Apple Speech captures its own audio).
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Voice model downloader (task-1408). The whisper.cpp base.en model and the Piper en_GB-alan-medium
|
|
2
|
+
// voice are NOT shipped (free-forever rule: no bundled large binaries, no keys). dexterm downloads them
|
|
3
|
+
// on request, and ONLY on request:
|
|
4
|
+
// - from the OFFICIAL source (the ggml-org / rhasspy repositories on Hugging Face),
|
|
5
|
+
// - after EXPLICIT user confirmation (never a silent auto-download),
|
|
6
|
+
// - and verified against a checksum pinned here from the upstream LFS pointer, so a corrupted or
|
|
7
|
+
// tampered download is rejected and never written into place.
|
|
8
|
+
//
|
|
9
|
+
// This module is pure logic + injected fetch/confirm/log, so it is unit-testable with a fake registry
|
|
10
|
+
// and never touches the real network in tests. The CLI (`dexterm voice download …`) wires the real
|
|
11
|
+
// global fetch, a readline y/N prompt, and stdout.
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { createWriteStream } from 'node:fs';
|
|
14
|
+
import { mkdir, rename, rm, readFile } from 'node:fs/promises';
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { Readable } from 'node:stream';
|
|
19
|
+
import { once } from 'node:events';
|
|
20
|
+
/** Total download size of a spec, in whole MB (for the prompt). */
|
|
21
|
+
export function specSizeMB(spec) {
|
|
22
|
+
return Math.round(spec.files.reduce((n, f) => n + (f.size ?? 0), 0) / (1024 * 1024));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The downloadable voice models, keyed by id. `home` is the dexterm home (~/.dexterm) so tests can
|
|
26
|
+
* point it at a temp dir. Checksums are pinned from the upstream Hugging Face LFS pointers (verified
|
|
27
|
+
* 2026-10-16): ggerganov/whisper.cpp and rhasspy/piper-voices@v1.0.0.
|
|
28
|
+
*/
|
|
29
|
+
export function modelRegistry(home = join(homedir(), '.dexterm')) {
|
|
30
|
+
const models = join(home, 'models');
|
|
31
|
+
return {
|
|
32
|
+
whisper: {
|
|
33
|
+
id: 'whisper',
|
|
34
|
+
label: 'whisper.cpp base.en (speech-to-text)',
|
|
35
|
+
note: 'whisper.cpp base.en — on-device speech recognition, ~141 MB, MIT (ggml-org/whisper.cpp)',
|
|
36
|
+
files: [{
|
|
37
|
+
url: 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin',
|
|
38
|
+
dest: join(models, 'ggml-base.en.bin'),
|
|
39
|
+
sha256: 'a03779c86df3323075f5e796cb2ce5029f00ec8869eee3fdfb897afe36c6d002',
|
|
40
|
+
size: 147964211,
|
|
41
|
+
}],
|
|
42
|
+
},
|
|
43
|
+
'piper-voice': {
|
|
44
|
+
id: 'piper-voice',
|
|
45
|
+
label: 'Piper en_GB-alan-medium (neural voice)',
|
|
46
|
+
note: 'Piper en_GB-alan-medium — British male neural voice, ~61 MB, MIT (rhasspy/piper-voices)',
|
|
47
|
+
files: [
|
|
48
|
+
{
|
|
49
|
+
url: 'https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_GB/alan/medium/en_GB-alan-medium.onnx',
|
|
50
|
+
dest: join(models, 'en_GB-alan-medium.onnx'),
|
|
51
|
+
sha256: '0a309668932205e762801f1efc2736cd4b0120329622adf62be09e56339d3330',
|
|
52
|
+
size: 63201294,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
url: 'https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_GB/alan/medium/en_GB-alan-medium.onnx.json',
|
|
56
|
+
dest: join(models, 'en_GB-alan-medium.onnx.json'),
|
|
57
|
+
validateJson: true,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** sha256 of an existing file, or null if it cannot be read. */
|
|
64
|
+
async function fileSha256(path) {
|
|
65
|
+
try {
|
|
66
|
+
const h = createHash('sha256');
|
|
67
|
+
h.update(await readFile(path));
|
|
68
|
+
return h.digest('hex');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** True when `file` is already present AND valid (checksum match, or parses as JSON for config files). */
|
|
75
|
+
async function isPresentAndValid(file) {
|
|
76
|
+
if (!existsSync(file.dest))
|
|
77
|
+
return false;
|
|
78
|
+
if (file.validateJson) {
|
|
79
|
+
try {
|
|
80
|
+
JSON.parse(await readFile(file.dest, 'utf8'));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (file.sha256)
|
|
88
|
+
return (await fileSha256(file.dest)) === file.sha256.toLowerCase();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Download and verify one file. Streams to a `.part` temp alongside the destination (bounded memory),
|
|
93
|
+
* verifies size + sha256 (or JSON), then atomically renames into place. On ANY failure the temp is
|
|
94
|
+
* removed and nothing is written to the destination.
|
|
95
|
+
*/
|
|
96
|
+
async function downloadFile(file, fetchImpl, log) {
|
|
97
|
+
await mkdir(dirname(file.dest), { recursive: true });
|
|
98
|
+
const tmp = `${file.dest}.part`;
|
|
99
|
+
await rm(tmp, { force: true });
|
|
100
|
+
log(` ↓ ${file.url}`);
|
|
101
|
+
let res;
|
|
102
|
+
try {
|
|
103
|
+
res = await fetchImpl(file.url, { redirect: 'follow' });
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
throw new Error(`download failed (${file.url}): ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
if (!res.ok || !res.body)
|
|
109
|
+
throw new Error(`download failed (${file.url}): HTTP ${res.status}`);
|
|
110
|
+
const hash = createHash('sha256');
|
|
111
|
+
const out = createWriteStream(tmp);
|
|
112
|
+
let bytes = 0;
|
|
113
|
+
try {
|
|
114
|
+
for await (const chunk of Readable.fromWeb(res.body)) {
|
|
115
|
+
const buf = chunk;
|
|
116
|
+
bytes += buf.length;
|
|
117
|
+
hash.update(buf);
|
|
118
|
+
if (!out.write(buf))
|
|
119
|
+
await once(out, 'drain');
|
|
120
|
+
}
|
|
121
|
+
out.end();
|
|
122
|
+
await once(out, 'finish');
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
out.destroy();
|
|
126
|
+
await rm(tmp, { force: true });
|
|
127
|
+
throw new Error(`download failed (${file.url}): ${e.message}`);
|
|
128
|
+
}
|
|
129
|
+
// Verify before anything lands at the real path.
|
|
130
|
+
try {
|
|
131
|
+
if (file.size !== undefined && bytes !== file.size) {
|
|
132
|
+
throw new Error(`size mismatch: got ${bytes} bytes, expected ${file.size}`);
|
|
133
|
+
}
|
|
134
|
+
if (file.sha256) {
|
|
135
|
+
const got = hash.digest('hex');
|
|
136
|
+
if (got !== file.sha256.toLowerCase())
|
|
137
|
+
throw new Error(`checksum mismatch: got ${got}, expected ${file.sha256}`);
|
|
138
|
+
}
|
|
139
|
+
else if (file.validateJson) {
|
|
140
|
+
JSON.parse(await readFile(tmp, 'utf8'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
await rm(tmp, { force: true });
|
|
145
|
+
throw new Error(`verification failed (${file.url}): ${e.message}`);
|
|
146
|
+
}
|
|
147
|
+
await rename(tmp, file.dest);
|
|
148
|
+
const mb = (bytes / (1024 * 1024)).toFixed(1);
|
|
149
|
+
log(` ✓ ${file.dest} (${mb} MB${file.sha256 ? ', sha256 ok' : ', json ok'})`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Download a model after explicit confirmation. If every file is already present and valid, returns
|
|
153
|
+
* `already` WITHOUT prompting or touching the network. Otherwise it builds a one-prompt summary of what
|
|
154
|
+
* will be fetched and where, calls `confirm()`, and only then downloads + verifies each missing file.
|
|
155
|
+
*/
|
|
156
|
+
export async function downloadModel(spec, deps) {
|
|
157
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
158
|
+
const log = deps.log ?? (() => { });
|
|
159
|
+
const presence = await Promise.all(spec.files.map(isPresentAndValid));
|
|
160
|
+
const missing = spec.files.filter((_, i) => !presence[i]);
|
|
161
|
+
if (missing.length === 0) {
|
|
162
|
+
log(`${spec.label} is already installed and verified.`);
|
|
163
|
+
return { status: 'already', files: spec.files.map((f) => f.dest) };
|
|
164
|
+
}
|
|
165
|
+
const dir = dirname(spec.files[0].dest);
|
|
166
|
+
const prompt = [
|
|
167
|
+
`Download ${spec.note}`,
|
|
168
|
+
` from the official source to ${dir}`,
|
|
169
|
+
` ${missing.length} file${missing.length === 1 ? '' : 's'}, ~${specSizeMB(spec)} MB, checksum-verified. Proceed?`,
|
|
170
|
+
].join('\n');
|
|
171
|
+
if (!(await deps.confirm(prompt))) {
|
|
172
|
+
log('Cancelled — nothing downloaded.');
|
|
173
|
+
return { status: 'cancelled', files: spec.files.filter((_, i) => presence[i]).map((f) => f.dest) };
|
|
174
|
+
}
|
|
175
|
+
for (const f of missing)
|
|
176
|
+
await downloadFile(f, fetchImpl, log);
|
|
177
|
+
log(`${spec.label} installed.`);
|
|
178
|
+
return { status: 'ok', files: spec.files.map((f) => f.dest) };
|
|
179
|
+
}
|
|
180
|
+
/** Which model files exist and verify right now (for a status report; never touches the network). */
|
|
181
|
+
export async function modelStatus(spec) {
|
|
182
|
+
const files = await Promise.all(spec.files.map(async (f) => ({ dest: f.dest, ok: await isPresentAndValid(f) })));
|
|
183
|
+
return { installed: files.every((f) => f.ok), files };
|
|
184
|
+
}
|
|
@@ -5,7 +5,10 @@ import { AppleStt } from "./stt/apple.js";
|
|
|
5
5
|
import { WhisperStt } from "./stt/whisper.js";
|
|
6
6
|
import { FakeStt } from "./stt/fake.js";
|
|
7
7
|
import { SayTts } from "./tts/say.js";
|
|
8
|
+
import { EspeakTts, espeakVoiceFor } from "./tts/espeak.js";
|
|
9
|
+
import { PiperTts } from "./tts/piper.js";
|
|
8
10
|
import { FakeTts } from "./tts/fake.js";
|
|
11
|
+
import { resolveVoice } from "./tts/voices.js";
|
|
9
12
|
/**
|
|
10
13
|
* Resolve the STT engine. `auto` prefers Apple Speech (on-device, streaming), then whisper base.en,
|
|
11
14
|
* then none. An explicit choice is returned even if it later proves unavailable, so the doctor and the
|
|
@@ -29,16 +32,60 @@ export async function selectStt(choice = process.env.DEXTERM_VOICE_STT || 'auto'
|
|
|
29
32
|
return whisper;
|
|
30
33
|
return null;
|
|
31
34
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the default `say` voice for this machine: the user's configured voice if it is installed and
|
|
37
|
+
* safe, else a deep British male (Daniel first), never a novelty voice and never the OS system default.
|
|
38
|
+
* Returns the full resolution (voice + why) so the controller and `dexterm doctor` can show it.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveSayVoice(say, configured) {
|
|
41
|
+
const installed = await say.listVoicesDetailed();
|
|
42
|
+
return resolveVoice({ configured, installed });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the TTS engine. `auto` uses macOS `say` when present, else none (voice replies off). When
|
|
46
|
+
* `say` is chosen, its default voice is resolved to an explicit deep British male so it can NEVER fall
|
|
47
|
+
* back to the machine's system voice (task-1393). `configuredVoice` is the user's `config.voice.name`.
|
|
48
|
+
*/
|
|
49
|
+
export async function selectTts(choice = process.env.DEXTERM_VOICE_TTS || 'auto', configuredVoice) {
|
|
34
50
|
if (choice === 'none')
|
|
35
51
|
return null;
|
|
36
52
|
if (choice === 'fake')
|
|
37
53
|
return new FakeTts();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
// Piper and espeak are the off-macOS engines. A macOS voice name (config.voice.name is e.g.
|
|
55
|
+
// "Eddy (English (UK))") is NEVER passed to them verbatim: Piper's voice is its model, and espeak
|
|
56
|
+
// only honours a real espeak voice token (espeakVoiceFor drops a Mac name → the British default).
|
|
57
|
+
if (choice === 'piper') {
|
|
58
|
+
const p = new PiperTts();
|
|
59
|
+
return (await p.available()) ? p : null;
|
|
60
|
+
}
|
|
61
|
+
if (choice === 'espeak') {
|
|
62
|
+
const e = new EspeakTts(espeakVoiceFor(configuredVoice) ?? undefined);
|
|
63
|
+
return (await e.available()) ? e : null;
|
|
64
|
+
}
|
|
65
|
+
// On macOS, prefer Piper alan (neural) when the managed runtime is installed (task-1416:
|
|
66
|
+
// `dexterm voice setup` → venv + model + afplay), else the built-in `say`. Off macOS, `say` does not
|
|
67
|
+
// exist — use the portable engines below. An explicit `choice==='say'` always skips Piper.
|
|
68
|
+
if (choice === 'say' || (choice === 'auto' && process.platform === 'darwin')) {
|
|
69
|
+
if (choice === 'auto') {
|
|
70
|
+
const piperMac = new PiperTts();
|
|
71
|
+
if (await piperMac.available())
|
|
72
|
+
return piperMac;
|
|
73
|
+
}
|
|
74
|
+
const probe = new SayTts();
|
|
75
|
+
if (choice === 'auto' && !(await probe.available()))
|
|
76
|
+
return null;
|
|
77
|
+
const r = await resolveSayVoice(probe, configuredVoice);
|
|
78
|
+
// No safe voice installed (only novelty voices): voice-out stays OFF so `say` can never be invoked
|
|
79
|
+
// without an explicit voice and fall back to the machine's system voice (task-1393). Text still works.
|
|
80
|
+
if (r.voice === null)
|
|
81
|
+
return null;
|
|
82
|
+
return new SayTts(r.voice);
|
|
83
|
+
}
|
|
84
|
+
// auto, non-darwin: Piper (neural, higher quality) when it can speak aloud, else espeak-ng, else
|
|
85
|
+
// text-only. Neither engine is ever handed a macOS voice name (task-1408).
|
|
86
|
+
const piper = new PiperTts();
|
|
87
|
+
if (await piper.available())
|
|
88
|
+
return piper;
|
|
89
|
+
const espeak = new EspeakTts(espeakVoiceFor(configuredVoice) ?? undefined);
|
|
90
|
+
return (await espeak.available()) ? espeak : null;
|
|
44
91
|
}
|
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
export * from "./types.js";
|
|
7
7
|
export { route, isSafe } from "./router.js";
|
|
8
8
|
export { spokenSummary, splitSentences } from "./summary.js";
|
|
9
|
+
export { speakable, humanizeNumber } from "./speakable.js";
|
|
9
10
|
export { VoiceController } from "./controller.js";
|
|
10
|
-
export { selectStt, selectTts } from "./engines.js";
|
|
11
|
+
export { selectStt, selectTts, resolveSayVoice } from "./engines.js";
|
|
11
12
|
export { voiceDoctor } from "./doctor.js";
|
|
12
|
-
export {
|
|
13
|
+
export { resolveVoice, isBannedVoice, normalizeVoiceName, parseSayVoices, sanitizeSpeech, BANNED_VOICE_NAMES, BRITISH_MALE_PREFERENCE, MAX_SPEAK_CHARS, VOICE_STYLE_DEFAULTS, voiceStyleDefaults, } from "./tts/voices.js";
|
|
14
|
+
export { SayTts, DEFAULT_RATE } from "./tts/say.js";
|
|
15
|
+
export { EspeakTts, DEFAULT_ESPEAK_VOICE, resolveEspeakBin, espeakVoiceFor } from "./tts/espeak.js";
|
|
16
|
+
export { PiperTts, DEFAULT_PIPER_VOICE, PIPER_MODEL, PIPER_PLAYERS, resolvePiperBin, resolvePiperRuntime, managedVenvPython, pickPlayer, } from "./tts/piper.js";
|
|
17
|
+
export { setupPiperRuntime, uninstallPiperRuntime, piperRuntimeStatus, piperRuntimePaths, readRuntimeRecord, detectPython, parsePythonVersion, pythonVersionOk, pythonFixLine, PIPER_PYPI_VERSION, MIN_PYTHON, DEFAULT_PYTHON_CANDIDATES, } from "./tts/piper-setup.js";
|
|
13
18
|
export { FakeTts } from "./tts/fake.js";
|
|
14
|
-
export { WhisperStt, WHISPER_MODEL, WHISPER_MODEL_DIR } from "./stt/whisper.js";
|
|
19
|
+
export { WhisperStt, WHISPER_MODEL, WHISPER_MODEL_DIR, pickRecorder } from "./stt/whisper.js";
|
|
20
|
+
export { modelRegistry, downloadModel, modelStatus, specSizeMB, } from "./download.js";
|
|
15
21
|
export { AppleStt, ensureHelper } from "./stt/apple.js";
|
|
16
22
|
export { FakeStt } from "./stt/fake.js";
|