verbalcoding 0.2.7 → 0.2.8
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 +12 -27
- package/app-node/cli_install.test.mjs +15 -0
- package/docs/FRESH_INSTALL.md +8 -2
- package/docs/assets/figures/verbalcoding-flow.svg +45 -30
- package/docs/i18n/CONFIGURATION.es.md +138 -49
- package/docs/i18n/CONFIGURATION.fr.md +138 -49
- package/docs/i18n/CONFIGURATION.ja.md +137 -48
- package/docs/i18n/CONFIGURATION.ko.md +137 -48
- package/docs/i18n/CONFIGURATION.ru.md +138 -49
- package/docs/i18n/CONFIGURATION.zh.md +137 -48
- package/docs/i18n/FRESH_INSTALL.es.md +115 -32
- package/docs/i18n/FRESH_INSTALL.fr.md +115 -32
- package/docs/i18n/FRESH_INSTALL.ja.md +119 -36
- package/docs/i18n/FRESH_INSTALL.ko.md +120 -37
- package/docs/i18n/FRESH_INSTALL.ru.md +115 -32
- package/docs/i18n/FRESH_INSTALL.zh.md +119 -36
- package/docs/i18n/MULTI_INSTANCE.es.md +85 -26
- package/docs/i18n/MULTI_INSTANCE.fr.md +85 -26
- package/docs/i18n/MULTI_INSTANCE.ja.md +87 -29
- package/docs/i18n/MULTI_INSTANCE.ko.md +87 -29
- package/docs/i18n/MULTI_INSTANCE.ru.md +84 -26
- package/docs/i18n/MULTI_INSTANCE.zh.md +87 -29
- package/docs/i18n/README.es.md +109 -45
- package/docs/i18n/README.fr.md +109 -45
- package/docs/i18n/README.ja.md +109 -45
- package/docs/i18n/README.ko.md +108 -45
- package/docs/i18n/README.ru.md +109 -45
- package/docs/i18n/README.zh.md +108 -45
- package/docs/i18n/RELEASE.es.md +53 -37
- package/docs/i18n/RELEASE.fr.md +53 -37
- package/docs/i18n/RELEASE.ja.md +52 -36
- package/docs/i18n/RELEASE.ko.md +52 -36
- package/docs/i18n/RELEASE.ru.md +53 -37
- package/docs/i18n/RELEASE.zh.md +53 -37
- package/docs/i18n/USAGE.es.md +91 -64
- package/docs/i18n/USAGE.fr.md +91 -64
- package/docs/i18n/USAGE.ja.md +90 -63
- package/docs/i18n/USAGE.ko.md +90 -63
- package/docs/i18n/USAGE.ru.md +91 -64
- package/docs/i18n/USAGE.zh.md +90 -63
- package/package.json +1 -1
- package/scripts/bootstrap_prereqs.sh +15 -3
- package/scripts/cli.mjs +1 -1
- package/scripts/doctor.mjs +114 -8
package/docs/i18n/USAGE.zh.md
CHANGED
|
@@ -1,71 +1,78 @@
|
|
|
1
1
|
# VerbalCoding 使用指南
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本页面保存曾经让 README 过长的运维细节。
|
|
4
4
|
|
|
5
|
-
## CLI
|
|
5
|
+
## CLI 命令
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
vc status
|
|
9
|
-
vc language en
|
|
10
|
-
vc language ko
|
|
11
|
-
vc language auto
|
|
12
|
-
vc restart auto status
|
|
13
|
-
vc restart auto on
|
|
14
|
-
vc restart auto off
|
|
15
|
-
vc bot invite CLIENT_ID
|
|
16
|
-
vc instance status
|
|
17
|
-
vc instance setup NAME
|
|
18
|
-
vc instance start NAME
|
|
19
|
-
vc instance stop NAME
|
|
20
|
-
vc doctor
|
|
21
|
-
npm run mcp
|
|
8
|
+
vc status # 显示 STT 语言、进度语言和 TTS 声音
|
|
9
|
+
vc language en # 英语 STT + 英语进度/TTS 声音
|
|
10
|
+
vc language ko # 韩语 STT + 韩语进度/TTS 声音
|
|
11
|
+
vc language auto # Whisper 自动检测 STT + 英语进度/TTS 声音
|
|
12
|
+
vc restart auto status # 显示提交时语音机器人自动重启设置
|
|
13
|
+
vc restart auto on # 启用提交时语音机器人自动重启
|
|
14
|
+
vc restart auto off # 禁用它;这是默认值
|
|
15
|
+
vc bot invite CLIENT_ID # 打印带所需权限的 Discord 邀请 URL
|
|
16
|
+
vc instance status # 列出每实例桥接配置和进程状态
|
|
17
|
+
vc instance setup NAME # 写入 instances/NAME.env 并创建 ~/.hermes/profiles/NAME
|
|
18
|
+
vc instance start NAME # 分离启动 ./run.sh instances/NAME.env
|
|
19
|
+
vc instance stop NAME # 停止分离的实例并移除其 pid 文件
|
|
20
|
+
vc doctor # 运行脱敏 doctor 检查
|
|
21
|
+
npm run mcp # 运行 stdio MCP 服务器
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
语言更改会更新 `.env`;请使用 `./run.sh` 或你的进程管理器重启桥接,让更改生效。
|
|
25
|
+
|
|
26
|
+
## 运行模式
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
单实例桥接:
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
|
-
vc start
|
|
30
31
|
./run.sh
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
使用本地覆盖 env 的每实例桥接:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
31
37
|
./run.sh instances/my-project.env
|
|
38
|
+
# 或
|
|
32
39
|
VERBALCODING_INSTANCE_ENV=instances/my-project.env ./run.sh
|
|
33
40
|
```
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
机器人会自动加入第一个已配置的频道名称,默认是 `일반,General,general`。
|
|
36
43
|
|
|
37
|
-
## Discord
|
|
44
|
+
## Discord 命令
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
在接线命令之前,请使用上游指南设置 Discord 应用/机器人:
|
|
40
47
|
|
|
41
|
-
- Hermes Agent Discord
|
|
42
|
-
- Discord
|
|
48
|
+
- Hermes Agent Discord 指南:<https://hermes-agent.nousresearch.com/docs/user-guide/messaging/discord>
|
|
49
|
+
- Discord 官方机器人文档:<https://docs.discord.com/developers/bots/overview>
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
然后使用 `vc bot invite CLIENT_ID` 生成带文本和语音权限的 VerbalCoding 专用邀请 URL。
|
|
45
52
|
|
|
46
|
-
|
|
|
53
|
+
| 命令 | 用途 |
|
|
47
54
|
|---|---|
|
|
48
|
-
| `!ping` |
|
|
49
|
-
| `!join` / `!leave` |
|
|
50
|
-
| `!say <text>` |
|
|
51
|
-
| `!voice-test <text>` |
|
|
52
|
-
| `!voice-clone capture` |
|
|
53
|
-
| `!voice-clone status` / `!voice-clone cancel` |
|
|
54
|
-
| `!ask <prompt>` |
|
|
55
|
-
| `!session status` |
|
|
56
|
-
| `!session new <name> <workdir> [context] --voice <voice-channel>` |
|
|
57
|
-
| `!session attach-voice [sessionName] --voice <voice-channel>` |
|
|
58
|
-
| `!session list` |
|
|
59
|
-
| `!session reset` / `!reset-session` |
|
|
60
|
-
| `!verbose on/off` |
|
|
61
|
-
| `!latency` / `!metrics` |
|
|
62
|
-
| `!sensitivity normal/conservative` |
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
`vc language ko|en|auto`
|
|
55
|
+
| `!ping` | 基本机器人检查 |
|
|
56
|
+
| `!join` / `!leave` | 加入或离开语音 |
|
|
57
|
+
| `!say <text>` | 直接通过 TTS 朗读文本 |
|
|
58
|
+
| `!voice-test <text>` | 测试当前 TTS 后端/声音 |
|
|
59
|
+
| `!voice-clone capture` | 将下一个有效发言保存为 OpenVoice 参考样本 |
|
|
60
|
+
| `!voice-clone status` / `!voice-clone cancel` | 查看或取消采集 |
|
|
61
|
+
| `!ask <prompt>` | 通过与语音相同的已选择驱动适配器发送文本 |
|
|
62
|
+
| `!session status` | 显示当前项目/默认适配器会话 |
|
|
63
|
+
| `!session new <name> <workdir> [context] --voice <voice-channel>` | 创建项目范围的 Hermes 会话 |
|
|
64
|
+
| `!session attach-voice [sessionName] --voice <voice-channel>` | 将文本频道/thread 绑定到语音频道 |
|
|
65
|
+
| `!session list` | 列出已配置的项目会话 |
|
|
66
|
+
| `!session reset` / `!reset-session` | 清除当前项目/默认适配器会话文件 |
|
|
67
|
+
| `!verbose on/off` | 切换详细进度更新 |
|
|
68
|
+
| `!latency` / `!metrics` | 显示最近延迟摘要 |
|
|
69
|
+
| `!sensitivity normal/conservative` | 切换插话灵敏度 |
|
|
70
|
+
|
|
71
|
+
桥接会处理诸如“외부 모드”、“보수 모드”、“실내”、“기본 감도”等语音等价命令,以及“잠깐”、“멈춰”、“그만”等明确停止短语。你也可以说“상세 진행 켜” / “상세 진행 꺼”来通过语音切换详细进度。
|
|
72
|
+
|
|
73
|
+
## 更改声音
|
|
74
|
+
|
|
75
|
+
`vc language ko|en|auto` 会同时更改 STT 语言、进度语言和匹配的默认 TTS 声音。如果只想在桥接运行时更换说话人/声音,请在 Discord 语音中说:
|
|
69
76
|
|
|
70
77
|
```text
|
|
71
78
|
남자 한국어 목소리로 바꿔
|
|
@@ -74,9 +81,11 @@ change voice to Korean female
|
|
|
74
81
|
switch speaker to English
|
|
75
82
|
```
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
实时桥接会将这些识别为语音控制命令,更新 `config/tts-voices.json`,更新运行进程的有效 TTS env,并用类似“목소리를 Korean male로 바꿨어.”的简短确认作答。更改后立即使用 `!voice-test <text>` 来听当前后端和声音。
|
|
85
|
+
|
|
86
|
+
内置 Edge 声音类型:
|
|
78
87
|
|
|
79
|
-
|
|
|
88
|
+
| 声音类型 | Edge 声音 |
|
|
80
89
|
|---|---|
|
|
81
90
|
| `korean_male` | `ko-KR-InJoonNeural` |
|
|
82
91
|
| `korean_female` | `ko-KR-SunHiNeural` |
|
|
@@ -84,26 +93,32 @@ Built-in Edge types:
|
|
|
84
93
|
| `english_male` | `en-US-GuyNeural` |
|
|
85
94
|
| `english_female` | `en-US-AriaNeural` |
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
对于持久手动配置,请在 `.env` 中设置 `TTS_BACKEND=edge`、`TTS_VOICE_TYPE=<voice-type>`,并可选设置 `TTS_VOICE=<edge-voice>`;也可以编辑 `config/tts-voices.json` 以使用自定义声音目录。
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
后端专用声音旋钮:
|
|
99
|
+
|
|
100
|
+
| 后端 | 声音设置 | 常见选择 |
|
|
90
101
|
|---|---|---|
|
|
91
|
-
| Edge | `TTS_VOICE_TYPE`, `TTS_VOICE` |
|
|
92
|
-
| Supertonic | `SUPERTONIC_VOICE` | `M1`–`M5`, `F1`–`F5
|
|
93
|
-
| OpenVoice | `OPENVOICE_REF_AUDIO`, `OPENVOICE_STYLE` |
|
|
94
|
-
| SpeechSwift / CosyVoice | `SPEECHSWIFT_REF_AUDIO`, `SPEECHSWIFT_ENGINE`, `SPEECHSWIFT_SPEAKER` |
|
|
102
|
+
| Edge | `TTS_VOICE_TYPE`, `TTS_VOICE` | `korean_male`, `korean_female`, `korean_multilingual_male`, `english_male`, `english_female`;来自 `edge-tts --list-voices` 的任何 Edge 声音 |
|
|
103
|
+
| Supertonic | `SUPERTONIC_VOICE` | `M1`–`M5`, `F1`–`F5`;设置 `SUPERTONIC_LANGUAGE=ko|en|es|pt|fr` |
|
|
104
|
+
| OpenVoice | `OPENVOICE_REF_AUDIO`, `OPENVOICE_STYLE` | 获准使用的参考 WAV,以及如 `default` 的风格 |
|
|
105
|
+
| SpeechSwift / CosyVoice | `SPEECHSWIFT_REF_AUDIO`, `SPEECHSWIFT_ENGINE`, `SPEECHSWIFT_SPEAKER` | CosyVoice 的参考 WAV,或后端支持的说话人/模型值 |
|
|
106
|
+
|
|
107
|
+
对于 Supertonic 和本地克隆后端,请使用上面的后端 env vars 加 `!voice-test <text>` 来试听更改。语音命令切换目前映射到内置 Edge 风格声音类型;更丰富的后端目录可以添加到 `config/tts-voices.json`。
|
|
95
108
|
|
|
96
|
-
##
|
|
109
|
+
## 长听写和停顿
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
VerbalCoding 会等待一个空闲窗口后再把语音发送给 STT。默认 `UTTERANCE_IDLE_MS=4500` 有意稍微耐心一些,这样长指令中的自然停顿不会拆分句子、过早启动代理轮次,并把剩余语音当作处理期间的打断。
|
|
112
|
+
|
|
113
|
+
如果你偏好更快的短命令,请在 `.env` 中降低它;如果长韩语听写仍被拆分,请提高它:
|
|
99
114
|
|
|
100
115
|
```bash
|
|
101
116
|
UTTERANCE_IDLE_MS="6000"
|
|
102
117
|
```
|
|
103
118
|
|
|
104
|
-
##
|
|
119
|
+
## 详细进度模式
|
|
105
120
|
|
|
106
|
-
|
|
121
|
+
除非设置了 `AGENT_VERBOSE_PROGRESS=1`,详细进度默认关闭。可用 `!verbose on` 或“상세 진행 켜”等语音命令启用。它可以输出如下简短进度行:
|
|
107
122
|
|
|
108
123
|
```text
|
|
109
124
|
🤖 Hermes Agent 호출 시작
|
|
@@ -113,18 +128,28 @@ Enable with `!verbose on`, `AGENT_VERBOSE_PROGRESS=1`, or “상세 진행 켜
|
|
|
113
128
|
🤖 Hermes Agent 응답 수신
|
|
114
129
|
```
|
|
115
130
|
|
|
116
|
-
|
|
131
|
+
该模式会要求所选 CLI 驱动输出 `VERBALCODING_PROGRESS: ...` 行,并在可用时汇总流式 stdout/stderr 中的常见工具标记。看起来像密钥的字段会被脱敏,进度行会从最终朗读答案中移除。
|
|
132
|
+
|
|
133
|
+
## 延迟指标
|
|
134
|
+
|
|
135
|
+
VerbalCoding 会按轮次将延迟记录写为 JSONL。默认路径:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
./.logs/latency.jsonl
|
|
139
|
+
```
|
|
117
140
|
|
|
118
|
-
|
|
141
|
+
每条记录包含状态、总耗时、语音采集时间、发言空闲等待、STT 时间、代理时间、TTS 合成/播放时间、分块数量、转写长度、答案长度,以及可用时的音频电平。
|
|
119
142
|
|
|
120
|
-
|
|
143
|
+
在 Discord 中:
|
|
121
144
|
|
|
122
145
|
```text
|
|
123
146
|
!latency
|
|
124
147
|
!metrics
|
|
125
148
|
```
|
|
126
149
|
|
|
127
|
-
|
|
150
|
+
摘要使用最新 200 条记录:数量、平均值、p95、最大值和非 OK 状态。
|
|
151
|
+
|
|
152
|
+
## 测试
|
|
128
153
|
|
|
129
154
|
```bash
|
|
130
155
|
node --check app-node/main.mjs
|
|
@@ -132,3 +157,5 @@ npm test
|
|
|
132
157
|
bash -n run.sh scripts/install.sh
|
|
133
158
|
vc doctor
|
|
134
159
|
```
|
|
160
|
+
|
|
161
|
+
`vc doctor` 会有意脱敏密钥,并只报告必需值是否已配置。它还会检查 `instances/*.env` 中是否存在重复令牌指纹和冲突的运行时路径。
|
package/package.json
CHANGED
|
@@ -79,13 +79,25 @@ install_system_linux_packages() {
|
|
|
79
79
|
if has_cmd apt-get; then
|
|
80
80
|
log 'Installing Linux packages with apt-get'
|
|
81
81
|
run_sudo apt-get update
|
|
82
|
-
|
|
82
|
+
local packages=(curl ca-certificates git python3 python3-venv python3-pip build-essential cmake pkg-config ffmpeg)
|
|
83
|
+
# npm installs often run under NodeSource Node.js, whose `nodejs` package conflicts
|
|
84
|
+
# with Ubuntu's separate `npm` package. Do not request nodejs/npm again when the
|
|
85
|
+
# current npm-based installer is already running with working node and npm.
|
|
86
|
+
has_cmd node || packages+=(nodejs)
|
|
87
|
+
has_cmd npm || packages+=(npm)
|
|
88
|
+
run_sudo apt-get install -y "${packages[@]}"
|
|
83
89
|
elif has_cmd dnf; then
|
|
84
90
|
log 'Installing Linux packages with dnf'
|
|
85
|
-
|
|
91
|
+
local packages=(curl ca-certificates git python3 python3-pip gcc gcc-c++ make cmake pkgconf-pkg-config ffmpeg)
|
|
92
|
+
has_cmd node || packages+=(nodejs)
|
|
93
|
+
has_cmd npm || packages+=(npm)
|
|
94
|
+
run_sudo dnf install -y "${packages[@]}"
|
|
86
95
|
elif has_cmd pacman; then
|
|
87
96
|
log 'Installing Linux packages with pacman'
|
|
88
|
-
|
|
97
|
+
local packages=(curl ca-certificates git python python-pip base-devel cmake pkgconf ffmpeg)
|
|
98
|
+
has_cmd node || packages+=(nodejs)
|
|
99
|
+
has_cmd npm || packages+=(npm)
|
|
100
|
+
run_sudo pacman -Sy --needed --noconfirm "${packages[@]}"
|
|
89
101
|
else
|
|
90
102
|
warn 'No supported Linux package manager found. Install node/npm, ffmpeg, python3, git, cmake, and a C++ toolchain manually.'
|
|
91
103
|
return 1
|
package/scripts/cli.mjs
CHANGED
|
@@ -292,7 +292,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
292
292
|
}
|
|
293
293
|
if (command === 'doctor') {
|
|
294
294
|
const { spawnSync } = await import('node:child_process');
|
|
295
|
-
const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'doctor.mjs')], { stdio: 'inherit', cwd: ROOT });
|
|
295
|
+
const result = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'doctor.mjs'), ...argv.slice(1)], { stdio: 'inherit', cwd: ROOT });
|
|
296
296
|
process.exitCode = result.status ?? 1;
|
|
297
297
|
return;
|
|
298
298
|
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { checkInstanceConfigs, formatInstanceDoctor } from '../app-node/instance
|
|
|
7
7
|
import { autoRestartVoiceBotEnabled } from '../app-node/restart_policy.mjs';
|
|
8
8
|
|
|
9
9
|
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const autoFixEnabled = !args.includes('--no-fix') && !['0', 'false', 'no', 'off'].includes(String(process.env.VERBALCODING_DOCTOR_AUTO_FIX || '1').toLowerCase());
|
|
12
|
+
let autoFixAttempted = false;
|
|
10
13
|
|
|
11
14
|
function readEnvFile(file) {
|
|
12
15
|
try {
|
|
@@ -25,14 +28,56 @@ function mergeEnv() {
|
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
function quoteEnv(value) {
|
|
32
|
+
return JSON.stringify(String(value ?? ''));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function upsertEnvFile(file, updates) {
|
|
36
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const lines = existing.split(/\r?\n/).map(raw => {
|
|
39
|
+
const line = raw.trim();
|
|
40
|
+
if (!line || line.startsWith('#') || !line.includes('=')) return raw;
|
|
41
|
+
const idx = line.indexOf('=');
|
|
42
|
+
const key = line.slice(0, idx).trim().replace(/^export\s+/, '');
|
|
43
|
+
if (!(key in updates)) return raw;
|
|
44
|
+
seen.add(key);
|
|
45
|
+
return `${key}=${quoteEnv(updates[key])}`;
|
|
46
|
+
});
|
|
47
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
48
|
+
if (!seen.has(key)) lines.push(`${key}=${quoteEnv(value)}`);
|
|
49
|
+
}
|
|
50
|
+
const text = `${lines.filter((line, index, arr) => line !== '' || index < arr.length - 1).join('\n')}\n`;
|
|
51
|
+
fs.writeFileSync(file, text, { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isExecutable(file) {
|
|
55
|
+
try {
|
|
56
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
function commandExists(command) {
|
|
29
64
|
const result = spawnSync('bash', ['-lc', `command -v ${JSON.stringify(command)}`], {
|
|
30
65
|
cwd: ROOT,
|
|
31
66
|
encoding: 'utf8',
|
|
67
|
+
env: { ...process.env, PATH: `${path.join(ROOT, '.local', 'bin')}:${process.env.PATH || ''}` },
|
|
32
68
|
});
|
|
33
69
|
return result.status === 0 ? result.stdout.trim() : '';
|
|
34
70
|
}
|
|
35
71
|
|
|
72
|
+
function resolveCommand(command, fallbackPaths = []) {
|
|
73
|
+
const found = commandExists(command);
|
|
74
|
+
if (found) return found;
|
|
75
|
+
for (const candidate of fallbackPaths) {
|
|
76
|
+
if (isExecutable(candidate)) return candidate;
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
function check(label, ok, detail = '') {
|
|
37
82
|
const mark = ok ? '✓' : '✗';
|
|
38
83
|
console.log(`${mark} ${label}${detail ? ` — ${detail}` : ''}`);
|
|
@@ -43,7 +88,60 @@ function note(label, detail = '') {
|
|
|
43
88
|
console.log(`• ${label}${detail ? ` — ${detail}` : ''}`);
|
|
44
89
|
}
|
|
45
90
|
|
|
46
|
-
|
|
91
|
+
function fixablePrerequisites(env) {
|
|
92
|
+
const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
|
|
93
|
+
const missing = [];
|
|
94
|
+
if (!commandExists('ffmpeg')) missing.push('ffmpeg');
|
|
95
|
+
if (!resolveCommand(env.WHISPER_CPP_BIN || 'whisper-cli', [path.join(ROOT, '.local', 'bin', 'whisper-cli')])) missing.push('whisper-cli');
|
|
96
|
+
const modelPath = path.resolve(ROOT, env.WHISPER_CPP_MODEL || 'models/ggml-small-q5_1.bin');
|
|
97
|
+
if (!fs.existsSync(modelPath)) missing.push('whisper.cpp model');
|
|
98
|
+
if (ttsBackend === 'edge') {
|
|
99
|
+
const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
|
|
100
|
+
if (!resolveCommand(edgeCommand, [path.join(ROOT, '.venv-tts', 'bin', 'edge-tts')])) missing.push('edge-tts');
|
|
101
|
+
}
|
|
102
|
+
return missing;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function persistDiscoveredLocalHelpers(env) {
|
|
106
|
+
const updates = {};
|
|
107
|
+
const localWhisper = path.join(ROOT, '.local', 'bin', 'whisper-cli');
|
|
108
|
+
if (!commandExists(env.WHISPER_CPP_BIN || 'whisper-cli') && isExecutable(localWhisper)) {
|
|
109
|
+
updates.WHISPER_CPP_BIN = localWhisper;
|
|
110
|
+
}
|
|
111
|
+
const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
|
|
112
|
+
const localEdge = path.join(ROOT, '.venv-tts', 'bin', 'edge-tts');
|
|
113
|
+
const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
|
|
114
|
+
if (ttsBackend === 'edge' && !commandExists(edgeCommand) && isExecutable(localEdge)) {
|
|
115
|
+
updates.EDGE_TTS_COMMAND = localEdge;
|
|
116
|
+
}
|
|
117
|
+
if (Object.keys(updates).length > 0) {
|
|
118
|
+
upsertEnvFile(path.join(ROOT, '.env'), updates);
|
|
119
|
+
return updates;
|
|
120
|
+
}
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let env = mergeEnv();
|
|
125
|
+
const missingBeforeFix = fixablePrerequisites(env);
|
|
126
|
+
if (autoFixEnabled && missingBeforeFix.length > 0) {
|
|
127
|
+
console.log(`VerbalCoding doctor: missing ${missingBeforeFix.join(', ')}; running automatic prerequisite bootstrap...`);
|
|
128
|
+
const result = spawnSync('bash', [path.join(ROOT, 'scripts', 'bootstrap_prereqs.sh'), '--yes'], {
|
|
129
|
+
cwd: ROOT,
|
|
130
|
+
stdio: 'inherit',
|
|
131
|
+
env: { ...process.env, VERBALCODING_SKIP_CLI_LINK: process.env.VERBALCODING_SKIP_CLI_LINK || '1' },
|
|
132
|
+
});
|
|
133
|
+
autoFixAttempted = true;
|
|
134
|
+
const persisted = persistDiscoveredLocalHelpers(mergeEnv());
|
|
135
|
+
if (Object.keys(persisted).length > 0) {
|
|
136
|
+
console.log(`Doctor recorded local helper paths in .env: ${Object.keys(persisted).join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
if (result.status !== 0) {
|
|
139
|
+
console.log(`Doctor bootstrap exited with status ${result.status}. Continuing with checks.`);
|
|
140
|
+
}
|
|
141
|
+
console.log('');
|
|
142
|
+
env = mergeEnv();
|
|
143
|
+
}
|
|
144
|
+
|
|
47
145
|
const backend = (env.AGENT_BACKEND || 'hermes').toLowerCase();
|
|
48
146
|
const ttsBackend = (env.TTS_BACKEND || 'edge').toLowerCase();
|
|
49
147
|
let ok = true;
|
|
@@ -52,12 +150,18 @@ console.log('VerbalCoding doctor');
|
|
|
52
150
|
console.log(`Project: ${ROOT}`);
|
|
53
151
|
console.log(`Backend: ${backend}`);
|
|
54
152
|
console.log(`TTS backend: ${ttsBackend}`);
|
|
153
|
+
if (!autoFixEnabled) note('Automatic prerequisite bootstrap', 'off');
|
|
154
|
+
if (autoFixAttempted) note('Automatic prerequisite bootstrap', 'attempted');
|
|
55
155
|
console.log('');
|
|
56
156
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
157
|
+
const nodeCommand = commandExists('node');
|
|
158
|
+
const npmCommand = commandExists('npm');
|
|
159
|
+
const ffmpegCommand = commandExists('ffmpeg');
|
|
160
|
+
const whisperCommand = resolveCommand(env.WHISPER_CPP_BIN || 'whisper-cli', [path.join(ROOT, '.local', 'bin', 'whisper-cli')]);
|
|
161
|
+
ok = check('Node.js', nodeCommand, nodeCommand || 'missing') && ok;
|
|
162
|
+
ok = check('npm', npmCommand, npmCommand || 'missing') && ok;
|
|
163
|
+
ok = check('ffmpeg', ffmpegCommand, ffmpegCommand || 'missing') && ok;
|
|
164
|
+
ok = check('whisper-cli', whisperCommand, whisperCommand || 'missing') && ok;
|
|
61
165
|
|
|
62
166
|
const modelPath = path.resolve(ROOT, env.WHISPER_CPP_MODEL || 'models/ggml-small-q5_1.bin');
|
|
63
167
|
ok = check('whisper.cpp model', fs.existsSync(modelPath), path.relative(ROOT, modelPath)) && ok;
|
|
@@ -77,7 +181,8 @@ if (!['edge', 'openvoice', 'speechswift', 'supertonic'].includes(ttsBackend)) {
|
|
|
77
181
|
}
|
|
78
182
|
if (ttsBackend === 'edge') {
|
|
79
183
|
const edgeCommand = env.EDGE_TTS_COMMAND || env.TTS_EDGE_COMMAND || 'edge-tts';
|
|
80
|
-
|
|
184
|
+
const edgeFound = resolveCommand(edgeCommand, [path.join(ROOT, '.venv-tts', 'bin', 'edge-tts')]);
|
|
185
|
+
ok = check('edge-tts', edgeFound, edgeFound || 'missing') && ok;
|
|
81
186
|
} else if (ttsBackend === 'openvoice') {
|
|
82
187
|
ok = check('Python for OpenVoice', commandExists('python3'), commandExists('python3') || 'missing') && ok;
|
|
83
188
|
const openvoiceDir = path.resolve(ROOT, env.OPENVOICE_DIR || './vendor/OpenVoice');
|
|
@@ -127,8 +232,9 @@ ok = instanceResult.errors.length === 0 && ok;
|
|
|
127
232
|
|
|
128
233
|
console.log('');
|
|
129
234
|
if (ok) {
|
|
130
|
-
console.log('Doctor passed. Run
|
|
235
|
+
console.log('Doctor passed. Run vc start to start VerbalCoding.');
|
|
131
236
|
} else {
|
|
132
|
-
|
|
237
|
+
const suffix = autoFixEnabled ? 'Fix the remaining ✗ items, then rerun vc doctor.' : 'Fix the ✗ items, then rerun vc doctor.';
|
|
238
|
+
console.log(`Doctor found missing prerequisites. ${suffix}`);
|
|
133
239
|
process.exitCode = 1;
|
|
134
240
|
}
|