dsh-fish-pet 0.2.4 → 0.2.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/CHANGELOG.md +31 -1
- package/README.md +300 -240
- package/assets/asr-listener.ps1 +53 -35
- package/assets/fish-pet.ps1 +374 -40
- package/assets/tts_worker.py +67 -0
- package/package.json +1 -1
package/assets/asr-listener.ps1
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
# 一次连续采集,检测到"说完"立刻上传,不再固定切片
|
|
3
3
|
param(
|
|
4
4
|
[int]$MaxMs = 10000,
|
|
5
|
-
[int]$SilenceMs =
|
|
5
|
+
[int]$SilenceMs = 900,
|
|
6
6
|
[int]$Boost = 1,
|
|
7
|
-
[int]$SilenceGate =
|
|
7
|
+
[int]$SilenceGate = 600
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
-
# 单实例:防止耳朵双开(双开会重复识别、重复回话)
|
|
11
|
-
try {
|
|
12
|
-
$earMutex = New-Object System.Threading.Mutex($false, 'Global\dsh-fish-ear-single')
|
|
13
|
-
if (-not $earMutex.WaitOne(0)) { exit 0 }
|
|
14
|
-
} catch { }
|
|
10
|
+
# 单实例:防止耳朵双开(双开会重复识别、重复回话)
|
|
11
|
+
try {
|
|
12
|
+
$earMutex = New-Object System.Threading.Mutex($false, 'Global\dsh-fish-ear-single')
|
|
13
|
+
if (-not $earMutex.WaitOne(0)) { exit 0 }
|
|
14
|
+
} catch { }
|
|
15
15
|
$ErrorActionPreference = 'Stop'
|
|
16
16
|
try { Add-Type -AssemblyName System.Net.Http -ErrorAction Stop } catch { }
|
|
17
17
|
$DataDir = Join-Path $env:USERPROFILE '.dsh\fish-pet'
|
|
@@ -28,6 +28,7 @@ if (Test-Path $CfgPath) {
|
|
|
28
28
|
$cj = Get-Content -LiteralPath $CfgPath -Raw | ConvertFrom-Json
|
|
29
29
|
if ($cj.voice) {
|
|
30
30
|
if ($cj.voice.silenceGate) { $SilenceGate = [int]$cj.voice.silenceGate }
|
|
31
|
+
if ($cj.voice.silenceMs) { $SilenceMs = [int]$cj.voice.silenceMs }
|
|
31
32
|
if ($cj.voice.boost) { $Boost = [int]$cj.voice.boost }
|
|
32
33
|
if ($cj.voice.engine) { $script:engine = [string]$cj.voice.engine }
|
|
33
34
|
if ($cj.voice.localConf) { $script:localConf = [double]$cj.voice.localConf }
|
|
@@ -133,19 +134,25 @@ public class EarVad
|
|
|
133
134
|
all.AddRange(buf);
|
|
134
135
|
if (peakInt > RawPeak) RawPeak = peakInt;
|
|
135
136
|
if (peakInt < gate) silent += packetMs; else silent = 0;
|
|
136
|
-
if (silent >= silenceMs) break;
|
|
137
|
+
if (silent >= silenceMs && (total - silent) >= 1200) break;
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
ac.Stop();
|
|
140
141
|
if (!started || all.Count < rate / 2) return false;
|
|
141
142
|
|
|
142
|
-
// 轻声增益:整段峰值偏低时放大,响的句子不动
|
|
143
|
-
float maxA = 0; double sumSq = 0; int cntAll = 0;
|
|
144
|
-
for (int i = 0; i < all.Count; i++) { float a = Math.Abs(all[i]); if (a > maxA) maxA = a; sumSq += all[i] * all[i]; cntAll++; }
|
|
145
|
-
double peakInt2 = maxA * 32767.0;
|
|
146
|
-
double gain2 = 1.0;
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
// 轻声增益:整段峰值偏低时放大,响的句子不动
|
|
144
|
+
float maxA = 0; double sumSq = 0; int cntAll = 0;
|
|
145
|
+
for (int i = 0; i < all.Count; i++) { float a = Math.Abs(all[i]); if (a > maxA) maxA = a; sumSq += all[i] * all[i]; cntAll++; }
|
|
146
|
+
double peakInt2 = maxA * 32767.0;
|
|
147
|
+
double gain2 = 1.0;
|
|
148
|
+
// 峰值归一化:把这段音频拉到约 70% 满幅,轻声也能被云端听清
|
|
149
|
+
if (peakInt2 > 200) {
|
|
150
|
+
gain2 = 22936.0 / peakInt2;
|
|
151
|
+
if (gain2 < 1.0) gain2 = 1.0;
|
|
152
|
+
if (gain2 > 35.0) gain2 = 35.0;
|
|
153
|
+
// 极轻的声音额外再提一点(>35 倍会明显失真,所以封顶)
|
|
154
|
+
}
|
|
155
|
+
|
|
149
156
|
double step = (double)rate / 16000.0;
|
|
150
157
|
int framesIn = all.Count / ch;
|
|
151
158
|
int outCount = (int)(framesIn / step);
|
|
@@ -263,6 +270,12 @@ $script:lastFishCheck = Get-Date
|
|
|
263
270
|
if (-not $key) { Start-Sleep -Seconds 2; continue }
|
|
264
271
|
|
|
265
272
|
$ok = [EarVad]::Capture($MaxMs, $SilenceGate, $SilenceMs, $Boost, 2)
|
|
273
|
+
|
|
274
|
+
# 录音过程中鱼开口说话(或刚说完)→ 这段是自己的声音,丢掉
|
|
275
|
+
$talkF = Join-Path $DataDir 'asr.talking'
|
|
276
|
+
if (Test-Path $talkF) { continue }
|
|
277
|
+
$pauseF = Join-Path $DataDir 'asr.pause'
|
|
278
|
+
if ((Test-Path $pauseF) -and (((Get-Date) - (Get-Item $pauseF).LastWriteTime).TotalSeconds -lt 2.0)) { continue }
|
|
266
279
|
if (-not $ok) {
|
|
267
280
|
Start-Sleep -Milliseconds 80
|
|
268
281
|
continue
|
|
@@ -290,23 +303,23 @@ $script:lastFishCheck = Get-Date
|
|
|
290
303
|
}
|
|
291
304
|
}
|
|
292
305
|
if (-not $text -and $engine -ne 'local') {
|
|
293
|
-
if ($null -eq $script:http) {
|
|
294
|
-
$script:http = New-Object System.Net.Http.HttpClient
|
|
295
|
-
$script:http.Timeout = [TimeSpan]::FromSeconds(30)
|
|
296
|
-
$script:http.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $key)
|
|
297
|
-
}
|
|
298
|
-
$json = ''
|
|
299
|
-
try {
|
|
300
|
-
$mp = New-Object System.Net.Http.MultipartFormDataContent
|
|
301
|
-
$bytes = [IO.File]::ReadAllBytes($wav)
|
|
302
|
-
$bc = New-Object System.Net.Http.ByteArrayContent((,$bytes))
|
|
303
|
-
$bc.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('audio/wav')
|
|
304
|
-
$mp.Add($bc, 'file', 'a.wav')
|
|
305
|
-
$mp.Add((New-Object System.Net.Http.StringContent('Qwen/Qwen3-ASR-1.7B')), 'model')
|
|
306
|
-
|
|
307
|
-
$resp = $script:http.PostAsync('https://api.siliconflow.cn/v1/audio/transcriptions', $mp).GetAwaiter().GetResult()
|
|
308
|
-
$json = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
|
309
|
-
try { $resp.Dispose(); $mp.Dispose() } catch { }
|
|
306
|
+
if ($null -eq $script:http) {
|
|
307
|
+
$script:http = New-Object System.Net.Http.HttpClient
|
|
308
|
+
$script:http.Timeout = [TimeSpan]::FromSeconds(30)
|
|
309
|
+
$script:http.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $key)
|
|
310
|
+
}
|
|
311
|
+
$json = ''
|
|
312
|
+
try {
|
|
313
|
+
$mp = New-Object System.Net.Http.MultipartFormDataContent
|
|
314
|
+
$bytes = [IO.File]::ReadAllBytes($wav)
|
|
315
|
+
$bc = New-Object System.Net.Http.ByteArrayContent((,$bytes))
|
|
316
|
+
$bc.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('audio/wav')
|
|
317
|
+
$mp.Add($bc, 'file', 'a.wav')
|
|
318
|
+
$mp.Add((New-Object System.Net.Http.StringContent('Qwen/Qwen3-ASR-1.7B')), 'model')
|
|
319
|
+
# 不指定 language,交给模型自动判断中英文(指定 zh 时英文会被转成奇怪文字)
|
|
320
|
+
$resp = $script:http.PostAsync('https://api.siliconflow.cn/v1/audio/transcriptions', $mp).GetAwaiter().GetResult()
|
|
321
|
+
$json = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
|
322
|
+
try { $resp.Dispose(); $mp.Dispose() } catch { }
|
|
310
323
|
} catch { try { Add-Content -LiteralPath $ErrFile -Value ((Get-Date -Format 'HH:mm:ss') + ' HTTP ' + $_.Exception.Message) -Encoding UTF8 } catch { } }
|
|
311
324
|
if ($json) {
|
|
312
325
|
try { $obj = $json | ConvertFrom-Json; $text = [string]$obj.text; $via = 'cloud' } catch { }
|
|
@@ -315,9 +328,14 @@ $script:lastFishCheck = Get-Date
|
|
|
315
328
|
Remove-Item -LiteralPath $wav -Force -ErrorAction SilentlyContinue
|
|
316
329
|
Add-Content -LiteralPath $LevelFile -Value ((Get-Date -Format 'HH:mm:ss') + "`tengine=" + $via + "`ttext=" + $text) -Encoding UTF8
|
|
317
330
|
if ($text) { $text = $text.Trim() }
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
# 过滤乱语种:泰文/日文假名/西里尔/阿拉伯等说明识别跑偏了,当没听清
|
|
332
|
+
if ($text -and ($text -match '[\u0e00-\u0eff\u3040-\u30ff\u0400-\u04ff\u0600-\u06ff]')) {
|
|
333
|
+
Add-Content -LiteralPath $OutFile -Value ((Get-Date -Format 'HH:mm:ss') + "`t?`t(识别跑偏:" + $text + ")") -Encoding UTF8
|
|
334
|
+
$text = ''
|
|
335
|
+
}
|
|
336
|
+
if (-not $text -and [EarVad]::RawPeak -ge 800) {
|
|
337
|
+
Add-Content -LiteralPath $OutFile -Value ((Get-Date -Format 'HH:mm:ss') + "`t?`t(没听清)") -Encoding UTF8
|
|
338
|
+
}
|
|
321
339
|
if ($text -and ($text -match '[\u4e00-\u9fa5]' -or $text.Length -ge 4)) {
|
|
322
340
|
Add-Content -LiteralPath $OutFile -Value ((Get-Date -Format 'HH:mm:ss') + "`t1`t" + $text) -Encoding UTF8
|
|
323
341
|
}
|