dsh-live-speak 1.0.4 → 1.0.6

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/RELEASE-NOTES.md CHANGED
@@ -204,6 +204,42 @@ dsh-live-speak
204
204
 
205
205
  ---
206
206
 
207
+ ## 1.0.6 更新说明
208
+
209
+ **不会再把「※」念出来了**
210
+
211
+ 有人反馈:播报中间突然念出一个「※」。查下来是 markdown 的星号没洗干净——模型用 `**加粗**`、`*斜体*` 标重点,插件本该把这些标记剥掉再念;成对的标记好剥,**被截断的半对标记**就漏了。
212
+
213
+ 原因是模型逐字往外写,插件在字数够了就把已有文字送去合成。如果那一刀正好切在 `**` 中间(只剩下 `**核心`),成对正则匹配不上,星号就留在了要念的文本里,语音引擎把它念成一个音节。
214
+
215
+ 现在改成一条统一规则:**markdown 的元字符一律不念**(`*` `_` `~` 反引号 `#` `>` `|`,以及 `※` `★` `☆`)。不管它成对还是落单、写完还是被截断——只要是格式符号,就不进播报。
216
+
217
+ **中文标点一个都不删**:`。,!?;:、()《》` 是断句的唯一依据,删了长文会连成一片,标题和正文之间那 0.39 秒的停顿也就没了。
218
+
219
+ 验证覆盖 12 个样本:`**核心`(未闭合)、`*斜体*`、`海风酿造的浪漫。*。`(孤立星号)、标题、列表、表格、行内代码,以及 `50%`、`v1.0.5` 这类数字读数——星号全部消失,标点和数字一个没伤。
220
+
221
+ 当前版本:**1.0.6**
222
+
223
+ ---
224
+
225
+ ## 1.0.5 更新说明
226
+
227
+ **修好了「本地语音」在部分电脑上显示「未检测到可用声线」**
228
+
229
+ 有人反馈:设置页的「声线」下拉是空的,写着「未检测到可用声线」,可电脑里明明装着中文语音。
230
+
231
+ 问题不在语音包,在插件自己。它原来靠系统 `PATH` 去找 PowerShell 来读声线清单,而有些环境里 `PATH` 是被裁剪过的(精简启动器、部分终端、或以自动化方式拉起的进程),`powershell.exe` 叫不到,探测就静默失败、返回空列表——设置页于是显示「没有声线」。
232
+
233
+ 现在改成**直接按绝对路径调用系统自带的 PowerShell**:先查 `SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe`,找不到才退回按名字找。不再依赖 `PATH`。
234
+
235
+ 修完之后这台机器上认到的声线是 5 条:惠惠、瑶瑶、康康(Windows 新版语音层),加上惠惠桌面版、Zira(旧版桌面层);默认挑中文的「瑶瑶」,本地合成一路正常出声。
236
+
237
+ **说明**:这个毛病只影响「本地语音」档和设置页的下拉列表。用「线上语音」的话声线是固定的晓晓,从来没受影响。
238
+
239
+ 历史版本:**1.0.5**
240
+
241
+ ---
242
+
207
243
  ## 1.0.4 更新说明
208
244
 
209
245
  **新增:语音服务长时间没响应时,卡片会告诉你它在重试**
@@ -224,7 +260,7 @@ dsh-live-speak
224
260
 
225
261
  **一条不变的原则**:合成失败不跳过。任何一段合成不出来,就一直重试到成功为止,宁可晚几秒开口,也不让内容缺一段。
226
262
 
227
- 当前版本:**1.0.4**
263
+ 历史版本:**1.0.4**
228
264
 
229
265
  ---
230
266
 
package/index.js CHANGED
@@ -80,6 +80,27 @@ const IS_WIN = process.platform === 'win32'
80
80
  const IS_MAC = process.platform === 'darwin'
81
81
  const IS_LINUX = process.platform === 'linux'
82
82
 
83
+ /**
84
+ * Windows 上调 PowerShell 一律走这个路径,不再让 execFile 按 PATH 去猜。
85
+ *
86
+ * 之前直接写 'powershell.exe':PATH 被裁剪的环境里(精简启动器、部分终端或
87
+ * 自动化工具拉起的进程,PATH 里可能没有 System32)会直接 ENOENT,探测被
88
+ * catch 成空清单,设置页就显示「未检测到可用声线」——系统里明明装着 Huihui。
89
+ * 面向公众发行时这种机器不少,所以固定用绝对路径,找不到才退回按名查找。
90
+ */
91
+ const POWERSHELL_EXE = (() => {
92
+ if (!IS_WIN) return 'powershell.exe'
93
+ const root = process.env.SystemRoot || process.env.windir || 'C:\\Windows'
94
+ const candidates = [
95
+ path.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
96
+ 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
97
+ ]
98
+ for (const candidate of candidates) {
99
+ if (fs.existsSync(candidate)) return candidate
100
+ }
101
+ return 'powershell.exe'
102
+ })()
103
+
83
104
  /**
84
105
  * 用户没指定声线时,按这个顺序挑一条。
85
106
  *
@@ -458,10 +479,15 @@ function cleanForSpeech(raw) {
458
479
  text = text.replace(/^\s{0,3}>\s?/gmu, '')
459
480
  text = text.replace(/^\s{0,3}[-*+]\s+/gmu, '')
460
481
  text = text.replace(/^\s{0,3}\d+[.)]\s+/gmu, '')
461
- text = text.replace(/\*\*([^*]+)\*\*/gu, '$1')
462
- text = text.replace(/~~([^~]+)~~/gu, '$1')
463
- text = text.replace(/(^|[^*])\*([^*\n]+)\*/gu, '$1$2')
464
- text = text.replace(/^\s*([-*_])\1{2,}\s*$/gmu, ' ')
482
+ // 统一规则:markdown 元字符一次性清干净,不再逐条匹配「成对 / 落单」。
483
+ // 这几种都会被 TTS 念成怪音(`*` 念成"星号",用户实测听到"※"),对听感毫无意义;
484
+ // 被实时首块截断的半对标记(`**核心`)、落单的 `*`,也在这一条里一并消失。
485
+ // 但语言标点(。,!?;:、()《》)必须留——那是断句的唯一依据,
486
+ // 删了长文会连成一片,标题和正文之间那 0.39 秒的停顿就没了。
487
+ text = text.replace(/[*_~`#>|※★☆]/gu, '')
488
+ text = text.replace(/^\s*[-—–]\s*$/gmu, ' ')
489
+ // 星号删掉后会露出重复标点("浪漫。*。" → "浪漫。。"),合并成一个,免得念两次停顿
490
+ text = text.replace(/([。!?;:,、])\1+/gu, '$1')
465
491
 
466
492
  // 表格里残留的竖线、emoji、各种装饰符号
467
493
  text = text.replace(/[|]/gu, ' ')
@@ -1290,11 +1316,15 @@ export function apply(ctx, config = {}) {
1290
1316
  const probeWinrtVoices = () => new Promise((resolve) => {
1291
1317
  ensureWinrtScripts()
1292
1318
  execFile(
1293
- 'powershell.exe',
1319
+ POWERSHELL_EXE,
1294
1320
  ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', WINRT_PROBE_PS1],
1295
1321
  { timeout: 20_000, windowsHide: true },
1296
1322
  (error, stdout) => {
1297
- if (error) { resolve([]); return }
1323
+ if (error) {
1324
+ log(`voice probe failed: ${String(error?.message ?? error).split('\n')[0]}`)
1325
+ resolve([])
1326
+ return
1327
+ }
1298
1328
  resolve(String(stdout || '')
1299
1329
  .split('\n')
1300
1330
  .map((line) => line.trim())
@@ -1317,11 +1347,15 @@ export function apply(ctx, config = {}) {
1317
1347
  ].join('; ')
1318
1348
  const probeSapiVoices = () => new Promise((resolve) => {
1319
1349
  execFile(
1320
- 'powershell.exe',
1350
+ POWERSHELL_EXE,
1321
1351
  ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', SAPI_PROBE_CMD],
1322
1352
  { timeout: 20_000, windowsHide: true },
1323
1353
  (error, stdout) => {
1324
- if (error) { resolve([]); return }
1354
+ if (error) {
1355
+ log(`voice probe failed: ${String(error?.message ?? error).split('\n')[0]}`)
1356
+ resolve([])
1357
+ return
1358
+ }
1325
1359
  resolve(String(stdout || '')
1326
1360
  .split('\n')
1327
1361
  .map((line) => line.trim())
@@ -1342,7 +1376,11 @@ export function apply(ctx, config = {}) {
1342
1376
  */
1343
1377
  const probeSayVoices = () => new Promise((resolve) => {
1344
1378
  execFile('say', ['-v', '?'], { timeout: 15_000 }, (error, stdout) => {
1345
- if (error) { resolve([]); return }
1379
+ if (error) {
1380
+ log(`voice probe failed: ${String(error?.message ?? error).split('\n')[0]}`)
1381
+ resolve([])
1382
+ return
1383
+ }
1346
1384
  const list = []
1347
1385
  for (const raw of String(stdout || '').split('\n')) {
1348
1386
  const hit = /^(.+?)\s+([a-zA-Z]{2}[_-][a-zA-Z]{2,3}(?:[_-][A-Za-z0-9]+)?)\s+#/u.exec(raw)
@@ -1488,7 +1526,7 @@ export function apply(ctx, config = {}) {
1488
1526
  return
1489
1527
  }
1490
1528
  execFile(
1491
- 'powershell.exe',
1529
+ POWERSHELL_EXE,
1492
1530
  [
1493
1531
  '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', WINRT_SYNTH_PS1,
1494
1532
  '-TextFile', txtPath, '-OutWav', wavPath, '-VoiceMatch', voice ? voice.name : '', '-Rate', rateExpr,
@@ -1746,7 +1784,7 @@ export function apply(ctx, config = {}) {
1746
1784
  '$s.Dispose()',
1747
1785
  ].join('; ')
1748
1786
  execFile(
1749
- 'powershell.exe',
1787
+ POWERSHELL_EXE,
1750
1788
  ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
1751
1789
  {
1752
1790
  timeout: 25_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live-speak",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "用高保真神经语音实时播报模型输出(边生成边念),自带播放器,可随时暂停/恢复/停止。",
5
5
  "type": "module",
6
6
  "main": "./index.js",