dsh-fish-pet 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +177 -2
- package/assets/asr-listener.ps1 +146 -0
- package/assets/fish-pet.ps1 +1002 -0
- package/assets/start-ear.vbs +5 -0
- package/assets/start-pet.vbs +5 -0
- package/cordis.patch.yml +7 -0
- package/docs/banner.png +0 -0
- package/lib/index.js +146 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-fish-pet contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,178 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-fish-pet · 桌面蓝鱼
|
|
2
|
+
|
|
3
|
+

|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 养一条住在 Windows 桌面上的蓝鱼。
|
|
6
|
+
它不是网页贴图:是一个**逐像素透明的置顶窗口**,用 PowerShell + WinForms 逐帧绘制,还能**听你说话、跟你聊天、用中文语音回你**。
|
|
7
|
+
|
|
8
|
+
> 个人作品,与 DeepSeek 官方无关。
|
|
9
|
+
|
|
10
|
+
## 特性
|
|
11
|
+
|
|
12
|
+
- 桌面常驻:缓慢游动、随机转身、眨眼、吐泡泡、尾巴摆动
|
|
13
|
+
- 交互:拖动换位置、左键喂食、右键菜单(大小 / 回家 / 语音朗读 / 退出)
|
|
14
|
+
- 聊天:输入框打字 → 调 `deepseek-flash` → 气泡回复 + 语音朗读
|
|
15
|
+
- **语音对话**:独立“耳朵”进程用 WASAPI 采集麦克风 → 云端 SenseVoice 识别 → 自动接话
|
|
16
|
+
- 单实例守卫、异常落盘(不弹框)、耳朵掉线自动重启、位置记忆、开机自启
|
|
17
|
+
|
|
18
|
+
## 环境要求
|
|
19
|
+
|
|
20
|
+
- Windows 10 / 11
|
|
21
|
+
- PowerShell 5.1(系统自带)
|
|
22
|
+
- Node.js ≥ 22(仅作为 DSH 插件时需要)
|
|
23
|
+
- 麦克风(语音对话需要)
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
### 方式一:作为 DSH 插件
|
|
28
|
+
|
|
29
|
+
`dsh plugin` 把参数原样转给 pnpm,所以下面几种来源都能用,装完**重启 `dsh web`** 生效。
|
|
30
|
+
|
|
31
|
+
**① 直接从仓库装(推荐,已验证)**
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
dsh plugin --profile web add "git+https://gitee.com/adengou/dsh-fish-pet.git"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**② 从本地目录 / 离线 tgz 装**
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
dsh plugin --profile web add "file:C:/path/to/dsh-fish-pet"
|
|
41
|
+
dsh plugin --profile web add "file:C:/path/to/dsh-fish-pet-0.2.0.tgz"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**③ 从服务器上的 tgz 直链装**(把 tgz 传到 Release / 图床等可直链的地方)
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
dsh plugin --profile web add "https://你的地址/dsh-fish-pet-0.2.0.tgz"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> 版本要求:DSH `>= 0.2.1-rc.5`。版本不符时插件行会被判不兼容而禁用。
|
|
51
|
+
|
|
52
|
+
**暂未提供的方式(等发布后可用)**
|
|
53
|
+
|
|
54
|
+
| 方式 | 命令 | 现状 |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| GitHub | `dsh plugin --profile web add github:adengou/dsh-fish-pet` | ❌ 仓库尚未同步到 GitHub |
|
|
57
|
+
| npm | `dsh plugin --profile web add dsh-fish-pet` | ❌ 包尚未发布到 npm(名称目前未被占用) |
|
|
58
|
+
|
|
59
|
+
发布后会在这里补上,并更新本表。
|
|
60
|
+
|
|
61
|
+
### 方式二:独立运行(不装 DSH 也能用)
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
wscript.exe assets\start-pet.vbs # 桌面鱼
|
|
65
|
+
wscript.exe assets\start-ear.vbs # 云端耳朵(语音对话用)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 配置密钥(重要)
|
|
69
|
+
|
|
70
|
+
本仓库**不包含任何密钥**。两个 key 的读取顺序:
|
|
71
|
+
|
|
72
|
+
| 用途 | 环境变量(优先) | 文件(次选) |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| 对话模型(DeepSeek) | `DEEPSEEK_API_KEY` | `<数据目录>\deepseek.key` |
|
|
75
|
+
| 语音识别(SiliconFlow) | `SILICONFLOW_API_KEY` | `<数据目录>\siliconflow.key` |
|
|
76
|
+
|
|
77
|
+
- `<数据目录>` 默认 `%USERPROFILE%\.dsh\fish-pet`
|
|
78
|
+
- 兼容 DSH:DeepSeek key 找不到时还会读 `%USERPROFILE%\.dsh\.credentials.yaml` 的 `DEEPSEEK_API_KEY`
|
|
79
|
+
- 密钥文件已在 `.gitignore` 里,请勿提交
|
|
80
|
+
|
|
81
|
+
### 快速配置(PowerShell)
|
|
82
|
+
|
|
83
|
+
```powershell
|
|
84
|
+
mkdir "$env:USERPROFILE\.dsh\fish-pet" -Force
|
|
85
|
+
|
|
86
|
+
Set-Content "$env:USERPROFILE\.dsh\fish-pet\deepseek.key" -Value "sk-你的DeepSeek密钥" -NoNewline
|
|
87
|
+
Set-Content "$env:USERPROFILE\.dsh\fish-pet\siliconflow.key" -Value "sk-你的硅基流动密钥" -NoNewline
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
注意事项:
|
|
91
|
+
|
|
92
|
+
- 文件里**只放 key 本身**,一行、不加引号、不加说明文字
|
|
93
|
+
- 用记事本也行:`Win+R` → `%USERPROFILE%\.dsh\fish-pet` → 新建 `deepseek.key` / `siliconflow.key`(注意扩展名别留 `.txt`)
|
|
94
|
+
- 想改用环境变量:`setx DEEPSEEK_API_KEY "sk-..."` 和 `setx SILICONFLOW_API_KEY "sk-..."`,之后重开进程生效
|
|
95
|
+
- 验证:`Get-ChildItem "$env:USERPROFILE\.dsh\fish-pet\*.key" | Select-Object Name, Length`
|
|
96
|
+
|
|
97
|
+
> ⚠️ 不要把真实 key 写进 README 或任何提交到仓库的文件——仓库是公开的。
|
|
98
|
+
|
|
99
|
+
### 获取 SiliconFlow key
|
|
100
|
+
|
|
101
|
+
1. 打开 <https://cloud.siliconflow.cn>,手机号注册登录
|
|
102
|
+
2. 控制台 → API 密钥 → 新建密钥
|
|
103
|
+
3. 写入 `%USERPROFILE%\.dsh\fish-pet\siliconflow.key`,或设为环境变量
|
|
104
|
+
|
|
105
|
+
### 获取 DeepSeek key
|
|
106
|
+
|
|
107
|
+
<https://platform.deepseek.com> → API keys。
|
|
108
|
+
|
|
109
|
+
## 隐私
|
|
110
|
+
|
|
111
|
+
- 麦克风音频按约 5 秒切片,**上传 SiliconFlow 做识别**;识别出的文字再发 DeepSeek 生成回复
|
|
112
|
+
- 其余数据(位置、日志、密钥)只存本机
|
|
113
|
+
- 不想用语音:别启动 `start-ear.vbs`,只用打字聊天
|
|
114
|
+
|
|
115
|
+
## 目录结构
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
dsh-fish-pet/
|
|
119
|
+
├─ lib/index.js # DSH 插件宿主:拉起/守护桌宠进程
|
|
120
|
+
├─ assets/
|
|
121
|
+
│ ├─ fish-pet.ps1 # 桌宠主体(绘制 + 聊天 + 语音朗读)
|
|
122
|
+
│ ├─ asr-listener.ps1 # 云端耳朵(WASAPI → SiliconFlow)
|
|
123
|
+
│ ├─ start-pet.vbs # 独立启动器
|
|
124
|
+
│ └─ start-ear.vbs
|
|
125
|
+
├─ cordis.patch.yml # DSH 插件插入行
|
|
126
|
+
└─ package.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 运行数据(`%USERPROFILE%\.dsh\fish-pet\`)
|
|
130
|
+
|
|
131
|
+
| 文件 | 作用 |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `state.json` | 位置 / 大小 |
|
|
134
|
+
| `pet.pid` | 单实例守卫 |
|
|
135
|
+
| `error.log` | 桌宠异常 |
|
|
136
|
+
| `asr.log` | 识别结果(`H`=听到声音,`1`=识别成功) |
|
|
137
|
+
| `asr.level` | 每段麦克风峰值(排查“听不见”先看它) |
|
|
138
|
+
| `asr.err` | 耳朵异常 |
|
|
139
|
+
|
|
140
|
+
## 常见问题
|
|
141
|
+
|
|
142
|
+
**叫它没反应?**
|
|
143
|
+
1. 看 `asr.level`:说话时峰值应明显变大;一直是几十说明麦克风没进来
|
|
144
|
+
2. 看 `asr.log`:出现 `H` 说明听到了,出现 `1` 说明识别成功
|
|
145
|
+
3. Windows 设置 → 隐私和安全性 → 麦克风,允许桌面应用访问
|
|
146
|
+
|
|
147
|
+
**识别不准?**
|
|
148
|
+
- 安静环境、靠近麦克风最好
|
|
149
|
+
- 可调 `asr-listener.ps1` 顶部参数:`-ChunkMs`(片段长度)、`-SilenceGate`(静音门限)
|
|
150
|
+
|
|
151
|
+
**它会接别人的话?**
|
|
152
|
+
- 它是持续监听的;不想要时停掉 `start-ear.vbs`,或调高 `SilenceGate`
|
|
153
|
+
|
|
154
|
+
## 开发
|
|
155
|
+
|
|
156
|
+
纯 PowerShell + WinForms,无第三方依赖。语法检查:
|
|
157
|
+
|
|
158
|
+
```powershell
|
|
159
|
+
powershell -NoProfile -Command "$e=$null;$t=$null;[void][System.Management.Automation.Language.Parser]::ParseFile('assets\fish-pet.ps1',[ref]$t,[ref]$e);$e"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
> 脚本是 UTF-8 **带 BOM**;编辑器保存时保留 BOM,否则 PowerShell 5.1 会按 ANSI 读成乱码。
|
|
163
|
+
|
|
164
|
+
## 插件配置
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
- insert:
|
|
168
|
+
- id: fish-pet
|
|
169
|
+
name: dsh-fish-pet
|
|
170
|
+
config:
|
|
171
|
+
enabled: true # false 则不启动
|
|
172
|
+
scale: 1 # 0.5 ~ 2
|
|
173
|
+
speed: 26 # 5 ~ 120 像素/秒
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 许可
|
|
177
|
+
|
|
178
|
+
MIT,见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# asr-listener.ps1 · 云端耳朵:WASAPI 采集 → WAV → SiliconFlow SenseVoice → 结果落盘
|
|
2
|
+
param([int]$ChunkMs = 6500, [int]$Boost = 1, [int]$SilenceGate = 1200)
|
|
3
|
+
|
|
4
|
+
$ErrorActionPreference = 'Stop'
|
|
5
|
+
$DataDir = Join-Path $env:USERPROFILE '.dsh\fish-pet'
|
|
6
|
+
if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
|
|
7
|
+
$OutFile = Join-Path $DataDir 'asr.log'
|
|
8
|
+
$LevelFile = Join-Path $DataDir 'asr.level'
|
|
9
|
+
$ErrFile = Join-Path $DataDir 'asr.err'
|
|
10
|
+
$KeyFile = Join-Path $DataDir 'siliconflow.key'
|
|
11
|
+
|
|
12
|
+
Add-Type -TypeDefinition @"
|
|
13
|
+
using System;
|
|
14
|
+
using System.Runtime.InteropServices;
|
|
15
|
+
using System.Threading;
|
|
16
|
+
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMEnum { }
|
|
17
|
+
[ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
18
|
+
interface IMMDeviceEnumerator { int EnumAudioEndpoints(int f, int s, out IntPtr c); int GetDefaultAudioEndpoint(int f, int r, out IMMDevice d); }
|
|
19
|
+
[ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
20
|
+
interface IMMDevice { int Activate(ref Guid iid, int ctx, IntPtr p, [MarshalAs(UnmanagedType.IUnknown)] out object o); }
|
|
21
|
+
[ComImport, Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
22
|
+
interface IAudioClient {
|
|
23
|
+
int Initialize(int share, int flags, long dur, long period, IntPtr fmt, IntPtr sess);
|
|
24
|
+
int GetBufferSize(out uint frames); int GetStreamLatency(out long lat); int GetCurrentPadding(out uint pad);
|
|
25
|
+
int IsFormatSupported(int share, IntPtr fmt, out IntPtr closest); int GetMixFormat(out IntPtr fmt);
|
|
26
|
+
int GetDevicePeriod(out long def, out long min); int Start(); int Stop(); int Reset(); int SetEventHandle(IntPtr e);
|
|
27
|
+
int GetService(ref Guid iid, [MarshalAs(UnmanagedType.IUnknown)] out object o);
|
|
28
|
+
}
|
|
29
|
+
[ComImport, Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
30
|
+
interface IAudioCaptureClient {
|
|
31
|
+
int GetBuffer(out IntPtr data, out uint frames, out uint flags, out ulong devpos, out ulong qpc);
|
|
32
|
+
int ReleaseBuffer(uint frames); int GetNextPacketSize(out uint frames);
|
|
33
|
+
}
|
|
34
|
+
public class Ear {
|
|
35
|
+
public static byte[] Pcm; public static int Peak; public static int RawPeak;
|
|
36
|
+
public static bool Record(int ms, int boost, int role) {
|
|
37
|
+
Peak = 0; Pcm = new byte[0];
|
|
38
|
+
var en = (IMMDeviceEnumerator)(new MMEnum());
|
|
39
|
+
IMMDevice dev;
|
|
40
|
+
if (en.GetDefaultAudioEndpoint(1, role, out dev) != 0) { if (en.GetDefaultAudioEndpoint(1, 0, out dev) != 0) return false; }
|
|
41
|
+
Guid iid = typeof(IAudioClient).GUID; object o; dev.Activate(ref iid, 1, IntPtr.Zero, out o);
|
|
42
|
+
var ac = (IAudioClient)o;
|
|
43
|
+
IntPtr fmt; ac.GetMixFormat(out fmt);
|
|
44
|
+
int rate = Marshal.ReadInt32(fmt, 4); short ch = Marshal.ReadInt16(fmt, 2);
|
|
45
|
+
if (ac.Initialize(0, 0, 5000000, 0, fmt, IntPtr.Zero) != 0) return false;
|
|
46
|
+
Guid ci = typeof(IAudioCaptureClient).GUID; object co; ac.GetService(ref ci, out co);
|
|
47
|
+
var cc = (IAudioCaptureClient)co;
|
|
48
|
+
ac.Start();
|
|
49
|
+
var samples = new System.Collections.Generic.List<float>();
|
|
50
|
+
var b4 = new byte[4];
|
|
51
|
+
var end = DateTime.UtcNow.AddMilliseconds(ms);
|
|
52
|
+
while (DateTime.UtcNow < end) {
|
|
53
|
+
uint pkt; if (cc.GetNextPacketSize(out pkt) != 0 || pkt == 0) { Thread.Sleep(3); continue; }
|
|
54
|
+
IntPtr data; uint frames, flags; ulong dp, qp;
|
|
55
|
+
if (cc.GetBuffer(out data, out frames, out flags, out dp, out qp) != 0) break;
|
|
56
|
+
int n = (int)frames * ch * 4;
|
|
57
|
+
for (int i = 0; i + 3 < n; i += 4) {
|
|
58
|
+
b4[0] = Marshal.ReadByte(data, i); b4[1] = Marshal.ReadByte(data, i+1);
|
|
59
|
+
b4[2] = Marshal.ReadByte(data, i+2); b4[3] = Marshal.ReadByte(data, i+3);
|
|
60
|
+
samples.Add(BitConverter.ToSingle(b4, 0));
|
|
61
|
+
}
|
|
62
|
+
cc.ReleaseBuffer(frames);
|
|
63
|
+
}
|
|
64
|
+
ac.Stop();
|
|
65
|
+
if (samples.Count < 320) return false;
|
|
66
|
+
double step = (double)rate / 16000.0;
|
|
67
|
+
int framesIn = samples.Count / ch;
|
|
68
|
+
int outCount = (int)(framesIn / step);
|
|
69
|
+
if (outCount < 160) return false;
|
|
70
|
+
var tmp = new int[outCount];
|
|
71
|
+
int maxAbs = 0;
|
|
72
|
+
for (int i = 0; i < outCount; i++) {
|
|
73
|
+
int s0 = (int)(i * step);
|
|
74
|
+
int s1 = (int)Math.Min(framesIn - 1, (i + 1) * step);
|
|
75
|
+
double sum = 0; int cnt = 0;
|
|
76
|
+
for (int f = s0; f <= s1; f++) { sum += samples[f * ch]; cnt++; }
|
|
77
|
+
int v = cnt > 0 ? (int)((sum / cnt) * 32767.0 * boost) : 0;
|
|
78
|
+
tmp[i] = v; int a = Math.Abs(v); if (a > maxAbs) maxAbs = a;
|
|
79
|
+
}
|
|
80
|
+
RawPeak = maxAbs;
|
|
81
|
+
double gain = 1.0;
|
|
82
|
+
if (maxAbs > 200) { gain = Math.Min(12.0, 20000.0 / maxAbs); }
|
|
83
|
+
var outp = new byte[outCount * 2];
|
|
84
|
+
for (int i = 0; i < outCount; i++) {
|
|
85
|
+
int v = (int)(tmp[i] * gain);
|
|
86
|
+
if (v > 32767) v = 32767; if (v < -32768) v = -32768;
|
|
87
|
+
int a = Math.Abs(v); if (a > Peak) Peak = a;
|
|
88
|
+
outp[i*2] = (byte)(v & 0xFF); outp[i*2+1] = (byte)((v >> 8) & 0xFF);
|
|
89
|
+
}
|
|
90
|
+
Pcm = outp; return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
"@
|
|
94
|
+
|
|
95
|
+
function Write-Wav([string]$path, [byte[]]$pcm) {
|
|
96
|
+
$fs = [IO.File]::Open($path, 'Create', 'Write')
|
|
97
|
+
$bw = New-Object IO.BinaryWriter($fs)
|
|
98
|
+
$bw.Write([Text.Encoding]::ASCII.GetBytes('RIFF')); $bw.Write([int](36 + $pcm.Length))
|
|
99
|
+
$bw.Write([Text.Encoding]::ASCII.GetBytes('WAVE')); $bw.Write([Text.Encoding]::ASCII.GetBytes('fmt '))
|
|
100
|
+
$bw.Write([int]16); $bw.Write([int16]1); $bw.Write([int16]1); $bw.Write([int]16000)
|
|
101
|
+
$bw.Write([int]32000); $bw.Write([int16]2); $bw.Write([int16]16)
|
|
102
|
+
$bw.Write([Text.Encoding]::ASCII.GetBytes('data')); $bw.Write([int]$pcm.Length)
|
|
103
|
+
$bw.Write($pcm); $bw.Close(); $fs.Close()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Add-Content -LiteralPath $OutFile -Value ("# ear(start) pid=" + $PID) -Encoding UTF8
|
|
107
|
+
|
|
108
|
+
while ($true) {
|
|
109
|
+
try {
|
|
110
|
+
# 鱼正在说话时闭麦,避免把它自己的声音听回去
|
|
111
|
+
if (Test-Path (Join-Path $DataDir 'asr.talking')) { Start-Sleep -Milliseconds 200; continue }
|
|
112
|
+
$pf = Join-Path $DataDir 'asr.pause'
|
|
113
|
+
if (Test-Path $pf) {
|
|
114
|
+
$age = ((Get-Date) - (Get-Item $pf).LastWriteTime).TotalSeconds
|
|
115
|
+
if ($age -lt 1.2) { Start-Sleep -Milliseconds 200; continue }
|
|
116
|
+
}
|
|
117
|
+
$key = $env:SILICONFLOW_API_KEY
|
|
118
|
+
if (-not $key -and (Test-Path $KeyFile)) { $key = (Get-Content -LiteralPath $KeyFile -Raw -ErrorAction SilentlyContinue) }
|
|
119
|
+
if ($key) { $key = $key.Trim() }
|
|
120
|
+
if (-not $key) { Start-Sleep -Seconds 2; continue }
|
|
121
|
+
|
|
122
|
+
$ok = [Ear]::Record($ChunkMs, $Boost, 2)
|
|
123
|
+
if (-not $ok) { Start-Sleep -Milliseconds 300; continue }
|
|
124
|
+
$peak = [Ear]::Peak
|
|
125
|
+
$pcm = [Ear]::Pcm
|
|
126
|
+
$raw = [Ear]::RawPeak
|
|
127
|
+
Add-Content -LiteralPath $LevelFile -Value ((Get-Date -Format 'HH:mm:ss') + "`traw=" + $raw + "`tpeak=" + $peak) -Encoding UTF8
|
|
128
|
+
if ($raw -lt $SilenceGate -or $pcm.Length -lt 3200) { Start-Sleep -Milliseconds 120; continue }
|
|
129
|
+
Add-Content -LiteralPath $OutFile -Value ((Get-Date -Format 'HH:mm:ss') + "`tH`t(听到声音)") -Encoding UTF8
|
|
130
|
+
$wav = Join-Path $DataDir ('chunk-' + [guid]::NewGuid().ToString('N') + '.wav')
|
|
131
|
+
Write-Wav $wav $pcm
|
|
132
|
+
$json = & curl.exe -s -X POST 'https://api.siliconflow.cn/v1/audio/transcriptions' -H ("Authorization: Bearer " + $key) -F ("file=@" + $wav) -F 'model=FunAudioLLM/SenseVoiceSmall' 2>$null | Out-String
|
|
133
|
+
Remove-Item -LiteralPath $wav -Force -ErrorAction SilentlyContinue
|
|
134
|
+
if ($json) {
|
|
135
|
+
$obj = $json | ConvertFrom-Json
|
|
136
|
+
$text = [string]$obj.text
|
|
137
|
+
if ($text -and ($text -match '[\u4e00-\u9fa5]' -or $text.Length -ge 5)) {
|
|
138
|
+
Add-Content -LiteralPath $OutFile -Value ((Get-Date -Format 'HH:mm:ss') + "`t1`t" + $text.Trim()) -Encoding UTF8
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
Start-Sleep -Milliseconds 120
|
|
142
|
+
} catch {
|
|
143
|
+
Add-Content -LiteralPath $ErrFile -Value ((Get-Date -Format 'HH:mm:ss') + ' ' + $_.Exception.Message) -Encoding UTF8
|
|
144
|
+
Start-Sleep -Milliseconds 800
|
|
145
|
+
}
|
|
146
|
+
}
|