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 +6 -1
- package/lib/index.js +34 -0
- package/lib/settings-page.html +126 -0
- package/package.json +3 -2
- package/scripts/install-gpt-sovits.ps1 +115 -0
package/lib/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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.
|
|
4
|
-
"description": "流萤语音插件 for DeepSeek Harness:本地 edge-tts + RVC / GPT-SoVITS
|
|
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。"
|