@oadank/dsh-input-tools 0.3.20 → 0.3.21

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/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
- // 安装目录 = 插件包根目录下的 sherpa-onnx/(脚本 install-asr.ps1 同规则推导)
1267
+ // [BUG-4 修复 2026-08-23] 安装目录 = 独立目录 ~\.dsh\sherpa-onnxinstall-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(sherpaDir, 'bin', 'sherpa-onnx-offline.exe'),
1273
- 'C:\\D\\opt\\sherpa-onnx\\bin\\sherpa-onnx-offline.exe', // 兼容历史安装
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 { execFileSync('ffmpeg', ['-version'], { windowsHide: true, stdio: 'ignore', timeout: 5000 }); ffmpegOk = true } catch { /* 无 */ }
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: sherpaDir,
1317
+ installDir: detectedDir,
1301
1318
  },
1302
1319
  })
1303
1320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oadank/dsh-input-tools",
3
- "version": "0.3.20",
3
+ "version": "0.3.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -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 3; $attempt++) {
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
- cmd /c "curl.exe -sL -C - --retry 3 --retry-delay 2 --connect-timeout 20 -o `"$OutFile`" `"$Url`" 2>nul"
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
- Write-Host " 下载中断(exit=$code),3 秒后重试..." -ForegroundColor Yellow
85
- Start-Sleep -Seconds 3
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
- # 路径统一用正斜杠,避免 JS 字符串把 \s \b 当转义符
185
- $exePath = "$InstallDir\bin\sherpa-onnx-offline.exe".Replace('\', '/')
186
- $modelPath = "$InstallDir\models\sensevoice-int8".Replace('\', '/')
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
- const SHERPA_BIN = process.env.ASR_SHARPA_BIN || '$exePath';
193
- const MODEL_DIR = process.env.ASR_MODEL_DIR || '$modelPath';
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
- const r = spawnSync(SHERPA_BIN, [
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', audioPath,
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
- Set-Content -Path $serviceFile -Value $serviceScript -Encoding UTF8
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 asr 2>$null | Out-Null
248
- nssm remove asr confirm 2>$null | Out-Null
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
- nssm set asr AppEnvironmentExtra "ASR_SERVICE_PORT=$Port" | Out-Null
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 3; $attempt++) {
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 -sL -C - --retry 3 --retry-delay 2 --connect-timeout 20 -o `"$OutFile`" `"$Url`" 2>nul"
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
- Write-Host " 下载中断(exit=$code),3 秒后重试..." -ForegroundColor Yellow
43
- Start-Sleep -Seconds 3
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 SHERPA = '__SHERPA__'
184
- const BASE = '__BASE__'
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
- const tmp = join(process.env.TEMP ?? '/tmp', `dsh-local-tts-${process.pid}-${Date.now()}`)
191
- const wav = `${tmp}.wav`
192
- const mp3 = `${tmp}.mp3`
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
- $launcher = $launcherTemplate.Replace('__SHERPA__', $exePath).Replace('__BASE__', $basePath)
244
+ # 注意:上模板里 chdir HERE(= 脚本所在目录=安装目录),因此占位符不需要绝对路径,全部相对。
245
+ $launcher = $launcherTemplate
214
246
  $launcherFile = Join-Path $InstallDir "local-tts.mjs"
215
- Set-Content -Path $launcherFile -Value $launcher -Encoding UTF8
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. 完成提示(命令只在路径含空格时才加引号;插件已能正确处理引号)----