dsh-live-speak 1.0.3 → 1.0.5
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 +43 -1
- package/client/client.js +17 -1
- package/index.js +64 -7
- package/package.json +1 -1
package/RELEASE-NOTES.md
CHANGED
|
@@ -204,6 +204,48 @@ dsh-live-speak
|
|
|
204
204
|
|
|
205
205
|
---
|
|
206
206
|
|
|
207
|
+
## 1.0.5 更新说明
|
|
208
|
+
|
|
209
|
+
**修好了「本地语音」在部分电脑上显示「未检测到可用声线」**
|
|
210
|
+
|
|
211
|
+
有人反馈:设置页的「声线」下拉是空的,写着「未检测到可用声线」,可电脑里明明装着中文语音。
|
|
212
|
+
|
|
213
|
+
问题不在语音包,在插件自己。它原来靠系统 `PATH` 去找 PowerShell 来读声线清单,而有些环境里 `PATH` 是被裁剪过的(精简启动器、部分终端、或以自动化方式拉起的进程),`powershell.exe` 叫不到,探测就静默失败、返回空列表——设置页于是显示「没有声线」。
|
|
214
|
+
|
|
215
|
+
现在改成**直接按绝对路径调用系统自带的 PowerShell**:先查 `SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe`,找不到才退回按名字找。不再依赖 `PATH`。
|
|
216
|
+
|
|
217
|
+
修完之后这台机器上认到的声线是 5 条:惠惠、瑶瑶、康康(Windows 新版语音层),加上惠惠桌面版、Zira(旧版桌面层);默认挑中文的「瑶瑶」,本地合成一路正常出声。
|
|
218
|
+
|
|
219
|
+
**说明**:这个毛病只影响「本地语音」档和设置页的下拉列表。用「线上语音」的话声线是固定的晓晓,从来没受影响。
|
|
220
|
+
|
|
221
|
+
当前版本:**1.0.5**
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 1.0.4 更新说明
|
|
226
|
+
|
|
227
|
+
**新增:语音服务长时间没响应时,卡片会告诉你它在重试**
|
|
228
|
+
|
|
229
|
+
线上语音偶尔会遇到服务端「连得上但不回话」——不是你的网络断了,是对方没在给数据。原来的表现是:卡片上一直写着「正在合成下一段…」,进度不动、声音不出来,看起来像插件死了。
|
|
230
|
+
|
|
231
|
+
现在合成连续失败超过 15 秒,卡片那行字会变成「**语音服务暂无响应,正在重试(已 N 秒)**」。它同时做三件事:
|
|
232
|
+
|
|
233
|
+
- **说清状态**:你至少知道它在重试,不用去重启 DSH
|
|
234
|
+
- **给出现在**:秒数一直在走,能判断是真卡住还是只是慢
|
|
235
|
+
- **自己消失**:一恢复出声,或者你点了停止清空队列,这行字立刻撤掉
|
|
236
|
+
|
|
237
|
+
失败照样一直重试、绝不跳段,只是这次你会看见它在做什么。
|
|
238
|
+
|
|
239
|
+
**同时修掉一个残留问题**:清空播放队列之后,那行「正在重试」原来会一直挂在卡片上(失败计时没跟着清),现在只在「确实还在合成」时才显示。
|
|
240
|
+
|
|
241
|
+
**怎么验证的**:本地把引擎切到「本地语音」制造确定性失败——卡片在第 21 秒出现「语音服务暂无响应,正在重试(已 21 秒)」;随后清空队列,这行字撤掉、卡片回到正常状态。另外在真实的线上服务端故障窗口里(18:12 起连续不可用、重试 18 次),卡片同样在第 24 秒给出了这行提示。
|
|
242
|
+
|
|
243
|
+
**一条不变的原则**:合成失败不跳过。任何一段合成不出来,就一直重试到成功为止,宁可晚几秒开口,也不让内容缺一段。
|
|
244
|
+
|
|
245
|
+
历史版本:**1.0.4**
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
207
249
|
## 1.0.3 更新说明
|
|
208
250
|
|
|
209
251
|
**修掉:Linux 上本机没装语音后端时,会一直无声地重试**
|
|
@@ -221,7 +263,7 @@ macOS 上如果系统被裁剪到连 `say` 都没有,走同样的处理。**Wi
|
|
|
221
263
|
|
|
222
264
|
**一条不变的原则**:合成失败不跳过。任何一段合成不出来,就一直重试到成功为止,宁可晚几秒开口,也不让内容缺一段。
|
|
223
265
|
|
|
224
|
-
|
|
266
|
+
历史版本:**1.0.3**
|
|
225
267
|
|
|
226
268
|
---
|
|
227
269
|
|
package/client/client.js
CHANGED
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"empty.none": "没有待播内容",
|
|
65
65
|
"empty.streaming": "正在生成下一句…",
|
|
66
66
|
"empty.synth": "正在合成下一段…",
|
|
67
|
+
"empty.trouble": "语音服务暂无响应,正在重试(已 {s} 秒)",
|
|
67
68
|
"act.pause": "暂停",
|
|
68
69
|
"act.play": "播放 / 继续",
|
|
69
70
|
"act.stop": "停止(清空队列)",
|
|
@@ -151,6 +152,7 @@
|
|
|
151
152
|
"empty.none": "Nothing queued",
|
|
152
153
|
"empty.streaming": "Generating the next line…",
|
|
153
154
|
"empty.synth": "Voicing the next part…",
|
|
155
|
+
"empty.trouble": "Voice service not responding, retrying… ({s}s)",
|
|
154
156
|
"act.pause": "Pause",
|
|
155
157
|
"act.play": "Play / Resume",
|
|
156
158
|
"act.stop": "Stop (clear queue)",
|
|
@@ -238,6 +240,7 @@
|
|
|
238
240
|
"empty.none": "再生待ちはありません",
|
|
239
241
|
"empty.streaming": "次の文を生成中…",
|
|
240
242
|
"empty.synth": "次の部分を音声化中…",
|
|
243
|
+
"empty.trouble": "音声サービスが応答しません。再試行中…({s} 秒)",
|
|
241
244
|
"act.pause": "一時停止",
|
|
242
245
|
"act.play": "再生 / 再開",
|
|
243
246
|
"act.stop": "停止(キューを消去)",
|
|
@@ -642,6 +645,8 @@
|
|
|
642
645
|
// 空档里不能一律写"没有待播内容",那会让几秒静默看起来像卡死了。
|
|
643
646
|
pending: 0,
|
|
644
647
|
synthBusy: false,
|
|
648
|
+
// 合成连续失败的状态(宿主给):卡片据此说明"在重试",而不是装作没事
|
|
649
|
+
trouble: null,
|
|
645
650
|
listeners: [],
|
|
646
651
|
pollTimer: null,
|
|
647
652
|
stopTimer: null
|
|
@@ -723,6 +728,7 @@
|
|
|
723
728
|
// 后端还没合成完的段:空档期用来判断"在合成",而不是"没内容"
|
|
724
729
|
pending: manager.pending || 0,
|
|
725
730
|
synthBusy: manager.synthBusy === true,
|
|
731
|
+
trouble: manager.trouble,
|
|
726
732
|
progress: manager.progress,
|
|
727
733
|
duration: manager.duration,
|
|
728
734
|
error: manager.error,
|
|
@@ -751,6 +757,11 @@
|
|
|
751
757
|
* 中间两种以前都写成"没有待播内容",几秒静默看起来就像插件卡住了。
|
|
752
758
|
*/
|
|
753
759
|
function idleLabel(state, t) {
|
|
760
|
+
// 合成连续失败十几秒是最要紧的信息:这时卡片本来该是静止的,
|
|
761
|
+
// 不写"在重试"用户只会以为插件死了,然后去重启它。
|
|
762
|
+
if (state.trouble && Number(state.trouble.seconds) >= 15) {
|
|
763
|
+
return t("empty.trouble").replace("{s}", String(state.trouble.seconds));
|
|
764
|
+
}
|
|
754
765
|
if (state.error !== "") return state.error;
|
|
755
766
|
if (state.streaming) return t("empty.streaming");
|
|
756
767
|
if ((state.pending || 0) > 0 || state.synthBusy === true) return t("empty.synth");
|
|
@@ -1336,9 +1347,14 @@
|
|
|
1336
1347
|
// 空档期给歌词区一个准确说法:还在合成 / 在等模型 / 真没内容
|
|
1337
1348
|
var nextPending = Number(data.pending) || 0;
|
|
1338
1349
|
var nextBusy = data.synthesizing === true;
|
|
1339
|
-
|
|
1350
|
+
// 合成连续失败的状态:只在"从无到有"或秒数变化时重绘,避免每次轮询都白刷一遍
|
|
1351
|
+
var nextTrouble = data.trouble || null;
|
|
1352
|
+
var troubleChanged = (nextTrouble === null) !== (manager.trouble === null)
|
|
1353
|
+
|| (nextTrouble !== null && manager.trouble !== null && nextTrouble.seconds !== manager.trouble.seconds);
|
|
1354
|
+
if (nextPending !== manager.pending || nextBusy !== manager.synthBusy || troubleChanged) {
|
|
1340
1355
|
manager.pending = nextPending;
|
|
1341
1356
|
manager.synthBusy = nextBusy;
|
|
1357
|
+
manager.trouble = nextTrouble;
|
|
1342
1358
|
emit();
|
|
1343
1359
|
}
|
|
1344
1360
|
var appended = false;
|
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
|
*
|
|
@@ -1139,6 +1160,12 @@ export function apply(ctx, config = {}) {
|
|
|
1139
1160
|
let working = false
|
|
1140
1161
|
let lastError = ''
|
|
1141
1162
|
let synthFailCount = 0
|
|
1163
|
+
/**
|
|
1164
|
+
* 连续失败是从什么时候开始的(毫秒),0 表示当前正常。
|
|
1165
|
+
* 用来给界面一个明确说法:合成卡在门外十几秒时,用户需要知道"它在重试",
|
|
1166
|
+
* 而不是看着一张静止的卡片以为插件死了。
|
|
1167
|
+
*/
|
|
1168
|
+
let synthTroubleSince = 0
|
|
1142
1169
|
|
|
1143
1170
|
/** Edge TTS 实例懒加载:依赖缺失/网络异常都不该影响 DSH 启动。 */
|
|
1144
1171
|
let engine = null
|
|
@@ -1178,6 +1205,8 @@ export function apply(ctx, config = {}) {
|
|
|
1178
1205
|
ready.clear()
|
|
1179
1206
|
jobs.length = 0
|
|
1180
1207
|
if (epochChange) epoch += 1
|
|
1208
|
+
// 换一篇就把「合成异常」的计时归零,否则新一篇会带着上一篇的秒数继续报
|
|
1209
|
+
if (epochChange) synthTroubleSince = 0
|
|
1181
1210
|
bump()
|
|
1182
1211
|
}
|
|
1183
1212
|
|
|
@@ -1282,11 +1311,15 @@ export function apply(ctx, config = {}) {
|
|
|
1282
1311
|
const probeWinrtVoices = () => new Promise((resolve) => {
|
|
1283
1312
|
ensureWinrtScripts()
|
|
1284
1313
|
execFile(
|
|
1285
|
-
|
|
1314
|
+
POWERSHELL_EXE,
|
|
1286
1315
|
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', WINRT_PROBE_PS1],
|
|
1287
1316
|
{ timeout: 20_000, windowsHide: true },
|
|
1288
1317
|
(error, stdout) => {
|
|
1289
|
-
if (error) {
|
|
1318
|
+
if (error) {
|
|
1319
|
+
log(`voice probe failed: ${String(error?.message ?? error).split('\n')[0]}`)
|
|
1320
|
+
resolve([])
|
|
1321
|
+
return
|
|
1322
|
+
}
|
|
1290
1323
|
resolve(String(stdout || '')
|
|
1291
1324
|
.split('\n')
|
|
1292
1325
|
.map((line) => line.trim())
|
|
@@ -1309,11 +1342,15 @@ export function apply(ctx, config = {}) {
|
|
|
1309
1342
|
].join('; ')
|
|
1310
1343
|
const probeSapiVoices = () => new Promise((resolve) => {
|
|
1311
1344
|
execFile(
|
|
1312
|
-
|
|
1345
|
+
POWERSHELL_EXE,
|
|
1313
1346
|
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', SAPI_PROBE_CMD],
|
|
1314
1347
|
{ timeout: 20_000, windowsHide: true },
|
|
1315
1348
|
(error, stdout) => {
|
|
1316
|
-
if (error) {
|
|
1349
|
+
if (error) {
|
|
1350
|
+
log(`voice probe failed: ${String(error?.message ?? error).split('\n')[0]}`)
|
|
1351
|
+
resolve([])
|
|
1352
|
+
return
|
|
1353
|
+
}
|
|
1317
1354
|
resolve(String(stdout || '')
|
|
1318
1355
|
.split('\n')
|
|
1319
1356
|
.map((line) => line.trim())
|
|
@@ -1334,7 +1371,11 @@ export function apply(ctx, config = {}) {
|
|
|
1334
1371
|
*/
|
|
1335
1372
|
const probeSayVoices = () => new Promise((resolve) => {
|
|
1336
1373
|
execFile('say', ['-v', '?'], { timeout: 15_000 }, (error, stdout) => {
|
|
1337
|
-
if (error) {
|
|
1374
|
+
if (error) {
|
|
1375
|
+
log(`voice probe failed: ${String(error?.message ?? error).split('\n')[0]}`)
|
|
1376
|
+
resolve([])
|
|
1377
|
+
return
|
|
1378
|
+
}
|
|
1338
1379
|
const list = []
|
|
1339
1380
|
for (const raw of String(stdout || '').split('\n')) {
|
|
1340
1381
|
const hit = /^(.+?)\s+([a-zA-Z]{2}[_-][a-zA-Z]{2,3}(?:[_-][A-Za-z0-9]+)?)\s+#/u.exec(raw)
|
|
@@ -1480,7 +1521,7 @@ export function apply(ctx, config = {}) {
|
|
|
1480
1521
|
return
|
|
1481
1522
|
}
|
|
1482
1523
|
execFile(
|
|
1483
|
-
|
|
1524
|
+
POWERSHELL_EXE,
|
|
1484
1525
|
[
|
|
1485
1526
|
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', WINRT_SYNTH_PS1,
|
|
1486
1527
|
'-TextFile', txtPath, '-OutWav', wavPath, '-VoiceMatch', voice ? voice.name : '', '-Rate', rateExpr,
|
|
@@ -1738,7 +1779,7 @@ export function apply(ctx, config = {}) {
|
|
|
1738
1779
|
'$s.Dispose()',
|
|
1739
1780
|
].join('; ')
|
|
1740
1781
|
execFile(
|
|
1741
|
-
|
|
1782
|
+
POWERSHELL_EXE,
|
|
1742
1783
|
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
1743
1784
|
{
|
|
1744
1785
|
timeout: 25_000,
|
|
@@ -1902,6 +1943,7 @@ export function apply(ctx, config = {}) {
|
|
|
1902
1943
|
ready.set(item.id, item)
|
|
1903
1944
|
lastError = ''
|
|
1904
1945
|
synthFailCount = 0
|
|
1946
|
+
synthTroubleSince = 0
|
|
1905
1947
|
trimReady()
|
|
1906
1948
|
// 日志里的声线名要写「真正发声的那条」:本地档是系统声线(瑶瑶/Huihui),
|
|
1907
1949
|
// 不是 Edge 的 id,否则又会造出「显示晓晓、实际念瑶瑶」那种假记录
|
|
@@ -1911,6 +1953,10 @@ export function apply(ctx, config = {}) {
|
|
|
1911
1953
|
} catch (error) {
|
|
1912
1954
|
attempt += 1
|
|
1913
1955
|
synthFailCount += 1
|
|
1956
|
+
// 第一次失败就记下起点,之后一直累积;成功一次就清零(见上面的 synth ok)。
|
|
1957
|
+
// 只认当前这一篇的失败:被清空/换篇之后才返回的旧尝试不算,
|
|
1958
|
+
// 否则点完"停止"卡片会一直挂着"正在重试",实际什么都没在做。
|
|
1959
|
+
if (epoch === myEpoch && synthTroubleSince === 0) synthTroubleSince = Date.now()
|
|
1914
1960
|
lastError = String(error?.message ?? error).split('\n')[0]
|
|
1915
1961
|
bump()
|
|
1916
1962
|
if (epoch !== myEpoch) break
|
|
@@ -2141,6 +2187,17 @@ export function apply(ctx, config = {}) {
|
|
|
2141
2187
|
voices: VOICES,
|
|
2142
2188
|
error: lastError,
|
|
2143
2189
|
failCount: synthFailCount,
|
|
2190
|
+
/**
|
|
2191
|
+
* 合成连续失败超过 15 秒时的状态,正常为 null。
|
|
2192
|
+
* 只在"确实还在合成"时报:清空队列、或者这一篇被推走之后,
|
|
2193
|
+
* pump 已经不在跑,残留的失败计时不该继续挂在卡片上。
|
|
2194
|
+
*/
|
|
2195
|
+
trouble: (synthTroubleSince === 0 || !working)
|
|
2196
|
+
? null
|
|
2197
|
+
: (() => {
|
|
2198
|
+
const seconds = Math.round((Date.now() - synthTroubleSince) / 1000)
|
|
2199
|
+
return seconds >= 15 ? { seconds, error: lastError } : null
|
|
2200
|
+
})(),
|
|
2144
2201
|
}
|
|
2145
2202
|
}
|
|
2146
2203
|
|