dlinkclaw-openclaw 2026.8.24-alpha.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.en.md +46 -0
- package/README.md +25 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -0
- package/dist/package.json +95 -0
- package/dist/src/account.d.ts +53 -0
- package/dist/src/account.js +394 -0
- package/dist/src/channel.d.ts +17 -0
- package/dist/src/channel.js +153 -0
- package/dist/src/client/client-manager.d.ts +4 -0
- package/dist/src/client/client-manager.js +10 -0
- package/dist/src/client/const.d.ts +37 -0
- package/dist/src/client/const.js +155 -0
- package/dist/src/client/crypto.d.ts +47 -0
- package/dist/src/client/crypto.js +260 -0
- package/dist/src/client/dl-client.d.ts +99 -0
- package/dist/src/client/dl-client.js +535 -0
- package/dist/src/client/dl-http.d.ts +27 -0
- package/dist/src/client/dl-http.js +223 -0
- package/dist/src/client/media-handler.d.ts +21 -0
- package/dist/src/client/media-handler.js +196 -0
- package/dist/src/client/message.d.ts +96 -0
- package/dist/src/client/message.js +1204 -0
- package/dist/src/client/timeout.d.ts +20 -0
- package/dist/src/client/timeout.js +36 -0
- package/dist/src/client/types.d.ts +224 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/monitor.d.ts +17 -0
- package/dist/src/monitor.js +320 -0
- package/dist/src/outbound.d.ts +4 -0
- package/dist/src/outbound.js +83 -0
- package/dist/src/policy.d.ts +42 -0
- package/dist/src/policy.js +205 -0
- package/dist/src/runtime.d.ts +9 -0
- package/dist/src/runtime.js +65 -0
- package/dist/src/services/index.d.ts +41 -0
- package/dist/src/services/index.js +225 -0
- package/dist/src/services/services.d.ts +152 -0
- package/dist/src/services/services.js +1066 -0
- package/dist/src/services/types.d.ts +391 -0
- package/dist/src/services/types.js +5 -0
- package/dist/src/services/ws-client.d.ts +174 -0
- package/dist/src/services/ws-client.js +866 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.js +128 -0
- package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
- package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
- package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
- package/ext_capabilities/workrelay-speak/README.md +79 -0
- package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-speak/install.sh +280 -0
- package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
- package/ext_capabilities/workrelay-speak/manifest.json +49 -0
- package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
- package/ext_capabilities/workrelay-speak/status.sh +90 -0
- package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
- package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
- package/ext_capabilities/workrelay-speak-luxtts/install.sh +282 -0
- package/ext_capabilities/workrelay-speak-luxtts/manifest.json +49 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/status.sh +94 -0
- package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-transcribe/README.md +76 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/install.sh +207 -0
- package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
- package/ext_capabilities/workrelay-transcribe/status.sh +62 -0
- package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
- package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
- package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
- package/openclaw.plugin.json +142 -0
- package/package.json +95 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "workrelay-speak",
|
|
3
|
+
"displayName": "语音合成(TTS)",
|
|
4
|
+
"description": "Agent 语音回复:Docker 部署 Voicebox(Kokoro 引擎),50+ 预设音色,轻量开箱即用",
|
|
5
|
+
"requirements": [
|
|
6
|
+
{
|
|
7
|
+
"id": "os",
|
|
8
|
+
"label": "操作系统",
|
|
9
|
+
"expect": "Linux",
|
|
10
|
+
"check": "uname -s | grep -qi linux",
|
|
11
|
+
"hint": "TTS 依赖 Docker,仅支持 Linux(Windows 请用 WSL2)"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "docker",
|
|
15
|
+
"label": "Docker 已安装",
|
|
16
|
+
"expect": "已安装 docker",
|
|
17
|
+
"check": "command -v docker >/dev/null 2>&1",
|
|
18
|
+
"hint": "未找到 Docker,请先安装:sudo apt install docker.io docker-compose-v2"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "docker_access",
|
|
22
|
+
"label": "Docker 可访问(权限)",
|
|
23
|
+
"expect": "docker info 可正常执行",
|
|
24
|
+
"check": "docker info >/dev/null 2>&1 || (id -nG | grep -qw docker && sg docker -c 'docker info' >/dev/null 2>&1) || sudo -n docker info >/dev/null 2>&1",
|
|
25
|
+
"hint": "Docker 无权限。一次性解决(需 sudo 密码一次):① sudo systemctl start docker ② sudo usermod -aG docker $USER($USER 换成 openclaw 运行用户,whoami 可查)③ openclaw gateway restart 后重试"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "memory",
|
|
29
|
+
"label": "内存 ≥ 6GB",
|
|
30
|
+
"expect": "≥ 6GB 可用",
|
|
31
|
+
"check": "[ $(free -m | awk '/^Mem:/{print $2}') -ge 6144 ]",
|
|
32
|
+
"hint": "当前内存不足,TTS 至少需要 6GB 内存"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "disk",
|
|
36
|
+
"label": "磁盘 ≥ 10GB",
|
|
37
|
+
"expect": "≥ 10GB 可用",
|
|
38
|
+
"check": "[ $(df -BM \"$HOME\" | tail -1 | awk '{print $4}' | sed 's/M//') -ge 10240 ]",
|
|
39
|
+
"hint": "可用空间不足,镜像+模型约需 6GB,建议预留 10GB"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "network",
|
|
43
|
+
"label": "外网可达(GitHub)",
|
|
44
|
+
"expect": "能访问 github.com",
|
|
45
|
+
"check": "curl -sI --max-time 5 https://github.com >/dev/null 2>&1",
|
|
46
|
+
"hint": "无法访问 GitHub,将无法克隆 Voicebox 仓库和拉取 Docker 镜像"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""WorkRelay TTS: patch Voicebox kokoro_backend.py — 修复中文 pipeline 英文朗读。
|
|
3
|
+
|
|
4
|
+
问题:Kokoro 中文 pipeline 默认 version=None 走 legacy_call,英文文本被原样透传给模型,
|
|
5
|
+
模型把英文字母按日文假名音逐字读出(如 "Hello" -> L.O.H.O.R.Y.O.)。
|
|
6
|
+
|
|
7
|
+
方案:把 zh pipeline 的 g2p 替换为混合 G2P——
|
|
8
|
+
- 中文段:legacy(pypinyin→IPA),Kokoro-82M 认识的音素
|
|
9
|
+
- 英文段:misaki en G2P(→IPA)
|
|
10
|
+
- 纯数字段:cn2an 转中文数字再走 legacy
|
|
11
|
+
三类音素都是 Kokoro-82M 认识的 IPA,中英混读正常。
|
|
12
|
+
|
|
13
|
+
幂等:源码已包含 _MixedZhG2P 标记则跳过。用法:python3 patch-kokoro-en.py <kokoro_backend.py>
|
|
14
|
+
"""
|
|
15
|
+
import ast
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
MARKER = "_MixedZhG2P"
|
|
19
|
+
|
|
20
|
+
ANCHOR = """ self._pipelines[kokoro_lang] = KPipeline(
|
|
21
|
+
lang_code=kokoro_lang,
|
|
22
|
+
repo_id=KOKORO_HF_REPO,
|
|
23
|
+
model=self._model,
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
PATCH = ANCHOR + """
|
|
28
|
+
# 2026-08-13 修复(WorkRelay):Kokoro 中文 pipeline 的 legacy_call 会把英文原样
|
|
29
|
+
# 透传给模型,模型把字母按日文假名音逐字读出(如 Hello -> L.O.H.O.R.Y.O.)。
|
|
30
|
+
# 方案:自定义混合 G2P——中文段走 legacy(pypinyin→IPA),英文段走 misaki en G2P,
|
|
31
|
+
# 纯数字段 cn2an 转中文数字再走 legacy。三类音素都是 Kokoro-82M 认识的 IPA。
|
|
32
|
+
if kokoro_lang == "z":
|
|
33
|
+
from misaki import zh, en
|
|
34
|
+
import jieba
|
|
35
|
+
import re as _re
|
|
36
|
+
import cn2an
|
|
37
|
+
|
|
38
|
+
en_g2p = en.G2P(trf=False, british=False, unk="")
|
|
39
|
+
|
|
40
|
+
class _MixedZhG2P:
|
|
41
|
+
def __call__(self, text):
|
|
42
|
+
def zh_to_ipa(seg):
|
|
43
|
+
words = jieba.lcut(seg, cut_all=False)
|
|
44
|
+
return " ".join(zh.ZHG2P.word2ipa(w) for w in words)
|
|
45
|
+
result = ""
|
|
46
|
+
for segment in _re.findall(r"[\\u4E00-\\u9FFF]+|[^\\u4E00-\\u9FFF]+", text):
|
|
47
|
+
if _re.search(r"[\\u4E00-\\u9FFF]", segment):
|
|
48
|
+
seg = cn2an.transform(segment, "an2cn") if _re.search(r"\\d", segment) else segment
|
|
49
|
+
result += zh_to_ipa(seg)
|
|
50
|
+
elif _re.search(r"[A-Za-z]", segment):
|
|
51
|
+
ps, _ = en_g2p(segment.strip())
|
|
52
|
+
result += ps
|
|
53
|
+
elif _re.search(r"\\d", segment):
|
|
54
|
+
result += zh_to_ipa(cn2an.transform(segment, "an2cn"))
|
|
55
|
+
else:
|
|
56
|
+
result += segment
|
|
57
|
+
return result.replace(chr(815), ""), None
|
|
58
|
+
|
|
59
|
+
self._pipelines[kokoro_lang].g2p = _MixedZhG2P()
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> int:
|
|
64
|
+
if len(sys.argv) != 2:
|
|
65
|
+
print("usage: patch-kokoro-en.py <kokoro_backend.py>", file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
path = sys.argv[1]
|
|
68
|
+
with open(path, encoding="utf-8") as f:
|
|
69
|
+
src = f.read()
|
|
70
|
+
if MARKER in src:
|
|
71
|
+
print("already patched, skip")
|
|
72
|
+
return 0
|
|
73
|
+
if ANCHOR not in src:
|
|
74
|
+
print("ERROR: anchor not found in", path, file=sys.stderr)
|
|
75
|
+
return 1
|
|
76
|
+
new_src = src.replace(ANCHOR, PATCH, 1)
|
|
77
|
+
ast.parse(new_src) # 语法检查
|
|
78
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
79
|
+
f.write(new_src)
|
|
80
|
+
print("patched OK")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
sys.exit(main())
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# WorkRelay 语音合成(TTS)状态检查
|
|
3
|
+
# 返回值: 0=已启用, 1=未安装, 2=部分安装, 3=Docker未运行
|
|
4
|
+
# 同时输出 JSON 给客户端解析
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
STATUS_OK=0
|
|
9
|
+
STATUS_NOT_INSTALLED=1
|
|
10
|
+
STATUS_PARTIAL=2
|
|
11
|
+
STATUS_DOCKER_DOWN=3
|
|
12
|
+
|
|
13
|
+
VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
|
|
14
|
+
VB_PORT="${WR_VOICEBOX_PORT:-17493}"
|
|
15
|
+
VB_DOCKER_PORT="${WR_VOICEBOX_DOCKER_PORT:-17600}"
|
|
16
|
+
|
|
17
|
+
# 2026-08-11 修复:与 install.sh 保持一致——补全 PATH(snap docker)+ docker 权限自适应
|
|
18
|
+
# (当前用户无 docker 组权限时,用免密 sudo 包装 docker,否则状态会误报 Docker 未运行)
|
|
19
|
+
export PATH="$PATH:/snap/bin:/usr/local/bin"
|
|
20
|
+
# 2026-08-18:与 install.sh 保持一致的 docker 权限自适应(sg docker / 免密 sudo)
|
|
21
|
+
if ! docker info &>/dev/null 2>&1; then
|
|
22
|
+
if sg docker -c "docker info" &>/dev/null 2>&1; then
|
|
23
|
+
docker() { sg docker -c "docker $(printf '%q ' "$@")"; }
|
|
24
|
+
elif sudo -n docker info &>/dev/null 2>&1; then
|
|
25
|
+
docker() { sudo docker "$@"; }
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 检测实际端口(优先 17493,fallback 17600)
|
|
30
|
+
check_port=$VB_PORT
|
|
31
|
+
curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1 && check_port=$VB_PORT || {
|
|
32
|
+
curl -s --max-time 2 "http://${VB_HOST}:${VB_DOCKER_PORT}/mcp" >/dev/null 2>&1 && check_port=$VB_DOCKER_PORT
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# 检查1: Docker
|
|
36
|
+
check_docker=0
|
|
37
|
+
docker info &>/dev/null 2>&1 && check_docker=1 || true
|
|
38
|
+
|
|
39
|
+
# 检查2: Voicebox 容器
|
|
40
|
+
check_container=0
|
|
41
|
+
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "voicebox" && check_container=1 || true
|
|
42
|
+
|
|
43
|
+
# 检查3: API
|
|
44
|
+
check_api=0
|
|
45
|
+
curl -s --max-time 3 "http://${VB_HOST}:${check_port}/mcp" >/dev/null 2>&1 && check_api=1 || true
|
|
46
|
+
|
|
47
|
+
# 检查4: TTS 桥接脚本
|
|
48
|
+
check_tts_script=0
|
|
49
|
+
[ -f "$HOME/.workrelay/speak/workrelay-tts.sh" ] && check_tts_script=1 || true
|
|
50
|
+
|
|
51
|
+
# 检查5: OpenClaw TTS 配置
|
|
52
|
+
check_config=0
|
|
53
|
+
if command -v openclaw &>/dev/null; then
|
|
54
|
+
openclaw config get messages.tts.providers.tts-local-cli.command &>/dev/null 2>&1 && check_config=1 || true
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ── 汇总状态 ──
|
|
58
|
+
total=$((check_docker + check_container + check_api + check_tts_script + check_config))
|
|
59
|
+
|
|
60
|
+
case $total in
|
|
61
|
+
5) status="enabled"; desc="语音合成已启用"; exit_code=$STATUS_OK ;;
|
|
62
|
+
0) status="disabled"; desc="未安装"; exit_code=$STATUS_NOT_INSTALLED ;;
|
|
63
|
+
*)
|
|
64
|
+
if [ "$check_container" -eq 0 ] && [ "$check_docker" -eq 1 ]; then
|
|
65
|
+
status="disabled"; desc="Docker 运行中但 Voicebox 未安装"; exit_code=$STATUS_NOT_INSTALLED
|
|
66
|
+
elif [ "$check_container" -eq 1 ] && [ "$check_api" -eq 0 ]; then
|
|
67
|
+
status="partial"; desc="Voicebox 容器存在但 API 未就绪(可能在下载模型)"; exit_code=$STATUS_DOCKER_DOWN
|
|
68
|
+
else
|
|
69
|
+
status="partial"; desc="部分安装"; exit_code=$STATUS_PARTIAL
|
|
70
|
+
fi
|
|
71
|
+
;;
|
|
72
|
+
esac
|
|
73
|
+
|
|
74
|
+
# ── 输出 JSON ──
|
|
75
|
+
cat <<EOF
|
|
76
|
+
{
|
|
77
|
+
"status": "$status",
|
|
78
|
+
"description": "$desc",
|
|
79
|
+
"endpoint": "http://${VB_HOST}:${check_port}",
|
|
80
|
+
"checks": {
|
|
81
|
+
"docker": $( [ $check_docker -eq 1 ] && echo true || echo false ),
|
|
82
|
+
"container": $( [ $check_container -eq 1 ] && echo true || echo false ),
|
|
83
|
+
"api": $( [ $check_api -eq 1 ] && echo true || echo false ),
|
|
84
|
+
"tts_script": $( [ $check_tts_script -eq 1 ] && echo true || echo false ),
|
|
85
|
+
"openclaw_config": $( [ $check_config -eq 1 ] && echo true || echo false )
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
EOF
|
|
89
|
+
|
|
90
|
+
exit $exit_code
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# WorkRelay TTS 桥接脚本 - 调用本地 Voicebox REST API
|
|
3
|
+
# 环境变量:
|
|
4
|
+
# WR_VOICEBOX_HOST - Voicebox 服务地址(默认 127.0.0.1)
|
|
5
|
+
# WR_VOICEBOX_PORT - Voicebox 端口(默认 17493)
|
|
6
|
+
# WR_VOICEBOX_PROFILE_ID - 声音 profile ID(可选,缺省时自动解析:
|
|
7
|
+
# ① ~/.workrelay/speak/profile_id(install.sh 写入)
|
|
8
|
+
# ② 从 Voicebox /profiles 查找 preset profile)
|
|
9
|
+
# WR_VOICEBOX_ENGINE - TTS 引擎(可选,缺省时按 profile 的 default_engine 自动匹配)
|
|
10
|
+
# WR_VOICEBOX_LANG - 语言(默认 zh)
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
TEXT="$1"
|
|
15
|
+
OUT="$2"
|
|
16
|
+
|
|
17
|
+
if [ -z "$TEXT" ] || [ -z "$OUT" ]; then
|
|
18
|
+
echo "usage: $0 <text> <output_path>" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
|
|
23
|
+
VB_PORT="${WR_VOICEBOX_PORT:-17493}"
|
|
24
|
+
VB_DOCKER_PORT="${WR_VOICEBOX_DOCKER_PORT:-17600}"
|
|
25
|
+
PROFILE_ID="${WR_VOICEBOX_PROFILE_ID:-}"
|
|
26
|
+
ENGINE="${WR_VOICEBOX_ENGINE:-}"
|
|
27
|
+
LANG="${WR_VOICEBOX_LANG:-zh}"
|
|
28
|
+
|
|
29
|
+
# 自动探测端口
|
|
30
|
+
BASE_URL="http://${VB_HOST}:${VB_PORT}"
|
|
31
|
+
curl -s --max-time 2 "${BASE_URL}/mcp" >/dev/null 2>&1 || {
|
|
32
|
+
BASE_URL="http://${VB_HOST}:${VB_DOCKER_PORT}"
|
|
33
|
+
curl -s --max-time 2 "${BASE_URL}/mcp" >/dev/null 2>&1 || {
|
|
34
|
+
echo "ERROR: Voicebox API 不可达 ($VB_HOST:$VB_PORT / $VB_DOCKER_PORT)" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# ── 2026-08-12:profile 自动解析(安装后开箱即用,无需手动传 ID)──
|
|
40
|
+
if [ -z "$PROFILE_ID" ] && [ -f "$HOME/.workrelay/speak/profile_id" ]; then
|
|
41
|
+
PROFILE_ID=$(cat "$HOME/.workrelay/speak/profile_id" 2>/dev/null | tr -d ' \n')
|
|
42
|
+
fi
|
|
43
|
+
if [ -z "$PROFILE_ID" ]; then
|
|
44
|
+
# 兜底:从 /profiles 找 preset profile(优先 kokoro + 中文)
|
|
45
|
+
PROFILE_ID=$(curl --noproxy '*' -s --max-time 5 "$BASE_URL/profiles" 2>/dev/null \
|
|
46
|
+
| python3 -c '
|
|
47
|
+
import json,sys
|
|
48
|
+
out=""
|
|
49
|
+
try:
|
|
50
|
+
d=json.load(sys.stdin)
|
|
51
|
+
for p in d:
|
|
52
|
+
if p.get("preset_engine")=="kokoro" and p.get("language")=="zh":
|
|
53
|
+
out=p.get("id",""); break
|
|
54
|
+
if not out:
|
|
55
|
+
for p in d:
|
|
56
|
+
if p.get("voice_type")=="preset":
|
|
57
|
+
out=p.get("id",""); break
|
|
58
|
+
print(out)
|
|
59
|
+
except Exception:
|
|
60
|
+
print("")
|
|
61
|
+
' 2>/dev/null)
|
|
62
|
+
fi
|
|
63
|
+
if [ -z "$PROFILE_ID" ]; then
|
|
64
|
+
echo "ERROR: 未找到可用的语音 profile(Voicebox /profiles 为空)" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ── 2026-08-12:引擎未指定时按 profile 的 default_engine 自动匹配 ──
|
|
69
|
+
if [ -z "$ENGINE" ]; then
|
|
70
|
+
ENGINE=$(curl --noproxy '*' -s --max-time 5 "$BASE_URL/profiles/$PROFILE_ID" 2>/dev/null \
|
|
71
|
+
| python3 -c '
|
|
72
|
+
import json,sys
|
|
73
|
+
try:
|
|
74
|
+
d=json.load(sys.stdin)
|
|
75
|
+
print(d.get("default_engine") or d.get("preset_engine") or "qwen")
|
|
76
|
+
except Exception:
|
|
77
|
+
print("qwen")
|
|
78
|
+
' 2>/dev/null)
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# JSON-escape text
|
|
82
|
+
TEXT_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$TEXT")
|
|
83
|
+
|
|
84
|
+
# 提交生成请求
|
|
85
|
+
RESP=$(curl --noproxy '*' -s --max-time 15 -X POST "$BASE_URL/generate" \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
-H "X-Voicebox-Client-Id: workrelay-tts" \
|
|
88
|
+
-d "{\"text\": $TEXT_JSON, \"profile_id\": \"$PROFILE_ID\", \"language\": \"$LANG\", \"engine\": \"$ENGINE\"}")
|
|
89
|
+
|
|
90
|
+
GEN_ID=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
|
|
91
|
+
if [ -z "$GEN_ID" ]; then
|
|
92
|
+
echo "ERROR: Voicebox 生成提交失败: $RESP" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# 轮询等待完成(CPU 推理约 10-180 秒,视文本长度)
|
|
97
|
+
MAX_POLLS=180
|
|
98
|
+
for i in $(seq 1 $MAX_POLLS); do
|
|
99
|
+
sleep 1
|
|
100
|
+
ST=$(curl --noproxy '*' -s --max-time 10 "$BASE_URL/history/$GEN_ID" 2>/dev/null \
|
|
101
|
+
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("status",""))' 2>/dev/null)
|
|
102
|
+
if [ "$ST" = "completed" ]; then
|
|
103
|
+
break
|
|
104
|
+
fi
|
|
105
|
+
if [ "$ST" = "error" ] || [ "$ST" = "failed" ]; then
|
|
106
|
+
echo "ERROR: Voicebox 生成失败 (id=$GEN_ID, status=$ST)" >&2
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
# 下载音频
|
|
112
|
+
curl --noproxy '*' -s --max-time 30 -o "$OUT" "$BASE_URL/audio/$GEN_ID"
|
|
113
|
+
if [ -s "$OUT" ]; then
|
|
114
|
+
exit 0
|
|
115
|
+
else
|
|
116
|
+
echo "ERROR: Voicebox 音频下载失败 (id=$GEN_ID)" >&2
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# WorkRelay 语音合成(LuxTTS 克隆声)
|
|
2
|
+
|
|
3
|
+
Agent 语音回复能力:Docker 部署 Voicebox(**LuxTTS 引擎**),内置**石头哥克隆声音**,音色自然。
|
|
4
|
+
|
|
5
|
+
## 特点
|
|
6
|
+
|
|
7
|
+
- **独立容器**:容器名 `voicebox-luxtts`,端口 `17601`,与 Kokoro 版(`voicebox` / 17493)**互不干扰,可同时安装**
|
|
8
|
+
- **克隆声音**:安装时自动创建克隆 profile(内置 4 个声音样本),开箱即有"石头哥克隆声"
|
|
9
|
+
- **引擎固定**:LuxTTS 为纯克隆声音引擎(无预设音色),桥接脚本固定 `engine=luxtts`
|
|
10
|
+
- **生产可用**:镜像含 LuxTTS 模型(1.1G),拉取即用,无需再下载模型
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
扩展能力页 → 语音合成(LuxTTS 克隆声)→ 启用,或手动执行:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bash install.sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
前置条件:Linux + Docker(6GB 内存、10GB 磁盘)。
|
|
21
|
+
|
|
22
|
+
## 使用
|
|
23
|
+
|
|
24
|
+
安装后 OpenClaw 自动配置 `messages.tts.provider=tts-local-cli-luxtts`,
|
|
25
|
+
桥接脚本 `~/.workrelay/speak-luxtts/workrelay-tts.sh` 调用 `http://127.0.0.1:17601`。
|
|
26
|
+
|
|
27
|
+
声音 profile 记录在 `~/.workrelay/speak-luxtts/profile_id`,可用环境变量覆盖:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
WR_VOICEBOX_PROFILE_ID=<profile_id> # 指定其它克隆声音
|
|
31
|
+
WR_VOICEBOX_LUXTTS_PORT=17601 # 覆盖端口
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 与 Kokoro 版(workrelay-speak)关系
|
|
35
|
+
|
|
36
|
+
| 项 | workrelay-speak | workrelay-speak-luxtts |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| 容器名 | voicebox | voicebox-luxtts |
|
|
39
|
+
| 端口 | 17493/17600 | 17601 |
|
|
40
|
+
| 引擎 | Kokoro-82M | LuxTTS |
|
|
41
|
+
| 声音 | 50+ 预设音色 | 石头哥克隆声(可换) |
|
|
42
|
+
| OpenClaw provider | tts-local-cli | tts-local-cli-luxtts |
|
|
43
|
+
|
|
44
|
+
两者可共存,OpenClaw 当前生效的 TTS 由 `messages.tts.provider` 决定(后启用者生效)。
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# WorkRelay 语音合成(LuxTTS 克隆声)一键安装脚本
|
|
3
|
+
# 独立容器 voicebox-luxtts(端口 17601),拉取含 LuxTTS 模型的阿里镜像,
|
|
4
|
+
# 自动创建"石头哥克隆声音"profile(内置样本),配置 OpenClaw TTS。
|
|
5
|
+
# 2026-08-15 新增能力:与 workrelay-speak(Kokoro)互不干扰,可同时安装。
|
|
6
|
+
set -eo pipefail
|
|
7
|
+
|
|
8
|
+
RED='\033[0;31m'
|
|
9
|
+
GREEN='\033[0;32m'
|
|
10
|
+
YELLOW='\033[1;33m'
|
|
11
|
+
NC='\033[0m'
|
|
12
|
+
|
|
13
|
+
VB_DIR="$HOME/.workrelay/voicebox-luxtts"
|
|
14
|
+
VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
|
|
15
|
+
# 独立端口 17601(Kokoro 版占用 17493/17600,避免冲突)
|
|
16
|
+
VB_PORT="${WR_VOICEBOX_LUXTTS_PORT:-17601}"
|
|
17
|
+
VB_CONTAINER_PORT=17493
|
|
18
|
+
# 阿里云镜像:石头哥推送后使用(含 LuxTTS 模型 + Kokoro + 英文补丁)
|
|
19
|
+
VB_IMAGE="${WR_VOICEBOX_IMAGE:-crpi-3zj44dlldca7vawm.cn-hangzhou.personal.cr.aliyuncs.com/shiduo/workrelay-tts-luxtts:20260815}"
|
|
20
|
+
INSTALL_DIR="$(dirname "$(readlink -f "$0")")"
|
|
21
|
+
|
|
22
|
+
echo "============================================"
|
|
23
|
+
echo " WorkRelay 语音合成(LuxTTS 克隆声)- 一键安装"
|
|
24
|
+
echo "============================================"
|
|
25
|
+
echo ""
|
|
26
|
+
|
|
27
|
+
# ── 系统检查 ──
|
|
28
|
+
echo -n "[1/6] 检查系统环境... "
|
|
29
|
+
|
|
30
|
+
OS="$(uname -s)"
|
|
31
|
+
case "$OS" in
|
|
32
|
+
Linux) ;;
|
|
33
|
+
*)
|
|
34
|
+
echo -e "${RED}失败${NC}"
|
|
35
|
+
echo "错误:当前仅支持 Linux(TTS 需要 Docker)"
|
|
36
|
+
exit 1
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
39
|
+
|
|
40
|
+
export PATH="$PATH:/snap/bin:/usr/local/bin"
|
|
41
|
+
|
|
42
|
+
if ! command -v docker &>/dev/null; then
|
|
43
|
+
echo -e "${RED}失败${NC}"
|
|
44
|
+
echo "错误:未找到 Docker,请先安装 Docker"
|
|
45
|
+
echo " Ubuntu: sudo apt install docker.io docker-compose-v2"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if ! docker info &>/dev/null 2>&1; then
|
|
50
|
+
# 2026-08-18 增强:docker 权限多级自适应(尽量零 sudo,任何机器可用):
|
|
51
|
+
# ① 用户已在 docker 组(含会话未刷新)→ sg docker 切组执行
|
|
52
|
+
# ② 免密 sudo(sudo -n)可用 → sudo 包装
|
|
53
|
+
# ③ 都不行 → 给出一键修复指引,而不是干失败
|
|
54
|
+
if sg docker -c "docker info" &>/dev/null 2>&1; then
|
|
55
|
+
echo " ℹ 用户已加入 docker 组,通过 sg docker 执行(无需 sudo)"
|
|
56
|
+
docker() { sg docker -c "docker $(printf '%q ' "$@")"; }
|
|
57
|
+
elif sudo -n docker info &>/dev/null 2>&1; then
|
|
58
|
+
echo " ⚠ 当前用户无 docker 组权限,检测到免密 sudo,自动使用 sudo 执行 docker"
|
|
59
|
+
docker() { sudo docker "$@"; }
|
|
60
|
+
else
|
|
61
|
+
echo -e "${RED}失败${NC}"
|
|
62
|
+
echo "错误:Docker 未运行或无权限。请先执行一次以下命令:"
|
|
63
|
+
echo ""
|
|
64
|
+
echo " ① 启动 Docker:sudo systemctl start docker"
|
|
65
|
+
echo " ② 把当前用户加入 docker 组(避免以后每次要密码):"
|
|
66
|
+
echo " sudo usermod -aG docker \$USER"
|
|
67
|
+
echo " ③ 重启 openclaw 服务(或重新登录会话),然后重新安装"
|
|
68
|
+
echo ""
|
|
69
|
+
echo " 备选:为当前用户配置免密 sudo(sudo visudo 添加:\$USER ALL=(ALL) NOPASSWD: ALL)"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo -e "${GREEN}OK${NC} ($(docker --version | head -1), $(uname -o))"
|
|
75
|
+
|
|
76
|
+
# snap docker 项目目录适配(与 workrelay-speak 一致)
|
|
77
|
+
if [ -d /var/snap/docker/common ]; then
|
|
78
|
+
VB_DIR="/var/snap/docker/common/workrelay-voicebox-luxtts"
|
|
79
|
+
echo " ℹ 检测到 snap docker,项目目录使用 $VB_DIR(AppArmor 放行读写)"
|
|
80
|
+
# 2026-08-18:无免密 sudo 时给出手动命令指引(不依赖 sudo 密码)。
|
|
81
|
+
if [ -d "$VB_DIR" ]; then
|
|
82
|
+
sudo -n chown -R "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
|
|
83
|
+
elif ! sudo -n mkdir -p "$VB_DIR" 2>/dev/null; then
|
|
84
|
+
echo -e "${RED}失败${NC}"
|
|
85
|
+
echo "错误:无法创建项目目录 $VB_DIR(需要 root 权限)。"
|
|
86
|
+
echo "请手动执行一次以下命令后重新安装:"
|
|
87
|
+
echo " sudo mkdir -p $VB_DIR && sudo chown \$USER:\$USER $VB_DIR"
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
sudo -n chown "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# ── 磁盘空间 ──
|
|
94
|
+
echo -n "[2/6] 检查磁盘空间... "
|
|
95
|
+
DF_TARGET="$VB_DIR"; [ -d "$DF_TARGET" ] || DF_TARGET="$HOME"
|
|
96
|
+
AVAIL=$(df -BM "$DF_TARGET" | tail -1 | awk '{print $4}' | sed 's/M//')
|
|
97
|
+
if [ "$AVAIL" -lt 10000 ]; then
|
|
98
|
+
echo -e "${RED}不足${NC}"
|
|
99
|
+
echo "错误:可用空间 ${AVAIL}MB,至少需要 10GB(Docker 镜像 + 模型 ~6GB)"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
echo -e "${GREEN}OK${NC} (可用 ${AVAIL}MB)"
|
|
103
|
+
|
|
104
|
+
# ── 内存检查 ──
|
|
105
|
+
echo -n "[3/6] 检查内存... "
|
|
106
|
+
RAM=$(free -m | awk '/^Mem:/{print $2}')
|
|
107
|
+
if [ "$RAM" -lt 6000 ]; then
|
|
108
|
+
echo -e "${RED}不足 (${RAM}MB)${NC}"
|
|
109
|
+
echo "错误:至少需要 6GB 内存,当前仅 ${RAM}MB"
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
echo -e "${GREEN}OK${NC} (${RAM}MB)"
|
|
113
|
+
|
|
114
|
+
# ── 端口检查 ──
|
|
115
|
+
echo -n "[4/6] 检查端口 $VB_PORT... "
|
|
116
|
+
if ss -tlnp 2>/dev/null | grep -q ":$VB_PORT "; then
|
|
117
|
+
echo -e "${RED}已被占用${NC}"
|
|
118
|
+
echo "错误:端口 $VB_PORT 已被占用,请先停止占用进程或设置 WR_VOICEBOX_LUXTTS_PORT 换端口"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
echo -e "${GREEN}空闲${NC}"
|
|
122
|
+
|
|
123
|
+
# ── 拉取镜像 ──
|
|
124
|
+
echo -n "[5/6] 拉取 Voicebox (LuxTTS) 镜像... "
|
|
125
|
+
mkdir -p "$VB_DIR"
|
|
126
|
+
if docker pull "$VB_IMAGE" > /tmp/wr-pull-luxtts.log 2>&1; then
|
|
127
|
+
echo -e "${GREEN}OK${NC} ($(docker images --format '{{.Repository}}:{{.Tag}}' "$VB_IMAGE" | head -1))"
|
|
128
|
+
else
|
|
129
|
+
echo -e "${RED}失败${NC}"
|
|
130
|
+
tail -5 /tmp/wr-pull-luxtts.log
|
|
131
|
+
echo "错误:拉取镜像失败,检查网络连接"
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# ── 启动容器 ──
|
|
136
|
+
echo "[6/6] 启动 Voicebox (LuxTTS) 容器..."
|
|
137
|
+
|
|
138
|
+
mkdir -p "$VB_DIR/output"
|
|
139
|
+
chmod 777 "$VB_DIR/output" 2>/dev/null || true
|
|
140
|
+
|
|
141
|
+
docker rm -f voicebox-luxtts >/dev/null 2>&1 || true
|
|
142
|
+
if ! docker run -d --name voicebox-luxtts --restart unless-stopped \
|
|
143
|
+
-p "127.0.0.1:${VB_PORT}:${VB_CONTAINER_PORT}" \
|
|
144
|
+
-v "$VB_DIR/output:/app/data/generations" \
|
|
145
|
+
-e LOG_LEVEL=info \
|
|
146
|
+
-e NUMBA_CACHE_DIR=/tmp/numba_cache \
|
|
147
|
+
--cpus 4 --memory 8g \
|
|
148
|
+
"$VB_IMAGE" >/dev/null 2>&1; then
|
|
149
|
+
echo -e "${RED}失败${NC}"
|
|
150
|
+
echo "错误:docker run 失败,执行 docker logs voicebox-luxtts 查看日志"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
echo " ✅ docker run 启动成功"
|
|
154
|
+
|
|
155
|
+
# 2026-08-15 权限兜底:snap docker 的 bind mount 目录在容器内可能显示 root:root 755,
|
|
156
|
+
# voicebox 用户(uid 999)无法写入 /app/data/generations 导致生成保存失败。
|
|
157
|
+
# 容器内 chmod 777 兜底(docker run 后立即执行,幂等)。
|
|
158
|
+
docker exec voicebox-luxtts chmod 777 /app/data/generations 2>/dev/null || true
|
|
159
|
+
|
|
160
|
+
# 等待 API 就绪
|
|
161
|
+
echo -n " 等待 Voicebox API 就绪..."
|
|
162
|
+
for i in $(seq 1 90); do
|
|
163
|
+
if curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1; then
|
|
164
|
+
echo -e " ${GREEN}OK${NC} (耗时 ${i} 次检查)"
|
|
165
|
+
break
|
|
166
|
+
fi
|
|
167
|
+
sleep 5
|
|
168
|
+
echo -n "."
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
if ! curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1; then
|
|
172
|
+
echo ""
|
|
173
|
+
echo -e "${RED}Voicebox 启动失败${NC}"
|
|
174
|
+
echo " 检查日志: docker logs voicebox-luxtts"
|
|
175
|
+
exit 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# ── 创建石头哥克隆声音 profile ──
|
|
179
|
+
echo " ⚙ 配置克隆声音(石头哥克隆声)..."
|
|
180
|
+
|
|
181
|
+
mkdir -p "$HOME/.workrelay/speak-luxtts"
|
|
182
|
+
PROFILE_FILE="$HOME/.workrelay/speak-luxtts/profile_id"
|
|
183
|
+
SAMPLE_DIR="$INSTALL_DIR/samples"
|
|
184
|
+
|
|
185
|
+
# 查已存在的同款克隆 profile(按名字匹配,避免重复创建)
|
|
186
|
+
EXIST_ID=$(curl --noproxy '*' -s --max-time 5 "http://${VB_HOST}:${VB_PORT}/profiles" 2>/dev/null \
|
|
187
|
+
| python3 -c '
|
|
188
|
+
import json,sys
|
|
189
|
+
try:
|
|
190
|
+
d=json.load(sys.stdin)
|
|
191
|
+
for p in d:
|
|
192
|
+
if p.get("name")=="石头哥克隆声" and p.get("voice_type")=="cloned":
|
|
193
|
+
print(p.get("id","")); break
|
|
194
|
+
except Exception:
|
|
195
|
+
print("")
|
|
196
|
+
' 2>/dev/null)
|
|
197
|
+
|
|
198
|
+
if [ -z "$EXIST_ID" ]; then
|
|
199
|
+
# 创建克隆 profile
|
|
200
|
+
EXIST_ID=$(curl --noproxy '*' -s --max-time 10 -X POST "http://${VB_HOST}:${VB_PORT}/profiles" \
|
|
201
|
+
-H "Content-Type: application/json" \
|
|
202
|
+
-d '{"name":"石头哥克隆声","description":"WorkRelay LuxTTS 内置克隆声音(石头哥)","language":"zh","voice_type":"cloned"}' \
|
|
203
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if [ -n "$EXIST_ID" ]; then
|
|
207
|
+
# 上传样本(file + reference_text 多文件)
|
|
208
|
+
SAMPLE_COUNT=0
|
|
209
|
+
while IFS='|' read -r fname reftext; do
|
|
210
|
+
[ -z "$fname" ] && continue
|
|
211
|
+
fpath="$SAMPLE_DIR/$fname"
|
|
212
|
+
if [ -f "$fpath" ]; then
|
|
213
|
+
if curl --noproxy '*' -s --max-time 15 -X POST "http://${VB_HOST}:${VB_PORT}/profiles/${EXIST_ID}/samples" \
|
|
214
|
+
-F "file=@${fpath}" -F "reference_text=${reftext}" \
|
|
215
|
+
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("ok" if d.get("id") else "fail")' 2>/dev/null | grep -q ok; then
|
|
216
|
+
SAMPLE_COUNT=$((SAMPLE_COUNT+1))
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
done < "$SAMPLE_DIR/reference_text.txt"
|
|
220
|
+
echo " ✅ 克隆声音 profile 已就绪(${SAMPLE_COUNT} 个样本)"
|
|
221
|
+
echo "$EXIST_ID" > "$PROFILE_FILE"
|
|
222
|
+
else
|
|
223
|
+
echo " ⚠ 克隆 profile 创建失败,可手动设 WR_VOICEBOX_PROFILE_ID 指定声音"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# ── 部署脚本 & 配置 OpenClaw ──
|
|
227
|
+
echo -n "[收尾] 部署 TTS 桥接脚本 & 配置 OpenClaw... "
|
|
228
|
+
|
|
229
|
+
mkdir -p "$HOME/.workrelay/speak-luxtts"
|
|
230
|
+
cp "$INSTALL_DIR/workrelay-tts.sh" "$HOME/.workrelay/speak-luxtts/"
|
|
231
|
+
chmod +x "$HOME/.workrelay/speak-luxtts/workrelay-tts.sh"
|
|
232
|
+
cp "$INSTALL_DIR/README.md" "$HOME/.workrelay/speak-luxtts/" 2>/dev/null || true
|
|
233
|
+
|
|
234
|
+
if command -v openclaw &>/dev/null; then
|
|
235
|
+
TTS_SCRIPT="$HOME/.workrelay/speak-luxtts/workrelay-tts.sh"
|
|
236
|
+
# 独立 provider 名(tts-local-cli-luxtts),与 Kokoro 版 tts-local-cli 互不覆盖
|
|
237
|
+
openclaw config set messages.tts.provider tts-local-cli-luxtts 2>/dev/null || true
|
|
238
|
+
openclaw config set messages.tts.providers.tts-local-cli-luxtts.command "$TTS_SCRIPT" 2>/dev/null || true
|
|
239
|
+
openclaw config set messages.tts.providers.tts-local-cli-luxtts.args '["{{Text}}","{{OutputPath}}"]' 2>/dev/null || true
|
|
240
|
+
openclaw config set messages.tts.providers.tts-local-cli-luxtts.outputFormat wav 2>/dev/null || true
|
|
241
|
+
openclaw config set messages.tts.providers.tts-local-cli-luxtts.timeoutMs 180000 2>/dev/null || true
|
|
242
|
+
|
|
243
|
+
# MCP 配置(独立 server 名 voicebox-luxtts)
|
|
244
|
+
openclaw config set mcp.servers.voicebox-luxtts.url "http://${VB_HOST}:${VB_PORT}/mcp" 2>/dev/null || true
|
|
245
|
+
openclaw config set mcp.servers.voicebox-luxtts.transport streamable-http 2>/dev/null || true
|
|
246
|
+
openclaw config set mcp.servers.voicebox-luxtts.timeout 60 2>/dev/null || true
|
|
247
|
+
|
|
248
|
+
openclaw gateway restart 2>/dev/null || true
|
|
249
|
+
else
|
|
250
|
+
echo -e "${YELLOW}⚠ 未检测到 openclaw CLI,跳过自动配置${NC}"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
echo -e "${GREEN}OK${NC}"
|
|
254
|
+
echo ""
|
|
255
|
+
|
|
256
|
+
# ── 验证 ──
|
|
257
|
+
echo "============================================"
|
|
258
|
+
echo " 安装验证"
|
|
259
|
+
echo "============================================"
|
|
260
|
+
|
|
261
|
+
echo -n " Docker 容器: "
|
|
262
|
+
docker ps --format '{{.Names}}' 2>/dev/null | grep -q voicebox-luxtts && echo -e "${GREEN}运行中${NC}" || echo -e "${RED}未运行${NC}"
|
|
263
|
+
|
|
264
|
+
echo -n " API 连通性: "
|
|
265
|
+
curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1 \
|
|
266
|
+
&& echo -e "${GREEN}OK${NC} (http://${VB_HOST}:${VB_PORT})" \
|
|
267
|
+
|| echo -e "${RED}无法连接${NC}"
|
|
268
|
+
|
|
269
|
+
echo -n " TTS 桥接脚本: "
|
|
270
|
+
[ -f "$HOME/.workrelay/speak-luxtts/workrelay-tts.sh" ] && echo -e "${GREEN}已部署${NC}" || echo -e "${RED}缺失${NC}"
|
|
271
|
+
|
|
272
|
+
echo -n " 克隆声音 profile: "
|
|
273
|
+
[ -f "$PROFILE_FILE" ] && echo -e "${GREEN}$(cat "$PROFILE_FILE")${NC}" || echo -e "${RED}缺失${NC}"
|
|
274
|
+
|
|
275
|
+
echo ""
|
|
276
|
+
echo -e "${GREEN}✅ Voicebox (LuxTTS) 安装完成!${NC}"
|
|
277
|
+
echo " API 地址: http://${VB_HOST}:${VB_PORT}"
|
|
278
|
+
echo " 管理命令: docker ps | grep voicebox-luxtts"
|
|
279
|
+
echo " 查看日志: docker logs -f voicebox-luxtts"
|
|
280
|
+
echo ""
|
|
281
|
+
echo " 🎙️ 声音:内置石头哥克隆声(LuxTTS 引擎)"
|
|
282
|
+
echo ""
|