dsh-liuying-voice 1.1.0 → 1.2.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.
package/lib/config.js CHANGED
@@ -1,4 +1,4 @@
1
- /** 设置存储:$DSH_HOME/liuying-voice/settings.json(无敏感值)。 */
1
+ /** 设置存储:$DSH_HOME/liuying-voice/settings.json(无敏感值)。 */
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -59,6 +59,11 @@ export function describeSettings(settings) {
59
59
  pitch: Number.isFinite(Number(settings.pitch)) ? Math.min(12, Math.max(-12, Number(settings.pitch))) : 0,
60
60
  protect: Number.isFinite(Number(settings.protect)) ? Math.min(0.5, Math.max(0, Number(settings.protect))) : 0.33,
61
61
  outDir: pick(settings.outDir),
62
+ engine: settings.engine === "gptsovits" ? "gptsovits" : "rvc",
63
+ gptApi: pick(settings.gptApi),
64
+ gptRef: pick(settings.gptRef),
65
+ gptRefText: pick(settings.gptRefText),
66
+ gptSpeed: Number.isFinite(Number(settings.gptSpeed)) ? Math.min(2, Math.max(0.5, Number(settings.gptSpeed))) : 1.0,
62
67
  qqGuard: settings.qqGuard !== false,
63
68
  snowLumaDir: pick(settings.snowLumaDir),
64
69
  snowLumaNode: pick(settings.snowLumaNode),
package/lib/index.js CHANGED
@@ -363,6 +363,27 @@ function mountSettings(ctx) {
363
363
  });
364
364
  }
365
365
 
366
+ function mountSettingsPage(ctx) {
367
+ const web = ctx.get("webServer");
368
+ if (!web?.register) return null;
369
+ return web.register({
370
+ kind: "exact",
371
+ path: "/liuying-voice",
372
+ handler: async (req, res) => {
373
+ noteDshPort(req.headers?.host);
374
+ try {
375
+ const html = readFileSync(path.join(PLUGIN_ROOT, "lib", "settings-page.html"), "utf8");
376
+ res.statusCode = 200;
377
+ res.setHeader("content-type", "text/html; charset=utf-8");
378
+ res.end(html);
379
+ } catch {
380
+ res.statusCode = 500;
381
+ res.end("settings-page.html 缺失");
382
+ }
383
+ },
384
+ });
385
+ }
386
+
366
387
  export async function apply(ctx, config = {}) {
367
388
  diag(`apply 开始;webServer=${ctx.get("webServer") ? "有" : "无"};tools=${ctx.get("tools") ? "有" : "无"}`);
368
389
  // 一次性把可能影响 python 子进程的环境变量记下来(排查 edge-tts 用)
@@ -391,6 +412,19 @@ export async function apply(ctx, config = {}) {
391
412
  } catch (error) {
392
413
  log(`设置接口挂载失败:${error?.message ?? error}`);
393
414
  }
415
+ try {
416
+ const dispose = mountSettingsPage(ctx);
417
+ if (typeof dispose === "function") {
418
+ const prev = disposeSettings;
419
+ disposeSettings = () => {
420
+ if (typeof prev === "function") prev();
421
+ dispose();
422
+ };
423
+ }
424
+ log("设置页面已挂载:/liuying-voice");
425
+ } catch (error) {
426
+ log(`设置页面挂载失败:${error?.message ?? error}`);
427
+ }
394
428
  try {
395
429
  const dispose = mountMemory(ctx);
396
430
  disposeMemory = typeof dispose === "function" ? dispose : null;
@@ -0,0 +1,126 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>流萤语音 · 设置</title>
7
+ <style>
8
+ :root { color-scheme: dark; }
9
+ * { box-sizing: border-box; }
10
+ body { margin: 0; font: 14px/1.6 "Segoe UI", "Microsoft YaHei", sans-serif; background: #0f1417; color: #d7e4e6; }
11
+ .wrap { max-width: 760px; margin: 0 auto; padding: 28px 20px 60px; }
12
+ h1 { font-size: 20px; margin: 0 0 4px; letter-spacing: .04em; }
13
+ .sub { color: #6f8a8e; font-size: 12px; margin-bottom: 24px; }
14
+ .card { background: #161d21; border: 1px solid #243036; border-radius: 10px; padding: 18px 20px; margin-bottom: 16px; }
15
+ .card h2 { font-size: 13px; color: #8fd8c8; margin: 0 0 14px; font-weight: 600; letter-spacing: .06em; }
16
+ .row { display: flex; align-items: center; gap: 12px; margin: 10px 0; }
17
+ .row label { flex: 0 0 150px; color: #9db5b9; font-size: 13px; }
18
+ .row input[type=text], .row input[type=number], .row select, .row textarea {
19
+ flex: 1; background: #0f1417; color: #d7e4e6; border: 1px solid #2a383f; border-radius: 6px;
20
+ padding: 7px 10px; font: inherit; outline: none;
21
+ }
22
+ .row input:focus, .row select:focus, .row textarea:focus { border-color: #4db6a0; }
23
+ .row input[type=checkbox] { width: 18px; height: 18px; accent-color: #4db6a0; }
24
+ .hint { color: #5f7478; font-size: 12px; margin: -4px 0 10px 162px; }
25
+ .btns { display: flex; gap: 10px; margin-top: 22px; }
26
+ button { font: inherit; padding: 9px 20px; border-radius: 7px; border: 1px solid #2a383f; background: #1c262b; color: #cfe3e6; cursor: pointer; }
27
+ button.primary { background: #1f6f5f; border-color: #2b8a76; color: #eafaf5; }
28
+ button:hover { filter: brightness(1.15); }
29
+ .status { margin-top: 14px; font-size: 13px; min-height: 20px; color: #8fd8c8; }
30
+ .status.err { color: #e08a8a; }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <div class="wrap">
35
+ <h1>流萤语音</h1>
36
+ <div class="sub">dsh-liuying-voice · 独立设置页</div>
37
+
38
+ <form id="form">
39
+ <div class="card">
40
+ <h2>通用</h2>
41
+ <div class="row"><label>启用</label><input type="checkbox" id="enabled"></div>
42
+ <div class="row"><label>发送模式</label>
43
+ <select id="sendMode"><option value="mix">混合(文字+语音)</option><option value="voice">只用语音</option><option value="text">只用文字</option></select>
44
+ </div>
45
+ <div class="row"><label>语音比例(mix)</label><input type="number" id="voiceRatio" min="0" max="1" step="0.1"></div>
46
+ </div>
47
+
48
+ <div class="card">
49
+ <h2>引擎</h2>
50
+ <div class="row"><label>语音引擎</label>
51
+ <select id="engine">
52
+ <option value="rvc">RVC(本地 edge-tts + 变声,快)</option>
53
+ <option value="gptsovits">GPT-SoVITS(更自然,需 9880 服务)</option>
54
+ </select>
55
+ </div>
56
+ <div class="row"><label>GPT-SoVITS 地址</label><input type="text" id="gptApi" placeholder="http://127.0.0.1:9880/"></div>
57
+ <div class="row"><label>参考音频</label><input type="text" id="gptRef" placeholder="留空用插件自带 assets/firefly-ref-hf.wav"></div>
58
+ <div class="row"><label>参考文本</label><textarea id="gptRefText" rows="2"></textarea></div>
59
+ <div class="row"><label>语速</label><input type="number" id="gptSpeed" min="0.5" max="2" step="0.05"></div>
60
+ <div class="hint">GPT-SoVITS 首次使用会自动下载模型,需要本地 9880 服务在跑。</div>
61
+ </div>
62
+
63
+ <div class="card">
64
+ <h2>RVC(旧引擎)</h2>
65
+ <div class="row"><label>Python</label><input type="text" id="python"></div>
66
+ <div class="row"><label>Applio 目录</label><input type="text" id="applio"></div>
67
+ <div class="row"><label>模型 .pth</label><input type="text" id="model"></div>
68
+ <div class="row"><label>基础音色</label><input type="text" id="ttsVoice"></div>
69
+ <div class="row"><label>语速(负=慢)</label><input type="number" id="ttsRate" step="1"></div>
70
+ <div class="row"><label>音高</label><input type="number" id="pitch" step="1"></div>
71
+ <div class="row"><label>保护</label><input type="number" id="protect" min="0" max="0.5" step="0.01"></div>
72
+ </div>
73
+
74
+ <div class="btns">
75
+ <button type="submit" class="primary">保存</button>
76
+ <button type="button" id="reload">重新载入</button>
77
+ </div>
78
+ <div class="status" id="status"></div>
79
+ </form>
80
+ </div>
81
+ <script>
82
+ const API = "/liuying-voice/settings";
83
+ const FIELDS = ["enabled","sendMode","voiceRatio","engine","gptApi","gptRef","gptRefText","gptSpeed",
84
+ "python","applio","model","ttsVoice","ttsRate","pitch","protect"];
85
+ const $ = (id) => document.getElementById(id);
86
+ function setStatus(msg, err) { const s = $("status"); s.textContent = msg; s.className = "status" + (err ? " err" : ""); }
87
+
88
+ async function load() {
89
+ setStatus("读取中…");
90
+ try {
91
+ const r = await fetch(API, { cache: "no-store" });
92
+ const s = await r.json();
93
+ for (const k of FIELDS) {
94
+ const el = $(k);
95
+ if (!el) continue;
96
+ if (el.type === "checkbox") el.checked = s[k] !== false;
97
+ else el.value = s[k] ?? "";
98
+ }
99
+ setStatus("已载入");
100
+ } catch (e) { setStatus("读取失败:" + e.message, true); }
101
+ }
102
+
103
+ $("form").addEventListener("submit", async (ev) => {
104
+ ev.preventDefault();
105
+ const body = {};
106
+ for (const k of FIELDS) {
107
+ const el = $(k);
108
+ if (!el) continue;
109
+ if (el.type === "checkbox") body[k] = el.checked;
110
+ else if (el.type === "number") body[k] = Number(el.value);
111
+ else body[k] = el.value;
112
+ }
113
+ setStatus("保存中…");
114
+ try {
115
+ const r = await fetch(API, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
116
+ const j = await r.json();
117
+ if (!j.ok) throw new Error(j.error || "保存失败");
118
+ setStatus("已保存");
119
+ } catch (e) { setStatus("保存失败:" + e.message, true); }
120
+ });
121
+
122
+ $("reload").addEventListener("click", load);
123
+ load();
124
+ </script>
125
+ </body>
126
+ </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-liuying-voice",
3
- "version": "1.1.0",
4
- "description": "流萤语音插件 for DeepSeek Harness:本地 edge-tts + RVC / GPT-SoVITS 双引擎,自带流萤模型与参考音频,让 agent 用「流萤」的声音说话。开源 MIT。",
3
+ "version": "1.2.0",
4
+ "description": "流萤语音插件 for DeepSeek Harness:本地 edge-tts + RVC / GPT-SoVITS 双引擎,自带流萤参考音频、一键环境安装脚本和独立设置页面。开源 MIT。",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -29,6 +29,7 @@
29
29
  "assets",
30
30
  "scripts/*.py",
31
31
  "scripts/*.mjs",
32
+ "scripts/*.ps1",
32
33
  "memory",
33
34
  "test",
34
35
  "cordis.patch.yml",
@@ -0,0 +1,115 @@
1
+ # ============================================================
2
+ # GPT-SoVITS 简易环境安装脚本(CPU 推理,配合 dsh-liuying-voice 使用)
3
+ #
4
+ # 做什么:
5
+ # 1. 检查 Python 3.10/3.11
6
+ # 2. 下载 GPT-SoVITS 源码
7
+ # 3. 创建虚拟环境并安装推理依赖
8
+ # 4. 下载 hubert / bert 预训练
9
+ # 5. 下载流萤 GPT/SoVITS 权重
10
+ # 6. 写入 CPU 推理配置并生成启动脚本
11
+ #
12
+ # 用法(PowerShell):
13
+ # powershell -ExecutionPolicy Bypass -File install-gpt-sovits.ps1
14
+ # ============================================================
15
+ $ErrorActionPreference = 'Stop'
16
+ $repoUrl = 'https://github.com/RVC-Boss/GPT-SoVITS'
17
+ $pipIndex = 'https://mirrors.aliyun.com/pypi/simple'
18
+ $env:HF_ENDPOINT = 'https://hf-mirror.com'
19
+ $env:PYTHONIOENCODING = 'utf-8'
20
+ $root = Join-Path $env:USERPROFILE '.dsh\gpt-sovits'
21
+
22
+ function Step($m) { Write-Host "`n== $m" -ForegroundColor Cyan }
23
+
24
+ Step "安装目录:$root"
25
+ New-Item -ItemType Directory -Path $root -Force | Out-Null
26
+
27
+ # 1. Python
28
+ $python = $null
29
+ foreach ($cand in @('python3.11', 'python3.10', 'python')) {
30
+ $cmd = Get-Command $cand -ErrorAction SilentlyContinue
31
+ if ($cmd) { $python = $cmd.Source; break }
32
+ }
33
+ if (-not $python) { throw "需要 Python 3.10/3.11(GPT-SoVITS 依赖),请先安装并加入 PATH" }
34
+ Write-Host "Python: $python"
35
+
36
+ # 2. 源码
37
+ $repo = Join-Path $root 'GPT-SoVITS'
38
+ if (-not (Test-Path (Join-Path $repo 'api_v2.py'))) {
39
+ Step '下载 GPT-SoVITS 源码(约 60 MB,慢时请耐心等)'
40
+ git clone --depth 1 $repoUrl $repo
41
+ }
42
+ if (-not (Test-Path (Join-Path $repo 'api_v2.py'))) { throw "源码下载失败,请检查网络或代理" }
43
+
44
+ # 3. 虚拟环境
45
+ $venv = Join-Path $root 'venv'
46
+ if (-not (Test-Path (Join-Path $venv 'Scripts\python.exe'))) {
47
+ Step '创建虚拟环境'
48
+ & $python -m venv $venv
49
+ }
50
+ $py = Join-Path $venv 'Scripts\python.exe'
51
+ & $py -m pip install -U pip -i $pipIndex
52
+
53
+ Step '安装推理依赖(阿里镜像)'
54
+ & $py -m pip install -r (Join-Path $repo 'requirements.txt') -i $pipIndex
55
+ & $py -m pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu
56
+
57
+ # 4. 预训练
58
+ $pretrained = Join-Path $repo 'GPT_SoVITS\pretrained_models'
59
+ Step '下载 hubert / bert 预训练(约 1.7 GB,走 HF 镜像)'
60
+ & $py -c "from huggingface_hub import snapshot_download; snapshot_download('hfl/chinese-hubert-base', local_dir=r'$pretrained\chinese-hubert-base')"
61
+ & $py -c "from huggingface_hub import snapshot_download; snapshot_download('hfl/chinese-roberta-wwm-ext-large', local_dir=r'$pretrained\chinese-roberta-wwm-ext-large')"
62
+
63
+ # 5. 流萤模型
64
+ $gw = Join-Path $repo 'GPT_weights'
65
+ $sw = Join-Path $repo 'SoVITS_weights'
66
+ New-Item -ItemType Directory -Path $gw, $sw -Force | Out-Null
67
+ Step '下载流萤 GPT 权重(约 148 MB)'
68
+ $gptFile = Join-Path $gw '流萤-e10.ckpt'
69
+ if (-not (Test-Path $gptFile)) {
70
+ Invoke-WebRequest -Uri 'https://hf-mirror.com/Shiina-Mahiru/Firefly-Neko/resolve/main/GPT_weights_v2%5C%E6%B5%81%E8%90%A4-e10.ckpt' -OutFile $gptFile -TimeoutSec 3600
71
+ }
72
+ Step '下载流萤 SoVITS 权重(约 81 MB)'
73
+ $sovitsFile = Join-Path $sw '流萤_e15_s810.pth'
74
+ if (-not (Test-Path $sovitsFile)) {
75
+ Invoke-WebRequest -Uri 'https://hf-mirror.com/Shiina-Mahiru/Firefly-Neko/resolve/main/SoVITS_weights_v2%5C%E6%B5%81%E8%90%A4_e15_s810.pth' -OutFile $sovitsFile -TimeoutSec 3600
76
+ }
77
+
78
+ # 6. 配置(CPU)
79
+ Step '写入推理配置(CPU 模式)'
80
+ $cfg = Join-Path $repo 'GPT_SoVITS\configs\tts_infer.yaml'
81
+ $cfgText = @"
82
+ custom:
83
+ bert_base_path: GPT_SoVITS/pretrained_models/chinese-roberta-wwm-ext-large
84
+ cnhuhbert_base_path: GPT_SoVITS/pretrained_models/chinese-hubert-base
85
+ device: cpu
86
+ is_half: false
87
+ t2s_weights_path: GPT_weights/流萤-e10.ckpt
88
+ vits_weights_path: SoVITS_weights/流萤_e15_s810.pth
89
+ default:
90
+ bert_base_path: GPT_SoVITS/pretrained_models/chinese-roberta-wwm-ext-large
91
+ cnhuhbert_base_path: GPT_SoVITS/pretrained_models/chinese-hubert-base
92
+ device: cpu
93
+ is_half: false
94
+ t2s_weights_path: GPT_weights/流萤-e10.ckpt
95
+ vits_weights_path: SoVITS_weights/流萤_e15_s810.pth
96
+ "@
97
+ [System.IO.File]::WriteAllText($cfg, $cfgText, (New-Object System.Text.UTF8Encoding($false)))
98
+
99
+ # 7. 启动脚本
100
+ $start = Join-Path $root 'start-api.cmd'
101
+ $startText = @"
102
+ @echo off
103
+ cd /d "$repo"
104
+ set PATH=%PATH%;.\ffmpeg\bin
105
+ set CUDA_VISIBLE_DEVICES=-1
106
+ set OMP_NUM_THREADS=12
107
+ set MKL_NUM_THREADS=12
108
+ "$py" api_v2.py
109
+ "@
110
+ [System.IO.File]::WriteAllText($start, $startText, (New-Object System.Text.UTF8Encoding($false)))
111
+
112
+ Write-Host "`n完成!" -ForegroundColor Green
113
+ Write-Host "启动 GPT-SoVITS 服务:$start"
114
+ Write-Host "服务地址:http://127.0.0.1:9880/"
115
+ Write-Host "然后在流萤语音插件设置里把 engine 设为 gptsovits。"