dlinkclaw-openclaw 1.0.0

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.
Files changed (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +46 -0
  3. package/README.md +15 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +1 -0
  6. package/dist/package.json +95 -0
  7. package/dist/src/account.d.ts +54 -0
  8. package/dist/src/account.js +394 -0
  9. package/dist/src/channel.d.ts +17 -0
  10. package/dist/src/channel.js +153 -0
  11. package/dist/src/client/client-manager.d.ts +4 -0
  12. package/dist/src/client/client-manager.js +10 -0
  13. package/dist/src/client/const.d.ts +37 -0
  14. package/dist/src/client/const.js +155 -0
  15. package/dist/src/client/crypto.d.ts +47 -0
  16. package/dist/src/client/crypto.js +260 -0
  17. package/dist/src/client/dl-client.d.ts +117 -0
  18. package/dist/src/client/dl-client.js +588 -0
  19. package/dist/src/client/dl-http.d.ts +27 -0
  20. package/dist/src/client/dl-http.js +223 -0
  21. package/dist/src/client/media-handler.d.ts +21 -0
  22. package/dist/src/client/media-handler.js +196 -0
  23. package/dist/src/client/message.d.ts +96 -0
  24. package/dist/src/client/message.js +1211 -0
  25. package/dist/src/client/timeout.d.ts +20 -0
  26. package/dist/src/client/timeout.js +36 -0
  27. package/dist/src/client/types.d.ts +226 -0
  28. package/dist/src/client/types.js +1 -0
  29. package/dist/src/monitor.d.ts +18 -0
  30. package/dist/src/monitor.js +322 -0
  31. package/dist/src/outbound.d.ts +6 -0
  32. package/dist/src/outbound.js +78 -0
  33. package/dist/src/policy.d.ts +42 -0
  34. package/dist/src/policy.js +216 -0
  35. package/dist/src/runtime.d.ts +9 -0
  36. package/dist/src/runtime.js +65 -0
  37. package/dist/src/services/gateway-rpc.d.ts +37 -0
  38. package/dist/src/services/gateway-rpc.js +37 -0
  39. package/dist/src/services/index.d.ts +41 -0
  40. package/dist/src/services/index.js +245 -0
  41. package/dist/src/services/services.d.ts +169 -0
  42. package/dist/src/services/services.js +1160 -0
  43. package/dist/src/services/types.d.ts +391 -0
  44. package/dist/src/services/types.js +5 -0
  45. package/dist/src/services/ws-client.d.ts +186 -0
  46. package/dist/src/services/ws-client.js +896 -0
  47. package/dist/src/utils.d.ts +20 -0
  48. package/dist/src/utils.js +128 -0
  49. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  50. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  51. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  52. package/ext_capabilities/workrelay-speak/README.md +79 -0
  53. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  54. package/ext_capabilities/workrelay-speak/install.sh +329 -0
  55. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  56. package/ext_capabilities/workrelay-speak/manifest.json +63 -0
  57. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  58. package/ext_capabilities/workrelay-speak/status.sh +111 -0
  59. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  67. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  68. package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
  69. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  70. package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
  71. package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
  72. package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
  73. package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
  74. package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
  75. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  76. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  77. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  78. package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
  79. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  80. package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
  81. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  82. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  83. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  84. package/openclaw.plugin.json +142 -0
  85. package/package.json +95 -0
@@ -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,330 @@
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
+ # 2026-09-14 双源:国内=阿里云公共仓库;国际=GitHub Container Registry (ghcr.io)
20
+ # 2026-09-14 改名为 dlinkclaw-tts(统一仓库名,tag 区分模型)
21
+ IMG_TAG="${WR_IMAGE_TAG:-luxtts-20260914}"
22
+ # 国内源:暂未发布(留空自动跳过);发布后填入 registry.cn-hangzhou.aliyuncs.com/NAMESPACE/dlinkclaw-tts:${IMG_TAG}
23
+ IMG_CN="${WR_IMAGE_CN:-}"
24
+ IMG_INTL="${WR_IMAGE_INTL:-ghcr.io/rockychen2016/dlinkclaw-tts:${IMG_TAG}}"
25
+ VB_IMAGE=""
26
+ INSTALL_DIR="$(dirname "$(readlink -f "$0")")"
27
+
28
+ # ── 2026-09-14 镜像双源(国内=阿里云公共仓库 / 国际=ghcr.io)──
29
+ # 按 WR_IMAGE_REGION(auto|cn|intl)优先尝试,失败自动回退另一源;WR_VOICEBOX_IMAGE 可显式覆盖。
30
+ IMAGE_CANDIDATES=()
31
+ if [ -n "${WR_VOICEBOX_IMAGE:-}" ]; then
32
+ IMAGE_CANDIDATES=("$WR_VOICEBOX_IMAGE")
33
+ else
34
+ # 2026-09-14:国内源暂未发布(IMG_CN 为空则自动跳过),默认国际源优先;国内源上架后自动生效
35
+ case "${WR_IMAGE_REGION:-auto}" in
36
+ cn) CAND_PRIMARY="$IMG_CN"; CAND_SECONDARY="$IMG_INTL" ;;
37
+ intl) CAND_PRIMARY="$IMG_INTL"; CAND_SECONDARY="$IMG_CN" ;;
38
+ *) CAND_PRIMARY="$IMG_INTL"; CAND_SECONDARY="$IMG_CN" ;;
39
+ esac
40
+ [ -n "$CAND_PRIMARY" ] && IMAGE_CANDIDATES+=("$CAND_PRIMARY")
41
+ [ -n "$CAND_SECONDARY" ] && IMAGE_CANDIDATES+=("$CAND_SECONDARY")
42
+ fi
43
+
44
+ pull_image_with_fallback() {
45
+ local log="$1"
46
+ for img in "${IMAGE_CANDIDATES[@]}"; do
47
+ echo " → 尝试镜像源: $img"
48
+ if docker pull "$img" > "$log" 2>&1; then
49
+ VB_IMAGE="$img"
50
+ return 0
51
+ fi
52
+ echo " ⚠ 该源拉取失败,尝试下一个..."
53
+ done
54
+ return 1
55
+ }
56
+
57
+
58
+ echo "============================================"
59
+ echo " WorkRelay 语音合成(LuxTTS 克隆声)- 一键安装"
60
+ echo "============================================"
61
+ echo ""
62
+
63
+ # ── 系统检查 ──
64
+ echo -n "[1/6] 检查系统环境... "
65
+
66
+ OS="$(uname -s)"
67
+ case "$OS" in
68
+ Linux) ;;
69
+ *)
70
+ echo -e "${RED}失败${NC}"
71
+ echo "错误:当前仅支持 Linux(TTS 需要 Docker)"
72
+ exit 1
73
+ ;;
74
+ esac
75
+
76
+ export PATH="$PATH:/snap/bin:/usr/local/bin"
77
+
78
+ if ! command -v docker &>/dev/null; then
79
+ echo -e "${RED}失败${NC}"
80
+ echo "错误:未找到 Docker,请先安装 Docker"
81
+ echo " Ubuntu: sudo apt install docker.io docker-compose-v2"
82
+ exit 1
83
+ fi
84
+
85
+ if ! docker info &>/dev/null 2>&1; then
86
+ # 2026-08-18 增强:docker 权限多级自适应(尽量零 sudo,任何机器可用):
87
+ # ① 用户已在 docker 组(含会话未刷新)→ sg docker 切组执行
88
+ # ② 免密 sudo(sudo -n)可用 → sudo 包装
89
+ # ③ 都不行 → 给出一键修复指引,而不是干失败
90
+ if sg docker -c "docker info" &>/dev/null 2>&1; then
91
+ echo " ℹ 用户已加入 docker 组,通过 sg docker 执行(无需 sudo)"
92
+ docker() { sg docker -c "docker $(printf '%q ' "$@")"; }
93
+ elif sudo -n docker info &>/dev/null 2>&1; then
94
+ echo " ⚠ 当前用户无 docker 组权限,检测到免密 sudo,自动使用 sudo 执行 docker"
95
+ docker() { sudo docker "$@"; }
96
+ else
97
+ echo -e "${RED}失败${NC}"
98
+ echo "错误:Docker 未运行或无权限。请先执行一次以下命令:"
99
+ echo ""
100
+ echo " ① 启动 Docker:sudo systemctl start docker"
101
+ echo " ② 把当前用户加入 docker 组(避免以后每次要密码):"
102
+ echo " sudo usermod -aG docker \$USER"
103
+ echo " ③ 重启 openclaw 服务(或重新登录会话),然后重新安装"
104
+ echo ""
105
+ echo " 备选:为当前用户配置免密 sudo(sudo visudo 添加:\$USER ALL=(ALL) NOPASSWD: ALL)"
106
+ exit 1
107
+ fi
108
+ fi
109
+
110
+ echo -e "${GREEN}OK${NC} ($(docker --version | head -1), $(uname -o))"
111
+
112
+ # snap docker 项目目录适配(与 workrelay-speak 一致)
113
+ if [ -d /var/snap/docker/common ]; then
114
+ VB_DIR="/var/snap/docker/common/workrelay-voicebox-luxtts"
115
+ echo " ℹ 检测到 snap docker,项目目录使用 $VB_DIR(AppArmor 放行读写)"
116
+ # 2026-08-18:无免密 sudo 时给出手动命令指引(不依赖 sudo 密码)。
117
+ if [ -d "$VB_DIR" ]; then
118
+ sudo -n chown -R "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
119
+ elif ! sudo -n mkdir -p "$VB_DIR" 2>/dev/null; then
120
+ echo -e "${RED}失败${NC}"
121
+ echo "错误:无法创建项目目录 $VB_DIR(需要 root 权限)。"
122
+ echo "请手动执行一次以下命令后重新安装:"
123
+ echo " sudo mkdir -p $VB_DIR && sudo chown \$USER:\$USER $VB_DIR"
124
+ exit 1
125
+ fi
126
+ sudo -n chown "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
127
+ fi
128
+
129
+ # ── 磁盘空间 ──
130
+ echo -n "[2/6] 检查磁盘空间... "
131
+ DF_TARGET="$VB_DIR"; [ -d "$DF_TARGET" ] || DF_TARGET="$HOME"
132
+ AVAIL=$(df -BM "$DF_TARGET" | tail -1 | awk '{print $4}' | sed 's/M//')
133
+ if [ "$AVAIL" -lt 10000 ]; then
134
+ echo -e "${RED}不足${NC}"
135
+ echo "错误:可用空间 ${AVAIL}MB,至少需要 10GB(Docker 镜像 + 模型 ~6GB)"
136
+ exit 1
137
+ fi
138
+ echo -e "${GREEN}OK${NC} (可用 ${AVAIL}MB)"
139
+
140
+ # ── 内存检查 ──
141
+ echo -n "[3/6] 检查内存... "
142
+ # 2026-09-15 修复:中文 locale 下 free -m 输出“内存:”,awk 匹配 Mem: 失败会误报内存不足 → 强制 LANG=C
143
+ RAM=$(LANG=C free -m | awk '/^Mem:/{print $2}')
144
+ if [ "$RAM" -lt 6000 ]; then
145
+ echo -e "${RED}不足 (${RAM}MB)${NC}"
146
+ echo "错误:至少需要 6GB 内存,当前仅 ${RAM}MB"
147
+ exit 1
148
+ fi
149
+ echo -e "${GREEN}OK${NC} (${RAM}MB)"
150
+
151
+ # ── 端口检查 ──
152
+ echo -n "[4/6] 检查端口 $VB_PORT... "
153
+ if ss -tlnp 2>/dev/null | grep -q ":$VB_PORT "; then
154
+ echo -e "${RED}已被占用${NC}"
155
+ echo "错误:端口 $VB_PORT 已被占用,请先停止占用进程或设置 WR_VOICEBOX_LUXTTS_PORT 换端口"
156
+ exit 1
157
+ fi
158
+ echo -e "${GREEN}空闲${NC}"
159
+
160
+ # ── 拉取镜像 ──
161
+ echo -n "[5/6] 拉取 Voicebox (LuxTTS) 镜像... "
162
+ mkdir -p "$VB_DIR"
163
+ if pull_image_with_fallback /tmp/wr-pull-luxtts.log; then
164
+ echo -e "${GREEN}OK${NC} ($VB_IMAGE)"
165
+ else
166
+ echo -e "${RED}失败${NC}"
167
+ tail -5 /tmp/wr-pull-luxtts.log
168
+ echo "错误:所有镜像源均拉取失败,检查网络连接(国内=阿里云公共仓库 / 国际=ghcr.io)"
169
+ exit 1
170
+ fi
171
+
172
+ # ── 启动容器 ──
173
+ echo "[6/6] 启动 Voicebox (LuxTTS) 容器..."
174
+
175
+ # 2026-09-15 修复(生产环境反馈):持久化整个 /app/data
176
+ # 含 voicebox.db(音色库)+ profiles + generations,避免容器重建后音色/数据丢失
177
+ mkdir -p "$VB_DIR/data/generations" "$VB_DIR/data/profiles" "$VB_DIR/data/cache"
178
+ chmod -R 777 "$VB_DIR/data" 2>/dev/null || true
179
+
180
+ docker rm -f voicebox-luxtts >/dev/null 2>&1 || true
181
+ if ! docker run -d --name voicebox-luxtts --restart unless-stopped \
182
+ -p "127.0.0.1:${VB_PORT}:${VB_CONTAINER_PORT}" \
183
+ -v "$VB_DIR/data:/app/data" \
184
+ -e LOG_LEVEL=info \
185
+ -e NUMBA_CACHE_DIR=/tmp/numba_cache \
186
+ --cpus 4 --memory 8g \
187
+ "$VB_IMAGE" >/dev/null 2>&1; then
188
+ echo -e "${RED}失败${NC}"
189
+ echo "错误:docker run 失败,执行 docker logs voicebox-luxtts 查看日志"
190
+ exit 1
191
+ fi
192
+ echo " ✅ docker run 启动成功"
193
+
194
+ # 2026-08-15 权限兜底:snap docker 的 bind mount 目录在容器内可能显示 root:root 755,
195
+ # voicebox 用户(uid 999)无法写入 /app/data/generations 导致生成保存失败。
196
+ # 容器内 chmod 777 兜底(docker run 后立即执行,幂等)。
197
+ docker exec voicebox-luxtts chmod -R 777 /app/data 2>/dev/null || true
198
+
199
+ # 等待 API 就绪
200
+ echo -n " 等待 Voicebox API 就绪..."
201
+ for i in $(seq 1 90); do
202
+ if curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1; then
203
+ echo -e " ${GREEN}OK${NC} (耗时 ${i} 次检查)"
204
+ break
205
+ fi
206
+ sleep 5
207
+ echo -n "."
208
+ done
209
+
210
+ if ! curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1; then
211
+ echo ""
212
+ echo -e "${RED}Voicebox 启动失败${NC}"
213
+ echo " 检查日志: docker logs voicebox-luxtts"
214
+ exit 1
215
+ fi
216
+
217
+ # ── 创建石头哥克隆声音 profile ──
218
+ echo " ⚙ 配置克隆声音(石头哥克隆声)..."
219
+
220
+ mkdir -p "$HOME/.workrelay/speak-luxtts"
221
+ PROFILE_FILE="$HOME/.workrelay/speak-luxtts/profile_id"
222
+ SAMPLE_DIR="$INSTALL_DIR/samples"
223
+
224
+ # 查已存在的同款克隆 profile(按名字匹配,避免重复创建)
225
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 5 "http://${VB_HOST}:${VB_PORT}/profiles" 2>/dev/null \
226
+ | python3 -c '
227
+ import json,sys
228
+ try:
229
+ d=json.load(sys.stdin)
230
+ for p in d:
231
+ if p.get("name")=="石头哥克隆声" and p.get("voice_type")=="cloned":
232
+ print(p.get("id","")); break
233
+ except Exception:
234
+ print("")
235
+ ' 2>/dev/null)
236
+
237
+ if [ -z "$EXIST_ID" ]; then
238
+ # 创建克隆 profile
239
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 10 -X POST "http://${VB_HOST}:${VB_PORT}/profiles" \
240
+ -H "Content-Type: application/json" \
241
+ -d '{"name":"石头哥克隆声","description":"WorkRelay LuxTTS 内置克隆声音(石头哥)","language":"zh","voice_type":"cloned"}' \
242
+ | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
243
+ fi
244
+
245
+ if [ -n "$EXIST_ID" ]; then
246
+ # 上传样本(file + reference_text 多文件)
247
+ SAMPLE_COUNT=0
248
+ while IFS='|' read -r fname reftext; do
249
+ [ -z "$fname" ] && continue
250
+ fpath="$SAMPLE_DIR/$fname"
251
+ if [ -f "$fpath" ]; then
252
+ if curl --noproxy '*' -s --max-time 15 -X POST "http://${VB_HOST}:${VB_PORT}/profiles/${EXIST_ID}/samples" \
253
+ -F "file=@${fpath}" -F "reference_text=${reftext}" \
254
+ | 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
255
+ SAMPLE_COUNT=$((SAMPLE_COUNT+1))
256
+ fi
257
+ fi
258
+ done < "$SAMPLE_DIR/reference_text.txt"
259
+ echo " ✅ 克隆声音 profile 已就绪(${SAMPLE_COUNT} 个样本)"
260
+ echo "$EXIST_ID" > "$PROFILE_FILE"
261
+ else
262
+ echo " ⚠ 克隆 profile 创建失败,可手动设 WR_VOICEBOX_PROFILE_ID 指定声音"
263
+ fi
264
+
265
+ # ── TTS 互斥:停止其它 TTS 容器(2026-09-14:TTS 只能启用一个)──
266
+ echo " 🔒 互斥处理:停止其它 TTS 容器..."
267
+ for other in voicebox voicebox-qwen; do
268
+ if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$other"; then
269
+ docker stop "$other" >/dev/null 2>&1 && echo " ⏹ 已停止 $other" || true
270
+ fi
271
+ done
272
+
273
+ # ── 部署脚本 & 配置 OpenClaw ──
274
+ echo -n "[收尾] 部署 TTS 桥接脚本 & 配置 OpenClaw... "
275
+
276
+ mkdir -p "$HOME/.workrelay/speak-luxtts"
277
+ cp "$INSTALL_DIR/workrelay-tts.sh" "$HOME/.workrelay/speak-luxtts/"
278
+ chmod +x "$HOME/.workrelay/speak-luxtts/workrelay-tts.sh"
279
+ cp "$INSTALL_DIR/README.md" "$HOME/.workrelay/speak-luxtts/" 2>/dev/null || true
280
+
281
+ if command -v openclaw &>/dev/null; then
282
+ TTS_SCRIPT="$HOME/.workrelay/speak-luxtts/workrelay-tts.sh"
283
+ # 独立 provider 名(tts-local-cli-luxtts),与 Kokoro 版 tts-local-cli 互不覆盖
284
+ # 2026-09-14:openclaw 升级后 TTS 配置在顶层 tts.*(旧 messages.tts.* 已失效)
285
+ openclaw config set tts.provider tts-local-cli-luxtts 2>/dev/null || true
286
+ openclaw config set tts.providers.tts-local-cli-luxtts.command "$TTS_SCRIPT" 2>/dev/null || true
287
+ openclaw config set tts.providers.tts-local-cli-luxtts.args '["{{Text}}","{{OutputPath}}"]' 2>/dev/null || true
288
+ openclaw config set tts.providers.tts-local-cli-luxtts.outputFormat wav 2>/dev/null || true
289
+ openclaw config set tts.providers.tts-local-cli-luxtts.timeoutMs 180000 2>/dev/null || true
290
+
291
+ # MCP 配置(独立 server 名 voicebox-luxtts)
292
+ openclaw config set mcp.servers.voicebox-luxtts.url "http://${VB_HOST}:${VB_PORT}/mcp" 2>/dev/null || true
293
+ openclaw config set mcp.servers.voicebox-luxtts.transport streamable-http 2>/dev/null || true
294
+ openclaw config set mcp.servers.voicebox-luxtts.timeout 60 2>/dev/null || true
295
+
296
+ openclaw gateway restart 2>/dev/null || true
297
+ else
298
+ echo -e "${YELLOW}⚠ 未检测到 openclaw CLI,跳过自动配置${NC}"
299
+ fi
300
+
301
+ echo -e "${GREEN}OK${NC}"
302
+ echo ""
303
+
304
+ # ── 验证 ──
305
+ echo "============================================"
306
+ echo " 安装验证"
307
+ echo "============================================"
308
+
309
+ echo -n " Docker 容器: "
310
+ docker ps --format '{{.Names}}' 2>/dev/null | grep -q voicebox-luxtts && echo -e "${GREEN}运行中${NC}" || echo -e "${RED}未运行${NC}"
311
+
312
+ echo -n " API 连通性: "
313
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1 \
314
+ && echo -e "${GREEN}OK${NC} (http://${VB_HOST}:${VB_PORT})" \
315
+ || echo -e "${RED}无法连接${NC}"
316
+
317
+ echo -n " TTS 桥接脚本: "
318
+ [ -f "$HOME/.workrelay/speak-luxtts/workrelay-tts.sh" ] && echo -e "${GREEN}已部署${NC}" || echo -e "${RED}缺失${NC}"
319
+
320
+ echo -n " 克隆声音 profile: "
321
+ [ -f "$PROFILE_FILE" ] && echo -e "${GREEN}$(cat "$PROFILE_FILE")${NC}" || echo -e "${RED}缺失${NC}"
322
+
323
+ echo ""
324
+ echo -e "${GREEN}✅ Voicebox (LuxTTS) 安装完成!${NC}"
325
+ echo " API 地址: http://${VB_HOST}:${VB_PORT}"
326
+ echo " 管理命令: docker ps | grep voicebox-luxtts"
327
+ echo " 查看日志: docker logs -f voicebox-luxtts"
328
+ echo ""
329
+ echo " 🎙️ 声音:内置石头哥克隆声(LuxTTS 引擎)"
330
+ echo ""
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "workrelay-speak-luxtts",
3
+ "displayName": "语音合成(LuxTTS 克隆声)",
4
+ "description": "Agent 语音回复:Docker 部署 Voicebox(LuxTTS 引擎),内置石头哥克隆声音,音色自然,与其它 TTS 互斥(同时只能启用一个)",
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": "[ $(LANG=C 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": "外网可达(镜像仓库)",
44
+ "expect": "能访问阿里云镜像仓库",
45
+ "check": "curl -sI --max-time 5 https://crpi-3zj44dlldca7vawm.cn-hangzhou.personal.cr.aliyuncs.com >/dev/null 2>&1",
46
+ "hint": "无法访问阿里云镜像仓库,将无法拉取 Docker 镜像"
47
+ },
48
+ {
49
+ "id": "registry",
50
+ "label": "镜像仓库可达(ghcr.io / 国内公共仓库,自动回退)",
51
+ "expect": "能访问阿里云镜像仓库",
52
+ "check": "timeout 4 docker manifest inspect ghcr.io/rockychen2016/dlinkclaw-tts:luxtts-20260914 >/dev/null 2>&1 || timeout 4 docker manifest inspect registry.cn-hangzhou.aliyuncs.com/shiduo/dlinkclaw-tts:luxtts-20260914 >/dev/null 2>&1",
53
+ "hint": "镜像源不可达。国际源: ghcr.io(GitHub Container Registry,公开免登录);国内源: registry.cn-hangzhou.aliyuncs.com(待发布)。安装时自动按区域优先并回退"
54
+ },
55
+ {
56
+ "id": "gpu",
57
+ "label": "GPU 加速(可选)",
58
+ "expect": "检测到 NVIDIA GPU 时合成更快",
59
+ "check": "if command -v nvidia-smi >/dev/null 2>&1; then nvidia-smi -L >/dev/null 2>&1 && exit 0; fi; exit 0",
60
+ "hint": "未检测到 NVIDIA 独显,将使用 CPU 合成(慢但可用);有 NVIDIA GPU 时自动启用 CUDA 加速"
61
+ }
62
+ ]
63
+ }
@@ -0,0 +1,4 @@
1
+ 85fafe01-d4d6-4a48-890e-9ae113f21905.wav|我是想让你克隆一份我的声音,然后用我的声音来回复我,试一下。
2
+ 800e5af4-cba5-441e-8003-6d36f35409e6.wav|好,小朵听到吗?现在石头哥给小朵发语音信息,小朵收到吗?收到请回复。小朵,有没有收到?收到了没有?
3
+ 45ea2e2c-0863-4966-a739-8bff8817f4c0.wav|这是石头哥的声音样本,用于视频配音克隆。
4
+ stone-sample-6.4s.wav|这是石头哥的声音样本,用于视频配音克隆。