@oadank/dsh-input-tools 0.3.20 → 0.3.22
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/assets/vision-test.jpg +0 -0
- package/lib/index.js +22 -5
- package/package.json +1 -1
- package/scripts/install-asr.ps1 +71 -18
- package/scripts/install-local-tts.ps1 +52 -18
package/assets/vision-test.jpg
CHANGED
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -1264,15 +1264,20 @@ async function apply(ctx) {
|
|
|
1264
1264
|
}
|
|
1265
1265
|
// 探测本机 ASR:sherpa exe / 模型 / 18790 服务 / ffmpeg,返回可自动填写的配置
|
|
1266
1266
|
if (url.pathname === '/asr/detect' && req.method === 'GET') {
|
|
1267
|
-
// 安装目录 =
|
|
1267
|
+
// [BUG-4 修复 2026-08-23] 安装目录 = 独立目录 ~\.dsh\sherpa-onnx(install-asr.ps1 默认装这里),
|
|
1268
|
+
// 不再只查插件包内 sherpa-onnx/ 和 C:\D\opt——三处全查,installDir 返回实际检测到的目录
|
|
1268
1269
|
const here = join(fileURLToPath(import.meta.url), '..') // .../lib
|
|
1269
1270
|
const pluginRoot = join(here, '..') // .../(包根)
|
|
1270
1271
|
const sherpaDir = join(pluginRoot, 'sherpa-onnx')
|
|
1272
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
1273
|
+
const dshSherpaDir = join(dshHome, 'sherpa-onnx') // install-asr.ps1 默认安装目录
|
|
1271
1274
|
const candidates = [
|
|
1272
|
-
join(
|
|
1273
|
-
'
|
|
1275
|
+
join(dshSherpaDir, 'bin', 'sherpa-onnx-offline.exe'), // 默认独立目录(中文用户主目录也 OK,检测用 node 读文件)
|
|
1276
|
+
join(sherpaDir, 'bin', 'sherpa-onnx-offline.exe'), // 兼容:插件包内安装
|
|
1277
|
+
'C:\\D\\opt\\sherpa-onnx\\bin\\sherpa-onnx-offline.exe', // 兼容历史安装
|
|
1274
1278
|
]
|
|
1275
1279
|
const modelDirs = [
|
|
1280
|
+
join(dshSherpaDir, 'models', 'sensevoice-int8'),
|
|
1276
1281
|
join(sherpaDir, 'models', 'sensevoice-int8'),
|
|
1277
1282
|
'C:\\D\\opt\\sherpa-onnx\\models\\sensevoice-int8', // 兼容历史安装
|
|
1278
1283
|
]
|
|
@@ -1280,14 +1285,26 @@ async function apply(ctx) {
|
|
|
1280
1285
|
for (const c of candidates) { try { await readFile(c); exe = c; break } catch { /* 继续 */ } }
|
|
1281
1286
|
let modelDir = null
|
|
1282
1287
|
for (const m of modelDirs) { try { await readFile(join(m, 'model.int8.onnx')); modelDir = m; break } catch { /* 继续 */ } }
|
|
1288
|
+
// [BUG-5 修复 2026-08-23] ffmpeg 探测复用 resolveFfmpegBin():先读 DSH_VOICE_FFMPEG_BIN
|
|
1289
|
+
// (setup-service.ps1 已写入服务环境),再 PATH;不要裸 execFileSync('ffmpeg')——服务以
|
|
1290
|
+
// LocalSystem 运行读不到用户 PATH 的 ffmpeg(如 WinGet Links),会误报"未找到 ffmpeg"
|
|
1283
1291
|
let ffmpegOk = false
|
|
1284
|
-
try {
|
|
1292
|
+
try {
|
|
1293
|
+
const ff = resolveFfmpegBin()
|
|
1294
|
+
if (ff && ff.trim() !== '') {
|
|
1295
|
+
// 存在即可信(env 显式指定 or PATH 探测到);但 PATH 探测的返回值可能还是那个兜底硬编码
|
|
1296
|
+
// 路径,需要真实存在才算 ok
|
|
1297
|
+
try { await readFile(ff); ffmpegOk = true } catch { /* 兜底路径不存在 */ }
|
|
1298
|
+
}
|
|
1299
|
+
} catch { /* 无 */ }
|
|
1285
1300
|
// 探测 18790 服务
|
|
1286
1301
|
let serviceOk = false
|
|
1287
1302
|
try {
|
|
1288
1303
|
const r = await fetch('http://127.0.0.1:18790/health', { timeout: 3000 })
|
|
1289
1304
|
serviceOk = r.ok
|
|
1290
1305
|
} catch { /* 无 */ }
|
|
1306
|
+
// [BUG-4] installDir 返回实际检测到的目录(exe 所在目录),不再硬编码插件包内路径
|
|
1307
|
+
const detectedDir = exe !== null ? join(exe, '..', '..') : dshSherpaDir
|
|
1291
1308
|
const cmd = exe !== null && modelDir !== null
|
|
1292
1309
|
? `${exe} --tokens=${modelDir}\\tokens.txt --sense-voice-model=${modelDir}\\model.int8.onnx --num-threads=4`
|
|
1293
1310
|
: ''
|
|
@@ -1297,7 +1314,7 @@ async function apply(ctx) {
|
|
|
1297
1314
|
exe, modelDir, ffmpegOk, serviceOk,
|
|
1298
1315
|
url: serviceOk ? 'http://127.0.0.1:18790' : '',
|
|
1299
1316
|
cmd,
|
|
1300
|
-
installDir:
|
|
1317
|
+
installDir: detectedDir,
|
|
1301
1318
|
},
|
|
1302
1319
|
})
|
|
1303
1320
|
}
|
package/package.json
CHANGED
package/scripts/install-asr.ps1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ============================================================
|
|
1
|
+
# ============================================================
|
|
2
2
|
# dsh-host-voice ASR 一键安装脚本(Windows)
|
|
3
3
|
# 安装内容:
|
|
4
4
|
# 1. sherpa-onnx(含 sherpa-onnx-offline.exe 非流式识别)
|
|
@@ -71,23 +71,47 @@ New-Item -ItemType Directory -Force -Path "$InstallDir\tmp" | Out-Null
|
|
|
71
71
|
# ⚠️ PowerShell 坑:$ErrorActionPreference=Stop 时 curl/tar 写 stderr(进度/警告)会抛 NativeCommandError
|
|
72
72
|
# 中断脚本(0.3.5 实测踩中)。因此所有原生命令用 cmd /c 包装 + 2>nul 吞 stderr + 临时放宽 EAP,
|
|
73
73
|
# 只依据 $LASTEXITCODE 判断成败。
|
|
74
|
+
# [BUG-3 修复 2026-08-23]:①curl 加 -f(HTTP 错误即非零退出),去掉 -s 的双重静默保留可诊断输出进变量;
|
|
75
|
+
# ②弱网 exit 56 时不再误判($LASTEXITCODE 为准,-s 时 curl 也可能 0);③解压前先 tar -tjf 探完整性,
|
|
76
|
+
# 探目录成功才算完整,失败删除重下;④重试 3→5 次。
|
|
74
77
|
function Download-Resume {
|
|
75
78
|
param([string]$Url, [string]$OutFile)
|
|
76
|
-
for ($attempt = 1; $attempt -le
|
|
79
|
+
for ($attempt = 1; $attempt -le 5; $attempt++) {
|
|
77
80
|
Write-Host " 下载(第 $attempt 次尝试): $Url" -ForegroundColor Yellow
|
|
78
81
|
$prevEAP = $ErrorActionPreference
|
|
79
82
|
$ErrorActionPreference = "Continue"
|
|
80
|
-
|
|
83
|
+
# 不用 -s:错误信息保留(但要吞 stderr 防止 NativeCommandError,用 2>&1 捕获到变量)
|
|
84
|
+
$errOut = cmd /c "curl.exe -fL -C - --retry 3 --retry-delay 2 --connect-timeout 20 -o `"$OutFile`" `"$Url`" 2>&1"
|
|
81
85
|
$code = $LASTEXITCODE
|
|
82
86
|
$ErrorActionPreference = $prevEAP
|
|
83
87
|
if ($code -eq 0) { return $true }
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
if ($code -eq 33) {
|
|
89
|
+
# curl 33 = 服务器不支持续传,可能文件已完整(200 全量下载过)——校验一下
|
|
90
|
+
if (Test-Path $OutFile) {
|
|
91
|
+
$prevEAP2 = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
|
92
|
+
cmd /c "tar -tjf `"$OutFile`" 2>nul" | Out-Null
|
|
93
|
+
$probe = $LASTEXITCODE; $ErrorActionPreference = $prevEAP2
|
|
94
|
+
if ($probe -eq 0) { return $true }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
Write-Host " 下载中断(exit=$code,$($errOut | Select-Object -Last 1)),5 秒后重试..." -ForegroundColor Yellow
|
|
98
|
+
Start-Sleep -Seconds 5
|
|
86
99
|
}
|
|
87
100
|
return $false
|
|
88
101
|
}
|
|
89
102
|
function Expand-TarBz2 {
|
|
90
103
|
param([string]$Archive, [string]$Dest, [int]$Strip = 0)
|
|
104
|
+
# [BUG-3 修复] 解压前先探完整性:tar -tjf 能列出目录才算完整,避免"下载中断但被当成功 → 解压必败"
|
|
105
|
+
$prevEAP = $ErrorActionPreference
|
|
106
|
+
$ErrorActionPreference = "Continue"
|
|
107
|
+
cmd /c "tar -tjf `"$Archive`" 2>nul" | Out-Null
|
|
108
|
+
$probe = $LASTEXITCODE
|
|
109
|
+
$ErrorActionPreference = $prevEAP
|
|
110
|
+
if ($probe -ne 0) {
|
|
111
|
+
Write-Host " 压缩包不完整(tar 探目录失败):$Archive" -ForegroundColor Red
|
|
112
|
+
Remove-Item -Force $Archive -ErrorAction SilentlyContinue
|
|
113
|
+
return $false
|
|
114
|
+
}
|
|
91
115
|
$tmpDir = Join-Path $Dest ".extract-tmp"
|
|
92
116
|
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
|
|
93
117
|
$stripArgs = ""
|
|
@@ -181,21 +205,30 @@ if (-not (Test-Path "$modelDir\model.int8.onnx")) {
|
|
|
181
205
|
|
|
182
206
|
# ---- 5. 写 ASR 服务脚本 ----
|
|
183
207
|
Write-Host "`n[4/4] 写入 ASR 服务并注册 nssm..." -ForegroundColor Yellow
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
# ⚠️ 中文用户名路径(C:\Users\阿丹)P0 修复:sherpa-onnx 用 ANSI/GBK 解析命令行参数,
|
|
209
|
+
# 绝对路径含中文会变乱码(status=4294967295)。铁律——**进程先 chdir 到安装目录,所有参数用相对路径**;
|
|
210
|
+
# 输入音频若在中文路径,先 copyFileSync 到 tmp/(ASCII 相对路径)再调用 sherpa。
|
|
187
211
|
$serviceScript = @"
|
|
188
212
|
const http = require('node:http');
|
|
189
213
|
const { spawnSync } = require('node:child_process');
|
|
190
|
-
const { existsSync } = require('node:fs');
|
|
214
|
+
const { existsSync, copyFileSync, unlinkSync, mkdirSync } = require('node:fs');
|
|
215
|
+
const { join } = require('node:path');
|
|
191
216
|
const PORT = Number(process.env.ASR_SERVICE_PORT || $Port);
|
|
192
|
-
|
|
193
|
-
const
|
|
217
|
+
// [中文路径修复] 常量只存相对位置,进程 chdir 后使用;不把绝对路径传给 sherpa
|
|
218
|
+
const SHERPA_REL = 'bin/sherpa-onnx-offline.exe';
|
|
219
|
+
const MODEL_DIR_REL = 'models/sensevoice-int8';
|
|
220
|
+
const TMP_REL = 'tmp';
|
|
221
|
+
let ROOT = process.env.ASR_ROOT || process.cwd();
|
|
222
|
+
if (!existsSync(ROOT)) ROOT = process.cwd();
|
|
223
|
+
try { process.chdir(ROOT); } catch (e) { console.error('[ASR] chdir 失败: ' + e.message); }
|
|
224
|
+
const SHERPA = SHERPA_REL;
|
|
225
|
+
const MODEL_DIR = MODEL_DIR_REL;
|
|
194
226
|
const server = http.createServer(async (req, res) => {
|
|
195
227
|
if (req.method === 'POST' && req.url === '/transcribe') {
|
|
196
228
|
let body = '';
|
|
197
229
|
req.on('data', c => body += c);
|
|
198
230
|
req.on('end', async () => {
|
|
231
|
+
let tmpAudio = null;
|
|
199
232
|
try {
|
|
200
233
|
const { audioPath } = JSON.parse(body);
|
|
201
234
|
if (!audioPath || !existsSync(audioPath)) {
|
|
@@ -203,15 +236,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
203
236
|
res.end(JSON.stringify({ error: 'invalid audio path' }));
|
|
204
237
|
return;
|
|
205
238
|
}
|
|
206
|
-
|
|
239
|
+
// [中文路径修复] 输入音频可能位于中文路径——拷贝到 ASCII 相对 tmp/ 再调用
|
|
240
|
+
mkdirSync(join(ROOT, TMP_REL), { recursive: true });
|
|
241
|
+
tmpAudio = TMP_REL + '/asr-in-' + process.pid + '-' + Date.now() + '.wav';
|
|
242
|
+
copyFileSync(audioPath, join(ROOT, tmpAudio));
|
|
243
|
+
const r = spawnSync(SHERPA, [
|
|
207
244
|
'--tokens=' + MODEL_DIR + '/tokens.txt',
|
|
208
245
|
'--sense-voice-model=' + MODEL_DIR + '/model.int8.onnx',
|
|
209
|
-
'--num-threads=4',
|
|
246
|
+
'--num-threads=4', tmpAudio,
|
|
210
247
|
], { windowsHide: true, timeout: 30000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
211
248
|
const all = (r.stdout || '') + '\n' + (r.stderr || '');
|
|
212
249
|
if (r.status !== 0) {
|
|
250
|
+
// [诊断增强] stderr 前 200 字符带进响应,设置页可直接看到原因(之前全吞)
|
|
251
|
+
const head = (r.stderr || '').slice(0, 200);
|
|
213
252
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
214
|
-
res.end(JSON.stringify({ error: 'sherpa exit ' + r.status }));
|
|
253
|
+
res.end(JSON.stringify({ error: 'sherpa exit ' + r.status, detail: head }));
|
|
215
254
|
return;
|
|
216
255
|
}
|
|
217
256
|
const m = all.match(/"text"\s*:\s*"([^"]*)"/);
|
|
@@ -221,6 +260,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
221
260
|
} catch (err) {
|
|
222
261
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
223
262
|
res.end(JSON.stringify({ error: String(err && err.message || err) }));
|
|
263
|
+
} finally {
|
|
264
|
+
if (tmpAudio) { try { unlinkSync(join(ROOT, tmpAudio)); } catch (_) { /* 清理失败忽略 */ } }
|
|
224
265
|
}
|
|
225
266
|
});
|
|
226
267
|
} else if (req.method === 'GET' && req.url === '/health') {
|
|
@@ -235,7 +276,11 @@ server.listen(PORT, '127.0.0.1', () => console.log('[ASR] listening on 127.0.0.1
|
|
|
235
276
|
# 服务脚本用 .cjs:插件包 package.json 声明了 "type":"module",.js 会被当 ESM 解析导致 require 报错
|
|
236
277
|
# (2026-08-21 XDN 实测:asr-service.js 抛 ReferenceError: require is not defined)
|
|
237
278
|
$serviceFile = "$InstallDir\asr-service.cjs"
|
|
238
|
-
|
|
279
|
+
# [中文路径修复] BOM 不要加(node 按 UTF-8 读 .cjs,BOM 会报错);PowerShell 5.1 写 UTF8 时带 BOM,
|
|
280
|
+
# 用 UTF8 参数即可(PS5.1 的 UTF8 是带 BOM 的 UTF-8,node 对 .cjs BOM 容忍,但保险起见用无 BOM 写入)。
|
|
281
|
+
# PS 5.1 没有 -Encoding utf8NoBOM,用 .NET 写:
|
|
282
|
+
[System.IO.File]::WriteAllText($serviceFile, $serviceScript, (New-Object System.Text.UTF8Encoding($false)))
|
|
283
|
+
Write-Host " ASR 服务脚本: $serviceFile" -ForegroundColor Green
|
|
239
284
|
|
|
240
285
|
# ---- 6. 注册 nssm 服务 ----
|
|
241
286
|
$nssm = Get-Command nssm -ErrorAction SilentlyContinue
|
|
@@ -244,11 +289,19 @@ if (-not $nssm) {
|
|
|
244
289
|
exit 1
|
|
245
290
|
}
|
|
246
291
|
$nodeExe = (Get-Command node).Source
|
|
247
|
-
nssm stop
|
|
248
|
-
|
|
292
|
+
# [BUG-6 修复] nssm stop/remove 在服务不存在时写 stderr,EAP=Stop 下抛 NativeCommandError 中断脚本,
|
|
293
|
+
# 首次安装必崩(实测卡在 [4/4])。先检测存在再操作,并临时放宽 EAP 用 $LASTEXITCODE 判成败。
|
|
294
|
+
$prevEAP = $ErrorActionPreference
|
|
295
|
+
$ErrorActionPreference = "Continue"
|
|
296
|
+
if (Get-Service asr -ErrorAction SilentlyContinue) {
|
|
297
|
+
nssm stop asr 2>$null | Out-Null
|
|
298
|
+
nssm remove asr confirm 2>$null | Out-Null
|
|
299
|
+
}
|
|
300
|
+
$ErrorActionPreference = $prevEAP
|
|
249
301
|
nssm install asr "$nodeExe" "$serviceFile" | Out-Null
|
|
250
302
|
nssm set asr AppDirectory "$InstallDir" | Out-Null
|
|
251
|
-
|
|
303
|
+
# [中文路径修复] ASR_ROOT 让服务进程知道安装根目录(nssm cwd 可能不是 InstallDir),chdir 用
|
|
304
|
+
nssm set asr AppEnvironmentExtra "ASR_SERVICE_PORT=$Port" "ASR_ROOT=$InstallDir" | Out-Null
|
|
252
305
|
nssm set asr Start SERVICE_AUTO_START | Out-Null
|
|
253
306
|
nssm set asr AppStdout "$InstallDir\asr-stdout.log" | Out-Null
|
|
254
307
|
nssm set asr AppStderr "$InstallDir\asr-stderr.log" | Out-Null
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ============================================================
|
|
1
|
+
# ============================================================
|
|
2
2
|
# dsh-input-tools 本地 TTS 一键安装脚本(Windows)
|
|
3
3
|
# 安装内容:
|
|
4
4
|
# 1. sherpa-onnx(含 sherpa-onnx-offline-tts.exe 离线 TTS)
|
|
@@ -29,23 +29,43 @@ $Version = "v1.13.6"
|
|
|
29
29
|
# ---- 下载/解压工具函数(2026-08-21 增强:断点续传 + 完整性校验 + 损坏自动重试)----
|
|
30
30
|
# ⚠️ PowerShell 坑:$ErrorActionPreference=Stop 时 curl/tar 写 stderr 会抛 NativeCommandError(0.3.5 实测踩中)。
|
|
31
31
|
# 原生命令一律 cmd /c 包装 + 2>nul + 临时放宽 EAP,只依据 $LASTEXITCODE。
|
|
32
|
+
# [BUG-3 修复 2026-08-23]:①curl 加 -f(HTTP 错误即非零退出);②弱网 exit 56 不再误判成功;
|
|
33
|
+
# ③解压前 tar -tjf 探完整性,失败删重下;④重试 3→5 次。
|
|
32
34
|
function Download-Resume {
|
|
33
35
|
param([string]$Url, [string]$OutFile)
|
|
34
|
-
for ($attempt = 1; $attempt -le
|
|
36
|
+
for ($attempt = 1; $attempt -le 5; $attempt++) {
|
|
35
37
|
Write-Host " 下载(第 $attempt 次尝试): $Url" -ForegroundColor Yellow
|
|
36
38
|
$prevEAP = $ErrorActionPreference
|
|
37
39
|
$ErrorActionPreference = "Continue"
|
|
38
|
-
cmd /c "curl.exe -
|
|
40
|
+
$errOut = cmd /c "curl.exe -fL -C - --retry 3 --retry-delay 2 --connect-timeout 20 -o `"$OutFile`" `"$Url`" 2>&1"
|
|
39
41
|
$code = $LASTEXITCODE
|
|
40
42
|
$ErrorActionPreference = $prevEAP
|
|
41
43
|
if ($code -eq 0) { return $true }
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
if ($code -eq 33) {
|
|
45
|
+
if (Test-Path $OutFile) {
|
|
46
|
+
$prevEAP2 = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
|
47
|
+
cmd /c "tar -tjf `"$OutFile`" 2>nul" | Out-Null
|
|
48
|
+
$probe = $LASTEXITCODE; $ErrorActionPreference = $prevEAP2
|
|
49
|
+
if ($probe -eq 0) { return $true }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
Write-Host " 下载中断(exit=$code,$($errOut | Select-Object -Last 1)),5 秒后重试..." -ForegroundColor Yellow
|
|
53
|
+
Start-Sleep -Seconds 5
|
|
44
54
|
}
|
|
45
55
|
return $false
|
|
46
56
|
}
|
|
47
57
|
function Expand-TarBz2 {
|
|
48
58
|
param([string]$Archive, [string]$Dest, [int]$Strip = 0)
|
|
59
|
+
$prevEAP = $ErrorActionPreference
|
|
60
|
+
$ErrorActionPreference = "Continue"
|
|
61
|
+
cmd /c "tar -tjf `"$Archive`" 2>nul" | Out-Null
|
|
62
|
+
$probe = $LASTEXITCODE
|
|
63
|
+
$ErrorActionPreference = $prevEAP
|
|
64
|
+
if ($probe -ne 0) {
|
|
65
|
+
Write-Host " 压缩包不完整(tar 探目录失败):$Archive" -ForegroundColor Red
|
|
66
|
+
Remove-Item -Force $Archive -ErrorAction SilentlyContinue
|
|
67
|
+
return $false
|
|
68
|
+
}
|
|
49
69
|
$tmpDir = Join-Path $Dest ".extract-tmp"
|
|
50
70
|
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
|
|
51
71
|
$stripArgs = ""
|
|
@@ -170,26 +190,32 @@ if (-not (Test-Path "$modelDir\model.onnx")) {
|
|
|
170
190
|
# ---- 5. 写 local-tts.mjs 启动脚本(契约:node local-tts.mjs <文本> → stdout 输出 mp3)----
|
|
171
191
|
# 注意:JS 里含反引号模板字符串,必须用单引号 here-string(@'...'@)字面生成,再替换路径占位符
|
|
172
192
|
Write-Host "`n写入 local-tts.mjs 启动脚本..." -ForegroundColor Yellow
|
|
173
|
-
$exePath = "$InstallDir\bin\sherpa-onnx-offline-tts.exe".Replace('\', '/')
|
|
174
|
-
$basePath = "$InstallDir\models\melo".Replace('\', '/')
|
|
175
193
|
$launcherTemplate = @'
|
|
176
194
|
// local-tts.mjs — 本地 MeloTTS(sherpa-onnx VITS 中文模型)TTS 包装
|
|
177
195
|
// 契约(配合 dsh-input-tools 的"本地命令"):文本作末参,stdout 输出音频字节(mp3)。
|
|
178
196
|
// 本文件由 install-local-tts.ps1 自动生成,勿手改;重装会重新生成。
|
|
197
|
+
// [中文路径修复] sherpa-onnx 用 ANSI/GBK 解析参数,绝对路径含中文变乱码——chdir 后全用相对路径
|
|
179
198
|
import { execFileSync } from 'node:child_process'
|
|
180
|
-
import { readFileSync, unlinkSync } from 'node:fs'
|
|
181
|
-
import { join } from 'node:path'
|
|
199
|
+
import { readFileSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
200
|
+
import { join, dirname } from 'node:path'
|
|
201
|
+
import { fileURLToPath } from 'node:url'
|
|
182
202
|
|
|
183
|
-
const
|
|
184
|
-
|
|
203
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
204
|
+
try { process.chdir(HERE) } catch (e) { console.error('local-tts: chdir 失败: ' + e.message); process.exit(1) }
|
|
205
|
+
const SHERPA = 'bin/sherpa-onnx-offline-tts.exe'
|
|
206
|
+
const BASE = 'models/melo'
|
|
185
207
|
const text = process.argv.slice(2).join(' ')
|
|
186
208
|
if (!text) {
|
|
187
209
|
console.error('local-tts: 缺少文本参数')
|
|
188
210
|
process.exit(1)
|
|
189
211
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
212
|
+
// [BUG-9 修复] TEMP 可能含中文/无效——sherpa 写绝对路径会乱码,一律用**相对路径** tmp/ 子目录
|
|
213
|
+
// (chdir 后相对路径是 ASCII,绝对安全)。不依赖 process.env.TEMP。
|
|
214
|
+
const tmpDir = 'tmp'
|
|
215
|
+
mkdirSync(tmpDir, { recursive: true })
|
|
216
|
+
const stamp = `${process.pid}-${Date.now()}`
|
|
217
|
+
const wav = `${tmpDir}/dsh-local-tts-${stamp}.wav`
|
|
218
|
+
const mp3 = `${tmpDir}/dsh-local-tts-${stamp}.mp3`
|
|
193
219
|
try {
|
|
194
220
|
execFileSync(SHERPA, [
|
|
195
221
|
'--vits-model=' + BASE + '/model.onnx',
|
|
@@ -199,20 +225,28 @@ try {
|
|
|
199
225
|
'--vits-length-scale=1.0',
|
|
200
226
|
'--output-filename=' + wav,
|
|
201
227
|
text,
|
|
202
|
-
], { windowsHide: true, stdio: 'ignore', timeout: 120_000 })
|
|
228
|
+
], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000 })
|
|
203
229
|
// 本地合成音量偏小:ffmpeg 放大 4 倍并转 mp3
|
|
204
230
|
execFileSync('ffmpeg', ['-y', '-i', wav, '-af', 'volume=4.0', '-c:a', 'libmp3lame', '-b:a', '128k', mp3], {
|
|
205
|
-
windowsHide: true, stdio: 'ignore', timeout: 60_000,
|
|
231
|
+
windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000,
|
|
206
232
|
})
|
|
207
233
|
process.stdout.write(readFileSync(mp3))
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// [诊断增强] stderr 前 200 字符带出来,之前 stdio:'ignore' 失败完全无信息
|
|
236
|
+
const detail = (e && e.stderr ? String(e.stderr).slice(0, 200) : '') || String(e && e.message || e)
|
|
237
|
+
console.error('local-tts: ' + detail)
|
|
238
|
+
process.exit(1)
|
|
208
239
|
} finally {
|
|
209
240
|
try { unlinkSync(wav) } catch { /* 忽略 */ }
|
|
210
241
|
try { unlinkSync(mp3) } catch { /* 忽略 */ }
|
|
211
242
|
}
|
|
212
243
|
'@
|
|
213
|
-
|
|
244
|
+
# 注意:上模板里 chdir 到 HERE(= 脚本所在目录=安装目录),因此占位符不需要绝对路径,全部相对。
|
|
245
|
+
$launcher = $launcherTemplate
|
|
214
246
|
$launcherFile = Join-Path $InstallDir "local-tts.mjs"
|
|
215
|
-
Set-Content
|
|
247
|
+
# [中文路径修复] .mjs 按 UTF-8 读,不加 BOM;PS 5.1 Set-Content UTF8 带 BOM,node 对 .mjs BOM 会报
|
|
248
|
+
# "Unexpected token"——必须无 BOM 写(.NET UTF8Encoding(false))。
|
|
249
|
+
[System.IO.File]::WriteAllText($launcherFile, $launcher, (New-Object System.Text.UTF8Encoding($false)))
|
|
216
250
|
Write-Host " local-tts.mjs: $launcherFile" -ForegroundColor Green
|
|
217
251
|
|
|
218
252
|
# ---- 6. 完成提示(命令只在路径含空格时才加引号;插件已能正确处理引号)----
|