chenbhao Claude Opus 4.6 commited on
Commit
346a09e
·
1 Parent(s): f212f8b

refactor: remove friend feature, local whisper, doubao & anthropic STT; consolidate voice to Groq

Browse files

Remove the entire /friend desktop companion feature (Tauri frontend, FriendService,
SSE/STT server, skills, tools, routes) and the 11G Tauri build artifacts. Migrate the
edgeTts helper from friend/tts.ts into services/voice/edgeTTS.ts so auto-TTS survives.

Remove local whisper STT (whisperSTT.ts + whisper_server.py/transcribe.py) and the
doubao + anthropic voice_stream backends, since STT now runs exclusively on Groq
Whisper (whisper-large-v3 / -turbo). Shared STT connection types move into groqSTT.ts
and the voiceProvider setting is narrowed to ['groq'] with a default. Also rework
edgeTTS.ts to use the node-edge-tts package instead of a Python subprocess, dropping
the now-unused scripts/speak.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. assets/friend/friend_use.png +0 -3
  2. assets/friend/prompt_format.png +0 -3
  3. docs/friend/architecture.md +0 -419
  4. docs/friend/data-flow.md +0 -369
  5. docs/friend/emotion-map.md +0 -195
  6. docs/friend/frontend-3d.md +0 -573
  7. docs/friend/overview.md +0 -138
  8. docs/friend/voice-vad.md +0 -527
  9. package.json +1 -3
  10. scripts/build.ts +0 -27
  11. scripts/speak.py +0 -42
  12. scripts/transcribe.py +0 -81
  13. scripts/whisper_server.py +0 -80
  14. src/commands.ts +0 -2
  15. src/commands/friend/friend.tsx +0 -156
  16. src/commands/friend/index.ts +0 -11
  17. src/components/friend/frontend/App.tsx +0 -352
  18. src/components/friend/frontend/api.ts +0 -51
  19. src/components/friend/frontend/assets/lip-sync-profile.json +0 -1
  20. src/components/friend/frontend/components/ChatInput.tsx +0 -215
  21. src/components/friend/frontend/components/ResizeHandles.tsx +0 -7
  22. src/components/friend/frontend/components/SettingsPanel.tsx +0 -657
  23. src/components/friend/frontend/components/TextBubble.tsx +0 -609
  24. src/components/friend/frontend/components/VRMScene.tsx +0 -697
  25. src/components/friend/frontend/emote.ts +0 -206
  26. src/components/friend/frontend/hooks/usePassThrough.ts +0 -12
  27. src/components/friend/frontend/hooks/useServerStt.ts +0 -137
  28. src/components/friend/frontend/index.html +0 -63
  29. src/components/friend/frontend/lip-sync.ts +0 -192
  30. src/components/friend/frontend/main.tsx +0 -15
  31. src/components/friend/frontend/mixamo-loader.ts +0 -141
  32. src/components/friend/frontend/motion-controller.ts +0 -331
  33. src/components/friend/frontend/package-lock.json +0 -2701
  34. src/components/friend/frontend/package.json +0 -34
  35. src/components/friend/frontend/public/akimbo.vrma +0 -3
  36. src/components/friend/frontend/public/angry.fbx +0 -3
  37. src/components/friend/frontend/public/angryPump.fbx +0 -3
  38. src/components/friend/frontend/public/excited.fbx +0 -3
  39. src/components/friend/frontend/public/greeting.fbx +0 -3
  40. src/components/friend/frontend/public/happy.fbx +0 -3
  41. src/components/friend/frontend/public/idle_loop.vrma +0 -3
  42. src/components/friend/frontend/public/jile.vmd +0 -3
  43. src/components/friend/frontend/public/love.vmd +0 -3
  44. src/components/friend/frontend/public/playFingers.vrma +0 -3
  45. src/components/friend/frontend/public/point.fbx +0 -3
  46. src/components/friend/frontend/public/salute.fbx +0 -3
  47. src/components/friend/frontend/public/scratchHead.vrma +0 -3
  48. src/components/friend/frontend/public/shy.fbx +0 -3
  49. src/components/friend/frontend/public/stretch.vrma +0 -3
  50. src/components/friend/frontend/src-tauri/Cargo.lock +0 -0
assets/friend/friend_use.png DELETED

Git LFS Details

  • SHA256: 4cbec71c829ec3f5d408c15f142e0cb91f5057aeaee6ea51a88c2e1d752505f7
  • Pointer size: 132 Bytes
  • Size of remote file: 5.65 MB
assets/friend/prompt_format.png DELETED

Git LFS Details

  • SHA256: dfcb31e3ef93a8bf621f551c016237529a494c52fb6d59f9910d16a5bba4b215
  • Pointer size: 132 Bytes
  • Size of remote file: 3.17 MB
docs/friend/architecture.md DELETED
@@ -1,419 +0,0 @@
1
- # Friend 系统架构
2
-
3
- 本文档详细描述 Friend VRM 桌面伴侣系统的各个组件架构。
4
-
5
- ---
6
-
7
- ## 1. FriendService — 核心编排器
8
-
9
- **文件**: `src/friend/FriendService.ts` (~31KB, 897 行)
10
-
11
- ### 1.1 生命周期管理
12
-
13
- ```
14
- start() ──► 状态: 'starting'
15
-
16
- ├── 初始化 Silero VAD (非致命失败,失败后语音捕获降级为 F2-only)
17
- │ └── 配置: threshold 0.75, preSpeechTriggerFrames 10, redemptionFrames 20
18
-
19
- └── 状态: 'running'
20
-
21
- stop() ──► 停止语音捕获 → 清理 STT 连接 → 状态: 'stopped'
22
- ```
23
-
24
- ### 1.2 React external store 接口
25
-
26
- FriendService 实现了类似 React `useSyncExternalStore` 的接口:
27
-
28
- ```typescript
29
- subscribe(listener: Listener): () => void // 添加状态监听
30
- subscribeToInbound(listener): () => void // 监听用户输入事件
31
- getStateSnapshot(): FriendServiceState // 获取当前状态快照
32
- ```
33
-
34
- `FriendServiceState` 包含:
35
- - `status`: `'stopped' | 'starting' | 'running' | 'error'`
36
- - `lastError`: 可选错误信息
37
- - `displayClientCount`: SSE 显示器客户端数量
38
- - `captureStatus`: 语音捕获状态(`capturing` + `interimText`)
39
-
40
- ### 1.3 文本中继
41
-
42
- ```typescript
43
- sendText(text: string): void
44
- ```
45
-
46
- 流程:
47
- 1. 通知所有 `inboundListeners`(bridge hook 用于 turn tracking)
48
- 2. 动态导入 `messageQueueManager.enqueue()`(避免循环依赖)
49
- 3. 使用 `bridgeOrigin: true` 和 `origin: { kind: 'channel', server: 'friend' }` 标记来源
50
-
51
- ### 1.4 语音捕获
52
-
53
- 参见 [语音捕获与 VAD 策略](voice-vad.md) 详细文档。
54
-
55
- ```
56
- startVoiceCapture()
57
-
58
- ├── 检测 STT provider(自动降级)
59
- ├── 创建 STT 连接(8s 超时)
60
- ├── 启动 arecord/parecord 子进程(500ms 验证窗口)
61
- └── 启动 VAD 检测
62
- ```
63
-
64
- ### 1.5 静音系统
65
-
66
- AI 处理全过程阻止麦克风采音进入 STT/VAD,防止 TTS 播放时的回声。
67
-
68
- ```
69
- startAiTurnMute()
70
- │ muted = true, VAD pause
71
- │ 设置 30s 安全性计时器
72
-
73
- ├── AI 处理 → TTS 生成
74
-
75
- └── extendMuteForTts(audioId)
76
- │ 解析 MP3 精确时长
77
- │ 重置计时器为精确播放时长
78
-
79
- └── unmute()
80
- │ muted = false, VAD resume
81
- ```
82
-
83
- ### 1.6 STT 自动检测
84
-
85
- ```typescript
86
- detectAvailableSttProvider()
87
-
88
- ├── 1. Groq Whisper API (最快, REST 调用)
89
- ├── 2. Local Whisper (pip install openai-whisper)
90
- ├── 3. Anthropic Voice Stream
91
- ├── 4. Doubao ASR (检查 ~/.claude/tts/doubao/credentials.json)
92
-
93
- └── 全不可用则抛出错误
94
- ```
95
-
96
- ### 1.7 TTS 生成
97
-
98
- ```typescript
99
- generateTts(text: string)
100
-
101
- ├── prefs.provider === 'qwen' + prefs.qwenKey 存在
102
- │ └── Qwen DashScope TTS (qwen3-tts-flash)
103
-
104
- └── else
105
- └── Edge TTS (node-edge-tts, 默认 zh-CN-XiaoxiaoNeural)
106
- ```
107
-
108
- ### 1.8 SSE 广播
109
-
110
- ```typescript
111
- broadcastResponse(text: string)
112
-
113
- ├── broadcastToVrm({ text }) // 发送文字到前端 TextBubble
114
- ├── if TTS enabled:
115
- │ ├── generateTts(text)
116
- │ ├── broadcastToVrm({ audioUrl, sendFirstTts: true })
117
- │ └── extendMuteForTts(audioId)
118
- └── broadcastToVrm({ replyDone: true }) // 信号回复完成
119
- ```
120
-
121
- ### 1.9 MP3 时长解析
122
-
123
- `getMp3DurationMs()` 方法使用帧同步头扫描法精确计算 MP3 时长:
124
-
125
- 1. 跳过 ID3v2 标签头
126
- 2. 查找前两个帧同步字(0xFF + 0xE0)
127
- 3. 计算实际帧间隔(CBR 模式)
128
- 4. 解析帧头获取采样率和每帧采样数
129
- 5. 按步长计数帧数
130
- 6. 计算 `(帧数 * 每帧采样数 / 采样率) * 1000`
131
-
132
- ---
133
-
134
- ## 2. SSE 模块
135
-
136
- **文件**: `src/friend/sse.ts`
137
-
138
- ### 2.1 客户端注册表
139
-
140
- ```typescript
141
- Set<SseClient> // 全局 SSE 客户端集合
142
- ```
143
-
144
- - `addSseClient(client)`: 注册新客户端
145
- - `removeSseClient(client)`: 移除客户端
146
- - `getSseClientCount()`: 获取活跃客户端数
147
- - `createSseClientId()`: 生成唯一 ID (`sse-{counter}-{timestamp}`)
148
-
149
- ### 2.2 VrmBroadcastPayload 类型
150
-
151
- ```typescript
152
- type VrmBroadcastPayload = {
153
- text?: string; // 回复文字
154
- emotion?: string; // 表情名
155
- emotionIntensity?: number; // 表情强度 0-1
156
- audioUrl?: string; // TTS 音频 URL
157
- audioIndex?: number; // 音频索引(多句排序)
158
- clearText?: boolean; // 清空气泡文字
159
- imageUrl?: string; // 显示图片
160
- moodDelta?: number; // 心情变化
161
- moodIndex?: number; // 当前心情指数 0-100
162
- sendFirstTts?: boolean; // 开始 TTS 播放信号
163
- appendText?: boolean; // 追加文字(后续句子)
164
- replyDone?: boolean; // 回复结束信号
165
- };
166
- ```
167
-
168
- ### 2.3 广播机制
169
-
170
- ```typescript
171
- broadcastToVrm(payload: VrmBroadcastPayload)
172
- ├── 序列化为 SSE data 格式: `data: {json}\n\n`
173
- ├── 遍历所有客户端,逐个写入
174
- └── 写入失败的客户端自动移除
175
- ```
176
-
177
- ### 2.4 连接建立
178
-
179
- `createSseResponse()` 创建 Bun ReadableStream,返回 `text/event-stream` 响应。
180
- 初始发送空行以确认连接建立。客户端断开时自动取消注册。
181
-
182
- ---
183
-
184
- ## 3. HTTP 服务器
185
-
186
- **文件**: `src/friend/server.ts`
187
-
188
- ### 3.1 Bun.serve() 配置
189
-
190
- - 端口: 3456
191
- - 主机: 127.0.0.1
192
- - `idleTimeout`: 60s(容纳慢速 STT 初始化)
193
-
194
- ### 3.2 端口管理
195
-
196
- `freePort()` 方法在启动时尝试释放被占用的端口:
197
- 1. 使用 `ss -tlnp` 查找端口占用进程
198
- 2. 验证进程是否为 `bun`/`Codev`/`claude-*`/`node`
199
- 3. 发送 SIGTERM,等待 3s,失败则 SIGKILL
200
-
201
- ### 3.3 路由
202
-
203
- | 路径 | 方法 | 功能 |
204
- |------|------|------|
205
- | `/plugins/friend/events` | GET | SSE 事件流 |
206
- | `/plugins/friend/*` | ANY | Friend API 路由(见下文) |
207
- | `/friend/*` | GET | 静态文件 |
208
- | WebSocket 升级 | ANY | 返回 426(不支持) |
209
-
210
- ---
211
-
212
- ## 4. Friend API 路由
213
-
214
- **文件**: `src/server/api/friend.ts` (~793 行)
215
-
216
- ### 4.1 完整路由表
217
-
218
- | 端点 | 方法 | 功能 |
219
- |------|------|------|
220
- | `/plugins/friend/events` | GET | SSE 事件流 |
221
- | `/plugins/friend/audio/:id` | GET | 提供 TTS 音频文件 |
222
- | `/plugins/friend/media/:id` | GET | 提供媒体文件 |
223
- | `/plugins/friend/chat` | POST | 文字聊天消息 |
224
- | `/plugins/friend/voice/stt-segment` | POST | 浏览器 VAD 语音片段 |
225
- | `/plugins/friend/voice/start` | POST | 开始服务器端语音捕获 |
226
- | `/plugins/friend/voice/stop` | POST | 停止语音捕获 |
227
- | `/plugins/friend/voice/status` | POST | 获取捕获状态 |
228
- | `/plugins/friend/touch` | POST | 触摸交互事件 |
229
- | `/plugins/friend/voice` | GET/POST | 语音设置 |
230
- | `/plugins/friend/stt/config` | GET | STT 配置 |
231
- | `/plugins/friend/stt/file` | POST | 文件转录 |
232
- | `/plugins/friend/preview` | POST | TTS 预览 |
233
- | `/plugins/friend/settings` | GET/POST | 通用设置 |
234
- | `/plugins/friend/persona` | GET/POST | 角色设定 |
235
- | `/plugins/friend/model/list` | GET | 模型列表 |
236
- | `/plugins/friend/model/serve/:file` | GET | 提供 VRM 模型文件 |
237
- | `/plugins/friend/model/import` | POST | 导入 VRM 模型 |
238
- | `/plugins/friend/history` | GET | 对话历史 |
239
- | `/plugins/friend/context/clear` | POST | 清空上下文 |
240
- | `/plugins/friend/mood/adjust` | POST | 调整心情 |
241
- | `/plugins/friend/session/memo` | POST | 记录会话备注 |
242
- | `/plugins/friend/dance/list` | GET | 舞蹈列表 |
243
- | `/plugins/friend/dance/import` | POST | 导入舞蹈 VMD/MP3 |
244
- | `/plugins/friend/dance/delete` | POST | 删除舞蹈 |
245
- | `/plugins/friend/dance/serve/:file` | GET | 提供舞蹈文件 |
246
- | `/plugins/friend/persona/screenshot` | POST | 保存 VRM 截图 |
247
- | `/plugins/friend/persona/generate` | POST | AI 生成角色设定 |
248
- | `/plugins/friend/screen/observe` | POST | 屏观察触发 |
249
- | `/friend/api/window-close` | POST | Tauri 窗口关闭事件 |
250
-
251
- ### 4.2 MIME 类型支持
252
-
253
- - 音频: `.mp3`, `.opus`, `.ogg`, `.wav`, `.webm`
254
- - 图片: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`
255
-
256
- ---
257
-
258
- ## 5. TTS 服务
259
-
260
- **文件**: `src/friend/tts.ts`
261
-
262
- ### 5.1 Edge TTS
263
-
264
- ```typescript
265
- edgeTts({ text, voice? })
266
- ├── 使用 node-edge-tts 库
267
- ├── 默认语音: zh-CN-XiaoxiaoNeural
268
- ├── 输出: 临时目录中的 MP3 文件
269
- └── 返回: { success, audioPath, error }
270
- ```
271
-
272
- ### 5.2 Qwen DashScope TTS
273
-
274
- ```typescript
275
- qwenTts({ text, apiKey, voice?, model?, language? })
276
- ├── 端点: dashscope.aliyuncs.com / dashscope-intl.aliyuncs.com
277
- ├── 默认模型: qwen3-tts-flash
278
- ├── 默认语音: Cherry
279
- ├── 超时: 30s
280
- └── 返回: { success, audioPath, error }
281
- ```
282
-
283
- ### 5.3 音频文件注册表
284
-
285
- ```typescript
286
- audioFiles = Map<string, string> // id → 文件路径
287
-
288
- registerAudioFile(filePath) // 注册文件,5 分钟后自动过期
289
- getAudioFile(id) // 获取文件路径
290
- ```
291
-
292
- ---
293
-
294
- ## 6. STT 服务
295
-
296
- **文件**: `src/friend/stt-service.ts`
297
-
298
- 提供基于文件的语音转录,用于 REST 端点。流式/进程内捕获由 FriendService 处理。
299
-
300
- 支持 Provider:
301
- - `anthropic`: Anthropic Voice Stream
302
- - `local`: Local Whisper (openai-whisper)
303
- - `doubao`: Doubao ASR
304
- - `browser`: 浏览器 VAD (仅前端的占位符)
305
-
306
- ---
307
-
308
- ## 7. VAD 服务
309
-
310
- **文件**: `src/friend/voice/vad-service.ts`
311
-
312
- 参见 [语音捕获与 VAD 策略](voice-vad.md) 详细文档。
313
-
314
- ### 7.1 核心架构
315
-
316
- - **模型**: Silero VAD legacy ONNX (来自 `@ericedouard/vad-node-realtime`)
317
- - **运行时**: onnxruntime-web WASM 后端(Bun 不兼容 onnxruntime-node)
318
- - **帧大小**: 512 采样 @ 16kHz = 32ms/帧
319
- - **预处理**: RMS 能量预过滤 (<0.004 跳过推理)
320
-
321
- ### 7.2 状态机
322
-
323
- ```
324
- pre-speech phase (preSpeechCount < preSpeechTriggerFrames)
325
- │ ├── 非语音帧 → 重置计数
326
- │ └── 连续语音帧达到阈值 → 进入 speaking
327
-
328
- speaking (confirmed speech segment)
329
- │ ├── 语音帧 → 重置 redemptionCounter
330
- │ └── 静音帧 → redemptionCounter++
331
- │ └── redemptionCounter >= redemptionFrames → endSpeech()
332
-
333
- silence redemption (grace period)
334
- └── endSpeech() → onSpeechEnd 回调
335
- ```
336
-
337
- ---
338
-
339
- ## 8. 偏好设置
340
-
341
- **文件**: `src/friend/prefs.ts`
342
-
343
- ```typescript
344
- interface FriendPrefs {
345
- enabled?: boolean;
346
- voice?: string; // TTS 语音
347
- provider?: string; // TTS provider (edge | qwen)
348
- qwenKey?: string; // Qwen API Key
349
- qwenModel?: string; // Qwen 模型名
350
- modelPath?: string; // VRM 模型路径
351
- ttsEnabled?: boolean; // TTS 开启
352
- showText?: boolean; // 显示文字气泡
353
- hideUI?: boolean; // 隐藏 UI
354
- tracking?: 'mouse' | 'camera'; // 眼球追踪模式
355
- volume?: number; // 音量 0-1
356
- uiAlign?: 'left' | 'right'; // UI 对齐
357
- screenObserve?: boolean; // 屏幕观察
358
- screenObserveInterval?: number; // 观察间隔(秒)
359
- language?: 'zh' | 'en'; // 语言
360
- currentDance?: string; // 当前舞蹈
361
- hideMood?: boolean; // 隐藏心情
362
- sttProvider?: string; // STT provider
363
- sttLanguage?: string; // STT 语言
364
- groqApiKey?: string; // Groq API Key
365
- }
366
- ```
367
-
368
- 持久化路径: `~/.config/Codev/friend.json`
369
-
370
- ---
371
-
372
- ## 9. Tauri Launcher
373
-
374
- **文件**: `src/friend/tauri-launcher.ts`
375
-
376
- - 查找 Tauri 二进制文件(release → debug)
377
- - 启动为 detached 子进程
378
- - 管道 stdout/stderr 到主进程日志
379
- - 退出时自动清理
380
-
381
- 启动路径: `src/components/friend/frontend/src-tauri/target/{release|debug}/codev-friend`
382
-
383
- ---
384
-
385
- ## 10. 常量
386
-
387
- **文件**: `src/friend/constants.ts`
388
-
389
- ```typescript
390
- GATEWAY_URL = 'http://127.0.0.1:3456'
391
- FRIEND_SESSION_KEY = 'agent:main:main'
392
- CHANNEL_ID = 'friend'
393
- VALID_EMOTIONS = ['happy', 'sad', 'angry', 'surprised', 'think', 'awkward',
394
- 'question', 'curious', 'neutral', 'love', 'flirty',
395
- 'greeting', 'relaxed']
396
- ```
397
-
398
- ---
399
-
400
- ## 依赖关系图
401
-
402
- ```
403
- server.ts ──┬── sse.ts ──────────► FriendService.ts ──┬── tts.ts
404
- │ │ ├── vad-service.ts
405
- │ │ ├── prefs.ts
406
- │ │ └── text-utils.ts
407
- │ │
408
- └── api/friend.ts ───┤
409
- ├── sse.ts
410
- ├── prefs.ts
411
- ├── tts.ts
412
- ├── stt-service.ts
413
- └── FriendService.ts
414
-
415
- FriendEmotionTool.ts ──► sse.ts, prefs.ts
416
- FriendScreenObserveTool.ts ──► sse.ts
417
-
418
- tauri-launcher.ts (独立启动)
419
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/friend/data-flow.md DELETED
@@ -1,369 +0,0 @@
1
- # Friend 数据流
2
-
3
- 本文档详细描述 Friend 系统中的四种核心数据流,以及 SSE 事件格式规范。
4
-
5
- ---
6
-
7
- ## 1. 文字对话流
8
-
9
- 用户在前端输入框输入文字,按下 Enter 发送。
10
-
11
- ```
12
- 用户输入文字
13
-
14
-
15
- ChatInput.tsx ── POST /plugins/friend/chat ──────────────────┐
16
- │ { message: "你好" } │
17
- │ │
18
- ▼ │
19
- handleFriendApi() ── friendService.start() │
20
- │ friendService.sendText(message) │
21
- ▼ │
22
- FriendService.sendText() │
23
- │ │
24
- ├── 通知 inboundListeners (bridge hook turn tracking) │
25
- │ │
26
- └── messageQueueManager.enqueue() │
27
- │ { mode: 'prompt', skipSlashCommands: true, │
28
- │ bridgeOrigin: true, origin: { server: 'friend' }} │
29
- │ │
30
- ▼ │
31
- AI Provider (Anthropic/NVIDIA/OpenAI) │
32
- │ │
33
- ├── 处理消息 │
34
- ├── 可调用 friend_emotion 工具设置表情 │
35
- ├── 可调用 friend_screen_observe 观察屏幕 │
36
- │ │
37
- ▼ │
38
- AI 回复流回 (通过 useFriendBridge / REPL) │
39
- │ │
40
- ▼ │
41
- FriendService.broadcastResponse(text) │
42
- │ │
43
- ├── broadcastToVrm({ text }) │
44
- │ │ │
45
- │ ▼ SSE data │
46
- │ TextBubble 收到 │
47
- │ │ │
48
- │ ├── 重置气泡,显示文字 │
49
- │ ├── 启动打字机效果 (逐字符显示) │
50
- │ │ - CJK: 200ms/char (TTS开启) / 80ms (关闭) │
51
- │ │ - English: 60ms/char (TTS开启) / 30ms (关闭)│
52
- │ ├── 打字机完成后渲染 Markdown │
53
- │ └── 1秒后触发 onMessage → VRMScene 表情动作 │
54
- │ │
55
- ├── if TTS enabled: │
56
- │ ├── generateTts(text) │
57
- │ │ ├── stripForTts() 清洗文本 │
58
- │ │ └── EdgeTTS 或 QwenTTS 生成 MP3 │
59
- │ │ │
60
- │ ├── broadcastToVrm({ audioUrl, sendFirstTts }) │
61
- │ │ │ │
62
- │ │ ▼ SSE data │
63
- │ │ TextBubble 开始音频队列播放 │
64
- │ │ │ │
65
- │ │ └── LipSync.playAudio(url) │
66
- │ │ ├── fetch 音频文件 │
67
- │ │ ├── decodeAudioData │
68
- │ │ ├── 连接到 lipSyncNode (分析) + gainNode (扬声器) │
69
- │ │ └── 播放时实时更新 VRM 嘴形 │
70
- │ │ │
71
- │ └── extendMuteForTts(audioId) │
72
- │ └── 精确计算 MP3 时长,更新静音定时器 │
73
- │ │
74
- └── broadcastToVrm({ replyDone: true }) │
75
- │ │
76
- ▼ SSE data │
77
- TextBubble 调度气泡隐藏 (2s 延迟) │
78
- 或等待后续 appendText 消息 │
79
- ```
80
-
81
- ---
82
-
83
- ## 2. 语音捕获流 (F2 通话模式)
84
-
85
- 用户按下 F2 进入连续语音通话模式,再次按下 F2 结束通话。
86
-
87
- ### 2.1 启动通话
88
-
89
- ```
90
- 用户按 F2
91
-
92
-
93
- ChatInput.startVoiceCall()
94
-
95
- ├── setVoiceCallActive(true)
96
-
97
- └── useServerStt.startStreaming()
98
-
99
- └── POST /plugins/friend/voice/start
100
-
101
-
102
- handleFriendApi()
103
-
104
- └── friendService.startVoiceCapture()
105
-
106
- ├── 检测 STT provider (Groq→Whisper→Anthropic→Doubao)
107
-
108
- ├── startSttConnection() (8s 超时)
109
- │ ├── Groq: connectGroqStream()
110
- │ ├── Local: connectLocalWhisperStream()
111
- │ ├── Anthropic: connectVoiceStream()
112
- │ └── Doubao: connectDoubaoStream()
113
-
114
- ├── loadAudioCapture()
115
- │ ├── 尝试 arecord (ALSA)
116
- │ │ args: -D default -r 16000 -f S16_LE -c 1 -t raw -q
117
- │ ├── 失败则尝试 parecord (PulseAudio)
118
- │ │ args: --raw --rate=16000 --format=s16le --channels=1
119
- │ └── 500ms 验证窗口: 确认子进程输出音频数据
120
-
121
- ├── arecord 数据回调:
122
- │ ├── if not muted → 转发到 STT connection.send(chunk)
123
- │ └── if not muted → 转发到 VAD processAudio(float32)
124
-
125
- └── vadInstance.start()
126
- ```
127
-
128
- ### 2.2 语音检测与转录
129
-
130
- ```
131
- 麦克风音频流 (16kHz S16LE)
132
-
133
- ├──► STT Connection.send(chunk) (实时流式转录)
134
-
135
- └──► SileroVad.processAudio(float32)
136
-
137
- ├── RMS 预过滤 (阈值 0.004)
138
- │ ├── < 阈值 → 概率 = 0 (跳过推理)
139
- │ └── >= 阈值 → ONNX 推理
140
-
141
- ├── 状态机
142
- │ ├── pre-speech: 需要连续 10 帧 (~320ms) 确认说话
143
- │ ├── speaking: 语音持续中
144
- │ └── silence redemption: 连续 20 帧 (~640ms) 静音触发 endSpeech
145
-
146
- └── onSpeechEnd callback
147
-
148
-
149
- FriendService._flushVadSegment()
150
-
151
- ├── 创建新的 STT 连接 (旧的连接继续处理)
152
-
153
- ├── 等待旧连接 finalize()
154
- │ └── 获取转录文本推入 captureTranscripts
155
-
156
- ├── if 有转录文本:
157
- │ ├── this.sendText(transcript)
158
- │ │ │
159
- │ │ ▼
160
- │ │ messageQueueManager.enqueue() → AI 开始处理
161
- │ │
162
- │ └── this.startAiTurnMute()
163
- │ ├── muted = true
164
- │ ├── VAD pause
165
- │ └── 30s 超时安全性定时器
166
-
167
- └── 循环继续监听下一段语音
168
- ```
169
-
170
- ### 2.3 AI 回复与静音解除
171
-
172
- ```
173
- AI 处理完成
174
-
175
-
176
- FriendService.broadcastResponse(text)
177
-
178
- ├── broadcastToVrm({ text }) // 显示文字
179
-
180
- ├── if TTS enabled:
181
- │ ├── generateTts(text)
182
- │ │ │
183
- │ │ ▼
184
- │ ├── broadcastToVrm({ audioUrl, sendFirstTts: true })
185
- │ │
186
- │ └── extendMuteForTts(audioId)
187
- │ ├── getMp3DurationMs() 精确计算
188
- │ ├── 取消 30s 安全性定时器
189
- │ └── 设定精确的播放时长定时器
190
-
191
- └── broadcastToVrm({ replyDone: true })
192
-
193
-
194
- 播放完成后 → unmute()
195
- ├── muted = false
196
- └── VAD resume (可继续接收语音)
197
- ```
198
-
199
- ### 2.4 结束通话
200
-
201
- ```
202
- 用户按 F2 (再次)
203
-
204
-
205
- ChatInput.endVoiceCall()
206
-
207
- └── useServerStt.stopStreaming()
208
-
209
- └── POST /plugins/friend/voice/stop
210
-
211
-
212
- friendService.stopVoiceCapture()
213
-
214
- ├── arecord.kill('SIGTERM') → 2s 后 SIGKILL
215
- ├── capturing = false
216
- ├── clearMute()
217
- ├── VAD reset()
218
- ├── STT connection.finalize() + close()
219
- ├── 发送剩余转录文本
220
- └── 返回完整转录
221
- ```
222
-
223
- ---
224
-
225
- ## 3. 情绪表情流
226
-
227
- LLM 调用 `friend_emotion` 工具触发情绪更新。
228
-
229
- ```
230
- LLM 处理完成,调用 friend_emotion 工具
231
-
232
-
233
- FriendEmotionTool.call({ emotion: 'happy', intensity: 0.8, mood_delta: 2 })
234
-
235
- ├── broadcastToVrm({ emotion: 'happy', emotionIntensity: 0.8 })
236
- │ │
237
- │ ▼ SSE data
238
- │ App.tsx handleVrmMessage
239
- │ │
240
- │ ├── emotionActionMap['happy'] = 'happy'
241
- │ │
242
- │ ├── sceneRef.current.setEmotionWithReset('happy', 5000, 0.8)
243
- │ │ │
244
- │ │ ▼
245
- │ │ VRMScene.setEmotionWithReset (via forwardRef)
246
- │ │ │
247
- │ │ └── EmoteController.setEmotionWithReset('happy', 5000, 0.8)
248
- │ │ ├── setEmotion('happy', 0.8)
249
- │ │ │ ├── 获取 happy 的 blend shapes: [{name:'happy', val:0.2}, {name:'aa', val:0.8}]
250
- │ │ │ ├── 应用 intensity: aa = 0.8*0.8 = 0.64, happy = 0.2*0.8 = 0.16
251
- │ │ │ ├── isTransitioning = true
252
- │ │ │ └── 记录目标 blendshape 值
253
- │ │ │
254
- │ │ └── setTimeout(5000ms → setEmotion('neutral'))
255
- │ │
256
- │ └── sceneRef.current.playAction('happy')
257
- │ │
258
- │ ▼
259
- │ MotionController.playAction('happy')
260
- │ ├── loadClip('happy.fbx')
261
- │ ├── crossFadeTo(clip, 0.3s)
262
- │ ├── LoopOnce + clampWhenFinished
263
- │ └── 完成后 crossFade 回 idle 动画
264
-
265
- ├── 处理 mood_delta
266
- │ ├── 读取当前 moodIndex = 60
267
- │ ├── newMood = clamp(60 + 2, 0, 100) = 62
268
- │ ├── 持久化到 prefs
269
- │ └── broadcastToVrm({ moodDelta: 2, moodIndex: 62 })
270
- │ │
271
- │ ▼ SSE data
272
- │ MoodIndicator 收到
273
- │ ├── 显示心情数值变化气泡 (+2)
274
- │ ├── Canvas 动画: displayPercent 从 60 → 62 渐变
275
- │ └── 5s 后自动隐藏
276
-
277
- └── 返回 tool result
278
- ```
279
-
280
- ### 每帧更新循环 (VRMScene animate)
281
-
282
- ```
283
- requestAnimationFrame 循环 (约 60fps)
284
-
285
- ├── 1. MotionController.update(delta)
286
- │ └── AnimationMixer.update(delta)
287
-
288
- ├── 2. 应用 Relaxed Hand Pose (非舞蹈状态)
289
- │ └── 手指自然弯曲 + 微妙颤动
290
-
291
- ├── 3. Humanoid.update()
292
-
293
- ├── 4. 眼球追踪 (camera 模式)
294
- │ └── lookAtTarget = camera.position
295
-
296
- ├── 5. LookAt.update(delta)
297
-
298
- ├── 6. Eye Saccades Controller.update()
299
- │ └── 每隔 400-1200ms 添加随机眼球微动偏移
300
-
301
- ├── 7. Blink State Machine.update()
302
- │ └── 随机眨眼 (间隔 1-6s, 时长 150ms, sin 曲线)
303
-
304
- ├── 8. EmoteController.update(delta)
305
- │ └── cubic ease 过渡到目标 blendshape 值
306
-
307
- ├── 9. LipSync.update(vrm, delta)
308
- │ ├── 读取 wlipsync 音频分析节点的音素权重
309
- │ ├── 选择胜者/亚军音素
310
- │ ├── Attack/Release 平滑 (50/30)
311
- │ └── 设置 VRM 嘴形 blendshapes (aa, ee, ih, oh, ou)
312
-
313
- ├── 10. ExpressionManager.update()
314
-
315
- └── 11. SpringBoneManager.update(delta)
316
- └── 物理头发/衣服/饰品模拟
317
- ```
318
-
319
- ---
320
-
321
- ## 4. SSE 事件格式
322
-
323
- 所有前端 SSE 事件通过 `GET /plugins/friend/events` 接收,格式为标准 SSE (`data: {json}\n\n`)。
324
-
325
- ### 4.1 VrmBroadcastPayload 字段说明
326
-
327
- | 字段 | 类型 | 必填 | 说明 |
328
- |------|------|------|------|
329
- | `text` | string | 否 | AI 回复文字,TextBubble 显示并启动打字机效果 |
330
- | `emotion` | string | 否 | VRM 表情名,触发 EmoteController 切换 blend shapes |
331
- | `emotionIntensity` | number | 否 | 表情强度 0-1,默认 1 |
332
- | `audioUrl` | string | 否 | TTS 音频 URL,TextBubble 触发 LipSync 播放 |
333
- | `audioIndex` | number | 否 | 音频播放顺序索引,用于多句排序 |
334
- | `clearText` | boolean | 否 | 清空气泡文字和音频队列 |
335
- | `imageUrl` | string | 否 | 图片 URL,在气泡中显示 |
336
- | `moodDelta` | number | 否 | 心情变化量,MoodIndicator 显示浮动气泡 |
337
- | `moodIndex` | number | 否 | 当前心情指数 0-100,MoodIndicator Canvas 更新 |
338
- | `sendFirstTts` | boolean | 否 | 开始 TTS 播放的信号,重置音频队列 |
339
- | `appendText` | boolean | 否 | 追加文字模式,后续句子的文字和音频配对 |
340
- | `replyDone` | boolean | 否 | 回复完成信号,TextBubble 调度气泡隐藏 |
341
-
342
- ### 4.2 典型回复序列
343
-
344
- ```
345
- 1. { text: "你好!今天心情不错啊!", replyDone: true }
346
- → 显示文字,打字机效果,1s 后触发表情
347
-
348
- 2. { text: "一起玩吧!" }
349
- { audioUrl: "http://127.0.0.1:3456/plugins/friend/audio/123-1", sendFirstTts: true }
350
- { replyDone: true }
351
- → 显示文字 + 播放 TTS + 语音结束后隐藏气泡
352
-
353
- 3. { text: "今天天气真好。" }
354
- { audioUrl: "...", sendFirstTts: true, emotion: "happy", emotionIntensity: 0.8 }
355
- { appendText: true, text: "要不要出去走走?", audioUrl: "...", audioIndex: 1 }
356
- { appendText: true, text: "我知道一个好地方。", audioUrl: "...", audioIndex: 2 }
357
- { replyDone: true }
358
- → 多句子回复,每句独立音频,按索引顺序播放
359
- → 第一句发送时触发 happy 表情
360
-
361
- 4. { emotion: "think", emotionIntensity: 0.7 }
362
- → 思考阶段的表情更新(LLM 处理中)
363
-
364
- 5. { clearText: true }
365
- → 清空气泡(新会话)
366
-
367
- 6. { moodDelta: 3, moodIndex: 63 }
368
- → 心情更新,显示 +3 浮动气泡,Canvas 液态填充变化
369
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/friend/emotion-map.md DELETED
@@ -1,195 +0,0 @@
1
- # 情绪 → 3D 表情映射
2
-
3
- Friend 系统支持 13 种情绪,每种映射到 VRM blend shapes 组合、过渡时间和肢体动作。
4
-
5
- ---
6
-
7
- ## 完整映射表
8
-
9
- 数据来源: `src/components/friend/frontend/emote.ts`
10
-
11
- | 情绪 | Blend Shapes 组合 | 过渡时间 | 肢体动作 |
12
- |------|------------------|---------|---------|
13
- | `happy` | happy(0.2) + aa(0.8) | 0.4s | `happy` (开心.fbx) |
14
- | `sad` | sad(0.7) + oh(0.15) | 0.4s | `shy` (害羞.fbx) |
15
- | `angry` | angry(0.7) + ee(0.3) | 0.3s | `angry` (生气.fbx) |
16
- | `surprised` | surprised(0.8) + oh(0.4) | 0.15s | `excited` (兴奋.fbx) |
17
- | `think` | think(0.7) | 0.5s | `scratchHead` (挠头.vrma) |
18
- | `awkward` | sad(0.3) + ee(0.2) | 0.5s | `playFingers` (搓手.vrma) |
19
- | `question` | surprised(0.4) + think(0.3) | 0.4s | `point` (指点.fbx) |
20
- | `curious` | think(0.5) + surprised(0.2) | 0.4s | `scratchHead` (挠头.vrma) |
21
- | `neutral` | neutral(1.0) | 0.6s | `salute` (敬礼.fbx) |
22
- | `love` | happy(0.2) + relaxed(0.4) | 0.4s | `shy` (害羞.fbx) |
23
- | `flirty` | happy(0.2) + relaxed(0.3) + aa(0.15) | 0.4s | `shy` (害羞.fbx) |
24
- | `greeting` | happy(0.2) + aa(0.3) | 0.3s | `greeting` (招呼.fbx) |
25
- | `relaxed` | relaxed(0.8) | 0.5s | `salute` (敬礼.fbx) |
26
-
27
- ### 使用的 VRM Blend Shapes
28
-
29
- VRM 标准 blendshape 名称及其对应的面部区域:
30
-
31
- | Blend Shape | 面部区域 |
32
- |-------------|---------|
33
- | `happy` | 嘴角上扬 (smile) |
34
- | `sad` | 嘴角下垂 |
35
- | `angry` | 皱眉 |
36
- | `surprised` | 眉毛上抬 |
37
- | `think` | 思考表情 |
38
- | `neutral` | 自然表情 |
39
- | `relaxed` | 放松表情 |
40
- | `aa` | 张嘴 (A 音) |
41
- | `ee` | 露齿 (E 音) |
42
- | `oh` | 嘟嘴 (O 音) |
43
- | `ih` | 微张嘴 (I 音, 主要用于唇同步) |
44
- | `ou` | 收唇 (U 音, 主要用于唇同步) |
45
- | `blink` | 闭眼 (由眨眼系统独立控制) |
46
-
47
- ---
48
-
49
- ## EmoteController 过渡系统
50
-
51
- **文件**: `src/components/friend/frontend/emote.ts`
52
-
53
- ### 过渡算法
54
-
55
- ```typescript
56
- // cubic ease 缓动函数
57
- setEmotion('happy', intensity = 0.8)
58
-
59
- ├── 1. 获取情绪定义
60
- │ happy: [{name:'happy', value:0.2}, {name:'aa', value:0.8}]
61
-
62
- ├── 2. 乘以 intensity
63
- │ happy = 0.2 * 0.8 = 0.16
64
- │ aa = 0.8 * 0.8 = 0.64
65
-
66
- ├── 3. 记录当前 blendshape 值 (起始值)
67
- │ currentValues = { happy: 0.05, aa: 0, ... }
68
-
69
- ├── 4. 设置目标值
70
- │ targetValues = { happy: 0.16, aa: 0.64 }
71
-
72
- ├── 5. 开始过渡 (isTransitioning = true)
73
-
74
- └── 每帧 update(deltaTime):
75
-
76
- ├── transitionProgress += deltaTime / blendDuration(0.4s)
77
-
78
- ├── if progress >= 1: transition end
79
-
80
- ├── cubic ease:
81
- │ t < 0.5 → 4 * t³
82
- │ t >= 0.5 → 1 - (-2t + 2)³ / 2
83
-
84
- └── value = start + (target - start) * ease(t)
85
- ```
86
-
87
- ### 自动回中系统
88
-
89
- ```typescript
90
- setEmotionWithReset('happy', durationMs = 5000, intensity = 0.8)
91
- ├── setEmotion('happy', 0.8) // 立即开始过渡
92
- └── setTimeout(5000ms)
93
- └── setEmotion('neutral') // 自动回中到自然表情
94
- ```
95
-
96
- ### 完整重置
97
-
98
- ```typescript
99
- resetAll()
100
- ├── 清除 resetTimer
101
- ├── isTransitioning = false
102
- ├── 将所有 blendshape 值设为 0
103
- └── 清空 currentValues / targetValues
104
- ```
105
-
106
- ---
107
-
108
- ## 情绪 → 动作映射
109
-
110
- **文件**: `src/components/friend/frontend/App.tsx`
111
-
112
- App.tsx 中的 `emotionActionMap` 定义了情绪与动作的关联:
113
-
114
- ```typescript
115
- const emotionActionMap: Record<string, string> = {
116
- think: 'scratchHead', // 挠头
117
- question: 'point', // 指
118
- curious: 'scratchHead', // 挠头
119
- happy: 'happy', // 开心
120
- surprised: 'excited', // 兴奋
121
- angry: 'angry', // 生气
122
- awkward: 'playFingers', // 搓手指
123
- sad: 'shy', // 害羞
124
- love: 'shy', // 害羞
125
- flirty: 'shy', // 害羞
126
- greeting: 'greeting', // 招呼
127
- relaxed: 'salute', // 敬礼
128
- neutral: 'salute', // 敬礼
129
- }
130
- ```
131
-
132
- 动作文件类型:
133
- - `.vrma`: VRM Animation 格式 (挠头、搓手、伸展、叉腰)
134
- - `.fbx`: Mixamo FBX 格式 (开心、生气、招呼、兴奋、害羞、指点、敬礼、暴怒)
135
-
136
- ---
137
-
138
- ## 心情指数系统
139
-
140
- 除了即时表情,Friend 还有持续的心情指数系统:
141
-
142
- ```
143
- moodIndex: 0-100
144
- ├── 0-29: 低 (灰色, rgb(160,168,180))
145
- ├── 30-49: 偏低 (蓝色, rgb(78,168,222))
146
- ├── 50-69: 中等 (绿色, rgb(72,199,142))
147
- ├── 70-89: 良好 (橙色, rgb(255,165,70))
148
- └── 90-100: 优秀 (粉色, rgb(255,107,157))
149
- ```
150
-
151
- - LLM 通过 `friend_emotion` 工具的 `mood_delta` 参数调整 (-3 到 +3)
152
- - 前端 MoodIndicator 组件以液态填充柱状图 + 爱心图标可视化
153
- - Canvas 动画使用贝塞尔波浪动画和颜色渐变
154
-
155
- ---
156
-
157
- ## 触摸交互反应系统
158
-
159
- **文件**: `src/components/friend/frontend/App.tsx`
160
-
161
- 6 个触摸区域各自有多个可能的反应:
162
-
163
- | 区域 | 可能的反应 (情绪 + 动作) |
164
- |------|------------------------|
165
- | head (头) | relaxed+happy, relaxed+shy, angry+angryPump, relaxed+excited |
166
- | arm (手臂) | surprised+excited, happy+happy, relaxed+greeting, relaxed+akimbo |
167
- | chest (胸) | angry+angryPump, angry+angry, angry+point |
168
- | belly (肚子) | angry+angryPump, angry+angry, awkward+playFingers |
169
- | buttocks (屁股) | angry+angryPump, angry+point, sad+shy |
170
- | leg (腿) | sad+shy, angry+angry, awkward+playFingers |
171
-
172
- 触摸交互逻辑:
173
- 1. 双击模型触发区域检测(射线检测 + 最近骨骼匹配)
174
- 2. 随机选择该区域的一个反应组合
175
- 3. 立即播放表情 + 动作
176
- 4. 3s 冷却期写入 session memo([用户摸了摸你的xx])
177
- 5. 60s 冷却期 + 50% 概率发送文字回复到 AI(POST /plugins/friend/touch)
178
-
179
- ---
180
-
181
- ## 空闲小动作系统
182
-
183
- 当用户 30 秒无活动时,VRM 角色会自动做一些小动作:
184
-
185
- ```
186
- idleMs >= 30,000ms → 每 15s 检查一次
187
-
188
- ├── 50% 概率触发
189
- ├── 随机选择 13 种情绪之一
190
- ├── 随机选择 12 种动作之一
191
- ├── 强度: 0.4-0.8 (随机)
192
- └── 持续: 3-5s (随机)
193
- ```
194
-
195
- 此系统防止角色长时间静止不动,增加生动感。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/friend/frontend-3d.md DELETED
@@ -1,573 +0,0 @@
1
- # 前端 3D 渲染
2
-
3
- 本文档描述 Friend VRM 桌面伴侣的前端 3D 渲染系统。
4
-
5
- ---
6
-
7
- ## 技术栈
8
-
9
- | 技术 | 用途 |
10
- |------|------|
11
- | **Three.js** | 3D 渲染引擎 |
12
- | **@pixiv/three-vrm** | VRM 模型加载与控制 |
13
- | **@pixiv/three-vrm-animation** | VRM 动画加载与 LookAt |
14
- | **@pixiv/three-vrm-animation** | VRMA 动画支持 |
15
- | **Tauri (WebKitGTK)** | 桌面窗口外壳 |
16
- | **React** | UI 框架 |
17
- | **Vite** | 前端构建工具 |
18
- | **Bun** | 构建运行环境 |
19
- | **CSS-in-JS** (内联 style) | 组件样式 |
20
- | **Lucide React** | 图标库 |
21
- | **marked** | Markdown 渲染 |
22
- | **wlipsync** | WebAudio 唇形同步 |
23
- | **Intl.Segmenter** | Unicode 字符分割 |
24
-
25
- ---
26
-
27
- ## VRMScene.tsx — 核心 3D 场景
28
-
29
- **文件**: `src/components/friend/frontend/components/VRMScene.tsx` (~871 行)
30
-
31
- ### 组件接口
32
-
33
- 通过 `forwardRef` 暴露的操作句柄:
34
-
35
- ```typescript
36
- interface VRMSceneHandle {
37
- setEmotion(emotion: string, intensity?: number): void
38
- setEmotionWithReset(emotion: string, durationMs: number, intensity?: number): void
39
- resetCamera(): void
40
- setTrackingMode(mode: 'mouse' | 'camera'): void
41
- playAction(name: string, hold?: boolean): void
42
- captureScreenshot(): string | null
43
- panCamera(dx: number, dy: number): void
44
- rotateCamera(dx: number, dy: number): void
45
- playDance(nameOrPreset: string | DancePreset): void
46
- stopDance(): void
47
- isDancing(): boolean
48
- setBgmVolume(v: number): void
49
- reset(): void
50
- }
51
- ```
52
-
53
- ### 场景初始化
54
-
55
- ```typescript
56
- // VRMScene 组件创建时
57
- const renderer = new THREE.WebGLRenderer({
58
- canvas,
59
- alpha: true, // 透明背景
60
- antialias: true, // 抗锯齿
61
- preserveDrawingBuffer: true, // 截图支持
62
- })
63
- renderer.setClearColor(0x000000, 0) // 完全透明
64
-
65
- // 透视相机
66
- const FOV = 40
67
- const camera = new THREE.PerspectiveCamera(FOV, aspect, 0.1, 100)
68
- // 轨道控制: 围绕 pivot 点的球面坐标
69
-
70
- // 光照
71
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
72
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2) // 主光
73
- const fillLight = new THREE.DirectionalLight(0xffffff, 0.4) // 补光
74
- ```
75
-
76
- ### 模型加载
77
-
78
- ```typescript
79
- loader.load(modelPath, async (gltf) => {
80
- // 1. 获取 VRM 数据
81
- const loadedVrm = gltf.userData.vrm
82
-
83
- // 2. 优化: 移除冗余顶点 + 合并骨架
84
- VRMUtils.removeUnnecessaryVertices(loadedVrm.scene)
85
- VRMUtils.combineSkeletons(loadedVrm.scene)
86
-
87
- // 3. 添加 LookAt 四元数代理
88
- const lookAtQuatProxy = new VRMLookAtQuaternionProxy(loadedVrm.lookAt)
89
- loadedVrm.scene.add(lookAtQuatProxy)
90
-
91
- // 4. 标准化 VRM 0.x → 1.0 姿态
92
- VRMUtils.rotateVRM0(loadedVrm)
93
-
94
- // 5. 计算自动相机位置 (根据模型包围盒)
95
- const box = new THREE.Box3().setFromObject(loadedVrm.scene)
96
- // pivot 在颈部高度
97
- // orbitRadius = modelSize.y / 4.2 / tan(FOV/2)
98
-
99
- // 6. 初始化控制器
100
- emote = new EmoteController(loadedVrm)
101
- motion = new MotionController(loadedVrm)
102
- handPoseCache = buildHandPoseCache(loadedVrm)
103
-
104
- // 7. 加载空闲动画
105
- motion.loadIdle('/friend/idle_loop.vrma')
106
- })
107
- ```
108
-
109
- ### 每帧更新循环 (11 步)
110
-
111
- ```typescript
112
- function animate() {
113
- // 1. Animation Mixer (MotionController)
114
- motion.update(delta)
115
-
116
- // 2. 放松手部姿态 (非舞蹈时)
117
- if (handPose && !motion.isDancing)
118
- applyRelaxedHandPose(handPose, elapsedTime)
119
-
120
- // 3. Humanoid 骨骼更新
121
- vrm.humanoid.update()
122
-
123
- // 4. Camera tracking mode → lookAt
124
- if (trackingMode === 'camera')
125
- saccades.instantUpdate(vrm, camera.position)
126
-
127
- // 5. LookAt 更新
128
- vrm.lookAt.update(delta)
129
-
130
- // 6. 眼球微动 (saccades)
131
- saccadesController.update(vrm, lookAtTarget, delta)
132
-
133
- // 7. 眨眼 (blink)
134
- updateBlink(vrm, delta, blinkState)
135
-
136
- // 8. 表情过渡 (EmoteController)
137
- emote.update(delta)
138
-
139
- // 9. 唇形同步 (LipSync)
140
- lipSync.update(vrm, delta)
141
-
142
- // 10. Expression Manager
143
- vrm.expressionManager.update()
144
-
145
- // 11. Spring Bone 物理
146
- vrm.springBoneManager.update(delta)
147
-
148
- renderer.render(scene, camera)
149
- }
150
- ```
151
-
152
- ### 相机控制系统
153
-
154
- 球面坐标相机控制:
155
-
156
- ```typescript
157
- // 参数: pivot(旋转中心), orbitRadius(距离), orbitTheta(水平角), orbitPhi(垂直角)
158
- camera.position.set(
159
- pivot.x + radius * sin(phi) * sin(theta),
160
- pivot.y + radius * cos(phi),
161
- pivot.z + radius * sin(phi) * cos(theta),
162
- )
163
- camera.lookAt(pivot)
164
- ```
165
-
166
- - 鼠标滚轮: 缩放 (0.8-5.0)
167
- - 左键拖拽 VRM 模型: 双击触发射线碰撞检测身体区域
168
- - 中键拖拽: 推拉 (dolly)
169
- - 右键拖拽: 旋转视角
170
- - 菜单栏拖拽按钮: 平移 / 旋转 视角
171
-
172
- ### 眼球追踪系统
173
-
174
- 两种模式:
175
- - `mouse`: 鼠标在屏幕上的位置决定 VRM 视线的交点(通过射线平面求交)
176
- - `camera`: VRM 始终看向相机位置
177
-
178
- 眼球微动控制器 (`EyeSaccadeController`):
179
- - 每 400-1200ms 添加随机偏移 (-0.25 到 0.25 units)
180
- - 瞬间更新 + 每帧 lerp 平滑
181
-
182
- ### 眨眼系统
183
-
184
- - 随机间��: 1-6s
185
- - 眨眼时长: 150ms
186
- - 使用 sin(π * progress) 曲线实现自然闭合
187
- - 每帧更新 `blink` blendshape
188
-
189
- ### 放松手部姿态
190
-
191
- 当 VRM 模型没有手指动画轨道时,手指会保持 T-pose 僵硬状态。
192
- 解决方案:每帧手动设置手指旋转。
193
-
194
- ```typescript
195
- // 手指自然弯曲 (从拇指到小指递增)
196
- const curlMap = {
197
- Thumb: [0.25, 0.15, 0.10],
198
- Index: [0.20, 0.30, 0.20],
199
- Middle: [0.25, 0.35, 0.25],
200
- Ring: [0.30, 0.40, 0.30],
201
- Little: [0.35, 0.45, 0.30],
202
- }
203
- // 手指自然张开
204
- const spreadMap = { Thumb: 0.15, Index: 0.04, Middle: 0, Ring: -0.04, Little: -0.08 }
205
- // 微妙颤动: sin(time * freq + seed) * 0.02
206
- ```
207
-
208
- ### 触摸区域检测
209
-
210
- 射线检测 + 最近骨骼匹配:
211
-
212
- ```typescript
213
- const boneRegionMap: [string, TouchRegion][] = [
214
- ['head', 'head'], ['neck', 'head'],
215
- ['leftShoulder', 'arm'], /* ...所有手臂骨骼... */
216
- ['chest', 'chest'], ['spine', 'belly'],
217
- ['hips', 'buttocks'],
218
- ['leftUpperLeg', 'leg'], /* ...所有腿部骨骼... */
219
- ]
220
- // 计算点击点与所有骨骼的世界坐标距离
221
- // 选择最近的骨骼对应的区域
222
- ```
223
-
224
- 双击确认(500ms 窗口)+ 5s 冷却。
225
-
226
- ### 窗口穿透点击检测
227
-
228
- ```typescript
229
- // 渲染到 1x1 offscreen render target
230
- // 读取光标位置的 alpha 通道
231
- // alpha > 10 → 点击在模型上 → 不穿透
232
- // alpha <= 10 → 点击在透明背景 → 穿透窗口
233
- ```
234
-
235
- ---
236
-
237
- ## EmoteController — Blend Shape 动画系统
238
-
239
- **文件**: `src/components/friend/frontend/emote.ts` (~207 行)
240
-
241
- 详细情绪映射见 [情绪 → 3D 表情映射](emotion-map.md)。
242
-
243
- ### 核心机制
244
-
245
- ```
246
- EmoteController
247
- ├── emotionStates: Map<emotionName, { expression[], blendDuration }>
248
- │ 13 种情绪的 blend shapes 组合定义
249
-
250
- ├── setEmotion(name, intensity)
251
- │ 开始过渡:记录起始值、设置目标值、启动过渡
252
-
253
- ├── setEmotionWithReset(name, durationMs, intensity)
254
- │ 设置表情 + 定时自动回中到 neutral
255
-
256
- ├── update(deltaTime)
257
- │ 每帧计算 cubic ease 过渡
258
-
259
- └── resetAll()
260
- 立即清零所有 blendshape
261
- ```
262
-
263
- ---
264
-
265
- ## MotionController — 动画系统
266
-
267
- **文件**: `src/components/friend/frontend/motion-controller.ts` (~421 行)
268
-
269
- ### 支持的文件格式
270
-
271
- | 格式 | 描述 | 来源 |
272
- |------|------|------|
273
- | VRMA | VRM Animation 格式 | 标准动画文件 |
274
- | VMD | MikuMikuDance 格式 | 舞蹈动画 (极乐净土/恋爱循环) |
275
- | FBX | Autodesk FBX 格式 | Mixamo 动画 (开心/生气等) |
276
-
277
- ### 动作预设
278
-
279
- **短动作** (单次触发,完成后回归 idle):
280
-
281
- ```typescript
282
- akimbo: { label: '叉腰', type: 'vrma' }
283
- playFingers: { label: '搓手', type: 'vrma' }
284
- scratchHead: { label: '挠头', type: 'vrma' }
285
- stretch: { label: '伸展', type: 'vrma' }
286
- happy: { label: '开心', type: 'fbx' }
287
- angry: { label: '生气', type: 'fbx' }
288
- greeting: { label: '招呼', type: 'fbx' }
289
- excited: { label: '兴奋', type: 'fbx' }
290
- shy: { label: '害羞', type: 'fbx' }
291
- point: { label: '指点', type: 'fbx' }
292
- salute: { label: '敬礼', type: 'fbx' }
293
- angryPump: { label: '暴怒', type: 'fbx' }
294
- ```
295
-
296
- **舞蹈** (循环播放,支持 BGM):
297
-
298
- ```typescript
299
- jile: { label: '极乐净土', type: 'vmd', bgm: '/friend/jile.mp3' }
300
- love: { label: '恋爱循环', type: 'vmd', bgm: '/friend/love.mp3' }
301
- ```
302
-
303
- ### 动画过渡系统
304
-
305
- 使用单个持久的 `AnimationMixer` 配合 `crossFade` 过渡,避免 T-pose 闪烁:
306
-
307
- ```typescript
308
- private crossFadeTo(newAction, duration = 0.3s) {
309
- newAction.reset().setEffectiveWeight(1).play()
310
- const prev = this.currentAction ?? this.idleAction
311
- if (prev && prev !== newAction) {
312
- prev.crossFadeTo(newAction, duration, false)
313
- }
314
- this.currentAction = newAction
315
- }
316
- ```
317
-
318
- ### 动作生命周期
319
-
320
- ```
321
- playAction('happy')
322
-
323
- ├── 检查并发锁 (_actionPlaying / _isDancing)
324
-
325
- ├── 异步加载动画文件 (带 generation 标记)
326
- │ ├── VRMA: GLTFLoader + VRMAnimationLoaderPlugin
327
- │ ├── FBX: loadMixamoAnimation
328
- │ └── VMD: parseVMDAnimation + bindVMDToVRM + IK
329
-
330
- ├── crossFadeTo(action, 0.3s)
331
-
332
- ├── LoopOnce + clampWhenFinished
333
-
334
- ├── finished 事件 → 回归 idle
335
- │ └── hold 模式: 保持 10s 后回归 idle
336
-
337
- └── 安全性超时: (duration + 1s) 后强制释放
338
- ```
339
-
340
- ### VMD 舞蹈系统
341
-
342
- 特殊处理 VMD 格式:
343
-
344
- ```typescript
345
- loadVMDWithIK(url)
346
- ├── 1. 解析 VMD 文件 (parseVMDAnimation)
347
- │ - 解析 VMD 二进制格式
348
- │ - 从 VRM 骨骼映射到关键帧
349
- │ - 缓存解析结果 (解析开销大)
350
-
351
- ├── 2. 绑定到 VRM (bindVMDToVRM)
352
- │ - 生成 Three.js AnimationClip
353
- │ - 创建 IK 目标对象
354
- │ - 启用 IK 处理器
355
-
356
- └── 3. 建立复用的 IK 处理器 (VRMIKHandler)
357
- - 使用 FABRIK 算法
358
- - 支持手臂 IK
359
- - 每帧在 mixer.update 后运行
360
- ```
361
-
362
- 舞蹈启动时自动切换相机视角到合适位置(臀部高度为中心)。
363
-
364
- ### BGM 系统
365
-
366
- ```typescript
367
- // 舞蹈开始时
368
- this.bgmAudio = new Audio(preset.bgm)
369
- this.bgmAudio.loop = true
370
- this.bgmAudio.volume = this._volume
371
- this.bgmAudio.play()
372
-
373
- // 舞蹈停止时: 淡出 (每隔 50ms 降低 0.1)
374
- const fadeInterval = setInterval(() => {
375
- audio.volume = Math.max(0, audio.volume - 0.1)
376
- if (audio.volume <= 0) { clearInterval(fadeInterval); audio.pause() }
377
- }, 50)
378
- ```
379
-
380
- ---
381
-
382
- ## LipSync — 唇形同步
383
-
384
- **文件**: `src/components/friend/frontend/lip-sync.ts` (~187 行)
385
-
386
- ### 技术实现
387
-
388
- 使用 `wlipsync` 库的 WebAudio 音频分析节点:
389
-
390
- ```typescript
391
- // 初始化
392
- this.lipSyncNode = await createWLipSyncNode(audioContext, profile)
393
- // lipSyncNode 只分析音频,不连接扬声器
394
- // gainNode 连接扬声器
395
-
396
- // 播放音频时,同时连接到 lipSyncNode 和 gainNode
397
- source.connect(this.lipSyncNode) // 分析
398
- source.connect(this.gainNode) // 扬声器
399
- ```
400
-
401
- ### 音素 → VRM Blend Shape 映射
402
-
403
- | wlipsync 分析键 | VRM Blend Shape |
404
- |----------------|----------------|
405
- | `A` | `aa` (张嘴) |
406
- | `E` | `ee` (露齿) |
407
- | `I` | `ih` (微张嘴) |
408
- | `O` | `oh` (嘟嘴) |
409
- | `U` | `ou` (收唇) |
410
- | `S` | 映射到 `I`/`ih` |
411
-
412
- 双胜者策略:取概率最高的两个音素,胜者 cap 0.7,亚军 cap 0.35。
413
-
414
- ### 平滑参数
415
-
416
- - `ATTACK`: 50 (上升速率)
417
- - `RELEASE`: 30 (衰减速率)
418
- - `CAP`: 0.7 (最大权重)
419
- - `SILENCE_VOL`: 0.04 (静音音量阈值)
420
- - `SILENCE_GAIN`: 0.05 (静音增益阈值)
421
- - `IDLE_MS`: 160 (静音判定窗口)
422
-
423
- 公式: `smoothed = from + (to - from) * (1 - exp(-rate * delta))`
424
-
425
- 静音时完全跳过 blendshape 设置(让 EmoteController 控制嘴部)。
426
-
427
- ---
428
-
429
- ## TextBubble.tsx — 文字气泡
430
-
431
- **文件**: `src/components/friend/frontend/components/TextBubble.tsx` (~608 行)
432
-
433
- ### 核心功能
434
-
435
- 1. **SSE 驱动**:通过 `EventSource` 实时接收 `VrmBroadcastPayload`
436
- 2. **打字机效果**:逐字符显示,CJK/English 自适应速率
437
- 3. **音频队列**:多句子音频按索引顺序播放,支持 `appendText` 配对
438
- 4. **Markdown 渲染**:打字机完成后使用 `marked` 渲染
439
- 5. **发送首 TTS 队列**:支持排队 `sendFirstTts` 信号
440
- 6. **看门狗**:30s 超时自动隐藏(防止卡死)
441
-
442
- ### 打字机速率计算
443
-
444
- ```typescript
445
- function getCharRate(text: string, ttsEnabled: boolean): number {
446
- const ratio = cjkRatio(text)
447
- if (ttsEnabled) {
448
- return Math.round(200 * ratio + 60 * (1 - ratio)) // CJK: 200ms, EN: 60ms
449
- }
450
- return Math.round(80 * ratio + 30 * (1 - ratio)) // CJK: 80ms, EN: 30ms
451
- }
452
- ```
453
-
454
- ### 消息处理逻辑
455
-
456
- ```
457
- handleMessage(msg)
458
-
459
- ├── Audio-only: 直接入音频队列
460
-
461
- ├── appendText: 配对文字和音频索引,等待播放时揭示文字
462
-
463
- ├── Image-only: 显示图片,15s 自动隐藏
464
-
465
- ├── Emotion-only: 转发 onMessage 回调 (不改变气泡)
466
-
467
- └── Text message:
468
- ├── sendFirstTts: 重置音频队列,播放首句 TTS
469
- ├── 设置文字 → 启动打字机
470
- ├── 打字机完成后隐藏调度 (2s)
471
- └── 如果没有 TTS → 打字机完成后直接调度隐藏
472
- ```
473
-
474
- ### 隐藏调度逻辑
475
-
476
- ```
477
- tryScheduleHide()
478
- ├── 打字机完成? (typewriterRef === null)
479
- ├── 音频播放完毕? (!audioPlaying && queue empty)
480
- ├── sendFirstTts 队列为空?
481
- ├── replyDone 已收到?
482
-
483
- └── 全部满足 → setTimeout(hideBubble, 2000ms)
484
- ```
485
-
486
- ---
487
-
488
- ## ChatInput.tsx — 输入栏
489
-
490
- **文件**: `src/components/friend/frontend/components/ChatInput.tsx` (~475 行)
491
-
492
- ### 三种模式
493
-
494
- 1. **文字输入模式**: 输入框 + 发送按钮 + 回车发送
495
- 2. **PTT 模式**: 按住麦克风按钮录音,松开停止并转录
496
- 3. **语音通话模式** (F2): 连续语音,VAD 自动分段
497
-
498
- ### 全局快捷键
499
-
500
- - `Enter`: 打开输入栏 / 发送消息
501
- - `Escape`: 关闭输入栏
502
- - `F2`: 切换语音通话
503
- - `F4`: 设置面板
504
- - `F5`: 刷新前端
505
- - `Tab`: 折叠/展开菜单
506
- - `Ctrl+D`: 清空输入
507
-
508
- ### 语音通话 TTS 中断
509
-
510
- ```typescript
511
- const scheduleInterrupt = useCallback(() => {
512
- // 1s 延迟中断 TTS 播放
513
- // 当用户开始说话时,延迟 1s 后中断当前 TTS
514
- // 避免用户的"嗯"等短暂声音打断对话
515
- setTimeout(() => {
516
- ;(window as any).__clawInterruptAudio?.()
517
- }, 1000)
518
- }, [])
519
- ```
520
-
521
- ---
522
-
523
- ## MoodIndicator.tsx — 心情指示器
524
-
525
- **文件**: `src/components/friend/frontend/components/MoodIndicator.tsx` (~373 行)
526
-
527
- ### Canvas 动画
528
-
529
- - **液态填充柱状图**: 使用二次贝塞尔波浪动画 + Canvas clip
530
- - **爱心图标**: 同样的波浪填充,经典心形路径
531
- - **双波浪层**: 不同速度和透明度叠加,产���液态流动效果
532
- - **颜色分级**: 90+ 粉色 → 70+ 橙色 → 50+ 绿色 → 30+ 蓝色 → 0+ 灰色
533
- - **浮动气泡**: 心情变化时显示 `❤️+3` 或 `🩶-2` 浮动动画
534
-
535
- ### 交互
536
-
537
- - 正常状态: 半透明 (opacity 0.5)
538
- - 鼠标悬停: 全透明 (opacity 1.0)
539
- - 拖拽: 可自由移动到任意位置
540
- - 自动隐藏: 5s 无交互后恢复半透明
541
-
542
- ### 波浪参数
543
-
544
- ```typescript
545
- WAVE_LENGTH = 8 // 波长
546
- WAVE_HEIGHT = 2.5 // 波高 (px)
547
-
548
- // 双波浪叠加
549
- drawWave(scrollDir: -1, alpha: 0.55) // 底层波浪,慢速反向
550
- drawWave(scrollDir: 1, alpha: 1.0) // 顶层波浪,快速正向
551
- ```
552
-
553
- ---
554
-
555
- ## 其他组件
556
-
557
- ### ResizeHandles
558
- 窗口大小调整手柄,支持拖拽调整窗口尺寸。
559
-
560
- ### SettingsPanel
561
- 设置面板 (F4 打开),包含:
562
- - VRM 模型选择/导入
563
- - TTS 开关 + 语音选择
564
- - 显示设置 (文字气泡、UI、心情)
565
- - 追踪模式 (鼠标/相机)
566
- - 音量控制
567
- - 屏幕观察设置
568
- - 语言设置
569
- - STT Provider 选择
570
- - 舞蹈选择
571
-
572
- ### HistoryPanel
573
- 对话历史面板,显示最近 100 条消息,支持拖拽移动位置。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/friend/overview.md DELETED
@@ -1,138 +0,0 @@
1
- # VRM 桌面伴侣系统
2
-
3
- ## 定位
4
-
5
- Friend 是 Codev 的 VRM 3D 桌面伙伴系统,与 CLI 共享同一进程运行。它通过 SSE + HTTP 与 Tauri 前端通信,无需独立的子进程或外部服务。用户可以与 VRM 角色进行文字聊天、语音对话,角色会通过 3D 表情、肢体动作和语音进行反馈。
6
-
7
- 核心设计理念:**同进程集成** — FriendService 作为单例运行在 CLI 主进程中,消息通过 `messageQueueManager.enqueue()` 直接注入到对话流程中,AI 回复通过 SSE 实时广播到前端显示。
8
-
9
- ---
10
-
11
- ## 核心组件
12
-
13
- | 组件 | 文件 | 职责 |
14
- |------|------|------|
15
- | **FriendService** | `src/friend/FriendService.ts` (~31KB) | 核心编排器,管理生命周期、语音捕获、STT/TTS、SSE 广播、静音系统 |
16
- | **SSE 模块** | `src/friend/sse.ts` | 客户端注册表,类型化广播 `VrmBroadcastPayload` |
17
- | **HTTP 服务器** | `src/friend/server.ts` | Bun.serve() 在 3456 端口,处理静态文件、API 路由、SSE 连接 |
18
- | **API 路由** | `src/server/api/friend.ts` | `/plugins/friend/*` 的所有 REST 端点 |
19
- | **TTS 服务** | `src/friend/tts.ts` | Edge TTS + Qwen DashScope TTS,音频文件注册表 |
20
- | **STT 服务** | `src/friend/stt-service.ts` | 基于文件的语音转录(REST 端点) |
21
- | **VAD 服务** | `src/friend/voice/vad-service.ts` | Silero VAD ONNX 模型,onnxruntime-web WASM 后端 |
22
- | **偏好设置** | `src/friend/prefs.ts` | 持久化到 `~/.config/Codev/friend.json` |
23
- | **Tauri 启动器** | `src/friend/tauri-launcher.ts` | 启动 Tauri 桌面窗口 |
24
- | **前端应用** | `src/components/friend/frontend/` | React + Three.js + @pixiv/three-vrm |
25
-
26
- ---
27
-
28
- ## LLM 集成
29
-
30
- Friend 通过三种方式与 LLM 深度集成:
31
-
32
- ### 1. `friend_emotion` 工具
33
- - 定义在 `src/tools/FriendEmotionTool.ts`
34
- - LLM 可在每次回复后调用,设置角色表情和心情
35
- - 参数:`emotion` (13种情绪之一)、`intensity` (0-1)、`mood_delta` (-3 到 +3)
36
- - 通过 `broadcastToVrm()` 向 SSE 客户端广播表情切换
37
- - `mood_delta` 会持久化到 prefs 中的 `_moodIndex`,并广播给前端心情指示器
38
-
39
- ### 2. `friend_screen_observe` 工具
40
- - 定义在 `src/tools/FriendScreenObserveTool.ts`
41
- - 捕获桌面截图,返回图片路径
42
- - LLM 使用 Read 工具查看截图后,以同伴身份回应
43
- - 自动广播 `think` 表情到前端
44
-
45
- ### 3. friendPrompt 技能注入
46
- - 定义在 `src/skills/bundled/friendPrompt.ts`
47
- - 自动注入系统提示,告知 LLM 拥有 VRM 虚拟形象
48
- - 包含情绪列表、心情指数、对话风格指引
49
-
50
- ---
51
-
52
- ## 情绪系统
53
-
54
- Friend 支持 13 种情绪,映射到 VRM blend shapes 和骨骼动画:
55
-
56
- - `happy`, `sad`, `angry`, `surprised`, `think`, `awkward`, `question`, `curious`, `neutral`, `love`, `flirty`, `greeting`, `relaxed`
57
-
58
- 每种情绪在 `src/components/friend/frontend/emote.ts` 中定义了:
59
- - VRM blend shapes 组合及权重
60
- - 过渡时间(cubic ease 缓动)
61
- - 自动回中到 neutral 的定时器
62
-
63
- 情绪与肢体动作在 `App.tsx` 的 `emotionActionMap` 中关联。
64
-
65
- ---
66
-
67
- ## 交互模式
68
-
69
- ### 文字聊天
70
- 用户在输入框输入文字,通过 `POST /plugins/friend/chat` 发送到后端,FriendService 调用 `sendText()` 将消息通过 `messageQueueManager.enqueue()` 注入 CLI 对话流程。
71
-
72
- ### PTT 按键通话
73
- 按住麦克风按钮进行语音录制,松开后语音数据被发送到 STT 服务转录为文字,然后提交到对话流程。
74
-
75
- ### F2 语音通话
76
- 按下 F2 进入连续语音通话模式。服务器端通过 `arecord`/`parecord` 持续捕获麦克风音频,Silero VAD 自动检测语音段落边界,转录后自动发送给 AI 处理。AI 回复通过 TTS 播放,期间静音系统阻止回声。
77
-
78
- ---
79
-
80
- ## 简要架构图
81
-
82
- ```
83
- ┌─────────────────────────────────────────────────────────────────┐
84
- │ Codev 主进程 (Bun) │
85
- │ │
86
- │ ┌──────────────┐ ┌──────────────────┐ ┌───────────────┐ │
87
- │ │ CLI TUI │ │ LLM Provider │ │ Bridge API │ │
88
- │ │ (React Ink) │◄──►│ (Anthropic/NIM) │◄──►│ (SSE/HTTP) │ │
89
- │ └──────┬───────┘ └────────┬─────────┘ └───────────────┘ │
90
- │ │ │ │
91
- │ ┌──────▼─────────────────────▼──────────────────────────────┐ │
92
- │ │ FriendService (单例) │ │
93
- ��� │ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌───────┐ │ │
94
- │ │ │ STT 连接器 │ │ TTS 生成器│ │ VAD 检测 │ │静音系统│ │ │
95
- │ │ │(Groq/等) │ │(Edge/Qwen)│ │(Silero) │ │ │ │ │
96
- │ │ └────────────┘ └───────────┘ └───────────┘ └───────┘ │ │
97
- │ └───────────────────────┬────────────────────────────────────┘ │
98
- │ │ │
99
- │ ┌───────────────────────▼────────────────────────────────────┐ │
100
- │ │ HTTP 服务器 (Bun.serve :3456) │ │
101
- │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
102
- │ │ │ SSE /events │ │ REST API │ │ 静态文件服务 │ │ │
103
- │ │ │ │ │ /chat /voice │ │ /friend/* │ │ │
104
- │ │ └─────────────┘ └──────────────┘ └──────────────────┘ │ │
105
- │ └───────────────────────┬────────────────────────────────────┘ │
106
- └──────────────────────────┼────────────────────────────────────────┘
107
- │ SSE + HTTP
108
-
109
- ┌──────────────────────────────────────────────────────────────────┐
110
- │ Tauri 桌面窗口 (WebKitGTK) │
111
- │ │
112
- │ ┌──────────────────────────────────────────────────────────┐ │
113
- │ │ App.tsx (React 应用) │ │
114
- │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ │
115
- │ │ │ VRMScene │ │TextBubble│ │ChatInput│ │ MoodIndicator│ │ │
116
- │ │ │ Three.js │ │SSE驱动 │ │PTT/通话│ │ Canvas 动画 │ │ │
117
- │ │ └──────────┘ └──────────┘ └────────┘ └──────────────┘ │ │
118
- │ └──────────────────────────────────────────────────────────┘ │
119
- │ │
120
- │ 核心 3D 子系统: │
121
- │ ┌─────────────┐ ┌────────────────┐ ┌──────────┐ ┌──────────┐ │
122
- │ │EmoteController│ MotionController │ LipSync │ TextBubble │ │
123
- │ │blend shapes │ VRMA/VMD/FBX │ WebAudio │ 打字机效果 │ │
124
- │ │13种情绪映射 │ 舞蹈系统 │ 唇形同步 │ Markdown │ │
125
- │ └─────────────┘ └────────────────┘ └──────────┘ └──────────┘ │
126
- └──────────────────────────────────────────────────────────────────┘
127
- ```
128
-
129
- ---
130
-
131
- ## 文件位置
132
-
133
- - 后端: `src/friend/` — FriendService, SSE, TTS, STT, VAD, prefs, server, launcher
134
- - 前端: `src/components/friend/frontend/` — React app, Three.js 3D 场景
135
- - API 路由: `src/server/api/friend.ts` — 所有 HTTP 端点
136
- - LLM 工具: `src/tools/FriendEmotionTool.ts`, `src/tools/FriendScreenObserveTool.ts`
137
- - 技能注入: `src/skills/bundled/friendPrompt.ts`
138
- - 配置: `~/.config/Codev/friend.json`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/friend/voice-vad.md DELETED
@@ -1,527 +0,0 @@
1
- # 语音捕获与 VAD 策略
2
-
3
- 本文档详细描述 Friend 系统的音频捕获、语音活动检测 (VAD)、语音转文字 (STT) 和静音管理策略。
4
-
5
- ---
6
-
7
- ## 1. 音频捕获
8
-
9
- ### 1.1 子进程架构
10
-
11
- Friend 使用子进程方式进行音频捕获,而非原生 NAPI 绑定。这是因为 cpal 的同步 NAPI 调用在 ALSA 初始化卡顿时会阻塞事件循环,且无法从 JS 侧超时。
12
-
13
- **文件**: `src/friend/FriendService.ts` — `loadAudioCapture()` 方法
14
-
15
- ```typescript
16
- private async loadAudioCapture(): Promise<AudioCaptureProvider> {
17
- // 使用 arecord / parecord 子进程
18
- // 尝试顺序: arecord (ALSA) → parecord (PulseAudio)
19
- }
20
- ```
21
-
22
- ### 1.2 工具选择与参数
23
-
24
- **arecord** (ALSA):
25
- ```
26
- -D default # 默认设备
27
- -r 16000 # 采样率 16kHz
28
- -f S16_LE # 16位有符号小端 PCM
29
- -c 1 # 单声道
30
- -t raw # 原始 PCM 格式
31
- -q # 静默模式
32
- ```
33
-
34
- **parecord** (PulseAudio):
35
- ```
36
- --raw # 原始 PCM
37
- --rate=16000 # 16kHz
38
- --format=s16le # 16位有符号小端
39
- --channels=1 # 单声道
40
- --latency-msec=20 # 低延迟
41
- ```
42
-
43
- ### 1.3 验证机制 (500ms 窗口)
44
-
45
- 子进程启动后有 500ms 验证窗口:
46
-
47
- ```typescript
48
- // 启动子进程后等待 500ms
49
- // 若子进程在此时间内产生了音频数据 → 验证通过
50
- // 若子进程退出且未产出数据 → 验证失败,尝试下一个工具
51
- // 若 500ms 无数据 → 验证失败
52
- ```
53
-
54
- 这防止了 `parecord` 在 PulseAudio 不可用时静默失败的问题(进程启动但立即退出)。
55
-
56
- ### 1.4 数据回调
57
-
58
- ```typescript
59
- const feedAudio = (chunk: Buffer) => {
60
- // 1. 静音过滤: muted 时不转发(防止 TTS 回声)
61
- if (this.muted) return;
62
-
63
- // 2. 转发到 STT 连接
64
- onData(chunk);
65
-
66
- // 3. 转发到 VAD 检测
67
- if (this.vadInstance) {
68
- const float32 = new Float32Array(chunk.length / 2);
69
- for (let i = 0; i < float32.length; i++) {
70
- float32[i] = chunk.readInt16LE(i * 2) / 32768;
71
- }
72
- this.vadInstance.processAudio(float32).catch(() => {});
73
- }
74
- };
75
- ```
76
-
77
- ### 1.5 停止逻辑
78
-
79
- ```typescript
80
- stopRecording: async () => {
81
- if (captureProc) {
82
- captureProc.kill('SIGTERM');
83
- // 2s 后强制 SIGKILL(防止僵进程)
84
- setTimeout(() => {
85
- try { captureProc?.kill('SIGKILL'); } catch {}
86
- }, 2000);
87
- captureProc = null;
88
- }
89
- },
90
- ```
91
-
92
- ---
93
-
94
- ## 2. STT Provider 检测与降级
95
-
96
- ### 2.1 自动检测链
97
-
98
- **文件**: `src/friend/FriendService.ts` — `detectAvailableSttProvider()` 方法
99
-
100
- ```typescript
101
- detectAvailableSttProvider()
102
-
103
- ├── 1. Groq Whisper (isGroqAvailable)
104
- │ 最快 - REST API 调用,无需 Python
105
- │ 检测: 检查 API key 是否存在
106
-
107
- ├── 2. Local Whisper (checkLocalWhisperAvailable)
108
- │ 本地运行,无需网络
109
- │ 检测: 导入 connectLocalWhisperStream 检查
110
-
111
- ├── 3. Anthropic Voice Stream (isVoiceStreamAvailable)
112
- │ 通过 Anthropic API 的流式语音识别
113
- │ 检测: 检查登录状态和 API key
114
-
115
- ├── 4. Doubao ASR
116
- │ 通过豆包 API
117
- │ 检测: 检查 ~/.claude/tts/doubao/credentials.json
118
-
119
- └── 全不可用 → 抛出错误
120
- "No STT provider available. Install local Whisper: pip install openai-whisper"
121
- ```
122
-
123
- ### 2.2 STT Provider 特性对比
124
-
125
- | Provider | 延迟 | 依赖 | 是否需要网络 | 支持语言 |
126
- |----------|------|------|-------------|---------|
127
- | **Groq Whisper** | 低 | API key | 是 | 多语言 |
128
- | **Local Whisper** | 中 | Python + pip | 否 | 多语言 |
129
- | **Anthropic Voice Stream** | 低 | Anthropic 登录 | 是 | 多语言 (keyterms支持) |
130
- | **Doubao ASR** | 中 | 凭据文件 | 是 | 中文最佳 |
131
-
132
- ### 2.3 STT 连接超时
133
-
134
- 所有 provider 连接都有 8 秒超时:
135
-
136
- ```typescript
137
- startSttConnectionWithTimeout(provider, language)
138
- ├── 超时 8s
139
- └── 超时错误提示: "STT provider '{provider}' timed out after 8s."
140
- └── provider === 'local' 时附带 pip 安装提示
141
- ```
142
-
143
- ### 2.4 连接工厂
144
-
145
- ```typescript
146
- startSttConnection(provider, language)
147
-
148
- ├── anthropic: connectVoiceStream(callbacks, { language, keyterms })
149
- │ keyterms: ['code', 'codev'] 提高相关词汇识别率
150
-
151
- ├── local: preloadWhisperModel + connectLocalWhisperStream
152
- │ 需预加载模型(首次加载较慢)
153
-
154
- ├── doubao: connectDoubaoStream(callbacks, { language })
155
-
156
- └── groq: connectGroqStream(callbacks, { language })
157
- 最快,纯 REST 流式调用
158
- ```
159
-
160
- ### 2.5 回调接口
161
-
162
- ```typescript
163
- const callbacks = {
164
- onTranscript: (text: string, isFinal: boolean) => {
165
- if (isFinal) {
166
- this.captureTranscripts.push(text); // 最终文本入队列
167
- this.captureInterimText = '';
168
- } else {
169
- this.captureInterimText = text; // 临时文本(前端轮询显示)
170
- }
171
- // ���新状态供前端轮询
172
- this.setState({
173
- captureStatus: { capturing: true, interimText: this.captureInterimText },
174
- });
175
- },
176
- onError: (_error: string) => {},
177
- onClose: () => {},
178
- onReady: (_conn: any) => {},
179
- };
180
- ```
181
-
182
- ---
183
-
184
- ## 3. Silero VAD 架构
185
-
186
- **文件**: `src/friend/voice/vad-service.ts` (~322 行)
187
-
188
- ### 3.1 为什么选择 onnxruntime-web WASM
189
-
190
- Bun 不支持 onnxruntime-node 原生插件(会触发 segfault),因此使用 onnxruntime-web 的 WASM 后端。WASM 二进制文件来自 `onnxruntime-web/dist`。
191
-
192
- ### 3.2 模型规格
193
-
194
- - **模型**: Silero VAD legacy ONNX (来自 `@ericedouard/vad-node-realtime`)
195
- - **模型文件**: `silero_vad_legacy.onnx`
196
- - **输入**: 512 采样帧 @ 16kHz (32ms)
197
- - **输出**: 语音概率 (0-1)
198
- - **LSTM 状态**: h=[2,1,64], c=[2,1,64]
199
-
200
- ### 3.3 配置参数
201
-
202
- ```typescript
203
- this.opts = {
204
- // 说话判定阈值
205
- positiveSpeechThreshold: 0.75, // 超过此值判定为语音帧
206
- negativeSpeechThreshold: 0.50, // 低于此值判定为静音帧
207
-
208
- // 触发条件
209
- preSpeechTriggerFrames: 10, // 需要连续 10 帧 (320ms) 确认说话
210
- minSpeechFrames: 6, // 最少 6 帧 (192ms) 有效语音
211
-
212
- // 静音消音
213
- redemptionFrames: 20, // 连续 20 帧 (640ms) 静音结束段落
214
-
215
- // 前置填充
216
- preSpeechPadFrames: 10, // 段落开头包含 10 帧前置音频
217
-
218
- // 能量过滤
219
- rmsThreshold: 0.004, // RMS 能量阈值 (-48dBFS 噪声底限)
220
-
221
- // 采样率
222
- sampleRate: 16000, // 16kHz
223
- };
224
- ```
225
-
226
- ### 3.4 RMS 能量预过滤(噪声抑制)
227
-
228
- RMS 预过滤是 VAD 的**第一道噪声防线**。在运行 ONNX 推理之前,先计算帧的 RMS 能量:
229
-
230
- ```typescript
231
- let sumSq = 0;
232
- for (let i = 0; i < frame.length; i++) {
233
- sumSq += frame[i] * frame[i];
234
- }
235
- const rms = Math.sqrt(sumSq / frame.length);
236
-
237
- if (rms < this.opts.rmsThreshold) {
238
- prob = 0; // 低于噪声底限 → 跳过 ONNX 推理
239
- } else {
240
- // 执行 ONNX 推理
241
- prob = await this.session.run({ input, sr, h, c });
242
- }
243
- ```
244
-
245
- 作用:
246
- - 节省 CPU 资源(大量帧无语音信号)
247
- - 过滤机械噪声/麦克风碰撞/环境静音(噪声抑制)
248
- - 降低误触发率
249
-
250
- 注:FriendService 中使用更严格的阈值 `0.01`(-40dBFS)而非 SileroVAD 的默认 `0.004`,以减少非语音噪音造成的误触发。
251
-
252
- ### 3.5 状态机详解
253
-
254
- ```
255
- ┌──────────────────────────────────────────────────┐
256
- │ pre-speech phase │
257
- │ preSpeechCount < preSpeechTriggerFrames (10) │
258
- │ │
259
- │ 语音帧 → preSpeechCount++ │
260
- │ 非语音帧 → preSpeechCount = 0 │
261
- │ │
262
- │ 当 preSpeechCount >= 10: │
263
- │ → speaking = true │
264
- │ → speechFrameCount = preSpeechCount │
265
- │ → onSpeechStart() │
266
- └──────────────────────┬───────────────────────────┘
267
-
268
-
269
- ┌──────────────────────────────────────────────────┐
270
- │ speaking phase │
271
- │ │
272
- │ 语音帧 → redemptionCounter = 0 │
273
- │ 静音帧 → redemptionCounter++ │
274
- │ │
275
- │ 当 redemptionCounter >= 20 (640ms): │
276
- │ → endSpeech() │
277
- │ → onSpeechEnd(audioSegment) │
278
- └──────────────────────────────────────────────────┘
279
- ```
280
-
281
- ### 3.6 段落构建
282
-
283
- 当 onSpeechEnd 触发时,构建包含前置填充的音频段:
284
-
285
- ```typescript
286
- private endSpeech(): void {
287
- // 1. 检查最小语音帧数 (防止误触发)
288
- if (this.speechFrameCount < this.opts.minSpeechFrames) {
289
- this.callbacks.onVADMisfire(); // 误触发回调
290
- return;
291
- }
292
-
293
- // 2. 构建音频段 (含前置填充)
294
- const total = this.frameHistory.length;
295
- const prePad = Math.min(this.opts.preSpeechPadFrames, total);
296
- const segFrames = this.frameHistory.slice(
297
- total - prePad - this.speechFrameCount,
298
- total
299
- );
300
- // 合并所有帧为一个 Float32Array
301
- const segment = new Float32Array(totalSamples);
302
- for (const f of segFrames) { segment.set(f.frame, offset); offset += ... }
303
-
304
- // 3. 回调
305
- this.callbacks.onSpeechEnd(segment);
306
- }
307
- ```
308
-
309
- ### 3.7 VAD 生命周期
310
-
311
- ```typescript
312
- class SileroVad {
313
- async init() // 加载 ONNX 模型 (初始化)
314
- start() // 激活 VAD 处理
315
- pause() // 暂停 + 结束当前语音段
316
- processAudio() // 处理 PCM 音频帧
317
- flush() // 刷新剩余缓冲区 + 结束段
318
- reset() // 重置全部状态 (保留 session)
319
- destroy() // 清理资源
320
- }
321
- ```
322
-
323
- ### 3.8 VAD 初始化失败的处理
324
-
325
- VAD 初始化失败是非致命的:
326
-
327
- ```typescript
328
- vad.init()
329
- .then(() => { this.vadInstance = vad; })
330
- .catch((e) => {
331
- console.warn('[FriendService] VAD init failed (non-fatal, voice capture falls back to F2-only):', e);
332
- });
333
- ```
334
-
335
- 当 VAD 不可用时,F2 语音通话模式降级为手动分段(仍可通过 PTT 模式使用语音)。
336
-
337
- ---
338
-
339
- ## 4. 静音系统
340
-
341
- ### 4.1 为什么需要静音
342
-
343
- 当 AI 回复通过 TTS 播放时,扬声器声音会被麦克风捕获,如果不做静音处理会产生两种问题:
344
- 1. **TTS 回声**: 自己的语音进入 STT 造成重复识别
345
- 2. **打断 AI 回复**: 用户未说话但环境噪声导致 VAD 误触发
346
-
347
- ### 4.2 静音策略
348
-
349
- ```
350
- startAiTurnMute() ──── 在语音片段提交时立即静音
351
-
352
- ├── muted = true
353
- ├── vadInstance.pause()
354
- └── 30s 超时定时器 (安全性保障)
355
-
356
-
357
- AI 处理 (工具调用、深度思考)
358
-
359
-
360
- broadcastResponse() → generateTts()
361
-
362
- ├── TTS 成功:
363
- │ └── extendMuteForTts(audioId)
364
- │ ├── 取消 30s 定时器
365
- │ └── 设置精确的 TTS 播放时长定时器
366
-
367
- ├── TTS 失败:
368
- │ └── unmute() (立即解除静音)
369
-
370
-
371
- TTS 播放完毕 → unmute()
372
- ├── muted = false
373
- ├── muteTimer = null
374
- └── vadInstance.start() (恢复 VAD 监听)
375
- ```
376
-
377
- ### 4.3 定时器精确控制
378
-
379
- - **初始静音**: 30s(覆盖几乎所有 AI 响应周期)
380
- - **精确调整**: TTS 生成后通过 MP3 时长解析精确控制
381
- - **安全性保障**: 任何情况下都不会永久静音
382
-
383
- ### 4.4 清除静音 (紧急情况)
384
-
385
- ```typescript
386
- stopVoiceCapture() → _stopCapture()
387
- ├── 停止音频捕获
388
- ├── clearMute() (立即解除静音)
389
- └── VAD reset()
390
- ```
391
-
392
- ---
393
-
394
- ## 5. MP3 时长解析
395
-
396
- **文件**: `src/friend/FriendService.ts` — `getMp3DurationMs()` 方法
397
-
398
- ### 5.1 帧同步头扫描法
399
-
400
- 不使用 bitrate 查找表(容易出错),而是通过实际帧间隔计算:
401
-
402
- ```typescript
403
- private getMp3DurationMs(audioId: string): number {
404
- // 1. 获取音频文件路径
405
- const filePath = getAudioFile(audioId);
406
- const buf = readFileSync(filePath);
407
-
408
- // 2. 跳过 ID3v2 标签 (如果存在)
409
- if (buf[0..2] === 'ID3') {
410
- offset = 10 + syncsafe_int(buf[6..9]);
411
- }
412
-
413
- // 3. 找到前两个帧同步字
414
- // 帧同步: 0xFF 字节 + 0xE0 掩码
415
- for (let i = offset; i < buf.length - 3; i++) {
416
- if (isSync(i)) {
417
- if (firstSync === -1) firstSync = i;
418
- else { secondSync = i; break; }
419
- }
420
- }
421
-
422
- // 4. 计算帧间隔 (CBR 模式)
423
- const frameSize = secondSync - firstSync;
424
-
425
- // 5. 解析帧头获取采样率
426
- const h = read32BE(firstSync);
427
- const version = (h >> 19) & 0x3;
428
- const sampleRateIdx = (h >> 10) & 0x3;
429
- // MPEG1 → 1152 samples/frame, MPEG2/2.5 → 576
430
-
431
- // 6. 按步长计数帧数 (帧损坏时扫描到下一个同步字)
432
- for (let pos = firstSync; pos + 3 < buf.length; pos += frameSize) {
433
- if (!isSync(pos)) {
434
- // 损坏帧: 扫描到下一个同步字
435
- while (pos < buf.length - 3 && !isSync(pos)) pos++;
436
- }
437
- frames++;
438
- }
439
-
440
- // 7. 计算时长
441
- return Math.round((frames * spf) / sampleRate * 1000);
442
- }
443
- ```
444
-
445
- ### 5.2 为什么自实现 MP3 解析
446
-
447
- - Edge TTS 输出 CBR MP3
448
- - 不依赖外部库(减少依赖项)
449
- - 帧同步头扫描法对 CBR 精确且鲁棒
450
- - 处理文件损坏的帧 (向前扫描下一个同步字)
451
-
452
- ---
453
-
454
- ## 6. 前端语音捕获 (useServerStt hook)
455
-
456
- **文件**: `src/components/friend/frontend/hooks/useServerStt.ts`
457
-
458
- ### 6.1 设计原因
459
-
460
- Tauri 使用 WebKitGTK,不兼容 `onnxruntime-web` WASM 后端,因此浏览器 VAD (`@ricky0123/vad-web`) 不可用。前端将语音捕获完全委托给后端。
461
-
462
- ### 6.2 两种模式
463
-
464
- **Push-to-Talk (PTT)**:
465
- ```typescript
466
- startPushToTalk() → POST /voice/start // 后端开始捕获
467
- stopPushToTalk() → POST /voice/stop // 后端停止并返回转录
468
- ```
469
-
470
- **Voice Call (F2 模式)**:
471
- ```typescript
472
- startStreaming(onTranscript, onError) → POST /voice/start
473
-
474
- ├── 后端持续捕获麦克风音频
475
- ├── 后端自动分段 (VAD / 定时 5s) 并转录
476
- ├── 前端每 1s 轮询 POST /voice/status 获取 interimText
477
- └── 显示在输入栏中
478
-
479
- stopStreaming() → POST /voice/stop
480
- └── 结束后端捕获,返回完整转录
481
- ```
482
-
483
- ### 6.3 前端语音通话交互
484
-
485
- ```typescript
486
- // TTS 中断: 用户说话时延迟 1s 中断当前 TTS
487
- const scheduleInterrupt = useCallback(() => {
488
- setTimeout(() => {
489
- ;(window as any).__clawInterruptAudio?.()
490
- }, 1000)
491
- }, [])
492
-
493
- // 显示控制: 转录文字显示 3s 后自动清除
494
- setTimeout(() => {
495
- if (voiceCallActiveRef.current) {
496
- setText('')
497
- }
498
- }, 3000)
499
- ```
500
-
501
- ---
502
-
503
- ## 7. 浏览器 VAD 片段转录
504
-
505
- 前端浏览器 VAD(非 WebKitGTK 环境)检测到语音段落后,通过 HTTP 发送到后端:
506
-
507
- ```typescript
508
- // POST /plugins/friend/voice/stt-segment
509
- // Body: raw PCM/WAV buffer
510
- handleFriendApi → friendService.transcribeAudioSegment(audioBuffer)
511
-
512
- ├── 检测 STT provider (自动降级)
513
- ├── 创建临时 STT 连接
514
- ├── 发送音频 → 等待 finalize
515
- └── 发送转录文本到 AI 对话
516
- ```
517
-
518
- ---
519
-
520
- ## 总结: 语音路径选择
521
-
522
- | 使用场景 | VAD | STT Provider | 音频来源 | 静音需求 |
523
- |---------|-----|------------|---------|---------|
524
- | Push-to-Talk (PTT) | 否 (人工分段) | 自动检测 | arecord/parecord | 否 |
525
- | F2 语音通话 | Silero VAD | 自动检测 → 分段 flush | arecord/parecord | 是 |
526
- | 浏览器 VAD (非 Tauri) | 浏览器 VAD | STT segment API | getUserMedia | 否 |
527
- | 文字输入 | 不适用 | 不适用 | 键盘 | 不适用 |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json CHANGED
@@ -17,9 +17,7 @@
17
  },
18
  "scripts": {
19
  "build": "bun run ./scripts/build.ts",
20
- "dev": "bun run ./scripts/dev.ts",
21
- "friend:build": "cd src/components/friend/frontend && npx vite build",
22
- "friend:dev": "cd src/components/friend/frontend && npx vite"
23
  },
24
  "dependencies": {
25
  "@alcalzone/ansi-tokenize": "^0.3.0",
 
17
  },
18
  "scripts": {
19
  "build": "bun run ./scripts/build.ts",
20
+ "dev": "bun run ./scripts/dev.ts"
 
 
21
  },
22
  "dependencies": {
23
  "@alcalzone/ansi-tokenize": "^0.3.0",
scripts/build.ts CHANGED
@@ -51,33 +51,6 @@ const features = [...featureSet]
51
 
52
  const outfile = join('dist', 'codev')
53
 
54
- // ── Pre-step: build Friend VRM frontend ──────────────────────────────────
55
- function buildFriendFrontend(): boolean {
56
- const frontendDir = join(process.cwd(), 'src', 'components', 'friend', 'frontend')
57
- const distIndex = join(frontendDir, 'dist', 'index.html')
58
- if (existsSync(distIndex)) {
59
- console.log('Friend frontend already built, skipping.')
60
- return true
61
- }
62
- console.log('Building Friend VRM frontend...')
63
- const proc = Bun.spawnSync({
64
- cmd: ['npm', 'run', 'build'],
65
- cwd: frontendDir,
66
- stdout: 'inherit',
67
- stderr: 'inherit',
68
- })
69
- if (proc.exitCode !== 0) {
70
- console.error('Friend frontend build failed.')
71
- return false
72
- }
73
- console.log('Friend frontend built.')
74
- return true
75
- }
76
-
77
- if (!buildFriendFrontend()) {
78
- process.exit(1)
79
- }
80
-
81
  // ──────────────────────────────────────────────────────────────────────────
82
 
83
  const buildTime = new Date().toISOString()
 
51
 
52
  const outfile = join('dist', 'codev')
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  // ──────────────────────────────────────────────────────────────────────────
55
 
56
  const buildTime = new Date().toISOString()
scripts/speak.py DELETED
@@ -1,42 +0,0 @@
1
- import json, os, sys, tempfile, asyncio
2
-
3
- async def speak(text: str, voice: str = "en-US-JennyNeural", output_path: str | None = None) -> dict:
4
- try:
5
- import edge_tts
6
- communicate = edge_tts.Communicate(text, voice)
7
- if output_path:
8
- await communicate.save(output_path)
9
- return {"success": True, "audio_path": output_path}
10
- tmp = tempfile.mktemp(suffix=".mp3")
11
- await communicate.save(tmp)
12
- return {"success": True, "audio_path": tmp}
13
- except ImportError:
14
- pass
15
-
16
- return {"success": False, "error": "edge-tts is not installed. Run: pip install edge-tts"}
17
-
18
- def main():
19
- if len(sys.argv) < 2:
20
- print(json.dumps({"success": False, "error": "Usage: speak.py <text> [--voice <name>] [--output <path>]"}))
21
- sys.exit(1)
22
-
23
- text = sys.argv[1]
24
- voice = "en-US-JennyNeural"
25
- output_path = None
26
-
27
- i = 2
28
- while i < len(sys.argv):
29
- if sys.argv[i] == "--voice" and i + 1 < len(sys.argv):
30
- voice = sys.argv[i + 1]
31
- i += 2
32
- elif sys.argv[i] == "--output" and i + 1 < len(sys.argv):
33
- output_path = sys.argv[i + 1]
34
- i += 2
35
- else:
36
- i += 1
37
-
38
- result = asyncio.run(speak(text, voice, output_path))
39
- print(json.dumps(result))
40
-
41
- if __name__ == "__main__":
42
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/transcribe.py DELETED
@@ -1,81 +0,0 @@
1
- import json, os, sys, tempfile, wave
2
-
3
- WAV_HEADER_SIZE = 44
4
-
5
- WHISPER_CACHE = os.path.expanduser("~/.cache/whisper")
6
-
7
- def write_wav(path: str, raw_pcm: bytes, sample_rate: int = 16000):
8
- with wave.open(path, 'wb') as w:
9
- w.setnchannels(1)
10
- w.setsampwidth(2)
11
- w.setframerate(sample_rate)
12
- w.writeframes(raw_pcm)
13
-
14
- def get_model_path(name):
15
- lp = os.path.expanduser(f"~/.cache/huggingface/hub/models--Systran--faster-whisper-{name}")
16
- if os.path.isdir(lp):
17
- return lp
18
- return name
19
-
20
- def transcribe(wav_path: str, model_size: str = "small", language: str | None = None) -> dict:
21
- try:
22
- from faster_whisper import WhisperModel
23
- model_path = get_model_path(model_size)
24
- model = WhisperModel(model_path, device="cuda", compute_type="int8_float16")
25
- opts = {"beam_size": 5}
26
- if language:
27
- opts["language"] = language
28
- segments, info = model.transcribe(wav_path, **opts)
29
- text = " ".join(seg.text for seg in segments)
30
- return {"success": True, "text": text.strip(), "language": info.language}
31
- except ImportError:
32
- pass
33
-
34
- try:
35
- import whisper
36
- model = whisper.load_model(model_size, device="cuda", download_root=WHISPER_CACHE)
37
- opts = {}
38
- if language:
39
- opts["language"] = language
40
- result = model.transcribe(wav_path, **opts)
41
- return {"success": True, "text": result["text"].strip(), "language": result.get("language", "")}
42
- except ImportError:
43
- pass
44
-
45
- return {"success": False, "error": "Neither faster-whisper nor openai-whisper is installed. Run: pip install faster-whisper"}
46
-
47
- def main():
48
- if len(sys.argv) < 2:
49
- print(json.dumps({"success": False, "error": "Usage: transcribe.py <wav_path> [--model <size>] [--language <code>]"}))
50
- sys.exit(1)
51
-
52
- wav_path = sys.argv[1]
53
- model_size = "small"
54
- language = None
55
-
56
- i = 2
57
- while i < len(sys.argv):
58
- if sys.argv[i] == "--model" and i + 1 < len(sys.argv):
59
- model_size = sys.argv[i + 1]
60
- i += 2
61
- elif sys.argv[i] == "--language" and i + 1 < len(sys.argv):
62
- language = sys.argv[i + 1]
63
- i += 2
64
- else:
65
- i += 1
66
-
67
- if sys.argv[1] == "--stdin-pcm":
68
- sample_rate = int(sys.argv[2]) if len(sys.argv) > 2 else 16000
69
- raw = sys.stdin.buffer.read()
70
- tmp = tempfile.mktemp(suffix=".wav")
71
- write_wav(tmp, raw, sample_rate)
72
- wav_path = tmp
73
- result = transcribe(wav_path, model_size, language)
74
- os.unlink(tmp)
75
- else:
76
- result = transcribe(wav_path, model_size, language)
77
-
78
- print(json.dumps(result))
79
-
80
- if __name__ == "__main__":
81
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/whisper_server.py DELETED
@@ -1,80 +0,0 @@
1
- import json, os, sys, wave
2
-
3
- WHISPER_CACHE = os.path.expanduser("~/.cache/whisper")
4
-
5
- def write_wav(path, raw_pcm, sample_rate=16000):
6
- with wave.open(path, 'wb') as w:
7
- w.setnchannels(1)
8
- w.setsampwidth(2)
9
- w.setframerate(sample_rate)
10
- w.writeframes(raw_pcm)
11
-
12
- def get_model_path(name):
13
- lp = os.path.expanduser(f'~/.cache/huggingface/hub/models--Systran--faster-whisper-{name}')
14
- if os.path.isdir(lp):
15
- return lp
16
- return name
17
-
18
- def main():
19
- model = None
20
- model_name = None
21
-
22
- for line in sys.stdin:
23
- line = line.strip()
24
- if not line:
25
- continue
26
- try:
27
- msg = json.loads(line)
28
- except:
29
- continue
30
-
31
- msg_type = msg.get('type')
32
-
33
- if msg_type == 'load':
34
- name = msg.get('model', 'small')
35
- print(json.dumps({'type': 'ready', 'model': name}), flush=True)
36
- sys.stdout.flush()
37
-
38
- elif msg_type == 'transcribe':
39
- wav_path = msg.get('wav')
40
- language = msg.get('language')
41
-
42
- if not model or model_name != msg.get('model'):
43
- model_name = msg.get('model', 'base')
44
- print(json.dumps({'type': 'status', 'message': f'Loading model {model_name}...'}), flush=True)
45
- try:
46
- import whisper
47
- model = whisper.load_model(model_name, device='cuda', download_root=WHISPER_CACHE)
48
- print(json.dumps({'type': 'status', 'message': 'Model loaded'}), flush=True)
49
- except ImportError:
50
- try:
51
- from faster_whisper import WhisperModel
52
- mp = get_model_path(model_name)
53
- model = WhisperModel(mp, device='cuda', compute_type='int8_float16')
54
- print(json.dumps({'type': 'status', 'message': 'Model loaded'}), flush=True)
55
- except (ImportError, RuntimeError) as e:
56
- print(json.dumps({'type': 'error', 'message': f'No whisper library available: {e}'}), flush=True)
57
- sys.stdout.flush()
58
- continue
59
-
60
- if not wav_path or not os.path.exists(wav_path):
61
- print(json.dumps({'type': 'error', 'message': 'WAV file not found'}), flush=True)
62
- sys.stdout.flush()
63
- continue
64
-
65
- try:
66
- opts = {}
67
- if language:
68
- opts['language'] = language
69
-
70
- result = model.transcribe(wav_path, **opts)
71
- text = result['text'].strip() if isinstance(result, dict) else ''
72
- lang = result.get('language', language or 'en') if isinstance(result, dict) else (language or 'en')
73
-
74
- print(json.dumps({'type': 'result', 'text': text, 'language': lang}), flush=True)
75
- except Exception as e:
76
- print(json.dumps({'type': 'error', 'message': str(e)}), flush=True)
77
- sys.stdout.flush()
78
-
79
- if __name__ == '__main__':
80
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/commands.ts CHANGED
@@ -44,7 +44,6 @@ import skills from './commands/skills/index.js'
44
  import status from './commands/status/index.js'
45
  import tasks from './commands/tasks/index.js'
46
  import feishu from './commands/feishu/index.js'
47
- import friend from './commands/friend/index.js'
48
  import goals from './commands/goal/index.js'
49
  import telegram from './commands/telegram/index.js'
50
  import teleport from './commands/teleport/index.js'
@@ -310,7 +309,6 @@ const COMMANDS = memoize((): Command[] => [
310
  theme,
311
  feedback,
312
  feishu,
313
- friend,
314
  goals,
315
  review,
316
  ultrareview,
 
44
  import status from './commands/status/index.js'
45
  import tasks from './commands/tasks/index.js'
46
  import feishu from './commands/feishu/index.js'
 
47
  import goals from './commands/goal/index.js'
48
  import telegram from './commands/telegram/index.js'
49
  import teleport from './commands/teleport/index.js'
 
309
  theme,
310
  feedback,
311
  feishu,
 
312
  goals,
313
  review,
314
  ultrareview,
src/commands/friend/friend.tsx DELETED
@@ -1,156 +0,0 @@
1
- /**
2
- * /friend TUI command — manage VRM desktop pet companion.
3
- *
4
- * Ink-based terminal UI that shows status and controls for the
5
- * Friend desktop pet feature.
6
- */
7
- import React, { useEffect } from 'react'
8
- import { Box, Text, useInput } from '../../ink.js'
9
- import { getPrefs, updatePrefs } from '../../friend/prefs.js'
10
- import {
11
- launchTauri,
12
- stopTauri,
13
- } from '../../friend/tauri-launcher.js'
14
- import { startFriendServer, stopFriendServer, getServerPort } from '../../friend/server.js'
15
- import { friendService } from '../../friend/FriendService.js'
16
- import type { LocalJSXCommandOnDone, CommandResultDisplay } from '../../types/command.js'
17
-
18
- const FRIEND_URL = 'http://127.0.0.1:3456/friend/'
19
-
20
- const logger = () => ({
21
- info: (msg: string) => console.log(`[Friend] ${msg}`),
22
- warn: (msg: string) => console.warn(`[Friend] ${msg}`),
23
- })
24
-
25
- export async function call(
26
- onDone: (result?: string, options?: { display?: CommandResultDisplay }) => void,
27
- _context: unknown,
28
- args?: string,
29
- ): Promise<React.ReactNode> {
30
- const trimmed = args?.trim().toLowerCase()
31
-
32
- // /friend (no args) or /friend start → start the service
33
- if (!trimmed || trimmed === 'start') {
34
- updatePrefs({ enabled: true })
35
- // Start in-process HTTP server for friend API and SSE
36
- try {
37
- startFriendServer(3456, '127.0.0.1')
38
- } catch (err) {
39
- console.warn(`[Friend] HTTP server start failed: ${err}`)
40
- console.warn('[Friend] The Tauri app may need the main server on port 3456.')
41
- }
42
- // FriendService runs in-process; messages enqueue into the CLI queue
43
- await friendService.start().catch((err) => {
44
- console.warn(`[Friend] Service start failed: ${err}`)
45
- })
46
- // Launch Tauri display window (thin client)
47
- launchTauri(logger())
48
- return <FriendStartView onDone={onDone} />
49
- }
50
-
51
- if (trimmed === 'stop') {
52
- updatePrefs({ enabled: false })
53
- await friendService.stop().catch(() => {})
54
- stopFriendServer()
55
- stopTauri(logger())
56
- return <FriendStopView onDone={onDone} />
57
- }
58
-
59
- // /friend help → show manager/status view
60
- return <FriendManager onDone={onDone} />
61
- }
62
-
63
- function FriendManager({ onDone }: { onDone: LocalJSXCommandOnDone }) {
64
- useInput((_input, key) => {
65
- if (key.escape || key.return) {
66
- onDone(undefined, { display: 'skip' })
67
- }
68
- })
69
-
70
- return (
71
- <Box flexDirection="column" padding={1}>
72
- <Box marginBottom={1}>
73
- <Text bold underline>
74
- Friend VRM Desktop Pet
75
- </Text>
76
- </Box>
77
-
78
- <FriendStatus prefs={getPrefs()} />
79
-
80
- <Box marginTop={1} flexDirection="column">
81
- <Text dimColor>Usage:</Text>
82
- <Text dimColor> /friend Interactive status & controls</Text>
83
- <Text dimColor> /friend start Launch friend window</Text>
84
- <Text dimColor> /friend stop Stop friend window</Text>
85
- </Box>
86
-
87
- <Box marginTop={1}>
88
- <Text dimColor>Press Esc or Enter to close.</Text>
89
- </Box>
90
- </Box>
91
- )
92
- }
93
-
94
- function FriendStopView({ onDone }: { onDone: LocalJSXCommandOnDone }) {
95
- useEffect(() => {
96
- const t = setTimeout(() => onDone(undefined, { display: 'skip' }), 1500)
97
- return () => clearTimeout(t)
98
- }, [onDone])
99
-
100
- return (
101
- <Box flexDirection="column">
102
- <Text>Friend companion stopped.</Text>
103
- </Box>
104
- )
105
- }
106
-
107
- function FriendStartView({ onDone }: { onDone: LocalJSXCommandOnDone }) {
108
- useEffect(() => {
109
- const t = setTimeout(() => onDone(undefined, { display: 'skip' }), 1500)
110
- return () => clearTimeout(t)
111
- }, [onDone])
112
-
113
- return (
114
- <Box flexDirection="column">
115
- <Text>Starting Friend VRM companion...</Text>
116
- <Text dimColor>A Tauri window will open.</Text>
117
- <Text dimColor>Also available at: {FRIEND_URL}</Text>
118
- </Box>
119
- )
120
- }
121
-
122
- function FriendStatus({
123
- prefs,
124
- }: {
125
- prefs: ReturnType<typeof getPrefs>
126
- }) {
127
- const statusDot = prefs.enabled ? '●' : '○'
128
- const statusColor = prefs.enabled ? 'green' : 'gray'
129
-
130
- return (
131
- <Box flexDirection="column">
132
- <Box>
133
- <Text color={statusColor}>{statusDot} Status: </Text>
134
- <Text bold color={statusColor}>
135
- {prefs.enabled ? 'Enabled' : 'Disabled'}
136
- </Text>
137
- </Box>
138
- <Box>
139
- <Text> URL: </Text>
140
- <Text dimColor>{FRIEND_URL}</Text>
141
- </Box>
142
- <Box>
143
- <Text> Voice: </Text>
144
- <Text dimColor>{prefs.voice ?? 'default'}</Text>
145
- </Box>
146
- <Box>
147
- <Text> TTS: </Text>
148
- <Text dimColor>{prefs.ttsEnabled ? 'on' : 'off'}</Text>
149
- </Box>
150
- <Box>
151
- <Text> Tracking: </Text>
152
- <Text dimColor>{prefs.tracking ?? 'mouse'}</Text>
153
- </Box>
154
- </Box>
155
- )
156
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/commands/friend/index.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { Command } from '../../commands.js'
2
-
3
- const friend = {
4
- type: 'local-jsx',
5
- name: 'friend',
6
- description: 'Manage the VRM desktop pet companion — start/stop the 3D avatar window and adjust settings',
7
- aliases: ['vrm'],
8
- load: () => import('./friend.js'),
9
- } satisfies Command
10
-
11
- export default friend
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/App.tsx DELETED
@@ -1,352 +0,0 @@
1
- import { useEffect, useRef, useState, useCallback } from 'react'
2
- import { VRMScene } from './components/VRMScene'
3
- import type { VRMSceneHandle } from './components/VRMScene'
4
- import { TextBubble } from './components/TextBubble'
5
- import type { OnVrmMessage } from './components/TextBubble'
6
- import { ChatInput } from './components/ChatInput'
7
- import { ResizeHandles } from './components/ResizeHandles'
8
- import { SettingsPanel } from './components/SettingsPanel'
9
- import { usePassThrough } from './hooks/usePassThrough'
10
- import { LipSync } from './lip-sync'
11
- import { FRIEND_API, bindScene } from './api'
12
- import { Menu, Move, Rotate3D, EyeOff, Settings, RefreshCw, Pin } from 'lucide-react'
13
-
14
- const DEFAULT_MODEL = '/friend/model1.vrm'
15
-
16
- // 情绪 → 动作映射
17
- // 只有有明确肢体动作关联的情绪才映射;neutral/relaxed 不绑动作,
18
- // 避免角色在无明确意图时做出违和姿势。
19
- const emotionActionMap: Record<string, string> = {
20
- think: 'scratchHead',
21
- question: 'point',
22
- curious: 'scratchHead',
23
- happy: 'happy',
24
- surprised: 'excited',
25
- angry: 'angry',
26
- awkward: 'playFingers',
27
- love: 'shy',
28
- flirty: 'shy',
29
- greeting: 'greeting',
30
- sad: '',
31
- relaxed: '',
32
- neutral: '',
33
- }
34
-
35
- const btnStyle: React.CSSProperties = {
36
- width: 32,
37
- height: 32,
38
- border: 'none',
39
- borderRadius: 6,
40
- background: 'rgba(125, 125, 125, 0.28)',
41
- backdropFilter: 'blur(6px)',
42
- display: 'flex',
43
- alignItems: 'center',
44
- justifyContent: 'center',
45
- color: 'rgba(255, 255, 255, 0.8)',
46
- fontSize: 16,
47
- cursor: 'pointer',
48
- }
49
-
50
- export default function App() {
51
- const sceneRef = useRef<VRMSceneHandle>(null)
52
- const [pinned, setPinned] = useState(true)
53
- const [tracking, setTracking] = useState<'mouse' | 'camera'>('mouse')
54
- const [showText, setShowText] = useState(true)
55
- const [collapsed, setCollapsed] = useState(false)
56
- const [ttsEnabled, setTtsEnabled] = useState(true)
57
- const [modelPath, setModelPath] = useState(DEFAULT_MODEL)
58
- const [settingsOpen, setSettingsOpen] = useState(false)
59
- const [hideUI, setHideUI] = useState(false)
60
- const [volume, setVolume] = useState(0.5)
61
- const [uiAlign, setUiAlign] = useState<'left' | 'right'>('right')
62
- const [language, setLanguage] = useState<'zh' | 'en'>(() => navigator.language.startsWith('zh') ? 'zh' : 'en')
63
- const [sttProvider, setSttProvider] = useState<'browser' | 'groq' | 'anthropic' | 'local' | 'doubao'>('browser')
64
- const t = (zh: string, en: string) => language === 'en' ? en : zh
65
- usePassThrough(!settingsOpen)
66
-
67
- // Load persisted settings on mount
68
- useEffect(() => {
69
- fetch(`${FRIEND_API}/settings`)
70
- .then((r) => r.json())
71
- .then((s) => {
72
- if (s.modelPath) setModelPath(s.modelPath)
73
- if (s.ttsEnabled !== undefined) setTtsEnabled(s.ttsEnabled)
74
- if (s.showText !== undefined) setShowText(s.showText)
75
- if (s.hideUI !== undefined) setHideUI(s.hideUI)
76
- if (s.tracking) { setTracking(s.tracking); sceneRef.current?.setTrackingMode(s.tracking) }
77
- if (s.volume !== undefined) { setVolume(s.volume); LipSync.getInstance().setVolume(s.volume) }
78
- if (s.uiAlign) setUiAlign(s.uiAlign)
79
- if (s.sttProvider) setSttProvider(s.sttProvider)
80
- if (s.language) {
81
- setLanguage(s.language)
82
- } else {
83
- const detected = navigator.language.startsWith('zh') ? 'zh' : 'en'
84
- fetch(`${FRIEND_API}/settings`, {
85
- method: 'POST',
86
- headers: { 'Content-Type': 'application/json' },
87
- body: JSON.stringify({ language: detected }),
88
- }).catch(() => {})
89
- }
90
- })
91
- .catch(() => {})
92
- }, [])
93
-
94
- const saveSettings = (patch: Record<string, unknown>) => {
95
- fetch(`${FRIEND_API}/settings`, {
96
- method: 'POST',
97
- headers: { 'Content-Type': 'application/json' },
98
- body: JSON.stringify(patch),
99
- }).catch(() => {})
100
- }
101
-
102
- useEffect(() => {
103
- bindScene(sceneRef.current)
104
- return () => bindScene(null)
105
- })
106
-
107
- const handleVolumeChange = useCallback((v: number) => {
108
- setVolume(v)
109
- LipSync.getInstance().setVolume(v)
110
- saveSettings({ volume: v })
111
- }, [])
112
-
113
- const handleTrackingChange = useCallback((mode: 'mouse' | 'camera') => {
114
- sceneRef.current?.setTrackingMode(mode)
115
- setTracking(mode)
116
- saveSettings({ tracking: mode })
117
- }, [])
118
-
119
- const handleVrmMessage: OnVrmMessage = useCallback((msg) => {
120
- if (msg.emotion && sceneRef.current) {
121
- const action = msg.action || emotionActionMap[msg.emotion]
122
- if (msg.text) {
123
- sceneRef.current.setEmotionWithReset(msg.emotion, msg.emotionDuration ?? 5000, msg.emotionIntensity)
124
- if (action) sceneRef.current.playAction(action)
125
- } else {
126
- sceneRef.current.setEmotionWithReset(msg.emotion, msg.emotionDuration ?? 10000, msg.emotionIntensity)
127
- // No hold — action plays once, emotion timer handles duration.
128
- // Hold would lock _actionPlaying for 10s, blocking text-message actions
129
- // when friend_emotion tool fires before broadcastResponse.
130
- if (action) sceneRef.current.playAction(action)
131
- }
132
- }
133
- }, [])
134
-
135
- // ── Idle fidget: natural micro-movements when no activity ─────────────────
136
- const lastActivityRef = useRef(Date.now())
137
- const originalHandleVrmMessage = handleVrmMessage
138
- const handleVrmMessageWithActivity: OnVrmMessage = useCallback((msg) => {
139
- lastActivityRef.current = Date.now()
140
- originalHandleVrmMessage(msg)
141
- }, [originalHandleVrmMessage])
142
-
143
- useEffect(() => {
144
- const allEmotions = [
145
- 'happy', 'sad', 'angry', 'surprised', 'think', 'awkward',
146
- 'question', 'curious', 'neutral', 'love', 'flirty', 'greeting', 'relaxed',
147
- ]
148
- const idleTimers: Array<{ emotion?: string; action?: string; minIdle: number }> = [
149
- // Emotion-only (subtle facial changes, no body action)
150
- { emotion: 'think', minIdle: 8 },
151
- { emotion: 'curious', minIdle: 12 },
152
- { emotion: 'relaxed', minIdle: 15 },
153
- { emotion: 'happy', minIdle: 20 },
154
- // Small body actions (no strong emotion)
155
- { action: 'stretch', minIdle: 25 },
156
- { action: 'scratchHead', minIdle: 10 },
157
- { action: 'playFingers', minIdle: 15 },
158
- { action: 'akimbo', minIdle: 30 },
159
- // Combined emotion + action
160
- { emotion: 'think', action: 'scratchHead', minIdle: 18 },
161
- { emotion: 'relaxed', action: 'akimbo', minIdle: 35 },
162
- { emotion: 'curious', action: 'point', minIdle: 22 },
163
- { emotion: 'happy', action: 'shy', minIdle: 28 },
164
- { emotion: 'surprised', action: 'excited', minIdle: 40 },
165
- ]
166
- const IDLE_THRESHOLD_MS = 15_000
167
- const FIDGET_CHECK_MS = 8_000
168
-
169
- const timer = setInterval(() => {
170
- const idleMs = Date.now() - lastActivityRef.current
171
- if (idleMs < IDLE_THRESHOLD_MS) return
172
- if (Math.random() > 0.55) return
173
-
174
- // Pick a weighted-random fidget based on how long we've been idle
175
- const candidates = idleTimers.filter((t) => idleMs >= t.minIdle * 1000)
176
- if (candidates.length === 0) return
177
- const pick = candidates[Math.floor(Math.random() * candidates.length)]
178
-
179
- if (pick.emotion) {
180
- const intensity = 0.3 + Math.random() * 0.5
181
- const duration = 2000 + Math.random() * 3000
182
- sceneRef.current?.setEmotionWithReset(pick.emotion, duration, intensity)
183
- }
184
- if (pick.action) {
185
- sceneRef.current?.playAction(pick.action)
186
- }
187
- lastActivityRef.current = Date.now()
188
- }, FIDGET_CHECK_MS)
189
-
190
- return () => clearInterval(timer)
191
- }, [])
192
-
193
- // 全局快捷键
194
- useEffect(() => {
195
- const onKeyDown = (e: KeyboardEvent) => {
196
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
197
- if (e.key === 'Tab') {
198
- e.preventDefault()
199
- setCollapsed((v) => !v)
200
- }
201
- if (e.key === 'F4') {
202
- e.preventDefault()
203
- setSettingsOpen((v) => !v)
204
- }
205
- if (e.key === 'F5') {
206
- e.preventDefault()
207
- window.location.reload()
208
- }
209
- }
210
- window.addEventListener('keydown', onKeyDown)
211
- return () => window.removeEventListener('keydown', onKeyDown)
212
- }, [])
213
-
214
- const togglePin = async () => {
215
- setPinned((v) => !v)
216
- }
217
-
218
- return (
219
- <div
220
- style={{
221
- width: '100vw',
222
- height: '100vh',
223
- background: 'transparent',
224
- overflow: 'hidden',
225
- position: 'relative',
226
- }}
227
- >
228
- <ResizeHandles />
229
- <VRMScene ref={sceneRef} modelPath={modelPath} />
230
- <TextBubble onMessage={handleVrmMessageWithActivity} enabled={showText} ttsEnabled={ttsEnabled} />
231
- {!hideUI && <ChatInput uiAlign={uiAlign} language={language} sttProvider={sttProvider} />}
232
- <SettingsPanel
233
- visible={settingsOpen}
234
- onClose={() => setSettingsOpen(false)}
235
- currentModel={modelPath}
236
- onModelChange={(m) => { setModelPath(m); saveSettings({ modelPath: m }) }}
237
- hideUI={hideUI}
238
- onHideUIChange={(v) => { setHideUI(v); saveSettings({ hideUI: v }) }}
239
- showText={showText}
240
- onShowTextChange={(v) => { setShowText(v); saveSettings({ showText: v }) }}
241
- ttsEnabled={ttsEnabled}
242
- onTtsEnabledChange={(v) => { setTtsEnabled(v); saveSettings({ ttsEnabled: v }) }}
243
- tracking={tracking}
244
- onTrackingChange={handleTrackingChange}
245
- volume={volume}
246
- onVolumeChange={handleVolumeChange}
247
- uiAlign={uiAlign}
248
- onUiAlignChange={(v) => { setUiAlign(v); saveSettings({ uiAlign: v }) }}
249
- language={language}
250
- onLanguageChange={(v) => { setLanguage(v); saveSettings({ language: v }) }}
251
- sttProvider={sttProvider}
252
- onSttProviderChange={(v) => { setSttProvider(v); saveSettings({ sttProvider: v }) }}
253
- />
254
- {!hideUI && <div
255
- style={{
256
- position: 'absolute',
257
- top: 8,
258
- ...(uiAlign === 'left' ? { left: 8 } : { right: 8 }),
259
- display: 'flex',
260
- flexDirection: 'column',
261
- gap: 4,
262
- }}
263
- >
264
- <button
265
- onClick={() => setCollapsed((v) => !v)}
266
- style={btnStyle}
267
- title={collapsed ? t('展开菜单 (Tab)', 'Expand Menu (Tab)') : t('折叠菜单 (Tab)', 'Collapse Menu (Tab)')}
268
- >
269
- <Menu size={16} />
270
- </button>
271
- {!collapsed && <>
272
- <button
273
- onClick={() => setSettingsOpen(true)}
274
- style={btnStyle}
275
- title={t('设置 (F4)', 'Settings (F4)')}
276
- >
277
- <Settings size={16} />
278
- </button>
279
- <button
280
- onClick={() => window.location.reload()}
281
- style={btnStyle}
282
- title={t('刷新 (F5)', 'Refresh (F5)')}
283
- >
284
- <RefreshCw size={16} />
285
- </button>
286
- <button
287
- onClick={() => window.close()}
288
- style={btnStyle}
289
- title={t('隐藏窗口', 'Hide Window')}
290
- >
291
- <EyeOff size={16} />
292
- </button>
293
- <button
294
- onClick={togglePin}
295
- style={{ ...btnStyle, opacity: pinned ? 1 : 0.5 }}
296
- title={pinned ? t('取消置顶', 'Unpin') : t('置顶窗口', 'Pin to Top')}
297
- >
298
- <Pin size={16} />
299
- </button>
300
- <button
301
- onMouseDown={(e) => {
302
- e.preventDefault()
303
- ;(window as any).__clawDragging = true
304
- let lastX = e.clientX
305
- let lastY = e.clientY
306
- const onMove = (ev: MouseEvent) => {
307
- sceneRef.current?.panCamera(ev.clientX - lastX, ev.clientY - lastY)
308
- lastX = ev.clientX
309
- lastY = ev.clientY
310
- }
311
- const onUp = () => {
312
- ;(window as any).__clawDragging = false
313
- window.removeEventListener('mousemove', onMove)
314
- window.removeEventListener('mouseup', onUp)
315
- }
316
- window.addEventListener('mousemove', onMove)
317
- window.addEventListener('mouseup', onUp)
318
- }}
319
- style={{ ...btnStyle, cursor: 'grab' }}
320
- title={t('拖动移动人物位置', 'Drag to Move')}
321
- >
322
- <Move size={16} />
323
- </button>
324
- <button
325
- onMouseDown={(e) => {
326
- e.preventDefault()
327
- ;(window as any).__clawDragging = true
328
- let lastX = e.clientX
329
- let lastY = e.clientY
330
- const onMove = (ev: MouseEvent) => {
331
- sceneRef.current?.rotateCamera(ev.clientX - lastX, ev.clientY - lastY)
332
- lastX = ev.clientX
333
- lastY = ev.clientY
334
- }
335
- const onUp = () => {
336
- ;(window as any).__clawDragging = false
337
- window.removeEventListener('mousemove', onMove)
338
- window.removeEventListener('mouseup', onUp)
339
- }
340
- window.addEventListener('mousemove', onMove)
341
- window.addEventListener('mouseup', onUp)
342
- }}
343
- style={{ ...btnStyle, cursor: 'grab' }}
344
- title={t('拖动旋转视角', 'Drag to Rotate')}
345
- >
346
- <Rotate3D size={16} />
347
- </button>
348
- </>}
349
- </div>}
350
- </div>
351
- )
352
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/api.ts DELETED
@@ -1,51 +0,0 @@
1
- import type { VRMSceneHandle } from './components/VRMScene'
2
-
3
- /** Codev server base URL */
4
- export const SERVER_URL = 'http://127.0.0.1:3456'
5
-
6
- /** Base path for the Friend VRM plugin API */
7
- export const FRIEND_API = `${SERVER_URL}/plugins/friend`
8
-
9
- let sceneHandle: VRMSceneHandle | null = null
10
-
11
- export function bindScene(handle: VRMSceneHandle | null) {
12
- sceneHandle = handle
13
- }
14
-
15
- /**
16
- * POST to a server API. If the request body contains an `emotion` field,
17
- * the VRM expression will automatically change to match.
18
- *
19
- * Body example:
20
- * { emotion: "happy", message: "hello", ... }
21
- *
22
- * Supported emotions: happy, sad, angry, surprised, think, neutral
23
- *
24
- * Optional fields:
25
- * emotionDuration — ms, auto-reset to neutral after this time
26
- * emotionIntensity — 0~1, defaults to 1
27
- */
28
- export async function postApi<T = any>(url: string, body: Record<string, any>): Promise<T> {
29
- // Trigger emotion from the request body
30
- if (body.emotion && sceneHandle) {
31
- const duration = body.emotionDuration as number | undefined
32
- const intensity = body.emotionIntensity as number | undefined
33
- if (duration) {
34
- sceneHandle.setEmotionWithReset(body.emotion, duration, intensity)
35
- } else {
36
- sceneHandle.setEmotion(body.emotion, intensity)
37
- }
38
- }
39
-
40
- const res = await fetch(url, {
41
- method: 'POST',
42
- headers: { 'Content-Type': 'application/json' },
43
- body: JSON.stringify(body),
44
- })
45
-
46
- if (!res.ok) {
47
- throw new Error(`API error: ${res.status} ${res.statusText}`)
48
- }
49
-
50
- return res.json()
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/assets/lip-sync-profile.json DELETED
@@ -1 +0,0 @@
1
- { "jsonPath": "/home/steamvr/projects/wLipSync/www/profile.json", "mfccNum": 12, "mfccDataCount": 12, "melFilterBankChannels": 30, "targetSampleRate": 16000, "sampleCount": 1024, "useStandardization": false, "compareMethod": 2, "mfccs": [{ "name": "A", "mfccCalibrationDataList": [{ "array": [94.40318298339844, 0.32245922088623049, -65.5116195678711, -29.537851333618165, 4.888294219970703, 14.523965835571289, -32.6411247253418, 1.6505765914916993, -9.960077285766602, -5.7025322914123539, -11.886154174804688, -24.35236358642578] }, { "array": [94.40318298339844, 0.32245922088623049, -65.5116195678711, -29.537851333618165, 4.888294219970703, 14.523965835571289, -32.6411247253418, 1.6505765914916993, -9.960077285766602, -5.7025322914123539, -11.886154174804688, -24.35236358642578] }, { "array": [102.16287231445313, -3.3587560653686525, -65.58428192138672, -25.24440574645996, 3.224522590637207, 12.005892753601075, -29.293079376220704, 0.6378564834594727, -10.817683219909668, -1.3263540267944337, -14.543159484863282, -24.169780731201173] }, { "array": [99.35592651367188, -3.681424140930176, -65.20439910888672, -23.45950698852539, 6.205645561218262, 14.96288013458252, -29.882709503173829, 0.6733551025390625, -7.077619552612305, -3.5570802688598635, -14.427347183227539, -23.340003967285158] }, { "array": [99.35592651367188, -3.681424140930176, -65.20439910888672, -23.45950698852539, 6.205645561218262, 14.96288013458252, -29.882709503173829, 0.6733551025390625, -7.077619552612305, -3.5570802688598635, -14.427347183227539, -23.340003967285158] }, { "array": [104.24951171875, -2.8328847885131838, -66.59016418457031, -22.962886810302736, 5.519782066345215, 16.50394058227539, -32.338768005371097, 5.820473670959473, -10.59586238861084, -2.7398462295532228, -12.5281400680542, -24.459365844726564] }, { "array": [104.24951171875, -2.8328847885131838, -66.59016418457031, -22.962886810302736, 5.519782066345215, 16.50394058227539, -32.338768005371097, 5.820473670959473, -10.59586238861084, -2.7398462295532228, -12.5281400680542, -24.459365844726564] }, { "array": [95.58644104003906, -5.775191307067871, -61.220008850097659, -24.658382415771486, 4.4112701416015629, 13.673284530639649, -25.223039627075197, -2.0546646118164064, -6.887641906738281, -5.683987617492676, -11.20918083190918, -23.215322494506837] }, { "array": [98.24864196777344, -3.8367862701416017, -62.34006118774414, -24.563793182373048, 4.608433723449707, 16.228965759277345, -28.992279052734376, 2.1237001419067385, -9.07174015045166, -4.581008434295654, -10.662440299987793, -26.19581413269043] }, { "array": [102.7921142578125, -4.580304145812988, -62.531837463378909, -26.292770385742189, 7.911410331726074, 17.136384963989259, -31.118263244628908, 5.196089744567871, -10.010396957397461, -0.8527965545654297, -12.346561431884766, -23.580944061279298] }, { "array": [102.12345123291016, -1.5254135131835938, -62.21220397949219, -26.728734970092775, 10.62057876586914, 16.918357849121095, -28.815664291381837, 3.6714258193969728, -9.673786163330079, -0.7385025024414063, -9.717185020446778, -27.09702491760254] }, { "array": [102.12345123291016, -1.5254135131835938, -62.21220397949219, -26.728734970092775, 10.62057876586914, 16.918357849121095, -28.815664291381837, 3.6714258193969728, -9.673786163330079, -0.7385025024414063, -9.717185020446778, -27.09702491760254] }] }, { "name": "I", "mfccCalibrationDataList": [{ "array": [14.441835403442383, 52.400115966796878, 57.377838134765628, -33.70046615600586, -12.751934051513672, -15.709930419921875, -41.381065368652347, -12.190519332885743, -2.863154411315918, -8.727733612060547, 2.6656012535095217, 1.4855976104736329] }, { "array": [14.441835403442383, 52.400115966796878, 57.377838134765628, -33.70046615600586, -12.751934051513672, -15.709930419921875, -41.381065368652347, -12.190519332885743, -2.863154411315918, -8.727733612060547, 2.6656012535095217, 1.4855976104736329] }, { "array": [15.294279098510743, 50.07628631591797, 57.262847900390628, -31.748844146728517, -13.642471313476563, -13.48408031463623, -41.535011291503909, -16.863862991333009, -1.739903450012207, -9.32723331451416, 8.31618881225586, -1.779850959777832] }, { "array": [15.685098648071289, 49.68647766113281, 58.447113037109378, -32.513519287109378, -16.664287567138673, -13.78364372253418, -40.48309326171875, -16.04582405090332, -3.5356569290161135, -8.654275894165039, 10.645575523376465, -2.556441307067871] }, { "array": [15.685098648071289, 49.68647766113281, 58.447113037109378, -32.513519287109378, -16.664287567138673, -13.78364372253418, -40.48309326171875, -16.04582405090332, -3.5356569290161135, -8.654275894165039, 10.645575523376465, -2.556441307067871] }, { "array": [15.685098648071289, 49.68647766113281, 58.447113037109378, -32.513519287109378, -16.664287567138673, -13.78364372253418, -40.48309326171875, -16.04582405090332, -3.5356569290161135, -8.654275894165039, 10.645575523376465, -2.556441307067871] }, { "array": [15.225826263427735, 43.872196197509769, 52.844512939453128, -32.51786804199219, -17.806241989135743, -10.609650611877442, -40.13084411621094, -11.58648681640625, -5.082568168640137, -14.396997451782227, 6.896979331970215, -0.785430908203125] }, { "array": [15.571629524230957, 48.634883880615237, 59.5339469909668, -34.59955596923828, -21.959871292114259, -10.298498153686524, -39.11286926269531, -11.998537063598633, -8.433327674865723, -10.80599594116211, 8.789299011230469, -1.884697437286377] }, { "array": [15.571629524230957, 48.634883880615237, 59.5339469909668, -34.59955596923828, -21.959871292114259, -10.298498153686524, -39.11286926269531, -11.998537063598633, -8.433327674865723, -10.80599594116211, 8.789299011230469, -1.884697437286377] }, { "array": [15.571629524230957, 48.634883880615237, 59.5339469909668, -34.59955596923828, -21.959871292114259, -10.298498153686524, -39.11286926269531, -11.998537063598633, -8.433327674865723, -10.80599594116211, 8.789299011230469, -1.884697437286377] }, { "array": [19.23290252685547, 47.433998107910159, 54.90937423706055, -33.7783203125, -13.836353302001954, -5.141571044921875, -39.34584045410156, -13.409493446350098, -4.945652008056641, -12.960502624511719, 12.210061073303223, 0.5807018280029297] }, { "array": [19.124774932861329, 46.46723937988281, 53.41281509399414, -34.65093994140625, -18.181049346923829, -7.733134746551514, -45.67931365966797, -10.64135456085205, -2.624391555786133, -15.708955764770508, 7.3649187088012699, -5.627689361572266] }] }, { "name": "U", "mfccCalibrationDataList": [{ "array": [83.38372802734375, 42.39790725708008, 27.812450408935548, 10.696150779724121, -13.612553596496582, -32.487091064453128, -35.2574348449707, -6.425739288330078, -4.214997291564941, -6.896385669708252, -3.49631404876709, 4.997060775756836] }, { "array": [103.65653228759766, 38.661563873291019, 30.985050201416017, 17.432451248168947, -14.383820533752442, -39.810001373291019, -39.63761901855469, 1.2333955764770508, -4.217883110046387, -3.005303382873535, -6.272947311401367, 4.751875877380371] }, { "array": [103.44242095947266, 45.238216400146487, 27.622669219970704, 18.682138442993165, -16.854982376098634, -39.85029602050781, -34.15940856933594, -2.7482595443725588, -3.7410573959350588, -1.0625238418579102, -4.215768814086914, 6.514510154724121] }, { "array": [103.44242095947266, 45.238216400146487, 27.622669219970704, 18.682138442993165, -16.854982376098634, -39.85029602050781, -34.15940856933594, -2.7482595443725588, -3.7410573959350588, -1.0625238418579102, -4.215768814086914, 6.514510154724121] }, { "array": [99.99739837646485, 39.48998260498047, 26.057384490966798, 23.26814079284668, -16.25522804260254, -38.15496063232422, -35.70051574707031, -1.2821111679077149, -2.3941946029663088, -0.15543842315673829, -4.757769584655762, 2.198577880859375] }, { "array": [99.99739837646485, 39.48998260498047, 26.057384490966798, 23.26814079284668, -16.25522804260254, -38.15496063232422, -35.70051574707031, -1.2821111679077149, -2.3941946029663088, -0.15543842315673829, -4.757769584655762, 2.198577880859375] }, { "array": [107.09538269042969, 37.704010009765628, 17.795482635498048, 21.882326126098634, -14.739266395568848, -36.407527923583987, -37.95854949951172, -1.4393510818481446, -1.9593324661254883, -0.7294750213623047, -7.93386173248291, 3.9560585021972658] }, { "array": [103.5069351196289, 35.08988952636719, 23.21630859375, 23.947580337524415, -14.157055854797364, -38.546836853027347, -39.75208282470703, 0.516876220703125, -2.715259552001953, -4.0768208503723148, -4.716378211975098, 4.662134170532227] }, { "array": [103.5069351196289, 35.08988952636719, 23.21630859375, 23.947580337524415, -14.157055854797364, -38.546836853027347, -39.75208282470703, 0.516876220703125, -2.715259552001953, -4.0768208503723148, -4.716378211975098, 4.662134170532227] }, { "array": [103.5069351196289, 35.08988952636719, 23.21630859375, 23.947580337524415, -14.157055854797364, -38.546836853027347, -39.75208282470703, 0.516876220703125, -2.715259552001953, -4.0768208503723148, -4.716378211975098, 4.662134170532227] }, { "array": [98.43344116210938, 35.42580032348633, 29.2958984375, 24.73729133605957, -15.485936164855957, -44.676483154296878, -39.978858947753909, 0.5548343658447266, -2.2034664154052736, -3.485844612121582, -7.421210289001465, 6.30616569519043] }, { "array": [94.0390625, 36.81925582885742, 24.73573875427246, 22.579418182373048, -14.354126930236817, -37.92849349975586, -44.69046401977539, 0.7474861145019531, -3.3195743560791017, -3.9850082397460939, -5.991059303283691, 6.134122848510742] }] }, { "name": "E", "mfccCalibrationDataList": [{ "array": [60.52040481567383, 14.444153785705567, 50.91899108886719, 6.730878829956055, -58.12107467651367, -16.403745651245118, -25.244909286499025, 5.399906158447266, -7.63681697845459, -2.4964828491210939, 7.271292209625244, 1.7322711944580079] }, { "array": [59.327247619628909, 14.82846450805664, 51.402244567871097, 5.413976669311523, -55.603240966796878, -15.348665237426758, -25.923606872558595, 3.0006580352783205, -4.183259963989258, -3.4587841033935549, 7.941498756408691, 3.4499120712280275] }, { "array": [59.327247619628909, 14.82846450805664, 51.402244567871097, 5.413976669311523, -55.603240966796878, -15.348665237426758, -25.923606872558595, 3.0006580352783205, -4.183259963989258, -3.4587841033935549, 7.941498756408691, 3.4499120712280275] }, { "array": [64.61061096191406, 7.8438310623168949, 54.753726959228519, 11.154451370239258, -62.99680709838867, -10.397377967834473, -36.124359130859378, 12.57413387298584, -6.086113452911377, -3.032306671142578, 10.453157424926758, -0.00012826919555664063] }, { "array": [59.8906135559082, 13.646936416625977, 53.14240646362305, 11.346290588378907, -60.17724609375, -15.942718505859375, -29.547088623046876, 8.241331100463868, -6.8904523849487309, -3.6554131507873537, 14.714229583740235, -2.811859607696533] }, { "array": [53.77520751953125, 6.071747779846191, 47.870723724365237, 5.943275451660156, -51.11442184448242, -16.625276565551759, -24.842336654663087, 4.076478004455566, -8.835965156555176, -4.306196689605713, 13.907751083374024, -0.6555676460266113] }, { "array": [53.77520751953125, 6.071747779846191, 47.870723724365237, 5.943275451660156, -51.11442184448242, -16.625276565551759, -24.842336654663087, 4.076478004455566, -8.835965156555176, -4.306196689605713, 13.907751083374024, -0.6555676460266113] }, { "array": [53.77520751953125, 6.071747779846191, 47.870723724365237, 5.943275451660156, -51.11442184448242, -16.625276565551759, -24.842336654663087, 4.076478004455566, -8.835965156555176, -4.306196689605713, 13.907751083374024, -0.6555676460266113] }, { "array": [62.81553649902344, 6.203365325927734, 48.45057678222656, 8.571174621582032, -53.907508850097659, -16.376169204711915, -25.989578247070314, 5.736949920654297, -8.150140762329102, -5.895424842834473, 13.745902061462403, -4.22935676574707] }, { "array": [53.43303680419922, 7.019550323486328, 43.32084655761719, 7.639513969421387, -49.81471633911133, -18.708377838134767, -21.690540313720704, 0.34458446502685549, -8.689970970153809, -3.96992826461792, 11.29841423034668, -4.165286540985107] }, { "array": [56.29218673706055, 7.115049362182617, 47.741546630859378, 4.102975845336914, -50.46143341064453, -18.235626220703126, -22.557659149169923, 6.925202369689941, -13.170380592346192, -1.0300326347351075, 8.813325881958008, -3.2347850799560549] }, { "array": [56.29218673706055, 7.115049362182617, 47.741546630859378, 4.102975845336914, -50.46143341064453, -18.235626220703126, -22.557659149169923, 6.925202369689941, -13.170380592346192, -1.0300326347351075, 8.813325881958008, -3.2347850799560549] }] }, { "name": "O", "mfccCalibrationDataList": [{ "array": [108.12348937988281, 57.48288345336914, 0.23154354095458985, -31.144771575927736, -38.093109130859378, -18.33026885986328, -11.250101089477539, -0.7636222839355469, -1.371236801147461, -9.181392669677735, -1.6202507019042969, -7.501105308532715] }, { "array": [108.12348937988281, 57.48288345336914, 0.23154354095458985, -31.144771575927736, -38.093109130859378, -18.33026885986328, -11.250101089477539, -0.7636222839355469, -1.371236801147461, -9.181392669677735, -1.6202507019042969, -7.501105308532715] }, { "array": [120.48326873779297, 61.383270263671878, -2.6785545349121095, -32.3900146484375, -40.94635772705078, -12.681024551391602, -10.979912757873536, -0.7160100936889648, -2.9078426361083986, -12.300739288330079, 1.8719825744628907, -6.2853875160217289] }, { "array": [109.58128356933594, 58.46321105957031, 0.1479501724243164, -31.080230712890626, -34.73053741455078, -17.423336029052736, -10.221624374389649, 0.3863086700439453, -1.2579708099365235, -7.037452220916748, -2.894855499267578, -7.0971550941467289] }, { "array": [109.58128356933594, 58.46321105957031, 0.1479501724243164, -31.080230712890626, -34.73053741455078, -17.423336029052736, -10.221624374389649, 0.3863086700439453, -1.2579708099365235, -7.037452220916748, -2.894855499267578, -7.0971550941467289] }, { "array": [116.19681549072266, 60.3573112487793, 0.07350921630859375, -32.384727478027347, -34.47290802001953, -16.373615264892579, -14.695085525512696, 0.3102083206176758, 0.28844451904296877, -8.948881149291993, -3.104994773864746, -9.900266647338868] }, { "array": [116.19681549072266, 60.3573112487793, 0.07350921630859375, -32.384727478027347, -34.47290802001953, -16.373615264892579, -14.695085525512696, 0.3102083206176758, 0.28844451904296877, -8.948881149291993, -3.104994773864746, -9.900266647338868] }, { "array": [98.63448333740235, 48.661781311035159, 2.394181251525879, -28.785797119140626, -31.548860549926759, -18.37759017944336, -14.998208999633789, -1.8050260543823243, -2.018402099609375, -4.584748268127441, -5.160560607910156, -7.968695163726807] }, { "array": [124.25032043457031, 59.27610397338867, 2.7454710006713869, -36.72577667236328, -38.65552520751953, -3.116687774658203, -24.24558448791504, 0.5085678100585938, 2.3633852005004885, -10.51361083984375, 1.7447805404663087, -13.22685432434082] }, { "array": [87.89213562011719, 45.08750534057617, 4.292821884155273, -26.482845306396486, -30.386096954345704, -20.410654067993165, -11.817208290100098, -3.1270408630371095, -1.1370172500610352, -6.159217357635498, -3.454045295715332, -5.7265400886535648] }, { "array": [87.89213562011719, 45.08750534057617, 4.292821884155273, -26.482845306396486, -30.386096954345704, -20.410654067993165, -11.817208290100098, -3.1270408630371095, -1.1370172500610352, -6.159217357635498, -3.454045295715332, -5.7265400886535648] }, { "array": [87.89213562011719, 45.08750534057617, 4.292821884155273, -26.482845306396486, -30.386096954345704, -20.410654067993165, -11.817208290100098, -3.1270408630371095, -1.1370172500610352, -6.159217357635498, -3.454045295715332, -5.7265400886535648] }] }, { "name": "S", "mfccCalibrationDataList": [{ "array": [-94.29214477539063, 9.299236297607422, -21.6169376373291, 4.123956203460693, 1.3645498752593995, -2.339733839035034, 12.92388916015625, -7.490560531616211, 10.520170211791993, -5.4832611083984379, 2.1621110439300539, 2.6961774826049806] }, { "array": [-94.29214477539063, 9.299236297607422, -21.6169376373291, 4.123956203460693, 1.3645498752593995, -2.339733839035034, 12.92388916015625, -7.490560531616211, 10.520170211791993, -5.4832611083984379, 2.1621110439300539, 2.6961774826049806] }, { "array": [-92.13811492919922, 8.712703704833985, -17.194181442260743, 4.387561798095703, 2.288078784942627, -9.562786102294922, 9.854814529418946, -7.843216896057129, 8.969221115112305, -7.9670305252075199, 0.38271141052246096, 2.731431007385254] }, { "array": [-92.13811492919922, 8.712703704833985, -17.194181442260743, 4.387561798095703, 2.288078784942627, -9.562786102294922, 9.854814529418946, -7.843216896057129, 8.969221115112305, -7.9670305252075199, 0.38271141052246096, 2.731431007385254] }, { "array": [-90.87933349609375, 12.622742652893067, -12.25172233581543, 6.775156497955322, 6.892632007598877, -4.708589553833008, 10.558273315429688, -10.194192886352539, 10.907587051391602, 0.7259445190429688, 6.631969451904297, 1.2803831100463868] }, { "array": [-90.87933349609375, 12.622742652893067, -12.25172233581543, 6.775156497955322, 6.892632007598877, -4.708589553833008, 10.558273315429688, -10.194192886352539, 10.907587051391602, 0.7259445190429688, 6.631969451904297, 1.2803831100463868] }, { "array": [-94.86066436767578, 18.40726089477539, -5.981902599334717, 7.446142196655273, 11.998884201049805, 1.1316719055175782, 5.726372718811035, -9.411809921264649, 9.966836929321289, 0.6692547798156738, 3.0949947834014894, -0.5439543724060059] }, { "array": [-94.86066436767578, 18.40726089477539, -5.981902599334717, 7.446142196655273, 11.998884201049805, 1.1316719055175782, 5.726372718811035, -9.411809921264649, 9.966836929321289, 0.6692547798156738, 3.0949947834014894, -0.5439543724060059] }, { "array": [-94.86066436767578, 18.40726089477539, -5.981902599334717, 7.446142196655273, 11.998884201049805, 1.1316719055175782, 5.726372718811035, -9.411809921264649, 9.966836929321289, 0.6692547798156738, 3.0949947834014894, -0.5439543724060059] }, { "array": [-100.9681396484375, 15.002283096313477, -4.994745254516602, 0.22259855270385743, -0.15606117248535157, -1.8661277294158936, 1.5652005672454835, -13.30648422241211, 12.554527282714844, -2.990779399871826, 3.5510034561157228, 5.119507312774658] }, { "array": [-100.9681396484375, 15.002283096313477, -4.994745254516602, 0.22259855270385743, -0.15606117248535157, -1.8661277294158936, 1.5652005672454835, -13.30648422241211, 12.554527282714844, -2.990779399871826, 3.5510034561157228, 5.119507312774658] }, { "array": [-101.28047943115235, 14.962509155273438, -10.988410949707032, 3.6384878158569338, -1.4698257446289063, -4.758091449737549, -1.3547701835632325, -12.941855430603028, 3.3519961833953859, -5.5131611824035648, 9.386914253234864, 3.8310816287994386] }] }, { "name": "A", "mfccCalibrationDataList": [{ "array": [4.20286750793457, -73.493896484375, -24.746726989746095, -41.51460266113281, 36.48657989501953, -18.2531795501709, -42.99116516113281, 25.612823486328126, -18.336681365966798, -15.366691589355469, -4.867555618286133, -8.545194625854493] }, { "array": [6.645953178405762, -70.99688720703125, -23.49920082092285, -40.70307922363281, 35.09113311767578, -19.63579750061035, -41.851219177246097, 26.548370361328126, -20.361244201660158, -15.091900825500489, -5.332237243652344, -7.199653625488281] }, { "array": [6.645953178405762, -70.99688720703125, -23.49920082092285, -40.70307922363281, 35.09113311767578, -19.63579750061035, -41.851219177246097, 26.548370361328126, -20.361244201660158, -15.091900825500489, -5.332237243652344, -7.199653625488281] }, { "array": [3.4860363006591799, -73.30689239501953, -21.244325637817384, -38.725765228271487, 41.22803497314453, -15.975458145141602, -42.09079360961914, 27.367385864257814, -19.52243995666504, -16.397735595703126, -3.7795209884643556, -4.958502292633057] }, { "array": [0.09921550750732422, -70.22235107421875, -20.888980865478517, -33.53620910644531, 48.01523208618164, -13.365336418151856, -42.88488006591797, 26.451934814453126, -13.952343940734864, -16.65743064880371, -1.894343376159668, -1.8812918663024903] }, { "array": [0.09921550750732422, -70.22235107421875, -20.888980865478517, -33.53620910644531, 48.01523208618164, -13.365336418151856, -42.88488006591797, 26.451934814453126, -13.952343940734864, -16.65743064880371, -1.894343376159668, -1.8812918663024903] }, { "array": [0.09921550750732422, -70.22235107421875, -20.888980865478517, -33.53620910644531, 48.01523208618164, -13.365336418151856, -42.88488006591797, 26.451934814453126, -13.952343940734864, -16.65743064880371, -1.894343376159668, -1.8812918663024903] }, { "array": [-4.117016792297363, -72.00682830810547, -19.490331649780275, -34.48163986206055, 51.383995056152347, -9.989368438720704, -41.8690185546875, 23.824867248535158, -11.14547061920166, -14.500547409057618, -2.504335403442383, 1.0616645812988282] }, { "array": [-3.0772790908813478, -77.95940399169922, -21.169992446899415, -43.19057846069336, 47.594181060791019, -14.239681243896485, -43.05297088623047, 23.796558380126954, -16.033828735351564, -14.308491706848145, -4.90071964263916, -0.6597251892089844] }, { "array": [-3.0772790908813478, -77.95940399169922, -21.169992446899415, -43.19057846069336, 47.594181060791019, -14.239681243896485, -43.05297088623047, 23.796558380126954, -16.033828735351564, -14.308491706848145, -4.90071964263916, -0.6597251892089844] }, { "array": [0.27674388885498049, -74.16876220703125, -19.241043090820314, -43.69765853881836, 45.94886779785156, -15.777923583984375, -40.226318359375, 25.209468841552736, -19.91909408569336, -14.123311042785645, -6.749327659606934, -2.186051368713379] }, { "array": [1.313084602355957, -70.17343139648438, -20.149150848388673, -40.1507568359375, 43.281288146972659, -17.598236083984376, -39.989742279052737, 19.475574493408204, -18.73434066772461, -15.377893447875977, -3.6761083602905275, -2.3720903396606447] }] }, { "name": "I", "mfccCalibrationDataList": [{ "array": [-43.23860549926758, 15.659211158752442, 48.96119689941406, -84.8009033203125, -2.832998275756836, -25.101383209228517, -19.283388137817384, -1.3310365676879883, -3.9067935943603517, -5.5547566413879398, -24.206161499023439, 19.690078735351564] }, { "array": [-43.23860549926758, 15.659211158752442, 48.96119689941406, -84.8009033203125, -2.832998275756836, -25.101383209228517, -19.283388137817384, -1.3310365676879883, -3.9067935943603517, -5.5547566413879398, -24.206161499023439, 19.690078735351564] }, { "array": [-43.23860549926758, 15.659211158752442, 48.96119689941406, -84.8009033203125, -2.832998275756836, -25.101383209228517, -19.283388137817384, -1.3310365676879883, -3.9067935943603517, -5.5547566413879398, -24.206161499023439, 19.690078735351564] }, { "array": [-35.28649139404297, 11.94938850402832, 46.56689453125, -80.19161987304688, -5.332358360290527, -16.747013092041017, -17.014617919921876, -6.106320381164551, -3.5759763717651369, -11.297148704528809, -19.73563575744629, 19.95285415649414] }, { "array": [-35.28649139404297, 11.94938850402832, 46.56689453125, -80.19161987304688, -5.332358360290527, -16.747013092041017, -17.014617919921876, -6.106320381164551, -3.5759763717651369, -11.297148704528809, -19.73563575744629, 19.95285415649414] }, { "array": [-33.85117721557617, 12.748700141906739, 48.141944885253909, -75.80270385742188, -0.9702749252319336, -14.077031135559082, -15.911153793334961, -4.433165073394775, -4.00740909576416, -9.756240844726563, -20.910476684570314, 18.42697525024414] }, { "array": [-29.72378921508789, 13.039468765258789, 47.7448616027832, -74.11089324951172, 0.8275318145751953, -16.012189865112306, -17.36796760559082, -1.0376081466674805, -4.8292741775512699, -6.667880058288574, -23.82168960571289, 16.032718658447267] }, { "array": [-29.259418487548829, 11.963765144348145, 41.54622268676758, -75.67298889160156, -2.0329294204711916, -20.586360931396486, -21.125045776367189, 0.5504961013793945, -5.419882774353027, -6.314876556396484, -25.0130615234375, 14.262750625610352] }, { "array": [-29.259418487548829, 11.963765144348145, 41.54622268676758, -75.67298889160156, -2.0329294204711916, -20.586360931396486, -21.125045776367189, 0.5504961013793945, -5.419882774353027, -6.314876556396484, -25.0130615234375, 14.262750625610352] }, { "array": [-28.383831024169923, 13.103409767150879, 39.94292449951172, -81.08953857421875, -4.134577751159668, -21.590072631835939, -23.217021942138673, -0.3798789978027344, -5.0724334716796879, -5.94474983215332, -26.63843536376953, 16.777332305908204] }, { "array": [-30.048954010009767, 16.332015991210939, 44.49327850341797, -81.91828155517578, -4.171995162963867, -19.618621826171876, -20.534595489501954, -0.9673957824707031, -3.2188777923583986, -6.572293758392334, -26.59181785583496, 18.48187255859375] }, { "array": [-30.987979888916017, 14.425168991088868, 48.951114654541019, -82.33119201660156, -4.302616119384766, -17.642169952392579, -19.921981811523439, -1.7414522171020508, -1.7319145202636719, -6.870545387268066, -22.85688591003418, 19.475980758666993] }] }, { "name": "U", "mfccCalibrationDataList": [{ "array": [35.53300476074219, -11.371288299560547, 16.699298858642579, -39.32943344116211, 2.8931827545166017, -39.35669708251953, -22.81580924987793, -8.255973815917969, -7.3601884841918949, 3.866161346435547, -36.18340301513672, 0.44779300689697268] }, { "array": [34.92810821533203, -12.66100025177002, 19.551185607910158, -38.589942932128909, 1.5609407424926758, -42.781375885009769, -22.405025482177736, -4.380008220672607, -9.27183723449707, 5.952349662780762, -38.181243896484378, 1.4162702560424805] }, { "array": [34.508079528808597, -10.0599946975708, 18.88445281982422, -34.42189407348633, -0.8881950378417969, -42.52379608154297, -20.37147331237793, -0.6865062713623047, -8.89057731628418, 6.381434440612793, -34.816715240478519, 3.530543327331543] }, { "array": [34.508079528808597, -10.0599946975708, 18.88445281982422, -34.42189407348633, -0.8881950378417969, -42.52379608154297, -20.37147331237793, -0.6865062713623047, -8.89057731628418, 6.381434440612793, -34.816715240478519, 3.530543327331543] }, { "array": [37.634185791015628, -14.223724365234375, 21.262420654296876, -39.50825119018555, 0.31142520904541018, -43.201324462890628, -19.136680603027345, -0.7461652755737305, -8.253379821777344, 9.12716293334961, -33.483253479003909, 9.340311050415039] }, { "array": [38.28554916381836, -15.814170837402344, 19.503570556640626, -40.19694900512695, -0.7106151580810547, -45.373783111572269, -20.631587982177736, -2.2332963943481447, -6.609874725341797, 8.43967342376709, -33.560943603515628, 8.549976348876954] }, { "array": [38.28554916381836, -15.814170837402344, 19.503570556640626, -40.19694900512695, -0.7106151580810547, -45.373783111572269, -20.631587982177736, -2.2332963943481447, -6.609874725341797, 8.43967342376709, -33.560943603515628, 8.549976348876954] }, { "array": [36.15192413330078, -19.43944549560547, 19.22710609436035, -40.229862213134769, -1.7119722366333008, -44.58899688720703, -22.39651870727539, -4.6873579025268559, -5.184035301208496, 5.601207733154297, -35.313777923583987, 6.856324195861816] }, { "array": [33.697601318359378, -19.53946304321289, 18.151920318603517, -38.29360580444336, -1.9536991119384766, -39.185218811035159, -21.181903839111329, -8.092260360717774, -6.441320419311523, 3.8472461700439455, -32.4590950012207, 8.79825210571289] }, { "array": [31.768585205078126, -17.137840270996095, 15.726847648620606, -36.897605895996097, -3.8221397399902345, -35.492652893066409, -19.920631408691408, -10.421395301818848, -7.471479415893555, 1.7216854095458985, -25.726573944091798, 12.090950965881348] }, { "array": [31.768585205078126, -17.137840270996095, 15.726847648620606, -36.897605895996097, -3.8221397399902345, -35.492652893066409, -19.920631408691408, -10.421395301818848, -7.471479415893555, 1.7216854095458985, -25.726573944091798, 12.090950965881348] }, { "array": [32.6759033203125, -15.928326606750489, 16.0853271484375, -34.126686096191409, -6.689325332641602, -33.79350662231445, -19.242847442626954, -10.59890079498291, -10.992877006530762, -2.9957642555236818, -23.329811096191408, 14.231583595275879] }] }, { "name": "U", "mfccCalibrationDataList": [{ "array": [50.45172882080078, -7.723395347595215, 32.294891357421878, -11.293773651123047, -22.777332305908204, -36.17817687988281, -17.044910430908204, -3.7672786712646486, -14.233147621154786, -17.250513076782228, -19.240345001220704, -6.971443176269531] }, { "array": [53.59954071044922, -9.874463081359864, 35.237457275390628, -11.929043769836426, -23.915904998779298, -38.07780838012695, -15.71041202545166, -6.737283706665039, -12.06786060333252, -14.436643600463868, -20.401880264282228, -7.594654560089111] }, { "array": [57.36424255371094, -14.57245922088623, 35.10681915283203, -13.044787406921387, -24.196090698242189, -36.5896110534668, -16.161855697631837, -10.228910446166993, -10.43470287322998, -10.997936248779297, -20.92641830444336, -7.631929397583008] }, { "array": [57.65166473388672, -16.92430305480957, 33.022151947021487, -12.667181968688965, -21.69562530517578, -32.90369415283203, -14.275350570678711, -10.332601547241211, -10.544659614562989, -6.801647186279297, -22.47809410095215, -8.94782543182373] }, { "array": [49.60702133178711, -9.94367504119873, 25.04156494140625, -9.98995304107666, -21.814006805419923, -28.99759292602539, -16.792327880859376, -9.5930814743042, -11.523345947265625, -7.568509578704834, -20.688356399536134, -10.5545015335083] }, { "array": [53.433990478515628, -10.947863578796387, 30.426055908203126, -10.506410598754883, -22.997278213500978, -31.270254135131837, -16.90880584716797, -9.606002807617188, -10.785472869873047, -9.04238510131836, -22.82281494140625, -12.42250919342041] }, { "array": [57.11727523803711, -14.77857780456543, 33.16322326660156, -13.437588691711426, -25.450626373291017, -33.84945297241211, -17.6593017578125, -11.152002334594727, -12.203851699829102, -9.72322940826416, -26.653217315673829, -12.098143577575684] }, { "array": [58.65357208251953, -15.040183067321778, 32.92494583129883, -12.598053932189942, -24.03311538696289, -33.4146728515625, -15.565327644348145, -11.081777572631836, -10.47522258758545, -9.327695846557618, -28.43020248413086, -11.224303245544434] }, { "array": [53.888484954833987, -13.592279434204102, 31.6711368560791, -9.440587043762207, -23.316177368164064, -35.3663444519043, -19.26239776611328, -13.472862243652344, -11.312352180480957, -7.2335309982299809, -25.466888427734376, -8.924440383911133] }, { "array": [54.30769348144531, -13.315286636352539, 26.960655212402345, -11.682543754577637, -23.105655670166017, -32.507049560546878, -17.883333206176759, -10.961587905883789, -11.377249717712403, -7.610130786895752, -24.951541900634767, -9.38066577911377] }, { "array": [57.15829849243164, -13.771659851074219, 31.29568099975586, -9.493865966796875, -23.708837509155275, -33.85169982910156, -19.99985122680664, -12.555244445800782, -14.447962760925293, -7.608822822570801, -23.899951934814454, -6.755941867828369] }, { "array": [47.781307220458987, -14.279045104980469, 23.228328704833986, -14.190330505371094, -23.399112701416017, -34.95072555541992, -21.406070709228517, -10.15461254119873, -14.686234474182129, -9.932022094726563, -22.141719818115236, -8.354757308959961] }] }, { "name": "E", "mfccCalibrationDataList": [{ "array": [7.131360054016113, -36.049346923828128, 45.31159210205078, -45.00178527832031, -15.53347110748291, 1.3071203231811524, 2.57974910736084, -1.8762083053588868, -25.02313995361328, -22.275257110595704, -16.383546829223634, -17.868574142456056] }, { "array": [7.131360054016113, -36.049346923828128, 45.31159210205078, -45.00178527832031, -15.53347110748291, 1.3071203231811524, 2.57974910736084, -1.8762083053588868, -25.02313995361328, -22.275257110595704, -16.383546829223634, -17.868574142456056] }, { "array": [11.067873001098633, -36.16781234741211, 45.893943786621097, -43.60368347167969, -15.017866134643555, -1.2796411514282227, 2.090773582458496, -2.00726318359375, -22.139572143554689, -22.111957550048829, -16.975831985473634, -17.187711715698243] }, { "array": [9.000146865844727, -36.58183288574219, 46.39983367919922, -45.5433349609375, -15.245804786682129, -2.223395347595215, 4.547385215759277, -1.7552833557128907, -24.691539764404298, -22.836109161376954, -15.247169494628907, -15.090204238891602] }, { "array": [6.99749755859375, -37.912235260009769, 46.3832893371582, -47.186683654785159, -13.492773056030274, -2.4662675857543947, 5.109303951263428, -1.4368247985839844, -25.965919494628908, -23.327587127685548, -14.712173461914063, -15.369802474975586] }, { "array": [8.53531551361084, -38.04619598388672, 47.71860885620117, -47.188323974609378, -12.696022987365723, 0.9119815826416016, 5.812184810638428, 1.9683570861816407, -28.441104888916017, -21.203857421875, -16.284610748291017, -15.28965950012207] }, { "array": [8.53531551361084, -38.04619598388672, 47.71860885620117, -47.188323974609378, -12.696022987365723, 0.9119815826416016, 5.812184810638428, 1.9683570861816407, -28.441104888916017, -21.203857421875, -16.284610748291017, -15.28965950012207] }, { "array": [10.728363037109375, -37.86867141723633, 48.31739044189453, -47.47754669189453, -12.409613609313965, -0.4410533905029297, 5.663397789001465, 3.449146270751953, -27.80557632446289, -20.967662811279298, -15.19067096710205, -14.877350807189942] }, { "array": [10.065123558044434, -36.660072326660159, 44.76460266113281, -47.56492233276367, -12.322249412536621, -2.3014774322509767, 3.445328712463379, 3.1899805068969728, -25.734312057495118, -21.715232849121095, -13.482653617858887, -17.041053771972658] }, { "array": [8.782588958740235, -36.92534255981445, 44.4034309387207, -46.98899841308594, -8.84085464477539, -3.2711610794067385, 2.8354501724243166, 3.1592531204223635, -24.939672470092775, -21.54747772216797, -15.242938041687012, -17.53165054321289] }, { "array": [6.508709907531738, -36.80110168457031, 42.792266845703128, -46.29795837402344, -6.919498443603516, -3.788252830505371, 2.5329980850219728, 4.23353385925293, -24.539836883544923, -21.791210174560548, -16.75529670715332, -18.159324645996095] }, { "array": [6.508709907531738, -36.80110168457031, 42.792266845703128, -46.29795837402344, -6.919498443603516, -3.788252830505371, 2.5329980850219728, 4.23353385925293, -24.539836883544923, -21.791210174560548, -16.75529670715332, -18.159324645996095] }] }, { "name": "O", "mfccCalibrationDataList": [{ "array": [53.443660736083987, -12.53360652923584, -26.871780395507814, -71.98885345458985, -5.861575126647949, -1.7120800018310547, -32.90825653076172, 23.514209747314454, -10.125606536865235, -11.977684020996094, 9.883563041687012, -5.261895179748535] }, { "array": [55.69048309326172, -10.316678047180176, -26.289718627929689, -74.05561828613281, -4.348355293273926, -1.1102313995361329, -32.498268127441409, 22.016807556152345, -11.227481842041016, -11.753466606140137, 10.583023071289063, -3.583785057067871] }, { "array": [55.69048309326172, -10.316678047180176, -26.289718627929689, -74.05561828613281, -4.348355293273926, -1.1102313995361329, -32.498268127441409, 22.016807556152345, -11.227481842041016, -11.753466606140137, 10.583023071289063, -3.583785057067871] }, { "array": [56.333030700683597, -9.761429786682129, -24.525028228759767, -75.67504119873047, -2.9411144256591799, -1.0509262084960938, -32.58983612060547, 20.397789001464845, -13.52730941772461, -10.221673965454102, 8.806441307067871, -6.1672868728637699] }, { "array": [58.425559997558597, -13.048576354980469, -26.326568603515626, -77.33265686035156, 0.8971290588378906, -0.13758087158203126, -34.79779052734375, 22.524978637695314, -15.138383865356446, -9.027335166931153, 10.94324779510498, -6.867808818817139] }, { "array": [57.16611099243164, -17.380069732666017, -26.70465087890625, -76.53448486328125, 3.203751564025879, 1.6217775344848633, -36.67759323120117, 24.14405059814453, -12.522785186767579, -8.60572338104248, 13.969680786132813, -4.909186840057373] }, { "array": [55.17543029785156, -22.491680145263673, -25.568838119506837, -74.10460662841797, 2.1666202545166017, 4.1397705078125, -37.21670913696289, 21.997055053710939, -11.680967330932618, -8.396781921386719, 13.454421997070313, -3.452665328979492] }, { "array": [55.18680953979492, -23.176528930664064, -23.64011573791504, -69.64533233642578, 1.9893865585327149, 7.129931449890137, -35.871803283691409, 19.410160064697267, -13.193324089050293, -8.643393516540528, 10.173726081848145, -3.2846717834472658] }, { "array": [55.18680953979492, -23.176528930664064, -23.64011573791504, -69.64533233642578, 1.9893865585327149, 7.129931449890137, -35.871803283691409, 19.410160064697267, -13.193324089050293, -8.643393516540528, 10.173726081848145, -3.2846717834472658] }, { "array": [52.92599105834961, -22.969005584716798, -25.62136459350586, -65.79485321044922, 0.01132965087890625, 5.457650184631348, -33.96955871582031, 16.389381408691408, -14.026248931884766, -7.287093162536621, 11.159339904785157, -3.7541093826293947] }, { "array": [51.816688537597659, -22.891870498657228, -29.805912017822267, -68.66901397705078, -1.9484624862670899, 1.6389532089233399, -33.593971252441409, 17.095460891723634, -13.046170234680176, -8.923750877380371, 13.052698135375977, -5.068996429443359] }, { "array": [54.392459869384769, -23.71658706665039, -31.424976348876954, -75.520263671875, -4.738470077514648, -0.12287521362304688, -36.41456604003906, 18.125713348388673, -14.02833366394043, -11.63118839263916, 12.234237670898438, -4.546117782592773] }] }] }
 
 
src/components/friend/frontend/components/ChatInput.tsx DELETED
@@ -1,215 +0,0 @@
1
- import { useState, useRef, useCallback, useEffect } from 'react'
2
- import { Phone, PhoneOff, Mic } from 'lucide-react'
3
- import { FRIEND_API } from '../api'
4
- import { useServerStt, type SttProvider } from '../hooks/useServerStt'
5
-
6
- export function ChatInput({ uiAlign = 'right', language = 'zh', sttProvider = 'browser' }: { uiAlign?: 'left' | 'right'; language?: 'zh' | 'en'; sttProvider?: SttProvider }) {
7
- const t = (zh: string, en: string) => language === 'en' ? en : zh
8
- const [voiceCallActive, setVoiceCallActive] = useState(false)
9
- const [text, setText] = useState('')
10
- const [recording, setRecording] = useState(false)
11
- const voiceCallActiveRef = useRef(false)
12
- const serverStt = useServerStt()
13
- const displayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
14
-
15
- // --- Voice Call ---
16
- const startVoiceCall = useCallback(async () => {
17
- setVoiceCallActive(true)
18
- voiceCallActiveRef.current = true
19
- setText('')
20
-
21
- serverStt.startStreaming(
22
- sttProvider,
23
- (partialText, _isFinal) => {
24
- if (partialText.trim()) {
25
- setText(partialText)
26
- if (displayTimerRef.current) clearTimeout(displayTimerRef.current)
27
- displayTimerRef.current = setTimeout(() => {
28
- if (voiceCallActiveRef.current) {
29
- setText('')
30
- }
31
- }, 3000)
32
- }
33
- },
34
- (err) => {
35
- console.error('Voice call error:', err)
36
- if (voiceCallActiveRef.current) {
37
- setText(`\u8bed\u97f3\u542f\u52a8\u5931\u8d25: ${err}`)
38
- setTimeout(() => {
39
- setText('')
40
- endVoiceCall()
41
- }, 3000)
42
- }
43
- },
44
- language === 'en' ? 'en' : 'zh',
45
- )
46
- setRecording(true)
47
- }, [sttProvider, serverStt, language])
48
-
49
- const endVoiceCall = useCallback(async () => {
50
- if (!voiceCallActiveRef.current) return
51
- voiceCallActiveRef.current = false
52
- setVoiceCallActive(false)
53
- setRecording(false)
54
- const finalText = await serverStt.stopStreaming()
55
- if (finalText.trim()) {
56
- // Send text via chat endpoint
57
- fetch(`${FRIEND_API}/chat`, {
58
- method: 'POST',
59
- headers: { 'Content-Type': 'application/json' },
60
- body: JSON.stringify({ message: finalText }),
61
- }).catch(() => {})
62
- }
63
- setText('')
64
- }, [serverStt])
65
-
66
- // Global keyboard shortcuts
67
- useEffect(() => {
68
- const onGlobalKeyDown = (e: KeyboardEvent) => {
69
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
70
-
71
- if (e.key === 'F2') {
72
- e.preventDefault()
73
- if (voiceCallActive) {
74
- endVoiceCall()
75
- } else {
76
- startVoiceCall()
77
- }
78
- }
79
- }
80
- window.addEventListener('keydown', onGlobalKeyDown)
81
- return () => window.removeEventListener('keydown', onGlobalKeyDown)
82
- }, [voiceCallActive, startVoiceCall, endVoiceCall])
83
-
84
- // Broadcast recording state for VRM "listening" response
85
- useEffect(() => {
86
- (window as any).__userRecording = recording
87
- return () => { (window as any).__userRecording = false }
88
- }, [recording])
89
-
90
- // Cleanup display timer
91
- useEffect(() => {
92
- return () => {
93
- if (displayTimerRef.current) clearTimeout(displayTimerRef.current)
94
- }
95
- }, [])
96
-
97
- if (voiceCallActive) {
98
- return (
99
- <div style={barStyle}>
100
- <div style={{ flex: 1, position: 'relative' }}>
101
- <div
102
- style={{
103
- ...inputStyle,
104
- paddingLeft: 48,
105
- paddingRight: 80,
106
- display: 'flex',
107
- alignItems: 'center',
108
- borderColor: recording ? 'rgba(255, 80, 80, 0.7)' : 'rgba(255, 80, 80, 0.25)',
109
- }}
110
- data-no-passthrough
111
- >
112
- <span style={{ color: text ? '#fff' : 'rgba(255, 255, 255, 0.45)', fontSize: 18, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif' }}>
113
- {text || (recording ? t('正在听...', 'Listening...') : t('等待说话...', 'Waiting to speak...'))}
114
- </span>
115
- </div>
116
- <span
117
- style={{
118
- position: 'absolute',
119
- top: '50%',
120
- transform: 'translateY(-50%)',
121
- right: 44,
122
- width: 32,
123
- height: 32,
124
- display: 'flex',
125
- alignItems: 'center',
126
- justifyContent: 'center',
127
- color: recording ? 'rgba(255, 80, 80, 0.9)' : 'rgba(80, 200, 120, 0.9)',
128
- animation: recording ? 'claw-pulse 0.8s ease-in-out infinite' : 'none',
129
- transition: 'color 0.2s',
130
- cursor: 'default',
131
- }}
132
- >
133
- <Mic size={22} />
134
- </span>
135
- <button
136
- onClick={endVoiceCall}
137
- style={{
138
- position: 'absolute',
139
- top: '50%',
140
- transform: 'translateY(-50%)',
141
- right: 12,
142
- width: 32,
143
- height: 32,
144
- border: 'none',
145
- borderRadius: 4,
146
- background: 'transparent',
147
- cursor: 'pointer',
148
- display: 'flex',
149
- alignItems: 'center',
150
- justifyContent: 'center',
151
- padding: 0,
152
- color: 'rgba(255, 80, 80, 0.9)',
153
- }}
154
- title={t('挂断 (F2)', 'Hang up (F2)')}
155
- >
156
- <PhoneOff size={22} />
157
- </button>
158
- </div>
159
- </div>
160
- )
161
- }
162
-
163
- return (
164
- <div style={{ position: 'absolute', bottom: 8, ...(uiAlign === 'left' ? { left: 12 } : { right: 12 }), zIndex: 300, pointerEvents: 'auto', display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' }}>
165
- <button
166
- onClick={startVoiceCall}
167
- style={{
168
- width: 32,
169
- height: 32,
170
- border: 'none',
171
- borderRadius: 6,
172
- background: 'rgba(125, 125, 125, 0.28)',
173
- backdropFilter: 'blur(6px)',
174
- color: 'rgba(255, 255, 255, 0.8)',
175
- fontSize: 16,
176
- cursor: 'pointer',
177
- display: 'flex',
178
- alignItems: 'center',
179
- justifyContent: 'center',
180
- zIndex: 300,
181
- pointerEvents: 'auto',
182
- }}
183
- title={t('语音通话 (F2)', 'Voice Call (F2)')}
184
- >
185
- <Phone size={16} />
186
- </button>
187
- </div>
188
- )
189
- }
190
-
191
- const barStyle: React.CSSProperties = {
192
- position: 'absolute',
193
- bottom: 16,
194
- left: 12,
195
- right: 12,
196
- display: 'flex',
197
- gap: 4,
198
- zIndex: 300,
199
- pointerEvents: 'auto',
200
- }
201
-
202
- const inputStyle: React.CSSProperties = {
203
- width: '100%',
204
- height: 50,
205
- boxSizing: 'border-box',
206
- border: '1px solid rgba(255, 255, 255, 0.2)',
207
- borderRadius: 25,
208
- background: 'rgba(0, 0, 0, 0.4)',
209
- backdropFilter: 'blur(6px)',
210
- color: '#fff',
211
- fontSize: 18,
212
- padding: '0 10px',
213
- outline: 'none',
214
- fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif',
215
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/components/ResizeHandles.tsx DELETED
@@ -1,7 +0,0 @@
1
- /**
2
- * ResizeHandles — stub for Tauri window resize.
3
- * Not applicable in web mode (browser window cannot be resized from content).
4
- */
5
- export function ResizeHandles() {
6
- return null
7
- }
 
 
 
 
 
 
 
 
src/components/friend/frontend/components/SettingsPanel.tsx DELETED
@@ -1,657 +0,0 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react'
2
- import { X, Play, Loader } from 'lucide-react'
3
- import { FRIEND_API } from '../api'
4
-
5
- type SttProvider = 'browser' | 'groq' | 'anthropic' | 'local' | 'doubao'
6
-
7
- interface SettingsPanelProps {
8
- visible: boolean
9
- onClose: () => void
10
- currentModel: string
11
- onModelChange: (path: string) => void
12
- hideUI: boolean
13
- onHideUIChange: (v: boolean) => void
14
- showText: boolean
15
- onShowTextChange: (v: boolean) => void
16
- ttsEnabled: boolean
17
- onTtsEnabledChange: (v: boolean) => void
18
- tracking: 'mouse' | 'camera'
19
- onTrackingChange: (v: 'mouse' | 'camera') => void
20
- volume: number
21
- onVolumeChange: (v: number) => void
22
- uiAlign: 'left' | 'right'
23
- onUiAlignChange: (v: 'left' | 'right') => void
24
- language: 'zh' | 'en'
25
- onLanguageChange: (v: 'zh' | 'en') => void
26
- sttProvider?: SttProvider
27
- onSttProviderChange?: (v: SttProvider) => void
28
- }
29
-
30
- type Tab = 'general' | 'voice' | 'model'
31
-
32
- const BUILTIN_MODELS = ['/friend/model1.vrm', '/friend/model2.vrm', '/friend/model3.vrm', '/friend/model4.vrm', '/friend/model5.vrm']
33
-
34
- const EDGE_VOICES = [
35
- { id: 'zh-CN-XiaoxiaoNeural', label: '晓晓 (女)' },
36
- { id: 'zh-CN-XiaoyiNeural', label: '晓依 (女)' },
37
- { id: 'zh-CN-YunxiNeural', label: '云希 (男)' },
38
- { id: 'zh-CN-YunjianNeural', label: '云健 (男)' },
39
- { id: 'zh-CN-XiaohanNeural', label: '晓涵 (女)' },
40
- { id: 'zh-CN-XiaomoNeural', label: '晓墨 (女)' },
41
- { id: 'zh-CN-XiaoxuanNeural', label: '晓萱 (女)' },
42
- { id: 'zh-CN-YunyangNeural', label: '云扬 (男)' },
43
- { id: 'zh-TW-HsiaoChenNeural', label: '曉臻 (女)' },
44
- { id: 'ja-JP-NanamiNeural', label: 'Nanami (女)' },
45
- { id: 'en-US-MichelleNeural', label: 'Michelle (F)' },
46
- { id: 'en-US-GuyNeural', label: 'Guy (M)' },
47
- ]
48
-
49
- const QWEN_VOICES = [
50
- { id: 'Cherry', label: '芊悦 - 阳光亲切 (女)' },
51
- { id: 'Serena', label: '苏瑶 - 温柔 (女)' },
52
- { id: 'Ethan', label: '晨煦 - 阳光温暖 (男)' },
53
- { id: 'Chelsie', label: '千雪 - 二次元 (女)' },
54
- { id: 'Momo', label: '茉兔 - 撒娇搞怪 (女)' },
55
- { id: 'Vivian', label: '十三 - 可爱小暴躁 (女)' },
56
- { id: 'Moon', label: '月白 - 率性帅气 (男)' },
57
- { id: 'Maia', label: '四月 - 知性温柔 (女)' },
58
- { id: 'Kai', label: '凯 - 耳朵SPA (男)' },
59
- { id: 'Nofish', label: '不吃鱼 - 设计师 (男)' },
60
- { id: 'Bella', label: '萌宝 - 小萝莉 (女)' },
61
- { id: 'Mia', label: '乖小妹 - 温顺乖巧 (女)' },
62
- { id: 'Mochi', label: '沙小弥 - 童真小大人 (男)' },
63
- { id: 'Bunny', label: '萌小姬 - 萌属性 (女)' },
64
- { id: 'Nini', label: '邻家妹妹 - 软糯甜蜜 (女)' },
65
- { id: 'Stella', label: '少女阿月 - 迷糊少女 (女)' },
66
- { id: 'Pip', label: '顽屁小孩 - 调皮捣蛋 (男)' },
67
- { id: 'Neil', label: '阿闻 - 新闻主持 (男)' },
68
- { id: 'Eldric Sage', label: '沧明子 - 沉稳老者 (男)' },
69
- { id: 'Vincent', label: '田叔 - 沙哑烟嗓 (男)' },
70
- { id: 'Bellona', label: '燕铮莺 - 有声书 (女)' },
71
- { id: 'Seren', label: '小婉 - 温柔助眠 (女)' },
72
- ]
73
-
74
- const QWEN_MODELS = [
75
- { id: 'qwen3-tts-flash', label: 'Qwen3 TTS Flash' },
76
- ]
77
-
78
- export function SettingsPanel({
79
- visible, onClose, currentModel, onModelChange,
80
- hideUI, onHideUIChange,
81
- showText, onShowTextChange,
82
- ttsEnabled, onTtsEnabledChange,
83
- tracking, onTrackingChange,
84
- volume, onVolumeChange,
85
- uiAlign, onUiAlignChange,
86
- language, onLanguageChange,
87
- sttProvider = 'browser', onSttProviderChange,
88
- }: SettingsPanelProps) {
89
- const t = (zh: string, en: string) => language === 'en' ? en : zh
90
-
91
- const [tab, setTab] = useState<Tab>('general')
92
- const [currentVoice, setCurrentVoice] = useState('')
93
- const [currentProvider, setCurrentProvider] = useState<string>('edge')
94
- const [qwenKey, setQwenKey] = useState('')
95
- const [qwenModel, setQwenModel] = useState('qwen3-tts-flash')
96
- const [previewingId, setPreviewingId] = useState<string | null>(null)
97
- const audioRef = useRef<HTMLAudioElement | null>(null)
98
-
99
- // Drag state
100
- const [panelPos, setPanelPos] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
101
- const dragRef = useRef<{ dragging: boolean; startX: number; startY: number; origX: number; origY: number }>({
102
- dragging: false, startX: 0, startY: 0, origX: 0, origY: 0,
103
- })
104
-
105
- const onDragStart = useCallback((e: React.MouseEvent) => {
106
- dragRef.current = { dragging: true, startX: e.clientX, startY: e.clientY, origX: panelPos.x, origY: panelPos.y }
107
- const onMove = (ev: MouseEvent) => {
108
- if (!dragRef.current.dragging) return
109
- setPanelPos({
110
- x: dragRef.current.origX + ev.clientX - dragRef.current.startX,
111
- y: dragRef.current.origY + ev.clientY - dragRef.current.startY,
112
- })
113
- }
114
- const onUp = () => {
115
- dragRef.current.dragging = false
116
- window.removeEventListener('mousemove', onMove)
117
- window.removeEventListener('mouseup', onUp)
118
- }
119
- window.addEventListener('mousemove', onMove)
120
- window.addEventListener('mouseup', onUp)
121
- }, [panelPos])
122
-
123
- useEffect(() => {
124
- if (visible) setPanelPos({ x: 0, y: 0 })
125
- }, [visible])
126
-
127
- useEffect(() => {
128
- if (!visible) return
129
- fetch(`${FRIEND_API}/voice`)
130
- .then((r) => r.json())
131
- .then((data) => {
132
- setCurrentVoice(data.voice || '')
133
- setCurrentProvider(data.provider || 'edge')
134
- if (data.qwenKey) setQwenKey(data.qwenKey)
135
- if (data.qwenModel) setQwenModel(data.qwenModel)
136
- })
137
- .catch(() => {})
138
- }, [visible])
139
-
140
- const postVoiceSettings = (body: Record<string, string | undefined>) => {
141
- return fetch(`${FRIEND_API}/voice`, {
142
- method: 'POST',
143
- headers: { 'Content-Type': 'application/json' },
144
- body: JSON.stringify(body),
145
- })
146
- }
147
-
148
- const saveModelPath = (modelPath: string) => {
149
- fetch(`${FRIEND_API}/settings`, {
150
- method: 'POST',
151
- headers: { 'Content-Type': 'application/json' },
152
- body: JSON.stringify({ modelPath }),
153
- }).catch(() => {})
154
- }
155
-
156
- const setVoice = (voice: string) => {
157
- postVoiceSettings({ voice }).then(() => setCurrentVoice(voice)).catch(() => {})
158
- }
159
-
160
- const setProvider = (provider: string) => {
161
- postVoiceSettings({ provider }).then(() => setCurrentProvider(provider)).catch(() => {})
162
- }
163
-
164
- const saveQwenKey = (key: string) => {
165
- postVoiceSettings({ qwenKey: key }).catch(() => {})
166
- }
167
-
168
- const saveQwenModel = (model: string) => {
169
- postVoiceSettings({ qwenModel: model }).then(() => setQwenModel(model)).catch(() => {})
170
- }
171
-
172
- const stopPreview = useCallback(() => {
173
- if (audioRef.current) {
174
- audioRef.current.onended = null
175
- audioRef.current.onerror = null
176
- audioRef.current.pause()
177
- audioRef.current = null
178
- }
179
- setPreviewingId(null)
180
- }, [])
181
-
182
- const preview = (voiceId: string) => {
183
- stopPreview()
184
- setPreviewingId(voiceId)
185
- fetch(`${FRIEND_API}/preview`, {
186
- method: 'POST',
187
- headers: { 'Content-Type': 'application/json' },
188
- body: JSON.stringify({ voice: voiceId, provider: currentProvider }),
189
- })
190
- .then((r) => r.json())
191
- .then((data) => {
192
- if (data.audioUrl) {
193
- const audio = new Audio(data.audioUrl)
194
- audioRef.current = audio
195
- audio.onended = () => stopPreview()
196
- audio.onerror = () => stopPreview()
197
- audio.play().catch(() => stopPreview())
198
- } else {
199
- if (data.error) console.warn('TTS preview error:', data.error)
200
- stopPreview()
201
- }
202
- })
203
- .catch(() => stopPreview())
204
- }
205
-
206
- if (!visible) return null
207
-
208
- const voices = currentProvider === 'qwen' ? QWEN_VOICES : EDGE_VOICES
209
-
210
- return (
211
- <div style={overlayStyle} data-no-passthrough onClick={onClose}>
212
- <div style={{ ...panelStyle, transform: `translate(${panelPos.x}px, ${panelPos.y}px)` }} data-no-passthrough onClick={(e) => e.stopPropagation()}>
213
- <div style={headerStyle} onMouseDown={onDragStart}>
214
- <span style={{ fontSize: 16, fontWeight: 600, cursor: 'grab' }}>{t('设置', 'Settings')}</span>
215
- <button onClick={onClose} style={closeBtnStyle}>
216
- <X size={16} />
217
- </button>
218
- </div>
219
-
220
- {/* Tabs */}
221
- <div style={tabBarStyle}>
222
- {(['general', 'voice', 'model'] as const).map((tb) => (
223
- <button
224
- key={tb}
225
- onClick={() => setTab(tb)}
226
- style={{ ...tabStyle, ...(tab === tb ? activeTabStyle : {}) }}
227
- >
228
- {{ general: t('常规', 'General'), voice: t('语音', 'Voice'), model: t('形象', 'Model') }[tb]}
229
- </button>
230
- ))}
231
- </div>
232
-
233
- {/* Tab content */}
234
- <div style={contentStyle}>
235
- {tab === 'general' && (
236
- <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
237
- <LangToggle language={language} onChange={onLanguageChange} t={t} />
238
- <ToggleRow label={t('显示字幕', 'Subtitles')} value={showText} onChange={onShowTextChange} />
239
- <ToggleRow label={t('语音播报', 'TTS')} value={ttsEnabled} onChange={onTtsEnabledChange} />
240
- <VolumeControl volume={volume} onChange={onVolumeChange} t={t} />
241
- <TrackingControl tracking={tracking} onChange={onTrackingChange} t={t} />
242
- <UIAlignControl uiAlign={uiAlign} onChange={onUiAlignChange} t={t} />
243
- <ToggleRow label={t('隐藏UI', 'Hide UI')} value={hideUI} onChange={onHideUIChange} />
244
- </div>
245
- )}
246
-
247
- {tab === 'voice' && (
248
- <div style={sectionStyle}>
249
- <div style={labelStyle}>{t('TTS 服务', 'TTS Provider')}</div>
250
- <div style={{ display: 'flex', gap: 4 }}>
251
- {(['edge', 'qwen'] as const).map((p) => (
252
- <button
253
- key={p}
254
- onClick={() => setProvider(p)}
255
- style={{
256
- ...modelBtnStyle,
257
- flex: 1,
258
- textAlign: 'center',
259
- padding: '6px 10px',
260
- fontSize: 13,
261
- background: p === currentProvider ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)',
262
- borderColor: p === currentProvider ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
263
- }}
264
- >
265
- {{ edge: 'Edge', qwen: t('千问 TTS', 'Qwen TTS') }[p]}
266
- </button>
267
- ))}
268
- </div>
269
-
270
- {currentProvider === 'qwen' && (
271
- <div style={{ marginTop: 4, display: 'flex', flexDirection: 'column', gap: 8 }}>
272
- <div>
273
- <div style={labelStyle}>{t('阿里云 API Key', 'Alibaba Cloud API Key')}</div>
274
- <input
275
- type="text"
276
- value={qwenKey}
277
- onChange={(e) => setQwenKey(e.target.value)}
278
- onBlur={() => saveQwenKey(qwenKey)}
279
- onKeyDown={(e) => { if (e.key === 'Enter') saveQwenKey(qwenKey) }}
280
- placeholder="sk-..."
281
- style={{ ...inputStyle, width: '100%' }}
282
- />
283
- <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 4 }}>
284
- {t('从阿里云百炼控制台获取 API Key', 'Get API Key from Alibaba Cloud console')}
285
- </div>
286
- </div>
287
- <div>
288
- <div style={labelStyle}>{t('语音模型', 'Voice Model')}</div>
289
- <select
290
- value={qwenModel}
291
- onChange={(e) => { setQwenModel(e.target.value); saveQwenModel(e.target.value) }}
292
- style={selectStyle}
293
- >
294
- {QWEN_MODELS.map((m) => (
295
- <option key={m.id} value={m.id}>{m.label}</option>
296
- ))}
297
- </select>
298
- </div>
299
- </div>
300
- )}
301
-
302
- <div style={{ marginTop: 8 }}>
303
- <div style={labelStyle}>{t('语音识别 (STT)', 'Speech Recognition (STT)')}</div>
304
- <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
305
- {(['browser', 'groq', 'anthropic', 'local', 'doubao'] as const).map((p) => (
306
- <button
307
- key={p}
308
- onClick={() => onSttProviderChange?.(p)}
309
- style={{
310
- ...smallBtnStyle,
311
- flex: 1,
312
- textAlign: 'center',
313
- padding: '6px 10px',
314
- fontSize: 12,
315
- background: p === sttProvider ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)',
316
- borderColor: p === sttProvider ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
317
- }}
318
- >
319
- {{ browser: t('浏览器', 'Browser'), groq: 'Groq', anthropic: 'Anthropic', local: 'Whisper', doubao: 'Doubao' }[p]}
320
- </button>
321
- ))}
322
- </div>
323
- </div>
324
-
325
- <div style={{ marginTop: 8 }}>
326
- <div style={labelStyle}>{currentProvider === 'qwen' ? t('千问语音', 'Qwen Voice') : t('Edge TTS 语音', 'Edge TTS Voice')}</div>
327
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 200, overflowY: 'auto' }}>
328
- {voices.map((v) => (
329
- <div
330
- key={v.id}
331
- onClick={() => setVoice(v.id)}
332
- style={{
333
- ...modelBtnStyle,
334
- display: 'flex',
335
- alignItems: 'center',
336
- justifyContent: 'space-between',
337
- padding: '6px 6px 6px 10px',
338
- fontSize: 13,
339
- background: v.id === currentVoice ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)',
340
- borderColor: v.id === currentVoice ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
341
- }}
342
- >
343
- <div>
344
- <div>{v.label}</div>
345
- <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.4)', marginTop: 1 }}>{v.id}</div>
346
- </div>
347
- <div
348
- onClick={(e) => { e.stopPropagation(); preview(v.id) }}
349
- style={{
350
- width: 28,
351
- height: 28,
352
- borderRadius: 6,
353
- display: 'flex',
354
- alignItems: 'center',
355
- justifyContent: 'center',
356
- background: previewingId === v.id ? 'rgba(100, 160, 255, 0.3)' : 'rgba(255, 255, 255, 0.08)',
357
- cursor: previewingId !== null ? 'default' : 'pointer',
358
- flexShrink: 0,
359
- opacity: previewingId !== null && previewingId !== v.id ? 0.3 : 0.7,
360
- }}
361
- title={t('试听', 'Preview')}
362
- >
363
- {previewingId === v.id ? <Loader size={13} style={{ animation: 'spin 1s linear infinite' }} /> : <Play size={13} />}
364
- </div>
365
- </div>
366
- ))}
367
- </div>
368
- </div>
369
- </div>
370
- )}
371
-
372
- {tab === 'model' && (
373
- <div style={sectionStyle}>
374
- <div style={labelStyle}>{t('内置VRM模型', 'Built-in VRM Models')}</div>
375
- <select
376
- value={BUILTIN_MODELS.includes(currentModel) ? currentModel : ''}
377
- onChange={(e) => { onModelChange(e.target.value); saveModelPath(e.target.value) }}
378
- style={selectStyle}
379
- >
380
- {!BUILTIN_MODELS.includes(currentModel) && <option value="" disabled>{t('未选择', 'Not selected')}</option>}
381
- {BUILTIN_MODELS.map((m) => (
382
- <option key={m} value={m}>{m.replace(/^\//, '')}</option>
383
- ))}
384
- </select>
385
- </div>
386
- )}
387
- </div>
388
- </div>
389
- </div>
390
- )
391
- }
392
-
393
- function LangToggle({ language, onChange, t }: { language: 'zh' | 'en'; onChange: (v: 'zh' | 'en') => void; t: (zh: string, en: string) => string }) {
394
- return (
395
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
396
- <span style={{ fontSize: 14 }}>{t('语言', 'Language')}</span>
397
- <div style={{ display: 'flex', gap: 4 }}>
398
- {(['zh', 'en'] as const).map((l) => (
399
- <button
400
- key={l}
401
- onClick={() => onChange(l)}
402
- style={{
403
- ...smallBtnStyle,
404
- background: language === l ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)',
405
- borderColor: language === l ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
406
- }}
407
- >
408
- {l === 'zh' ? '中文' : 'English'}
409
- </button>
410
- ))}
411
- </div>
412
- </div>
413
- )
414
- }
415
-
416
- function VolumeControl({ volume, onChange, t }: { volume: number; onChange: (v: number) => void; t: (zh: string, en: string) => string }) {
417
- return (
418
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
419
- <span style={{ fontSize: 14 }}>{t('音量', 'Volume')}</span>
420
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
421
- <input
422
- type="range"
423
- min={0}
424
- max={100}
425
- value={Math.round(volume * 100)}
426
- onChange={(e) => onChange(Number(e.target.value) / 100)}
427
- style={{ width: 100, accentColor: 'rgba(100, 160, 255, 0.8)' }}
428
- />
429
- <span style={{ fontSize: 12, color: 'rgba(255,255,255,0.5)', width: 28, textAlign: 'right' }}>{Math.round(volume * 100)}</span>
430
- </div>
431
- </div>
432
- )
433
- }
434
-
435
- function TrackingControl({ tracking, onChange, t }: { tracking: 'mouse' | 'camera'; onChange: (v: 'mouse' | 'camera') => void; t: (zh: string, en: string) => string }) {
436
- return (
437
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
438
- <span style={{ fontSize: 14 }}>{t('视线跟随', 'Eye Tracking')}</span>
439
- <div style={{ display: 'flex', gap: 4 }}>
440
- {(['mouse', 'camera'] as const).map((m) => (
441
- <button
442
- key={m}
443
- onClick={() => onChange(m)}
444
- style={{
445
- ...smallBtnStyle,
446
- background: tracking === m ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)',
447
- borderColor: tracking === m ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
448
- }}
449
- >
450
- {m === 'mouse' ? t('鼠标', 'Mouse') : t('镜头', 'Camera')}
451
- </button>
452
- ))}
453
- </div>
454
- </div>
455
- )
456
- }
457
-
458
- function UIAlignControl({ uiAlign, onChange, t }: { uiAlign: 'left' | 'right'; onChange: (v: 'left' | 'right') => void; t: (zh: string, en: string) => string }) {
459
- return (
460
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
461
- <span style={{ fontSize: 14 }}>{t('UI位置', 'UI Position')}</span>
462
- <div style={{ display: 'flex', gap: 4 }}>
463
- {(['left', 'right'] as const).map((a) => (
464
- <button
465
- key={a}
466
- onClick={() => onChange(a)}
467
- style={{
468
- ...smallBtnStyle,
469
- background: uiAlign === a ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)',
470
- borderColor: uiAlign === a ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
471
- }}
472
- >
473
- {a === 'left' ? t('靠左', 'Left') : t('靠右', 'Right')}
474
- </button>
475
- ))}
476
- </div>
477
- </div>
478
- )
479
- }
480
-
481
- function ToggleRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
482
- return (
483
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
484
- <span style={{ fontSize: 14 }}>{label}</span>
485
- <button
486
- onClick={() => onChange(!value)}
487
- style={{
488
- ...toggleStyle,
489
- background: value ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)',
490
- }}
491
- >
492
- <div style={{
493
- ...toggleKnobStyle,
494
- transform: value ? 'translateX(18px)' : 'translateX(2px)',
495
- }} />
496
- </button>
497
- </div>
498
- )
499
- }
500
-
501
- const overlayStyle: React.CSSProperties = {
502
- position: 'fixed',
503
- top: 0,
504
- left: 0,
505
- right: 0,
506
- bottom: 0,
507
- background: 'rgba(0, 0, 0, 0.5)',
508
- zIndex: 500,
509
- display: 'flex',
510
- alignItems: 'center',
511
- justifyContent: 'center',
512
- pointerEvents: 'auto',
513
- }
514
-
515
- const panelStyle: React.CSSProperties = {
516
- width: 320,
517
- background: 'rgba(30, 30, 40, 0.95)',
518
- backdropFilter: 'blur(12px)',
519
- borderRadius: 12,
520
- border: '1px solid rgba(255, 255, 255, 0.15)',
521
- boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5)',
522
- padding: 16,
523
- color: '#fff',
524
- fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif',
525
- }
526
-
527
- const headerStyle: React.CSSProperties = {
528
- display: 'flex',
529
- justifyContent: 'space-between',
530
- alignItems: 'center',
531
- marginBottom: 12,
532
- cursor: 'grab',
533
- userSelect: 'none',
534
- }
535
-
536
- const closeBtnStyle: React.CSSProperties = {
537
- width: 28,
538
- height: 28,
539
- border: 'none',
540
- borderRadius: 6,
541
- background: 'rgba(255, 255, 255, 0.1)',
542
- color: 'rgba(255, 255, 255, 0.7)',
543
- cursor: 'pointer',
544
- display: 'flex',
545
- alignItems: 'center',
546
- justifyContent: 'center',
547
- }
548
-
549
- const tabBarStyle: React.CSSProperties = {
550
- display: 'flex',
551
- gap: 2,
552
- marginBottom: 16,
553
- background: 'rgba(255, 255, 255, 0.06)',
554
- borderRadius: 8,
555
- padding: 2,
556
- }
557
-
558
- const tabStyle: React.CSSProperties = {
559
- flex: 1,
560
- height: 32,
561
- border: 'none',
562
- borderRadius: 6,
563
- background: 'transparent',
564
- color: 'rgba(255, 255, 255, 0.6)',
565
- fontSize: 13,
566
- fontWeight: 500,
567
- cursor: 'pointer',
568
- }
569
-
570
- const activeTabStyle: React.CSSProperties = {
571
- background: 'rgba(255, 255, 255, 0.12)',
572
- color: '#fff',
573
- }
574
-
575
- const contentStyle: React.CSSProperties = {
576
- minHeight: 120,
577
- }
578
-
579
- const sectionStyle: React.CSSProperties = {
580
- display: 'flex',
581
- flexDirection: 'column',
582
- gap: 8,
583
- }
584
-
585
- const labelStyle: React.CSSProperties = {
586
- fontSize: 13,
587
- color: 'rgba(255, 255, 255, 0.6)',
588
- marginBottom: 2,
589
- }
590
-
591
- const toggleStyle: React.CSSProperties = {
592
- width: 40,
593
- height: 22,
594
- borderRadius: 11,
595
- border: 'none',
596
- cursor: 'pointer',
597
- position: 'relative',
598
- transition: 'background 0.2s',
599
- padding: 0,
600
- }
601
-
602
- const toggleKnobStyle: React.CSSProperties = {
603
- width: 18,
604
- height: 18,
605
- borderRadius: 9,
606
- background: '#fff',
607
- transition: 'transform 0.2s',
608
- position: 'absolute',
609
- top: 2,
610
- }
611
-
612
- const smallBtnStyle: React.CSSProperties = {
613
- padding: '4px 10px',
614
- border: '1px solid',
615
- borderRadius: 6,
616
- color: '#fff',
617
- fontSize: 12,
618
- cursor: 'pointer',
619
- }
620
-
621
- const modelBtnStyle: React.CSSProperties = {
622
- padding: '8px 12px',
623
- border: '1px solid',
624
- borderRadius: 8,
625
- color: '#fff',
626
- fontSize: 14,
627
- cursor: 'pointer',
628
- textAlign: 'left',
629
- }
630
-
631
- const inputStyle: React.CSSProperties = {
632
- flex: 1,
633
- height: 32,
634
- boxSizing: 'border-box',
635
- border: '1px solid rgba(255, 255, 255, 0.2)',
636
- borderRadius: 6,
637
- background: 'rgba(0, 0, 0, 0.3)',
638
- color: '#fff',
639
- fontSize: 13,
640
- padding: '0 8px',
641
- outline: 'none',
642
- fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif',
643
- }
644
-
645
- const selectStyle: React.CSSProperties = {
646
- width: '100%',
647
- height: 32,
648
- boxSizing: 'border-box',
649
- border: '1px solid rgba(255, 255, 255, 0.2)',
650
- borderRadius: 6,
651
- background: 'rgba(0, 0, 0, 0.3)',
652
- color: '#fff',
653
- fontSize: 13,
654
- padding: '0 8px',
655
- outline: 'none',
656
- fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif',
657
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/components/TextBubble.tsx DELETED
@@ -1,609 +0,0 @@
1
- import { useEffect, useState, useRef, useCallback } from 'react'
2
- import { marked } from 'marked'
3
- import { FRIEND_API } from '../api'
4
- import { LipSync } from '../lip-sync'
5
-
6
- interface VrmMessage {
7
- text?: string
8
- emotion?: string
9
- emotionDuration?: number
10
- emotionIntensity?: number
11
- duration?: number
12
- audioUrl?: string
13
- audioIndex?: number
14
- imageUrl?: string
15
- action?: string
16
- sendFirstTts?: boolean
17
- appendText?: boolean
18
- replyDone?: boolean
19
- }
20
-
21
- export type OnVrmMessage = (msg: VrmMessage) => void
22
-
23
- // CJK detection: check proportion of CJK characters in text
24
- function cjkRatio(text: string): number {
25
- const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef\u3040-\u309f\u30a0-\u30ff]/g
26
- const matches = text.match(CJK_RE)
27
- return matches ? matches.length / text.length : 0
28
- }
29
-
30
- // Dynamic rate based on CJK proportion
31
- function getCharRate(text: string, ttsEnabled: boolean): number {
32
- const ratio = cjkRatio(text)
33
- if (ttsEnabled) {
34
- // CJK: 200ms/char, English: 60ms/char, interpolate
35
- return Math.round(200 * ratio + 60 * (1 - ratio))
36
- }
37
- // CJK: 80ms/char, English: 30ms/char, interpolate
38
- return Math.round(80 * ratio + 30 * (1 - ratio))
39
- }
40
- const HIDE_DELAY_MS = 2000 // delay after everything is done before hiding
41
- const POP_DURATION_MS = 300
42
-
43
- // Grapheme segmenter singleton
44
- const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
45
-
46
- // Keyframes (claw-pop-in) are in index.html <style>
47
-
48
- export function TextBubble({ onMessage, enabled = true, ttsEnabled = true }: { onMessage?: OnVrmMessage; enabled?: boolean; ttsEnabled?: boolean }) {
49
- const [text, setText] = useState('')
50
- const [visible, setVisible] = useState(false)
51
- const [charCount, setCharCount] = useState(0)
52
- const [thinking, setThinking] = useState(false)
53
- const [imageUrl, setImageUrl] = useState<string | null>(null)
54
- const [zoomedSrc, setZoomedSrc] = useState<string | null>(null)
55
- const [renderedHtml, setRenderedHtml] = useState('')
56
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
57
- const typewriterRef = useRef<ReturnType<typeof setInterval> | null>(null)
58
- const scrollRef = useRef<HTMLDivElement>(null)
59
-
60
- // Split text into grapheme clusters
61
- const chars = useRef<string[]>([])
62
- // Current full text (for appending subsequent sentences)
63
- const fullTextRef = useRef('')
64
- // Current char rate (for restarting typewriter on appendText)
65
- const charRateRef = useRef(100)
66
- // Current charCount as a ref (for restarting typewriter from correct position)
67
- const charCountRef = useRef(0)
68
- // Pending text to reveal when a given audio index starts playing
69
- const pendingTextForAudioRef = useRef<Map<number, string>>(new Map())
70
-
71
- // Audio queue for sequential playback — keyed by index for ordering
72
- const audioQueueRef = useRef<Map<number, string>>(new Map())
73
- const audioPlayingRef = useRef<boolean>(false)
74
- const audioNextIndexRef = useRef<number>(0)
75
- const audioReceivedRef = useRef<number>(0)
76
-
77
- // === sendFirstTts queue ===
78
- // When a new sendFirstTts arrives while a previous one is still playing,
79
- // queue it instead of interrupting. Processed when the current reply finishes.
80
- const pendingSendFirstTtsRef = useRef<VrmMessage[]>([])
81
- // appendText messages buffered while sendFirstTts is queued
82
- const pendingAppendRef = useRef<VrmMessage[]>([])
83
- // replyDone: all sentences dispatched for current reply — required before hide is scheduled
84
- // Default true so non-sendFirstTts messages (plain text, images) can still schedule hide.
85
- const replyDoneRef = useRef(true)
86
- // replyDone arrived while sendFirstTts was still queued — apply after draining queue
87
- const pendingReplyDoneRef = useRef(false)
88
-
89
- // Auto-scroll to bottom on char reveal
90
- useEffect(() => {
91
- if (scrollRef.current) {
92
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight
93
- }
94
- }, [charCount])
95
-
96
- // Render markdown when all text is revealed (typewriter complete)
97
- useEffect(() => {
98
- if (charCount >= chars.current.length && chars.current.length > 0) {
99
- try {
100
- const html = marked.parse(fullTextRef.current)
101
- setRenderedHtml(typeof html === 'string' ? html : String(html))
102
- } catch {
103
- setRenderedHtml('')
104
- }
105
- } else if (!text) {
106
- setRenderedHtml('')
107
- }
108
- }, [charCount, text])
109
-
110
- const hideBubble = useCallback(() => {
111
- setVisible(false)
112
- setText('')
113
- setCharCount(0)
114
- setRenderedHtml('')
115
- setImageUrl(null)
116
- setThinking(false)
117
- chars.current = []
118
- fullTextRef.current = ''
119
- charCountRef.current = 0
120
- pendingTextForAudioRef.current.clear()
121
- pendingAppendRef.current = []
122
- replyDoneRef.current = true
123
- pendingReplyDoneRef.current = false
124
- if (typewriterRef.current) { clearInterval(typewriterRef.current); typewriterRef.current = null }
125
- if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
126
- }, [])
127
-
128
- // Ref to handleMessage — used by tryScheduleHide to drain the pending queue
129
- const handleMessageRef = useRef<(msg: VrmMessage) => void>(() => {})
130
-
131
- // Central function: check if all conditions are met to schedule hide (or drain queue)
132
- const tryScheduleHide = useCallback(() => {
133
- // Don't schedule if already scheduled
134
- if (timerRef.current) return
135
-
136
- const typewriterDone = typewriterRef.current === null
137
- const audioDone = !audioPlayingRef.current && audioQueueRef.current.size === 0
138
-
139
- if (typewriterDone && audioDone) {
140
- // Check pending sendFirstTts queue before hiding
141
- if (pendingSendFirstTtsRef.current.length > 0) {
142
- const next = pendingSendFirstTtsRef.current.shift()!
143
- handleMessageRef.current(next)
144
- // Process any buffered appendText messages for this reply
145
- const appends = pendingAppendRef.current.splice(0)
146
- for (const a of appends) handleMessageRef.current(a)
147
- // Apply replyDone that arrived while this reply was queued
148
- if (pendingReplyDoneRef.current) {
149
- pendingReplyDoneRef.current = false
150
- replyDoneRef.current = true
151
- }
152
- return
153
- }
154
- if (!replyDoneRef.current) return
155
- timerRef.current = setTimeout(hideBubble, HIDE_DELAY_MS)
156
- }
157
- }, [hideBubble])
158
-
159
- // Interrupt: stop all audio playback and clear queue
160
- const interruptAudio = useCallback(() => {
161
- const lipSync = LipSync.getInstance()
162
- lipSync.stopAudio()
163
- audioQueueRef.current.clear()
164
- audioPlayingRef.current = false
165
- audioNextIndexRef.current = 0
166
- audioReceivedRef.current = 0
167
- pendingTextForAudioRef.current.clear()
168
- if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
169
- }, [])
170
-
171
- // Expose interrupt globally for voice call to use
172
- useEffect(() => {
173
- (window as any).__clawInterruptAudio = interruptAudio
174
- return () => { delete (window as any).__clawInterruptAudio }
175
- }, [interruptAudio])
176
-
177
- const playNextAudio = useCallback(() => {
178
- if (audioPlayingRef.current) return
179
- const nextIdx = audioNextIndexRef.current
180
- const url = audioQueueRef.current.get(nextIdx)
181
- if (!url) {
182
- // Queue empty — check if we can hide
183
- tryScheduleHide()
184
- return
185
- }
186
- audioQueueRef.current.delete(nextIdx)
187
- audioNextIndexRef.current = nextIdx + 1
188
- audioPlayingRef.current = true
189
-
190
- // Reveal text for this sentence if it arrived via appendText
191
- const pendingText = pendingTextForAudioRef.current.get(nextIdx)
192
- if (pendingText !== undefined) {
193
- pendingTextForAudioRef.current.delete(nextIdx)
194
- const newGraphemes = [...segmenter.segment(pendingText)].map((s) => s.segment)
195
- chars.current = [...chars.current, ...newGraphemes]
196
- fullTextRef.current = fullTextRef.current + pendingText
197
- setText(fullTextRef.current)
198
- if (!typewriterRef.current) {
199
- // Restart typewriter from current position
200
- let idx = charCountRef.current
201
- typewriterRef.current = setInterval(() => {
202
- idx++
203
- if (idx >= chars.current.length) {
204
- setCharCount(chars.current.length)
205
- charCountRef.current = chars.current.length
206
- clearInterval(typewriterRef.current!); typewriterRef.current = null
207
- tryScheduleHide()
208
- } else {
209
- setCharCount(idx)
210
- charCountRef.current = idx
211
- }
212
- }, charRateRef.current)
213
- }
214
- // else: typewriter is running and will pick up new chars via chars.current.length
215
- }
216
-
217
- const lipSync = LipSync.getInstance()
218
- const onDone = () => {
219
- audioPlayingRef.current = false
220
- if (audioQueueRef.current.has(audioNextIndexRef.current)) {
221
- playNextAudio()
222
- } else {
223
- // No more queued audio — try to hide (will only succeed if streaming & typewriter also done)
224
- tryScheduleHide()
225
- }
226
- }
227
- lipSync.playAudio(url).then((durationMs) => {
228
- setTimeout(onDone, durationMs)
229
- }).catch(onDone)
230
- }, [tryScheduleHide])
231
-
232
- // Use refs for callback dependencies to keep handleMessage stable
233
- const onMessageRef = useRef(onMessage)
234
- onMessageRef.current = onMessage
235
- const ttsEnabledRef = useRef(ttsEnabled)
236
- ttsEnabledRef.current = ttsEnabled
237
- const playNextAudioRef = useRef(playNextAudio)
238
- playNextAudioRef.current = playNextAudio
239
- const tryScheduleHideRef = useRef(tryScheduleHide)
240
- tryScheduleHideRef.current = tryScheduleHide
241
- const hideBubbleRef = useRef(hideBubble)
242
- hideBubbleRef.current = hideBubble
243
- const thinkingRef = useRef(thinking)
244
- thinkingRef.current = thinking
245
- const visibleRef = useRef(visible)
246
- visibleRef.current = visible
247
-
248
- // Watchdog: if bubble is stuck (no audio, no typewriter, no queued work), force-hide after 30s
249
- useEffect(() => {
250
- const id = setInterval(() => {
251
- if (!visibleRef.current) return
252
- if (audioPlayingRef.current) return
253
- if (typewriterRef.current !== null) return
254
- if (audioQueueRef.current.size > 0) return
255
- if (pendingSendFirstTtsRef.current.length > 0) return
256
- if (timerRef.current !== null) return // hide already scheduled
257
- console.warn('[claw-sama] watchdog: bubble stuck, forcing hide')
258
- hideBubbleRef.current()
259
- }, 30_000)
260
- return () => clearInterval(id)
261
- }, [])
262
-
263
- // Stable handleMessage — never causes SSE reconnect
264
- const handleMessage = useCallback((msg: VrmMessage) => {
265
- // Audio-only message (legacy path, kept for compatibility)
266
- if (!msg.text && msg.audioUrl && !msg.appendText) {
267
- audioReceivedRef.current++
268
- if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
269
- const idx = msg.audioIndex ?? audioReceivedRef.current - 1
270
- audioQueueRef.current.set(idx, msg.audioUrl)
271
- playNextAudioRef.current()
272
- return
273
- }
274
-
275
- // appendText: subsequent sentence with matching audio — queue both together
276
- if (msg.appendText && msg.text) {
277
- // If sendFirstTts is queued (previous reply still playing), buffer this too
278
- if (pendingSendFirstTtsRef.current.length > 0 || audioPlayingRef.current) {
279
- if (pendingSendFirstTtsRef.current.length > 0) {
280
- pendingAppendRef.current.push(msg)
281
- return
282
- }
283
- }
284
- if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
285
- const idx = msg.audioIndex ?? 0
286
- // Store text to reveal when this audio index starts playing
287
- pendingTextForAudioRef.current.set(idx, msg.text)
288
- if (msg.audioUrl) {
289
- audioQueueRef.current.set(idx, msg.audioUrl)
290
- playNextAudioRef.current()
291
- }
292
- return
293
- }
294
-
295
- // Image-only message
296
- if (!msg.text && msg.imageUrl) {
297
- setText('')
298
- setCharCount(0)
299
- if (typewriterRef.current) { clearInterval(typewriterRef.current); typewriterRef.current = null }
300
- setImageUrl(msg.imageUrl)
301
- setVisible(true)
302
- if (timerRef.current) clearTimeout(timerRef.current)
303
- timerRef.current = setTimeout(() => hideBubbleRef.current(), 15_000)
304
- return
305
- }
306
-
307
- // Emotion-only message
308
- if (!msg.text) {
309
- if (msg.emotion) {
310
- onMessageRef.current?.({ ...msg, emotionDuration: msg.emotionDuration ?? 10000 })
311
- if (thinkingRef.current) {
312
- setThinking(false)
313
- hideBubbleRef.current()
314
- }
315
- }
316
- return
317
- }
318
-
319
- // --- Text message ---
320
-
321
- // Cancel pending hide — new text arrived
322
- if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
323
- if (typewriterRef.current) { clearInterval(typewriterRef.current); typewriterRef.current = null }
324
-
325
- // sendFirstTts: queue if something is currently playing, otherwise reset and play
326
- if (msg.sendFirstTts) {
327
- const busy = audioPlayingRef.current || typewriterRef.current !== null
328
- if (busy) {
329
- pendingSendFirstTtsRef.current.push(msg)
330
- return
331
- }
332
- replyDoneRef.current = false
333
- const lipSync = LipSync.getInstance()
334
- lipSync.stopAudio()
335
- // Preserve early-arriving audio and text for this new reply (index >= 1)
336
- const earlyAudio = new Map<number, string>()
337
- for (const [idx, url] of audioQueueRef.current) {
338
- if (idx >= 1) earlyAudio.set(idx, url)
339
- }
340
- const earlyText = new Map<number, string>()
341
- for (const [idx, t] of pendingTextForAudioRef.current) {
342
- if (idx >= 1) earlyText.set(idx, t)
343
- }
344
- audioQueueRef.current.clear()
345
- pendingTextForAudioRef.current.clear()
346
- if (msg.audioUrl) audioQueueRef.current.set(0, msg.audioUrl)
347
- for (const [idx, url] of earlyAudio) audioQueueRef.current.set(idx, url)
348
- for (const [idx, t] of earlyText) pendingTextForAudioRef.current.set(idx, t)
349
- audioPlayingRef.current = false
350
- audioNextIndexRef.current = 0
351
- audioReceivedRef.current = 0
352
- } else {
353
- audioReceivedRef.current = 0
354
- audioNextIndexRef.current = 0
355
- }
356
-
357
- const fullText = msg.text!
358
- const graphemes = [...segmenter.segment(fullText)].map((s) => s.segment)
359
- chars.current = graphemes
360
- fullTextRef.current = fullText
361
- charCountRef.current = 0
362
-
363
- setText(fullText)
364
- setThinking(false)
365
- setVisible(true)
366
- setCharCount(0)
367
-
368
- const baseRate = getCharRate(fullText, ttsEnabledRef.current)
369
- charRateRef.current = baseRate
370
-
371
- if (graphemes.length === 0) {
372
- tryScheduleHideRef.current()
373
- } else {
374
- const emotionDuration = graphemes.length * baseRate + 5000
375
- setTimeout(() => onMessageRef.current?.({ ...msg, emotionDuration }), 1000)
376
-
377
- let idx = 0
378
- typewriterRef.current = setInterval(() => {
379
- idx++
380
- if (idx >= chars.current.length) {
381
- setCharCount(chars.current.length)
382
- charCountRef.current = chars.current.length
383
- if (typewriterRef.current) { clearInterval(typewriterRef.current); typewriterRef.current = null }
384
- tryScheduleHideRef.current()
385
- } else {
386
- setCharCount(idx)
387
- charCountRef.current = idx
388
- }
389
- }, baseRate)
390
- }
391
-
392
- // All audio goes through the queue via playNextAudio.
393
- if (msg.sendFirstTts && ttsEnabledRef.current) {
394
- playNextAudioRef.current()
395
- }
396
- }, []) // stable — no deps, uses refs for everything
397
-
398
- // Keep handleMessageRef in sync so tryScheduleHide can drain the queue
399
- handleMessageRef.current = handleMessage
400
-
401
- useEffect(() => {
402
- const es = new EventSource(`${FRIEND_API}/events`)
403
- es.onmessage = (e) => {
404
- try {
405
- const data = JSON.parse(e.data)
406
- if (data.clearText) {
407
- if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null }
408
- if (typewriterRef.current) { clearInterval(typewriterRef.current); typewriterRef.current = null }
409
- setText('')
410
- setCharCount(0)
411
- setThinking(false)
412
- setVisible(false)
413
- setImageUrl(null)
414
- audioQueueRef.current.clear()
415
- audioPlayingRef.current = false
416
- audioNextIndexRef.current = 0
417
- audioReceivedRef.current = 0
418
- pendingTextForAudioRef.current.clear()
419
- pendingSendFirstTtsRef.current = []
420
- pendingAppendRef.current = []
421
- replyDoneRef.current = true
422
- pendingReplyDoneRef.current = false
423
- return
424
- }
425
- if (data.replyDone) {
426
- if (pendingSendFirstTtsRef.current.length > 0) {
427
- // sendFirstTts for this reply is still queued — buffer replyDone until queue drains
428
- pendingReplyDoneRef.current = true
429
- } else {
430
- replyDoneRef.current = true
431
- tryScheduleHideRef.current()
432
- }
433
- return
434
- }
435
- const msg: VrmMessage = data
436
- handleMessage(msg)
437
- } catch { /* ignore malformed */ }
438
- }
439
- return () => {
440
- es.close()
441
- if (timerRef.current) clearTimeout(timerRef.current)
442
- if (typewriterRef.current) clearInterval(typewriterRef.current)
443
- }
444
- }, [handleMessage])
445
-
446
- const showBubble = enabled && visible && (!!text || !!imageUrl)
447
-
448
- return (
449
- <>
450
- {showBubble && (
451
- <div style={containerStyle}>
452
- <div ref={scrollRef} style={boxStyle} data-no-passthrough>
453
- {imageUrl && (
454
- <img
455
- src={imageUrl}
456
- alt=""
457
- style={imageThumbStyle}
458
- onClick={() => setZoomedSrc(imageUrl)}
459
- onError={() => setImageUrl(null)}
460
- />
461
- )}
462
- {text && (
463
- <div style={textStyle}>
464
- {renderedHtml && charCount >= chars.current.length ? (
465
- <>
466
- <style>{`
467
- .rendered-markdown p { margin: 0.3em 0; }
468
- .rendered-markdown ul, .rendered-markdown ol { margin: 0.3em 0; padding-left: 1.5em; }
469
- .rendered-markdown li { margin: 0.1em 0; }
470
- .rendered-markdown code { background: rgba(255,255,255,0.12); padding: 1px 5px; border-radius: 4px; font-size: 0.85em; }
471
- .rendered-markdown pre { background: rgba(0,0,0,0.35); padding: 10px; border-radius: 8px; overflow-x: auto; margin: 0.4em 0; }
472
- .rendered-markdown pre code { background: none; padding: 0; border-radius: 0; }
473
- .rendered-markdown strong { color: rgba(160,200,255,0.9); }
474
- .rendered-markdown em { color: rgba(200,200,255,0.8); }
475
- .rendered-markdown h1, .rendered-markdown h2, .rendered-markdown h3, .rendered-markdown h4 { margin: 0.4em 0 0.2em; }
476
- .rendered-markdown a { color: rgba(100,160,255,0.9); text-decoration: underline; }
477
- .rendered-markdown blockquote { border-left: 3px solid rgba(100,160,255,0.5); padding-left: 10px; margin: 0.4em 0; color: rgba(255,255,255,0.7); }
478
- .rendered-markdown hr { border: none; border-top: 1px solid rgba(255,255,255,0.15); margin: 0.5em 0; }
479
- `}</style>
480
- <div dangerouslySetInnerHTML={{ __html: renderedHtml }} className="rendered-markdown" />
481
- </>
482
- ) : (
483
- chars.current.map((ch, i) => (
484
- i < charCount ? (
485
- <span key={i} style={popCharStyle}>{ch === '\n' ? <br /> : ch}</span>
486
- ) : null
487
- ))
488
- )}
489
- </div>
490
- )}
491
- </div>
492
- </div>
493
- )}
494
- {zoomedSrc && (
495
- <div style={overlayStyle} data-no-passthrough>
496
- <div style={{ position: 'relative', display: 'inline-block', maxWidth: '90%', maxHeight: '90%' }}>
497
- <img
498
- src={zoomedSrc}
499
- alt=""
500
- style={zoomedImageStyle}
501
- onError={() => setZoomedSrc(null)}
502
- />
503
- <button
504
- style={closeButtonStyle}
505
- onClick={() => setZoomedSrc(null)}
506
- >
507
-
508
- </button>
509
- </div>
510
- </div>
511
- )}
512
- </>
513
- )
514
- }
515
-
516
- const imageThumbStyle: React.CSSProperties = {
517
- maxWidth: '80%',
518
- maxHeight: 120,
519
- borderRadius: 6,
520
- border: '1px solid rgba(255, 255, 255, 0.3)',
521
- objectFit: 'contain' as const,
522
- marginBottom: 4,
523
- display: 'block',
524
- cursor: 'pointer',
525
- }
526
-
527
- const overlayStyle: React.CSSProperties = {
528
- position: 'fixed',
529
- top: 0,
530
- left: 0,
531
- right: 0,
532
- bottom: 0,
533
- background: 'rgba(0, 0, 0, 0.7)',
534
- backdropFilter: 'blur(8px)',
535
- display: 'flex',
536
- alignItems: 'center',
537
- justifyContent: 'center',
538
- zIndex: 10000,
539
- pointerEvents: 'auto',
540
- }
541
-
542
- const closeButtonStyle: React.CSSProperties = {
543
- position: 'absolute',
544
- top: 8,
545
- right: 8,
546
- width: 32,
547
- height: 32,
548
- border: 'none',
549
- borderRadius: 6,
550
- background: 'rgba(125, 125, 125, 0.28)',
551
- backdropFilter: 'blur(6px)',
552
- color: 'rgba(255, 255, 255, 0.8)',
553
- fontSize: 16,
554
- cursor: 'pointer',
555
- display: 'flex',
556
- alignItems: 'center',
557
- justifyContent: 'center',
558
- zIndex: 10001,
559
- }
560
-
561
- const zoomedImageStyle: React.CSSProperties = {
562
- maxWidth: '100%',
563
- maxHeight: '100%',
564
- borderRadius: 8,
565
- border: '1px solid rgba(255, 255, 255, 0.3)',
566
- boxShadow: '0 0 24px rgba(100, 160, 255, 0.4)',
567
- objectFit: 'contain' as const,
568
- display: 'block',
569
- }
570
-
571
- const popCharStyle: React.CSSProperties = {
572
- display: 'inline-block',
573
- animation: `claw-pop-in ${POP_DURATION_MS}ms ease-out both`,
574
- whiteSpace: 'pre',
575
- }
576
-
577
- const containerStyle: React.CSSProperties = {
578
- position: 'absolute',
579
- bottom: 80,
580
- left: 0,
581
- width: '100%',
582
- zIndex: 200,
583
- pointerEvents: 'none',
584
- padding: 8,
585
- boxSizing: 'border-box',
586
- }
587
-
588
- const boxStyle: React.CSSProperties = {
589
- background: 'rgba(0, 0, 0, 0.35)',
590
- backdropFilter: 'blur(6px)',
591
- borderRadius: 12,
592
- border: '1px solid rgba(255, 255, 255, 0.15)',
593
- boxShadow: '0 0 12px rgba(100, 160, 255, 0.25), 0 0 24px rgba(100, 160, 255, 0.1)',
594
- padding: '8px 12px',
595
- height: 140,
596
- overflowY: 'auto' as const,
597
- pointerEvents: 'auto',
598
- userSelect: 'text',
599
- cursor: 'text',
600
- }
601
-
602
- const textStyle: React.CSSProperties = {
603
- color: '#fff',
604
- fontSize: 22,
605
- lineHeight: 1.6,
606
- wordBreak: 'break-word',
607
- fontFamily: '"Segoe UI", "Microsoft YaHei", "PingFang SC", sans-serif',
608
- textShadow: '0 0 6px rgba(255,255,255,0.5)',
609
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/components/VRMScene.tsx DELETED
@@ -1,697 +0,0 @@
1
- import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react'
2
- import * as THREE from 'three'
3
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
4
- import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
5
- import { VRMLookAtQuaternionProxy } from '@pixiv/three-vrm-animation'
6
- import type { VRM } from '@pixiv/three-vrm'
7
- import { EmoteController } from '../emote'
8
- import { LipSync } from '../lip-sync'
9
- import { MotionController } from '../motion-controller'
10
-
11
- interface VRMSceneProps {
12
- modelPath: string
13
- idleAnimationPath?: string
14
- }
15
-
16
- export type TrackingMode = 'mouse' | 'camera'
17
-
18
- export interface VRMSceneHandle {
19
- setEmotion: (emotion: string, intensity?: number) => void
20
- setEmotionWithReset: (emotion: string, durationMs: number, intensity?: number) => void
21
- resetCamera: () => void
22
- setTrackingMode: (mode: TrackingMode) => void
23
- playAction: (name: string, hold?: boolean) => void
24
- panCamera: (dx: number, dy: number) => void
25
- rotateCamera: (dx: number, dy: number) => void
26
- /** Unified reset: camera + resetToIdle + expressions to zero */
27
- reset: () => void
28
- }
29
-
30
- // ── Blink state ───────────────────────────────────────────────────────────────
31
- interface BlinkState {
32
- isBlinking: boolean
33
- blinkProgress: number
34
- timeSinceLastBlink: number
35
- nextBlinkTime: number
36
- blinkDuration: number
37
- doubleBlink: boolean
38
- doubleBlinkCount: number
39
- }
40
-
41
- function createBlinkState(): BlinkState {
42
- return {
43
- isBlinking: false,
44
- blinkProgress: 0,
45
- timeSinceLastBlink: 0,
46
- nextBlinkTime: Math.random() * 5 + 2,
47
- blinkDuration: 0.1 + Math.random() * 0.08,
48
- doubleBlink: false,
49
- doubleBlinkCount: 0,
50
- }
51
- }
52
-
53
- function updateBlink(vrm: VRM, delta: number, state: BlinkState) {
54
- if (!vrm.expressionManager) return
55
-
56
- state.timeSinceLastBlink += delta
57
-
58
- if (!state.isBlinking && state.timeSinceLastBlink >= state.nextBlinkTime) {
59
- state.isBlinking = true
60
- state.blinkProgress = 0
61
- state.blinkDuration = 0.08 + Math.random() * 0.12
62
- // ~8% chance of double blink (more natural)
63
- state.doubleBlink = Math.random() < 0.08
64
- state.doubleBlinkCount = 0
65
- }
66
-
67
- if (state.isBlinking) {
68
- state.blinkProgress += delta / state.blinkDuration
69
- const blinkValue = Math.sin(Math.PI * state.blinkProgress)
70
- vrm.expressionManager.setValue('blink', blinkValue)
71
-
72
- if (state.blinkProgress >= 1) {
73
- state.blinkProgress = 0
74
- state.doubleBlinkCount++
75
-
76
- if (state.doubleBlink && state.doubleBlinkCount < 2) {
77
- // Quick reopen then re-blink
78
- vrm.expressionManager.setValue('blink', 0)
79
- state.blinkDuration = 0.06 + Math.random() * 0.06
80
- } else {
81
- state.isBlinking = false
82
- state.timeSinceLastBlink = 0
83
- vrm.expressionManager.setValue('blink', 0)
84
- state.doubleBlink = false
85
- state.nextBlinkTime = Math.random() * 6 + 1.5
86
- }
87
- }
88
- }
89
- }
90
-
91
- // ── Relaxed hand pose ─────────────────────────────────────────────────────────
92
-
93
- interface HandPoseCache {
94
- bones: { bone: THREE.Object3D; z: number; y: number }[]
95
- }
96
-
97
- function buildHandPoseCache(vrm: VRM): HandPoseCache {
98
- const humanoid = vrm.humanoid
99
- const bones: HandPoseCache['bones'] = []
100
- if (!humanoid) return { bones }
101
-
102
- const fingers = ['Thumb', 'Index', 'Middle', 'Ring', 'Little'] as const
103
- const segments = ['Proximal', 'Intermediate', 'Distal'] as const
104
- const sides = ['left', 'right'] as const
105
-
106
- const curlMap: Record<string, [number, number, number]> = {
107
- Thumb: [0.25, 0.15, 0.10],
108
- Index: [0.20, 0.30, 0.20],
109
- Middle: [0.25, 0.35, 0.25],
110
- Ring: [0.30, 0.40, 0.30],
111
- Little: [0.35, 0.45, 0.30],
112
- }
113
-
114
- const spreadMap: Record<string, number> = {
115
- Thumb: 0.15,
116
- Index: 0.04,
117
- Middle: 0.0,
118
- Ring: -0.04,
119
- Little: -0.08,
120
- }
121
-
122
- for (const side of sides) {
123
- const sign = side === 'left' ? 1 : -1
124
-
125
- for (const finger of fingers) {
126
- const curls = curlMap[finger]
127
- const spread = spreadMap[finger]
128
-
129
- for (let s = 0; s < segments.length; s++) {
130
- const boneName = `${side}${finger}${segments[s]}` as any
131
- const bone = humanoid.getNormalizedBoneNode(boneName)
132
- if (!bone) continue
133
-
134
- const z = sign * curls[s]
135
- const y = s === 0 ? sign * spread : 0
136
-
137
- bones.push({ bone, z, y })
138
- }
139
- }
140
- }
141
-
142
- return { bones }
143
- }
144
-
145
- function applyRelaxedHandPose(cache: HandPoseCache, time: number) {
146
- // Slow hand tension cycle (~30s period): hands subtly change posture over time
147
- const tensionCycle = 0.5 + 0.5 * Math.sin(time * 0.035)
148
- const zScale = 0.85 + tensionCycle * 0.3 // 0.85-1.15
149
-
150
- for (const { bone, z, y } of cache.bones) {
151
- const freq = 0.3 + Math.abs(z) * 2
152
- const micro = Math.sin(time * freq + z * 50) * 0.02
153
- bone.rotation.z = z * zScale + micro
154
- if (y !== 0) bone.rotation.y = y
155
- }
156
- }
157
-
158
- export const VRMScene = forwardRef<VRMSceneHandle, VRMSceneProps>(function VRMScene({
159
- modelPath,
160
- idleAnimationPath = '/friend/idle_loop.vrma',
161
- }, ref) {
162
- const canvasRef = useRef<HTMLCanvasElement>(null)
163
-
164
- const emoteRef = useRef<EmoteController | null>(null)
165
- const resetCameraRef = useRef<(() => void) | null>(null)
166
- const trackingModeRef = useRef<TrackingMode>('mouse')
167
- const motionRef = useRef<MotionController | null>(null)
168
- const panCameraRef = useRef<((dx: number, dy: number) => void) | null>(null)
169
- const rotateCameraRef = useRef<((dx: number, dy: number) => void) | null>(null)
170
- const lipSyncRef = useRef<LipSync>(LipSync.getInstance())
171
-
172
- useImperativeHandle(ref, () => ({
173
- setEmotion(emotion: string, intensity?: number) {
174
- emoteRef.current?.setEmotion(emotion, intensity)
175
- },
176
- setEmotionWithReset(emotion: string, durationMs: number, intensity?: number) {
177
- emoteRef.current?.setEmotionWithReset(emotion, durationMs, intensity)
178
- },
179
- resetCamera() {
180
- resetCameraRef.current?.()
181
- },
182
- setTrackingMode(mode: TrackingMode) {
183
- trackingModeRef.current = mode
184
- },
185
- playAction(name: string, hold?: boolean) {
186
- motionRef.current?.playAction(name, hold)
187
- },
188
- panCamera(dx: number, dy: number) {
189
- panCameraRef.current?.(dx, dy)
190
- },
191
- rotateCamera(dx: number, dy: number) {
192
- rotateCameraRef.current?.(dx, dy)
193
- },
194
- reset() {
195
- resetCameraRef.current?.()
196
- motionRef.current?.resetToIdle()
197
- emoteRef.current?.resetAll()
198
- },
199
- }))
200
-
201
- useEffect(() => {
202
- const canvas = canvasRef.current
203
- if (!canvas) return
204
-
205
- // ── Renderer ──────────────────────────────────────────────────────────────
206
- const renderer = new THREE.WebGLRenderer({
207
- canvas,
208
- alpha: true,
209
- antialias: true,
210
- preserveDrawingBuffer: true,
211
- })
212
- renderer.setSize(window.innerWidth, window.innerHeight)
213
- renderer.setPixelRatio(window.devicePixelRatio)
214
- renderer.setClearColor(0x000000, 0)
215
-
216
- // ── Scene ─────────────────────────────────────────────────────────────────
217
- const scene = new THREE.Scene()
218
-
219
- // ── Camera ────────────────────────────────────────────────────────────────
220
- const FOV = 40
221
- const camera = new THREE.PerspectiveCamera(
222
- FOV,
223
- window.innerWidth / window.innerHeight,
224
- 0.1,
225
- 100,
226
- )
227
- const pivot = new THREE.Vector3(0, 0, 0)
228
- let orbitRadius = 2.0
229
- let orbitTheta = 0
230
- let orbitPhi = Math.PI / 2
231
-
232
- function updateCameraOrbit() {
233
- camera.position.set(
234
- pivot.x + orbitRadius * Math.sin(orbitPhi) * Math.sin(orbitTheta),
235
- pivot.y + orbitRadius * Math.cos(orbitPhi),
236
- pivot.z + orbitRadius * Math.sin(orbitPhi) * Math.cos(orbitTheta),
237
- )
238
- camera.lookAt(pivot)
239
- }
240
- updateCameraOrbit()
241
-
242
- // ── Lights ────────────────────────────────────────────────────────────────
243
- scene.add(new THREE.AmbientLight(0xffffff, 0.6))
244
- const dirLight = new THREE.DirectionalLight(0xffffff, 1.2)
245
- dirLight.position.set(1, 2, 3)
246
- scene.add(dirLight)
247
- const fillLight = new THREE.DirectionalLight(0xffffff, 0.4)
248
- fillLight.position.set(-2, 1, -1)
249
- scene.add(fillLight)
250
-
251
- // ── Loader ───────────────────────────────────────────────────────────────
252
- const loader = new GLTFLoader()
253
- loader.register((parser) => new VRMLoaderPlugin(parser))
254
-
255
- // ── State ─────────────────────────────────────────────────────────────────
256
- let vrm: VRM | null = null
257
- let motion: MotionController | null = null
258
- let emote: EmoteController | null = null
259
- let handPose: HandPoseCache | null = null
260
- const blinkState = createBlinkState()
261
- const saccades = new EyeSaccadeController()
262
- const lookAtTarget = { x: 0, y: 0, z: -100 }
263
-
264
- // ── Aliveness system state: breathing, sway, speech micro-movements ─────
265
- let alivenessBones = { chestBone: null as THREE.Object3D | null, spineBone: null as THREE.Object3D | null, neckBone: null as THREE.Object3D | null }
266
- let breathPhase = Math.random() * Math.PI * 2
267
- let swayPhase = Math.random() * Math.PI * 2
268
- let speechBlend = 0
269
- let userListenBlend = 0
270
- // Micro-expression state
271
- let microTimer = 5 + Math.random() * 10
272
- let microActive = false
273
- let microPhase = 0
274
- let microShape = ''
275
-
276
- // ── Load VRM model, then load idle animation ─────────────────────────────
277
- loader.load(
278
- modelPath,
279
- async (gltf) => {
280
- const loadedVrm = gltf.userData.vrm as VRM
281
- if (!loadedVrm) {
282
- console.error('No VRM data found in GLTF')
283
- return
284
- }
285
-
286
- VRMUtils.removeUnnecessaryVertices(loadedVrm.scene)
287
- VRMUtils.combineSkeletons(loadedVrm.scene)
288
- loadedVrm.scene.traverse((obj) => {
289
- obj.frustumCulled = false
290
- })
291
-
292
- if (loadedVrm.lookAt) {
293
- const lookAtQuatProxy = new VRMLookAtQuaternionProxy(loadedVrm.lookAt)
294
- lookAtQuatProxy.name = 'lookAtQuaternionProxy'
295
- loadedVrm.scene.add(lookAtQuatProxy)
296
- }
297
-
298
- VRMUtils.rotateVRM0(loadedVrm)
299
-
300
- scene.add(loadedVrm.scene)
301
- vrm = loadedVrm
302
-
303
- // ── Compute camera from model bounds ───────────────────
304
- const box = new THREE.Box3().setFromObject(loadedVrm.scene)
305
- const modelSize = new THREE.Vector3()
306
- const modelCenter = new THREE.Vector3()
307
- box.getSize(modelSize)
308
- box.getCenter(modelCenter)
309
- modelCenter.y += modelSize.y / 3.2
310
-
311
- const radians = (FOV / 2 * Math.PI) / 180
312
- const offsetX = modelSize.x / 16
313
- const offsetY = modelSize.y / 10
314
- const offsetZ = (modelSize.y / 4.2) / Math.tan(radians)
315
-
316
- pivot.copy(modelCenter)
317
- orbitRadius = offsetZ
318
- orbitTheta = Math.atan2(offsetX, offsetZ)
319
- orbitPhi = Math.PI / 2 - Math.atan2(offsetY, offsetZ)
320
- updateCameraOrbit()
321
-
322
- const initPivot = pivot.clone()
323
- const initRadius = orbitRadius
324
- const initTheta = orbitTheta
325
- const initPhi = orbitPhi
326
- resetCameraRef.current = () => {
327
- pivot.copy(initPivot)
328
- orbitRadius = initRadius
329
- orbitTheta = initTheta
330
- orbitPhi = initPhi
331
- updateCameraOrbit()
332
- }
333
-
334
- panCameraRef.current = (dx: number, dy: number) => {
335
- const right = new THREE.Vector3()
336
- const up = new THREE.Vector3()
337
- camera.getWorldDirection(new THREE.Vector3())
338
- right.setFromMatrixColumn(camera.matrixWorld, 0)
339
- up.setFromMatrixColumn(camera.matrixWorld, 1)
340
- pivot.addScaledVector(right, -dx * 0.003)
341
- pivot.addScaledVector(up, dy * 0.003)
342
- updateCameraOrbit()
343
- }
344
-
345
- rotateCameraRef.current = (dx: number, dy: number) => {
346
- orbitTheta -= dx * 0.005
347
- orbitPhi = THREE.MathUtils.clamp(
348
- orbitPhi - dy * 0.005,
349
- 0.1,
350
- Math.PI - 0.1,
351
- )
352
- updateCameraOrbit()
353
- }
354
-
355
- handPose = buildHandPoseCache(loadedVrm)
356
-
357
- // ── Initialize aliveness bone references ──
358
- const h = loadedVrm.humanoid
359
- alivenessBones = {
360
- chestBone: h?.getNormalizedBoneNode('chest') ?? null,
361
- spineBone: h?.getNormalizedBoneNode('spine') ?? null,
362
- neckBone: h?.getNormalizedBoneNode('neck') ?? null,
363
- }
364
-
365
- emote = new EmoteController(loadedVrm)
366
- emoteRef.current = emote
367
-
368
- motion = new MotionController(loadedVrm)
369
- motionRef.current = motion
370
-
371
- motion.loadIdle(idleAnimationPath).catch((err) =>
372
- console.warn('Failed to load idle animation:', err),
373
- )
374
-
375
- loadedVrm.springBoneManager?.reset()
376
- },
377
- () => {},
378
- (err) => {
379
- console.error('Failed to load VRM:', err)
380
- },
381
- )
382
-
383
- // ── Mouse tracking ────────────────────────────────────────────────────────
384
- const mouse = new THREE.Vector2(0, 0)
385
- const _raycaster = new THREE.Raycaster()
386
- const _mouseVec = new THREE.Vector2()
387
-
388
- function onMouseMove(e: MouseEvent) {
389
- mouse.x = (e.clientX / window.innerWidth) * 2 - 1
390
- mouse.y = -(e.clientY / window.innerHeight) * 2 + 1
391
-
392
- if (trackingModeRef.current !== 'mouse') return
393
-
394
- _mouseVec.set(mouse.x, mouse.y)
395
- _raycaster.setFromCamera(_mouseVec, camera)
396
- const camDir = new THREE.Vector3()
397
- camera.getWorldDirection(camDir)
398
- const plane = new THREE.Plane()
399
- plane.setFromNormalAndCoplanarPoint(
400
- camDir,
401
- camera.position.clone().add(camDir.multiplyScalar(1)),
402
- )
403
- const intersection = new THREE.Vector3()
404
- if (_raycaster.ray.intersectPlane(plane, intersection)) {
405
- lookAtTarget.x = intersection.x
406
- lookAtTarget.y = intersection.y
407
- lookAtTarget.z = intersection.z
408
- if (vrm) {
409
- saccades.instantUpdate(vrm, lookAtTarget)
410
- }
411
- }
412
- }
413
- window.addEventListener('mousemove', onMouseMove)
414
- document.addEventListener('mousemove', onMouseMove)
415
-
416
- // ── Scroll zoom ──────────────────────────────────────────────────────────
417
- const MIN_RADIUS = 0.8
418
- const MAX_RADIUS = 5.0
419
- const ZOOM_SPEED = 0.002
420
-
421
- function onWheel(e: WheelEvent) {
422
- e.preventDefault()
423
- orbitRadius = THREE.MathUtils.clamp(
424
- orbitRadius + e.deltaY * ZOOM_SPEED,
425
- MIN_RADIUS,
426
- MAX_RADIUS,
427
- )
428
- updateCameraOrbit()
429
- }
430
- canvas.addEventListener('wheel', onWheel, { passive: false })
431
-
432
- // ── Camera drag controls ──────────────────────────────────────────────
433
- let dragMode: 'rotate' | 'dolly' | null = null
434
- let prevX = 0
435
- let prevY = 0
436
- const ROTATE_SPEED = 0.005
437
- const DOLLY_SPEED = 0.01
438
-
439
- function onPointerDown(e: PointerEvent) {
440
- if (e.button === 1) {
441
- dragMode = 'dolly'
442
- e.preventDefault()
443
- } else if (e.button === 2) {
444
- dragMode = 'rotate'
445
- } else {
446
- return
447
- }
448
- prevX = e.clientX
449
- prevY = e.clientY
450
- canvas!.setPointerCapture(e.pointerId)
451
- }
452
-
453
- function onPointerMove(e: PointerEvent) {
454
- if (!dragMode) return
455
- const dx = e.clientX - prevX
456
- const dy = e.clientY - prevY
457
- prevX = e.clientX
458
- prevY = e.clientY
459
-
460
- if (dragMode === 'rotate') {
461
- orbitTheta -= dx * ROTATE_SPEED
462
- orbitPhi = THREE.MathUtils.clamp(
463
- orbitPhi - dy * ROTATE_SPEED,
464
- 0.1,
465
- Math.PI - 0.1,
466
- )
467
- } else if (dragMode === 'dolly') {
468
- orbitRadius = THREE.MathUtils.clamp(
469
- orbitRadius + dy * DOLLY_SPEED,
470
- MIN_RADIUS,
471
- MAX_RADIUS,
472
- )
473
- }
474
- updateCameraOrbit()
475
- }
476
-
477
- function onPointerUp() {
478
- if (dragMode) {
479
- dragMode = null
480
- }
481
- }
482
-
483
- function onContextMenu(e: Event) {
484
- e.preventDefault()
485
- }
486
-
487
- canvas.addEventListener('pointerdown', onPointerDown)
488
- canvas.addEventListener('pointermove', onPointerMove)
489
- canvas.addEventListener('pointerup', onPointerUp)
490
- canvas.addEventListener('contextmenu', onContextMenu)
491
-
492
- // ── Resize ────────────────────────────────────────────────────────────────
493
- function onResize() {
494
- camera.aspect = window.innerWidth / window.innerHeight
495
- camera.updateProjectionMatrix()
496
- renderer.setSize(window.innerWidth, window.innerHeight)
497
- }
498
- window.addEventListener('resize', onResize)
499
-
500
- // ── Animation loop ────────────────────────────────────────────────────────
501
- const clock = new THREE.Clock()
502
- let animFrameId: number
503
-
504
- function animate() {
505
- animFrameId = requestAnimationFrame(animate)
506
- const delta = clock.getDelta()
507
-
508
- if (vrm) {
509
- motion?.update(delta)
510
-
511
- if (handPose) applyRelaxedHandPose(handPose, clock.elapsedTime)
512
-
513
- vrm.humanoid?.update()
514
-
515
- if (trackingModeRef.current === 'camera') {
516
- lookAtTarget.x = camera.position.x
517
- lookAtTarget.y = camera.position.y
518
- lookAtTarget.z = camera.position.z
519
- saccades.instantUpdate(vrm, lookAtTarget)
520
- }
521
-
522
- vrm.lookAt?.update(delta)
523
- saccades.update(vrm, lookAtTarget, delta)
524
- updateBlink(vrm, delta, blinkState)
525
- emote?.update(delta)
526
- lipSyncRef.current.update(vrm, delta)
527
-
528
- // ── Aliveness: micro-expressions (asymmetric blink, subtle morphs) ──
529
- microTimer -= delta
530
- if (microTimer <= 0 && !microActive) {
531
- microShape = Math.random() > 0.5 ? 'blinkLeft' : 'blinkRight'
532
- microActive = true
533
- microPhase = 0
534
- }
535
- if (microActive && vrm.expressionManager) {
536
- microPhase += delta * 4
537
- const val = Math.sin(Math.PI * Math.min(microPhase, 1))
538
- vrm.expressionManager.setValue(microShape, val * 0.3)
539
- if (microPhase >= 2) {
540
- microActive = false
541
- vrm.expressionManager.setValue(microShape, 0)
542
- microTimer = 8 + Math.random() * 16
543
- }
544
- }
545
-
546
- vrm.expressionManager?.update()
547
- vrm.springBoneManager?.update(delta)
548
-
549
- // ── Aliveness: breathing (chest rise/fall, post-mixer) ──
550
- // Breathing rate varies naturally, amplitude tuned for VRM scale
551
- const breathRate = 1.8 + Math.sin(breathPhase * 0.05) * 0.4 + Math.sin(clock.elapsedTime * 0.1) * 0.3
552
- breathPhase += delta * breathRate
553
- const breathVal = Math.sin(breathPhase) * 0.006
554
- if (alivenessBones.chestBone) {
555
- alivenessBones.chestBone.position.y += breathVal
556
- }
557
-
558
- // ── Aliveness: postural sway ──
559
- swayPhase += delta * 0.35
560
- const swayZ = Math.sin(swayPhase) * 0.005
561
- if (alivenessBones.spineBone) {
562
- alivenessBones.spineBone.rotation.z += swayZ
563
- }
564
-
565
- // ── Aliveness: speech-driven micro-movements ──
566
- const isSpeaking = lipSyncRef.current.isActive()
567
- const targetBlend = isSpeaking ? 1 : 0
568
- speechBlend += (targetBlend - speechBlend) * Math.min(1, delta * 3)
569
- if (speechBlend > 0.01) {
570
- const t = clock.elapsedTime
571
- const headX = Math.sin(t * 3.7 + 1.2) * 0.02 * speechBlend
572
- const headZ = Math.sin(t * 2.3 + 0.7) * 0.015 * speechBlend
573
- const headY = Math.sin(t * 1.5 + 3.8) * 0.008 * speechBlend // slight rotation (looking around while talking)
574
- const spineSway = Math.sin(t * 1.8 + 0.3) * 0.006 * speechBlend
575
- if (alivenessBones.neckBone) {
576
- alivenessBones.neckBone.rotation.x += headX
577
- alivenessBones.neckBone.rotation.z += headZ
578
- alivenessBones.neckBone.rotation.y += headY
579
- }
580
- if (alivenessBones.spineBone) {
581
- alivenessBones.spineBone.rotation.x += spineSway
582
- }
583
- }
584
-
585
- // ── Aliveness: listening response when user is speaking ──
586
- const isUserSpeaking = !!(window as any).__userRecording
587
- userListenBlend += ((isUserSpeaking ? 1 : 0) - userListenBlend) * Math.min(1, delta * 2)
588
- if (userListenBlend > 0.01) {
589
- const t = clock.elapsedTime
590
- const listenTilt = Math.sin(t * 0.9 + 0.5) * 0.012 * userListenBlend // head tilt
591
- const listenNod = Math.sin(t * 2.3 + 3.1) * 0.008 * userListenBlend // subtle nodding
592
- if (alivenessBones.neckBone) {
593
- alivenessBones.neckBone.rotation.z += listenTilt
594
- alivenessBones.neckBone.rotation.x += listenNod
595
- }
596
- }
597
- }
598
-
599
- renderer.render(scene, camera)
600
- }
601
-
602
- animate()
603
-
604
- // ── Cleanup ───────────────────────────────────────────────────────────────
605
- return () => {
606
- cancelAnimationFrame(animFrameId)
607
- window.removeEventListener('mousemove', onMouseMove)
608
- document.removeEventListener('mousemove', onMouseMove)
609
- canvas.removeEventListener('wheel', onWheel)
610
- canvas.removeEventListener('pointerdown', onPointerDown)
611
- canvas.removeEventListener('pointermove', onPointerMove)
612
- canvas.removeEventListener('pointerup', onPointerUp)
613
- canvas.removeEventListener('contextmenu', onContextMenu)
614
- window.removeEventListener('resize', onResize)
615
- emote?.dispose()
616
- emoteRef.current = null
617
- motion?.dispose()
618
- motionRef.current = null
619
- delete (window as any).__clawHitTest
620
- renderer.dispose()
621
- }
622
- }, [modelPath, idleAnimationPath])
623
-
624
- return (
625
- <div style={{ position: 'relative', width: '100%', height: '100%' }}>
626
- <canvas
627
- ref={canvasRef}
628
- style={{
629
- display: 'block',
630
- width: '100%',
631
- height: '100%',
632
- background: 'transparent',
633
- cursor: 'grab',
634
- }}
635
- />
636
- </div>
637
- )
638
- })
639
-
640
- // ── Eye saccade interval ─────────────────────────────────────────
641
- const EYE_SACCADE_INT_STEP = 400
642
- const EYE_SACCADE_INT_P: number[][] = [
643
- [0.075, 800], [0.110, 0], [0.125, 0], [0.140, 0], [0.125, 0],
644
- [0.050, 0], [0.040, 0], [0.030, 0], [0.020, 0], [1.000, 0],
645
- ]
646
- for (let i = 1; i < EYE_SACCADE_INT_P.length; i++) {
647
- EYE_SACCADE_INT_P[i][0] += EYE_SACCADE_INT_P[i - 1][0]
648
- EYE_SACCADE_INT_P[i][1] = EYE_SACCADE_INT_P[i - 1][1] + EYE_SACCADE_INT_STEP
649
- }
650
-
651
- function randomSaccadeInterval(): number {
652
- const r = Math.random()
653
- for (let i = 0; i < EYE_SACCADE_INT_P.length; i++) {
654
- if (r <= EYE_SACCADE_INT_P[i][0]) {
655
- return EYE_SACCADE_INT_P[i][1] + Math.random() * EYE_SACCADE_INT_STEP
656
- }
657
- }
658
- return EYE_SACCADE_INT_P[EYE_SACCADE_INT_P.length - 1][1] + Math.random() * EYE_SACCADE_INT_STEP
659
- }
660
-
661
- class EyeSaccadeController {
662
- private nextSaccadeAfter = -1
663
- private timeSinceLastSaccade = 0
664
- private fixationTarget = new THREE.Vector3()
665
-
666
- instantUpdate(vrm: VRM, target: { x: number; y: number; z: number }) {
667
- this.fixationTarget.set(target.x, target.y, target.z)
668
- if (!vrm.lookAt) return
669
- if (!vrm.lookAt.target) {
670
- vrm.lookAt.target = new THREE.Object3D()
671
- }
672
- vrm.lookAt.target.position.copy(this.fixationTarget)
673
- vrm.lookAt.update(0.016)
674
- }
675
-
676
- update(vrm: VRM, lookAtTarget: { x: number; y: number; z: number }, delta: number) {
677
- if (!vrm.expressionManager || !vrm.lookAt) return
678
-
679
- if (this.timeSinceLastSaccade >= this.nextSaccadeAfter) {
680
- this.fixationTarget.set(
681
- lookAtTarget.x + THREE.MathUtils.randFloat(-0.25, 0.25),
682
- lookAtTarget.y + THREE.MathUtils.randFloat(-0.25, 0.25),
683
- lookAtTarget.z,
684
- )
685
- this.timeSinceLastSaccade = 0
686
- this.nextSaccadeAfter = randomSaccadeInterval() / 1000
687
- }
688
-
689
- if (!vrm.lookAt.target) {
690
- vrm.lookAt.target = new THREE.Object3D()
691
- }
692
- vrm.lookAt.target.position.lerp(this.fixationTarget, 1)
693
- vrm.lookAt.update(delta)
694
-
695
- this.timeSinceLastSaccade += delta
696
- }
697
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/emote.ts DELETED
@@ -1,206 +0,0 @@
1
- import type { VRM } from '@pixiv/three-vrm'
2
-
3
- interface EmotionExpr {
4
- name: string
5
- value: number
6
- }
7
-
8
- interface EmotionState {
9
- expression: EmotionExpr[]
10
- blendDuration: number
11
- }
12
-
13
- const emotionStates = new Map<string, EmotionState>([
14
- ['happy', {
15
- expression: [
16
- { name: 'happy', value: 0.2 },
17
- { name: 'aa', value: 0.8 },
18
- ],
19
- blendDuration: 0.4,
20
- }],
21
- ['sad', {
22
- expression: [
23
- { name: 'sad', value: 0.7 },
24
- { name: 'oh', value: 0.15 },
25
- ],
26
- blendDuration: 0.4,
27
- }],
28
- ['angry', {
29
- expression: [
30
- { name: 'angry', value: 0.7 },
31
- { name: 'ee', value: 0.3 },
32
- ],
33
- blendDuration: 0.3,
34
- }],
35
- ['surprised', {
36
- expression: [
37
- { name: 'surprised', value: 0.8 },
38
- { name: 'oh', value: 0.4 },
39
- ],
40
- blendDuration: 0.15,
41
- }],
42
- ['think', {
43
- expression: [
44
- { name: 'think', value: 0.7 },
45
- ],
46
- blendDuration: 0.5,
47
- }],
48
- ['awkward', {
49
- expression: [
50
- { name: 'sad', value: 0.3 },
51
- { name: 'ee', value: 0.2 },
52
- ],
53
- blendDuration: 0.5,
54
- }],
55
- ['question', {
56
- expression: [
57
- { name: 'surprised', value: 0.4 },
58
- { name: 'think', value: 0.3 },
59
- ],
60
- blendDuration: 0.4,
61
- }],
62
- ['curious', {
63
- expression: [
64
- { name: 'think', value: 0.5 },
65
- { name: 'surprised', value: 0.2 },
66
- ],
67
- blendDuration: 0.4,
68
- }],
69
- ['neutral', {
70
- expression: [
71
- { name: 'neutral', value: 1.0 },
72
- ],
73
- blendDuration: 0.6,
74
- }],
75
- ['love', {
76
- expression: [
77
- { name: 'happy', value: 0.2 },
78
- { name: 'relaxed', value: 0.4 },
79
- ],
80
- blendDuration: 0.4,
81
- }],
82
- ['flirty', {
83
- expression: [
84
- { name: 'happy', value: 0.2 },
85
- { name: 'relaxed', value: 0.3 },
86
- { name: 'aa', value: 0.15 },
87
- ],
88
- blendDuration: 0.4,
89
- }],
90
- ['greeting', {
91
- expression: [
92
- { name: 'happy', value: 0.2 },
93
- { name: 'aa', value: 0.3 },
94
- ],
95
- blendDuration: 0.3,
96
- }],
97
- ['relaxed', {
98
- expression: [
99
- { name: 'relaxed', value: 0.8 },
100
- ],
101
- blendDuration: 0.5,
102
- }],
103
- ])
104
-
105
- export class EmoteController {
106
- private vrm: VRM
107
- private currentEmotion: string | null = null
108
- private isTransitioning = false
109
- private transitionProgress = 0
110
- private currentValues = new Map<string, number>()
111
- private targetValues = new Map<string, number>()
112
- private resetTimer: ReturnType<typeof setTimeout> | null = null
113
-
114
- constructor(vrm: VRM) {
115
- this.vrm = vrm
116
- }
117
-
118
- setEmotion(emotionName: string, intensity = 1) {
119
- if (this.resetTimer) {
120
- clearTimeout(this.resetTimer)
121
- this.resetTimer = null
122
- }
123
-
124
- const state = emotionStates.get(emotionName)
125
- if (!state) {
126
- console.warn(`Emotion "${emotionName}" not found`)
127
- return
128
- }
129
-
130
- this.currentEmotion = emotionName
131
- this.isTransitioning = true
132
- this.transitionProgress = 0
133
- this.currentValues.clear()
134
- this.targetValues.clear()
135
-
136
- const clampedIntensity = Math.min(1, Math.max(0, intensity))
137
-
138
- // Capture current expression values as start point
139
- if (this.vrm.expressionManager) {
140
- const names = Object.keys(this.vrm.expressionManager.expressionMap)
141
- for (const name of names) {
142
- this.currentValues.set(name, this.vrm.expressionManager.getValue(name) || 0)
143
- this.targetValues.set(name, 0)
144
- }
145
- }
146
-
147
- // Set targets for this emotion
148
- for (const expr of state.expression) {
149
- this.targetValues.set(expr.name, expr.value * clampedIntensity)
150
- }
151
- }
152
-
153
- setEmotionWithReset(emotionName: string, durationMs: number, intensity = 1) {
154
- this.setEmotion(emotionName, intensity)
155
- this.resetTimer = setTimeout(() => {
156
- this.setEmotion('neutral')
157
- this.resetTimer = null
158
- }, durationMs)
159
- }
160
-
161
- update(deltaTime: number) {
162
- if (!this.isTransitioning || !this.currentEmotion) return
163
-
164
- const state = emotionStates.get(this.currentEmotion)!
165
- this.transitionProgress += deltaTime / state.blendDuration
166
-
167
- if (this.transitionProgress >= 1) {
168
- this.transitionProgress = 1
169
- this.isTransitioning = false
170
- }
171
-
172
- const t = this.transitionProgress
173
- const ease = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
174
-
175
- for (const [name, target] of this.targetValues) {
176
- const start = this.currentValues.get(name) || 0
177
- const value = start + (target - start) * ease
178
- this.vrm.expressionManager?.setValue(name, value)
179
- }
180
- }
181
-
182
- /** Reset all expressions to zero (idle state) without transition. */
183
- resetAll() {
184
- if (this.resetTimer) {
185
- clearTimeout(this.resetTimer)
186
- this.resetTimer = null
187
- }
188
- this.isTransitioning = false
189
- this.currentEmotion = null
190
- if (this.vrm.expressionManager) {
191
- const names = Object.keys(this.vrm.expressionManager.expressionMap)
192
- for (const name of names) {
193
- this.vrm.expressionManager.setValue(name, 0)
194
- }
195
- }
196
- this.currentValues.clear()
197
- this.targetValues.clear()
198
- }
199
-
200
- dispose() {
201
- if (this.resetTimer) {
202
- clearTimeout(this.resetTimer)
203
- this.resetTimer = null
204
- }
205
- }
206
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/hooks/usePassThrough.ts DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * Web fallback for Tauri window pass-through.
3
- * In the web version, cursor pass-through is not available.
4
- * This is a no-op.
5
- */
6
- import { useEffect } from 'react'
7
-
8
- export function usePassThrough(_enabled: boolean) {
9
- useEffect(() => {
10
- // No-op in web mode — pass-through is a Tauri-only feature
11
- }, [_enabled])
12
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/hooks/useServerStt.ts DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * Hook for server-side STT in the Friend Tauri app.
3
- *
4
- * Two modes:
5
- * - push-to-talk: POST /voice/start → capture → POST /voice/stop → get text
6
- * - voice-call: POST /voice/start → server-side periodic segmentation →
7
- * POST /voice/stop → get text
8
- *
9
- * Browser VAD (@ricky0123/vad-web) is NOT used — WebKitGTK (Tauri on Linux)
10
- * does not support onnxruntime-web WASM. Instead, the server segments audio
11
- * every ~5s and sends text to the CLI conversation automatically.
12
- */
13
- import { useRef, useCallback, useState } from 'react'
14
-
15
- const FRIEND_API_BASE = 'http://127.0.0.1:3456/plugins/friend'
16
-
17
- export type SttProvider = 'browser' | 'groq' | 'anthropic' | 'local' | 'doubao'
18
-
19
- export function useServerStt() {
20
- const [connected, setConnected] = useState(false)
21
- const [interimText, setInterimText] = useState('')
22
- const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
23
- const onTranscriptRef = useRef<((text: string, isFinal: boolean) => void) | null>(null)
24
- const onErrorRef = useRef<((err: string) => void) | null>(null)
25
-
26
- // ── Push-to-talk ──────────────────────────────────────────────────────
27
-
28
- /** Start push-to-talk: tell backend to start audio capture. */
29
- const startPushToTalk = useCallback(
30
- async (_provider: SttProvider, _language: string): Promise<void> => {
31
- const res = await fetch(`${FRIEND_API_BASE}/voice/start`, {
32
- method: 'POST',
33
- })
34
- if (!res.ok) {
35
- const err = await res.json().catch(() => ({ error: 'Unknown error' }))
36
- throw new Error(err.error || 'STT start failed')
37
- }
38
- setConnected(true)
39
- },
40
- [],
41
- )
42
-
43
- /** Stop push-to-talk: stop capture and return transcript text. */
44
- const stopPushToTalk = useCallback(async (): Promise<string> => {
45
- const res = await fetch(`${FRIEND_API_BASE}/voice/stop`, {
46
- method: 'POST',
47
- })
48
- setConnected(false)
49
- if (!res.ok) return ''
50
- const data = await res.json()
51
- return data.text || ''
52
- }, [])
53
-
54
- // ── Voice call (server-side segmentation, no browser VAD) ─────────────
55
-
56
- /**
57
- * Start voice call mode.
58
- *
59
- * Server captures microphone audio via cpal/arecord and segments it
60
- * every ~5 seconds, sending each segment to STT (Groq Whisper).
61
- * The transcript is enqueued to the CLI conversation automatically.
62
- */
63
- const startStreaming = useCallback(
64
- (
65
- _provider: SttProvider,
66
- onTranscript: (text: string, isFinal: boolean) => void,
67
- onError: (err: string) => void,
68
- _language = 'zh',
69
- ) => {
70
- onTranscriptRef.current = onTranscript
71
- onErrorRef.current = onError
72
-
73
- // Start server-side capture
74
- fetch(`${FRIEND_API_BASE}/voice/start`, { method: 'POST' })
75
- .then(async (res) => {
76
- if (!res.ok) {
77
- const err = await res.json().catch(() => ({ error: 'Unknown error' }))
78
- throw new Error(err.error || 'STT start failed')
79
- }
80
- setConnected(true)
81
-
82
- // Poll interim status every 1s for display
83
- const lastTextRef = { current: '' }
84
- pollTimerRef.current = setInterval(async () => {
85
- try {
86
- const statusRes = await fetch(`${FRIEND_API_BASE}/voice/status`, {
87
- method: 'POST',
88
- })
89
- if (!statusRes.ok) return
90
- const status = await statusRes.json()
91
- const text = status.interimText || ''
92
- if (text && text !== lastTextRef.current) {
93
- lastTextRef.current = text
94
- setInterimText(text)
95
- onTranscriptRef.current?.(text, false)
96
- }
97
- } catch {
98
- // Polling errors are non-fatal
99
- }
100
- }, 1000)
101
- })
102
- .catch((err) => {
103
- console.error('[VoiceCall] start failed:', err)
104
- onErrorRef.current?.(String(err))
105
- })
106
- },
107
- [],
108
- )
109
-
110
- /** Stop voice call mode. */
111
- const stopStreaming = useCallback(async (): Promise<string> => {
112
- // Stop polling
113
- if (pollTimerRef.current) {
114
- clearInterval(pollTimerRef.current)
115
- pollTimerRef.current = null
116
- }
117
- setInterimText('')
118
- setConnected(false)
119
-
120
- // Stop server-side capture
121
- const res = await fetch(`${FRIEND_API_BASE}/voice/stop`, {
122
- method: 'POST',
123
- })
124
- if (!res.ok) return ''
125
- const data = await res.json()
126
- return data.text || ''
127
- }, [])
128
-
129
- return {
130
- connected,
131
- interimText,
132
- startPushToTalk,
133
- stopPushToTalk,
134
- startStreaming,
135
- stopStreaming,
136
- }
137
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/index.html DELETED
@@ -1,63 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Claw Sama</title>
8
- <style>
9
- * {
10
- margin: 0;
11
- padding: 0;
12
- box-sizing: border-box;
13
- }
14
-
15
- html,
16
- body,
17
- #root {
18
- width: 100%;
19
- height: 100%;
20
- overflow: hidden;
21
- background: transparent;
22
- }
23
-
24
- @keyframes spin {
25
- from {
26
- transform: rotate(0deg);
27
- }
28
-
29
- to {
30
- transform: rotate(360deg);
31
- }
32
- }
33
-
34
- @keyframes claw-input-slide-up {
35
- 0% { opacity: 0; transform: translateY(20px) scale(0.95); }
36
- 100% { opacity: 1; transform: translateY(0) scale(1); }
37
- }
38
-
39
- @keyframes claw-input-slide-down {
40
- 0% { opacity: 1; transform: translateY(0) scale(1); }
41
- 100% { opacity: 0; transform: translateY(20px) scale(0.95); }
42
- }
43
-
44
- @keyframes claw-pulse {
45
- 0%, 100% { opacity: 1; }
46
- 50% { opacity: 0.4; }
47
- }
48
-
49
- @keyframes claw-pop-in {
50
- 0% { opacity: 0; transform: scale(0) translateY(0.5em); }
51
- 60% { opacity: 1; transform: scale(1.15) translateY(-0.05em); }
52
- 100% { opacity: 1; transform: scale(1) translateY(0); }
53
- }
54
-
55
- </style>
56
- </head>
57
-
58
- <body>
59
- <div id="root"></div>
60
- <script type="module" src="./main.tsx"></script>
61
- </body>
62
-
63
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/lip-sync.ts DELETED
@@ -1,192 +0,0 @@
1
- import type { VRM } from '@pixiv/three-vrm'
2
- import type { Profile } from 'wlipsync'
3
- import { createWLipSyncNode } from 'wlipsync'
4
- import profile from './assets/lip-sync-profile.json'
5
-
6
- type LipKey = 'A' | 'E' | 'I' | 'O' | 'U'
7
- const RAW_KEYS = ['A', 'E', 'I', 'O', 'U', 'S'] as const
8
- const LIP_KEYS: LipKey[] = ['A', 'E', 'I', 'O', 'U']
9
- const BLENDSHAPE_MAP: Record<LipKey, string> = {
10
- A: 'aa',
11
- E: 'ee',
12
- I: 'ih',
13
- O: 'oh',
14
- U: 'ou',
15
- }
16
- const RAW_TO_LIP: Record<typeof RAW_KEYS[number], LipKey> = {
17
- A: 'A', E: 'E', I: 'I', O: 'O', U: 'U', S: 'I',
18
- }
19
-
20
- const ATTACK = 50
21
- const RELEASE = 30
22
- const CAP = 0.7
23
- const SILENCE_VOL = 0.04
24
- const SILENCE_GAIN = 0.05
25
- const IDLE_MS = 160
26
-
27
- let singleton: LipSync | null = null
28
-
29
- export class LipSync {
30
- private audioContext: AudioContext | null = null
31
- private lipSyncNode: any = null
32
- private gainNode: GainNode | null = null
33
- private smoothState: Record<LipKey, number> = { A: 0, E: 0, I: 0, O: 0, U: 0 }
34
- private lastActiveAt = 0
35
- private currentSource: AudioBufferSourceNode | null = null
36
- private ready = false
37
- private initPromise: Promise<void> | null = null
38
-
39
- static getInstance(): LipSync {
40
- if (!singleton) singleton = new LipSync()
41
- return singleton
42
- }
43
-
44
- private constructor() {}
45
-
46
- private async ensureReady() {
47
- if (this.ready) return
48
- if (this.initPromise) return this.initPromise
49
- this.initPromise = (async () => {
50
- this.audioContext = new AudioContext()
51
- this.lipSyncNode = await createWLipSyncNode(this.audioContext, profile as Profile)
52
- // lipSyncNode is analysis-only, no need to connect to destination
53
- this.gainNode = this.audioContext.createGain()
54
- this.gainNode.connect(this.audioContext.destination)
55
- this.ready = true
56
- })()
57
- return this.initPromise
58
- }
59
-
60
- /**
61
- * Fetch audio from URL, decode it, and play through Web Audio API.
62
- * Returns duration (ms) once playback starts. Sound goes to both
63
- * lipSyncNode (for mouth analysis) and destination (for speakers).
64
- */
65
- async playAudio(url: string): Promise<number> {
66
- await this.ensureReady()
67
- if (!this.lipSyncNode || !this.audioContext) return 0
68
-
69
- // Stop previous source
70
- if (this.currentSource) {
71
- try { this.currentSource.stop() } catch {}
72
- try { this.currentSource.disconnect() } catch {}
73
- this.currentSource = null
74
- }
75
-
76
- if (this.audioContext.state === 'suspended') {
77
- await this.audioContext.resume()
78
- }
79
-
80
- let response: Response
81
- try {
82
- response = await fetch(url)
83
- } catch (err) {
84
- console.error('[LipSync] fetch failed:', url, err)
85
- return 0
86
- }
87
- if (!response.ok) {
88
- console.error('[LipSync] fetch error:', url, response.status)
89
- return 0
90
- }
91
- const arrayBuffer = await response.arrayBuffer()
92
- if (arrayBuffer.byteLength === 0) {
93
- console.error('[LipSync] empty response:', url)
94
- return 0
95
- }
96
- const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
97
-
98
- const source = this.audioContext.createBufferSource()
99
- source.buffer = audioBuffer
100
- source.connect(this.lipSyncNode)
101
- source.connect(this.gainNode!)
102
- this.currentSource = source
103
- source.start()
104
-
105
- return audioBuffer.duration * 1000
106
- }
107
-
108
- setVolume(value: number) {
109
- if (this.gainNode) {
110
- this.gainNode.gain.value = value
111
- }
112
- }
113
-
114
- update(vrm: VRM, delta: number) {
115
- const node = this.lipSyncNode
116
- if (!vrm.expressionManager || !node || !this.ready) return
117
-
118
- const vol = node.volume ?? 0
119
- const amp = Math.min(vol * 0.9, 1) ** 0.7
120
-
121
- const projected: Record<LipKey, number> = { A: 0, E: 0, I: 0, O: 0, U: 0 }
122
- for (const raw of RAW_KEYS) {
123
- const lip = RAW_TO_LIP[raw]
124
- const rawVal = node.weights[raw] ?? 0
125
- projected[lip] = Math.max(projected[lip], rawVal * amp)
126
- }
127
-
128
- let winner: LipKey = 'I'
129
- let runner: LipKey = 'E'
130
- let winnerVal = -Infinity
131
- let runnerVal = -Infinity
132
- for (const key of LIP_KEYS) {
133
- const val = projected[key]
134
- if (val > winnerVal) {
135
- runnerVal = winnerVal
136
- runner = winner
137
- winnerVal = val
138
- winner = key
139
- } else if (val > runnerVal) {
140
- runnerVal = val
141
- runner = key
142
- }
143
- }
144
-
145
- const now = performance.now()
146
- let silent = amp < SILENCE_VOL || winnerVal < SILENCE_GAIN
147
- if (!silent) this.lastActiveAt = now
148
- if (now - this.lastActiveAt > IDLE_MS) silent = true
149
-
150
- const target: Record<LipKey, number> = { A: 0, E: 0, I: 0, O: 0, U: 0 }
151
- if (!silent) {
152
- target[winner] = Math.min(CAP, winnerVal)
153
- target[runner] = Math.min(CAP * 0.5, runnerVal * 0.6)
154
- }
155
-
156
- for (const key of LIP_KEYS) {
157
- const from = this.smoothState[key]
158
- const to = target[key]
159
- const rate = 1 - Math.exp(-(to > from ? ATTACK : RELEASE) * delta)
160
- this.smoothState[key] = from + (to - from) * rate
161
-
162
- // When fully silent and decayed, skip writing so emote can control mouth morphs
163
- if (silent && this.smoothState[key] <= 0.01) continue
164
-
165
- const weight = (this.smoothState[key] <= 0.01 ? 0 : this.smoothState[key]) * 0.7
166
- vrm.expressionManager.setValue(BLENDSHAPE_MAP[key], weight)
167
- }
168
- }
169
-
170
- /** Check if currently playing audio (within last 300ms). */
171
- isActive(): boolean {
172
- return this.currentSource !== null && performance.now() - this.lastActiveAt < 300
173
- }
174
-
175
- /** Stop current audio playback immediately. */
176
- stopAudio() {
177
- if (this.currentSource) {
178
- try { this.currentSource.stop() } catch {}
179
- try { this.currentSource.disconnect() } catch {}
180
- this.currentSource = null
181
- }
182
- }
183
-
184
- dispose() {
185
- this.stopAudio()
186
- if (this.lipSyncNode) {
187
- try { this.lipSyncNode.disconnect() } catch {}
188
- }
189
- this.audioContext?.close()
190
- singleton = null
191
- }
192
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/main.tsx DELETED
@@ -1,15 +0,0 @@
1
- import React from 'react'
2
- import ReactDOM from 'react-dom/client'
3
- import App from './App'
4
- import { listen } from '@tauri-apps/api/event'
5
-
6
- // Listen for Tauri close event and notify backend
7
- listen('friend-window-close', () => {
8
- fetch('http://127.0.0.1:3456/friend/api/window-close', { method: 'POST' }).catch(() => {})
9
- })
10
-
11
- ReactDOM.createRoot(document.getElementById('root')!).render(
12
- <React.StrictMode>
13
- <App />
14
- </React.StrictMode>,
15
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/mixamo-loader.ts DELETED
@@ -1,141 +0,0 @@
1
- /**
2
- * Mixamo FBX animation loader for VRM models.
3
- * Ported from lobe-vidol's loadMixamoAnimation.
4
- */
5
-
6
- import type { VRM } from '@pixiv/three-vrm'
7
- import type { VRMHumanBoneName } from '@pixiv/three-vrm-core'
8
- import * as THREE from 'three'
9
- import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'
10
-
11
- const mixamoVRMRigMap: Record<string, VRMHumanBoneName> = {
12
- mixamorigHips: 'hips',
13
- mixamorigSpine: 'spine',
14
- mixamorigSpine1: 'chest',
15
- mixamorigSpine2: 'upperChest',
16
- mixamorigNeck: 'neck',
17
- mixamorigHead: 'head',
18
- mixamorigLeftShoulder: 'leftShoulder',
19
- mixamorigLeftArm: 'leftUpperArm',
20
- mixamorigLeftForeArm: 'leftLowerArm',
21
- mixamorigLeftHand: 'leftHand',
22
- mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
23
- mixamorigLeftHandThumb2: 'leftThumbProximal',
24
- mixamorigLeftHandThumb3: 'leftThumbDistal',
25
- mixamorigLeftHandIndex1: 'leftIndexProximal',
26
- mixamorigLeftHandIndex2: 'leftIndexIntermediate',
27
- mixamorigLeftHandIndex3: 'leftIndexDistal',
28
- mixamorigLeftHandMiddle1: 'leftMiddleProximal',
29
- mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
30
- mixamorigLeftHandMiddle3: 'leftMiddleDistal',
31
- mixamorigLeftHandRing1: 'leftRingProximal',
32
- mixamorigLeftHandRing2: 'leftRingIntermediate',
33
- mixamorigLeftHandRing3: 'leftRingDistal',
34
- mixamorigLeftHandPinky1: 'leftLittleProximal',
35
- mixamorigLeftHandPinky2: 'leftLittleIntermediate',
36
- mixamorigLeftHandPinky3: 'leftLittleDistal',
37
- mixamorigRightShoulder: 'rightShoulder',
38
- mixamorigRightArm: 'rightUpperArm',
39
- mixamorigRightForeArm: 'rightLowerArm',
40
- mixamorigRightHand: 'rightHand',
41
- mixamorigRightHandPinky1: 'rightLittleProximal',
42
- mixamorigRightHandPinky2: 'rightLittleIntermediate',
43
- mixamorigRightHandPinky3: 'rightLittleDistal',
44
- mixamorigRightHandRing1: 'rightRingProximal',
45
- mixamorigRightHandRing2: 'rightRingIntermediate',
46
- mixamorigRightHandRing3: 'rightRingDistal',
47
- mixamorigRightHandMiddle1: 'rightMiddleProximal',
48
- mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
49
- mixamorigRightHandMiddle3: 'rightMiddleDistal',
50
- mixamorigRightHandIndex1: 'rightIndexProximal',
51
- mixamorigRightHandIndex2: 'rightIndexIntermediate',
52
- mixamorigRightHandIndex3: 'rightIndexDistal',
53
- mixamorigRightHandThumb1: 'rightThumbMetacarpal',
54
- mixamorigRightHandThumb2: 'rightThumbProximal',
55
- mixamorigRightHandThumb3: 'rightThumbDistal',
56
- mixamorigLeftUpLeg: 'leftUpperLeg',
57
- mixamorigLeftLeg: 'leftLowerLeg',
58
- mixamorigLeftFoot: 'leftFoot',
59
- mixamorigLeftToeBase: 'leftToes',
60
- mixamorigRightUpLeg: 'rightUpperLeg',
61
- mixamorigRightLeg: 'rightLowerLeg',
62
- mixamorigRightFoot: 'rightFoot',
63
- mixamorigRightToeBase: 'rightToes',
64
- }
65
-
66
- export async function loadMixamoAnimation(url: string, vrm: VRM): Promise<THREE.AnimationClip> {
67
- const loader = new FBXLoader()
68
- let asset: THREE.Group
69
- try {
70
- asset = await loader.loadAsync(url) as THREE.Group
71
- } catch (err) {
72
- console.error('[MixamoLoader] FBXLoader.loadAsync failed:', url, err)
73
- throw err
74
- }
75
- // Try 'mixamo.com' first, fall back to first available animation
76
- const clip = THREE.AnimationClip.findByName(asset.animations, 'mixamo.com')
77
- ?? asset.animations[0]
78
- if (!clip) throw new Error('No animation clip found in FBX')
79
-
80
- const tracks: THREE.KeyframeTrack[] = []
81
- const restRotationInverse = new THREE.Quaternion()
82
- const parentRestWorldRotation = new THREE.Quaternion()
83
- const _quatA = new THREE.Quaternion()
84
- const _vec3 = new THREE.Vector3()
85
-
86
- // Scale based on hips height ratio
87
- const hipsObj = asset.getObjectByName('mixamorigHips')
88
- if (!hipsObj) throw new Error('No mixamorigHips bone found in FBX')
89
- const motionHipsHeight = hipsObj.position.y
90
- const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode('hips')?.getWorldPosition(_vec3).y || 0
91
- const vrmRootY = vrm.scene.getWorldPosition(_vec3).y
92
- const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY)
93
- const hipsPositionScale = vrmHipsHeight / motionHipsHeight
94
-
95
- clip.tracks.forEach((track) => {
96
- const trackSplitted = track.name.split('.')
97
- const mixamoRigName = trackSplitted[0]
98
- const vrmBoneName = mixamoVRMRigMap[mixamoRigName]
99
- const vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name
100
- const mixamoRigNode = asset.getObjectByName(mixamoRigName)
101
-
102
- if (vrmNodeName != null) {
103
- const propertyName = trackSplitted[1]
104
-
105
- if (mixamoRigNode) {
106
- mixamoRigNode.getWorldQuaternion(restRotationInverse).invert()
107
- if (mixamoRigNode.parent)
108
- mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation)
109
- }
110
-
111
- if (track instanceof THREE.QuaternionKeyframeTrack) {
112
- for (let i = 0; i < track.values.length; i += 4) {
113
- const flatQuaternion = track.values.slice(i, i + 4)
114
- _quatA.fromArray(flatQuaternion)
115
- _quatA.premultiply(parentRestWorldRotation).multiply(restRotationInverse)
116
- _quatA.toArray(flatQuaternion)
117
- flatQuaternion.forEach((v, index) => {
118
- track.values[index + i] = v
119
- })
120
- }
121
-
122
- tracks.push(
123
- new THREE.QuaternionKeyframeTrack(
124
- `${vrmNodeName}.${propertyName}`,
125
- Array.from(track.times),
126
- Array.from(track.values).map((v, i) => (vrm.meta?.metaVersion === '0' && i % 2 === 0 ? -v : v)),
127
- ),
128
- )
129
- } else if (track instanceof THREE.VectorKeyframeTrack) {
130
- const value = Array.from(track.values).map(
131
- (v, i) => (vrm.meta?.metaVersion === '0' && i % 3 !== 1 ? -v : v) * hipsPositionScale,
132
- )
133
- tracks.push(
134
- new THREE.VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, Array.from(track.times), value),
135
- )
136
- }
137
- }
138
- })
139
-
140
- return new THREE.AnimationClip('mixamoAnimation', clip.duration, tracks)
141
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/motion-controller.ts DELETED
@@ -1,331 +0,0 @@
1
- /**
2
- * MotionController — unified animation system supporting VRMA and FBX actions.
3
- *
4
- * Uses a single persistent AnimationMixer with crossFade transitions to avoid
5
- * T-pose flickering between animations.
6
- *
7
- * Simplified: removed dance system, kept action presets for human-like behavior.
8
- */
9
-
10
- import * as THREE from 'three'
11
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
12
- import { VRMAnimationLoaderPlugin, createVRMAnimationClip } from '@pixiv/three-vrm-animation'
13
- import type { VRMAnimation } from '@pixiv/three-vrm-animation'
14
- import type { VRM } from '@pixiv/three-vrm'
15
- import { loadMixamoAnimation } from './mixamo-loader'
16
-
17
- // ── Motion file types ───────────────────────────────────────────────────────
18
-
19
- export type MotionFileType = 'vrma' | 'fbx'
20
-
21
- // ── Motion presets ──────────────────────────────────────────────────────────
22
-
23
- export interface MotionPreset {
24
- label: string
25
- type: MotionFileType
26
- url: string
27
- }
28
-
29
- // Actions: short one-shot gestures triggered by emotions / interactions
30
- export const actionPresets: Record<string, MotionPreset> = {
31
- akimbo: { label: '叉腰', type: 'vrma', url: '/friend/akimbo.vrma' },
32
- playFingers: { label: '搓手', type: 'vrma', url: '/friend/playFingers.vrma' },
33
- scratchHead: { label: '挠头', type: 'vrma', url: '/friend/scratchHead.vrma' },
34
- stretch: { label: '伸展', type: 'vrma', url: '/friend/stretch.vrma' },
35
-
36
- happy: { label: '开心', type: 'fbx', url: '/friend/happy.fbx' },
37
- angry: { label: '生气', type: 'fbx', url: '/friend/angry.fbx' },
38
- greeting: { label: '招呼', type: 'fbx', url: '/friend/greeting.fbx' },
39
- excited: { label: '兴奋', type: 'fbx', url: '/friend/excited.fbx' },
40
- shy: { label: '害羞', type: 'fbx', url: '/friend/shy.fbx' },
41
- point: { label: '指点', type: 'fbx', url: '/friend/point.fbx' },
42
- salute: { label: '敬礼', type: 'fbx', url: '/friend/salute.fbx' },
43
- angryPump: { label: '暴怒', type: 'fbx', url: '/friend/angryPump.fbx' },
44
- }
45
-
46
- // ── Utility: re-anchor root position ────────────────────────────────────────
47
-
48
- function reAnchorRootPositionTrack(clip: THREE.AnimationClip, vrm: VRM) {
49
- const hipNode = vrm.humanoid?.getNormalizedBoneNode('hips')
50
- if (!hipNode) return
51
-
52
- hipNode.updateMatrixWorld(true)
53
- const defaultHipPos = new THREE.Vector3()
54
- hipNode.getWorldPosition(defaultHipPos)
55
-
56
- const hipsTrack = clip.tracks.find(
57
- (t) =>
58
- t instanceof THREE.VectorKeyframeTrack &&
59
- t.name === `${hipNode.name}.position`,
60
- )
61
- if (!(hipsTrack instanceof THREE.VectorKeyframeTrack)) return
62
-
63
- const animeHipPos = new THREE.Vector3(
64
- hipsTrack.values[0],
65
- hipsTrack.values[1],
66
- hipsTrack.values[2],
67
- )
68
- const delta = new THREE.Vector3().subVectors(animeHipPos, defaultHipPos)
69
-
70
- clip.tracks.forEach((track) => {
71
- if (
72
- track.name.endsWith('.position') &&
73
- track instanceof THREE.VectorKeyframeTrack
74
- ) {
75
- for (let i = 0; i < track.values.length; i += 3) {
76
- track.values[i] -= delta.x
77
- track.values[i + 1] -= delta.y
78
- track.values[i + 2] -= delta.z
79
- }
80
- }
81
- })
82
- }
83
-
84
- // ── MotionController ────────────────────────────────────────────────────────
85
-
86
- export class MotionController {
87
- private vrm: VRM
88
- private mixer: THREE.AnimationMixer | null = null
89
- private idleClip: THREE.AnimationClip | null = null
90
- private idleAction: THREE.AnimationAction | null = null
91
- private currentAction: THREE.AnimationAction | null = null
92
- private clipCache = new Map<string, THREE.AnimationClip>()
93
- private gltfLoader: GLTFLoader
94
- private _actionPlaying = false
95
- private holdTimer: ReturnType<typeof setTimeout> | null = null
96
- private _actionSafetyTimer: ReturnType<typeof setTimeout> | null = null
97
- private actionQueue: Array<{ name: string; hold: boolean }> = []
98
- private _settleGen = 0 // generation at which the current settle is valid
99
- private _settleHold = false // hold flag for current settle
100
-
101
- constructor(vrm: VRM) {
102
- this.vrm = vrm
103
- this.mixer = new THREE.AnimationMixer(vrm.scene)
104
- this.gltfLoader = new GLTFLoader()
105
- this.gltfLoader.register((parser) => new VRMAnimationLoaderPlugin(parser))
106
- }
107
-
108
- get actionPlaying() { return this._actionPlaying }
109
-
110
- update(delta: number) {
111
- if (this.mixer) {
112
- this.mixer.update(delta)
113
- this.checkActionCompletion()
114
- }
115
- }
116
-
117
- /**
118
- * Frame-accurate action completion detection.
119
- * THREE.AnimationMixer 'finished' event only fires when ALL actions finish,
120
- * which never happens with a looping idle. So we check per-frame instead.
121
- */
122
- private checkActionCompletion() {
123
- if (!this._actionPlaying || !this.currentAction) return
124
- const clip = this.currentAction.getClip()
125
- if (!clip || clip.duration <= 0) return
126
-
127
- // Allow a 1-frame epsilon (≈16ms at 60fps) to avoid precision issues
128
- if (this.currentAction.time >= clip.duration - 0.02) {
129
- this.finishCurrentAction()
130
- }
131
- }
132
-
133
- // ── CrossFade helper ─────────────────────────────────────────────────────
134
-
135
- private crossFadeTo(newAction: THREE.AnimationAction, duration = 0.3) {
136
- newAction.reset().setEffectiveWeight(1).play()
137
- const prev = this.currentAction ?? this.idleAction
138
- if (prev && prev !== newAction) {
139
- prev.crossFadeTo(newAction, duration, false)
140
- }
141
- this.currentAction = newAction
142
- }
143
-
144
- // ── Load & play idle animation ───────────────────────────────────────────
145
-
146
- async loadIdle(path: string) {
147
- const clip = await this.loadVRMA(path)
148
- if (!clip) return
149
- reAnchorRootPositionTrack(clip, this.vrm)
150
- this.idleClip = clip
151
- this.startIdle()
152
- }
153
-
154
- /** (Re)start idle via crossFade. */
155
- private startIdle() {
156
- if (!this.idleClip || !this.mixer) return
157
- this.idleAction = this.mixer.clipAction(this.idleClip)
158
- this.crossFadeTo(this.idleAction)
159
- }
160
-
161
- // ── Clear current action (private) ──────────────────────────────────────
162
-
163
- private clearTimers() {
164
- if (this.holdTimer) { clearTimeout(this.holdTimer); this.holdTimer = null }
165
- if (this._actionSafetyTimer) { clearTimeout(this._actionSafetyTimer); this._actionSafetyTimer = null }
166
- }
167
-
168
- // ── Reset to idle (public) ──────────────────────────────────────────────
169
-
170
- resetToIdle() {
171
- this.clearTimers()
172
- this._actionPlaying = false
173
- this._actionGeneration++
174
-
175
- // CrossFade back to idle
176
- if (this.mixer && this.idleClip) {
177
- this.idleAction = this.mixer.clipAction(this.idleClip)
178
- this.crossFadeTo(this.idleAction)
179
- }
180
- }
181
-
182
- // ── Play a one-shot action ──────────────────────────────────────────────
183
-
184
- private _actionGeneration = 0
185
-
186
- async playAction(name: string, hold = false) {
187
- const preset = actionPresets[name]
188
- if (!preset) { console.warn('[Motion] unknown action:', name); return }
189
-
190
- // If currently holding (pose frozen, not actively animating), interrupt hold
191
- // and play the new action immediately. This prevents LLM text-message actions
192
- // from being blocked by a stale 10s hold timer.
193
- if (this._actionPlaying && this.holdTimer !== null) {
194
- this.clearTimers()
195
- this._actionPlaying = false
196
- // fall through to play new action
197
- } else if (this._actionPlaying) {
198
- // Normal queue: only when an action is actively animating (e.g. two rapid
199
- // LLM tool calls). Hold does NOT queue — it interrupts.
200
- const last = this.actionQueue[this.actionQueue.length - 1]
201
- if (!last || last.name !== name) {
202
- this.actionQueue.push({ name, hold })
203
- }
204
- return
205
- }
206
-
207
- // Set lock BEFORE async load to prevent concurrent playAction calls
208
- this._actionPlaying = true
209
- const gen = ++this._actionGeneration
210
- this._settleGen = gen
211
- this._settleHold = hold
212
-
213
- const clip = await this.loadClip(preset)
214
-
215
- // Check if state was reset or another action started during await
216
- if (gen !== this._actionGeneration) return
217
- if (!clip) { console.warn('[Motion] clip load failed for:', name); this._actionPlaying = false; return }
218
- if (!this.mixer) { this._actionPlaying = false; return }
219
-
220
- this.clearTimers()
221
-
222
- const action = this.mixer.clipAction(clip)
223
- action.setLoop(THREE.LoopOnce, 1)
224
- action.clampWhenFinished = true
225
- this.crossFadeTo(action)
226
-
227
- // Frame-accurate completion is handled in checkActionCompletion() via update()
228
- // Safety timer: generous fallback (should never fire if checkActionCompletion works)
229
- const safeDuration = Math.max(clip.duration, 3) + 5
230
- this._actionSafetyTimer = setTimeout(() => {
231
- if (gen === this._actionGeneration && this._actionPlaying) {
232
- this.finishCurrentAction()
233
- }
234
- }, safeDuration * 1000)
235
- }
236
-
237
- /** Called by checkActionCompletion() or safety timer when current action ends. */
238
- private finishCurrentAction() {
239
- if (!this._actionPlaying) return
240
- this.clearTimers()
241
- const gen = this._settleGen
242
- const hold = this._settleHold
243
-
244
- if (gen !== this._actionGeneration) return
245
-
246
- // Process next queued action
247
- const next = this.actionQueue.shift()
248
- if (next) {
249
- this._actionPlaying = false
250
- this.playAction(next.name, next.hold)
251
- return
252
- }
253
-
254
- if (hold) {
255
- this.holdTimer = setTimeout(() => {
256
- if (gen !== this._actionGeneration) return
257
- this.clearTimers()
258
- // Drain any actions queued during hold before going idle —
259
- // prevents the queue leak where LLM actions are silently dropped.
260
- const queued = this.actionQueue.shift()
261
- if (queued) {
262
- this._actionPlaying = false
263
- this.playAction(queued.name, queued.hold)
264
- return
265
- }
266
- this._actionPlaying = false
267
- this.startIdle()
268
- }, 10000)
269
- } else {
270
- this._actionPlaying = false
271
- this.startIdle()
272
- }
273
- }
274
-
275
- /** Cleanup when controller is being destroyed (model reload etc.) */
276
- dispose() {
277
- this.clearTimers()
278
- this._actionPlaying = false
279
- this._actionGeneration++
280
- if (this.mixer) {
281
- this.mixer.stopAllAction()
282
- this.mixer.uncacheRoot(this.vrm.scene)
283
- this.mixer = null
284
- }
285
- this.idleAction = null
286
- this.currentAction = null
287
- }
288
-
289
- // ── Internal ────────────────────────────────────────────────────────────
290
-
291
- private async loadClip(preset: MotionPreset): Promise<THREE.AnimationClip | null> {
292
- const cached = this.clipCache.get(preset.url)
293
- if (cached) return cached
294
-
295
- let clip: THREE.AnimationClip | null = null
296
-
297
- try {
298
- switch (preset.type) {
299
- case 'vrma':
300
- clip = await this.loadVRMA(preset.url)
301
- if (clip) reAnchorRootPositionTrack(clip, this.vrm)
302
- break
303
- case 'fbx':
304
- clip = await loadMixamoAnimation(preset.url, this.vrm)
305
- break
306
- }
307
- } catch (err) {
308
- console.error('Failed to load clip:', preset.url, err)
309
- return null
310
- }
311
-
312
- if (clip) {
313
- clip.name = preset.url
314
- this.clipCache.set(preset.url, clip)
315
- }
316
- return clip
317
- }
318
-
319
- private async loadVRMA(url: string): Promise<THREE.AnimationClip | null> {
320
- try {
321
- const gltf = await this.gltfLoader.loadAsync(url)
322
- const anims = gltf.userData.vrmAnimations as VRMAnimation[]
323
- if (anims?.length) {
324
- return createVRMAnimationClip(anims[0], this.vrm)
325
- }
326
- } catch (err) {
327
- console.warn(`Failed to load VRMA: ${url}`, err)
328
- }
329
- return null
330
- }
331
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/package-lock.json DELETED
@@ -1,2701 +0,0 @@
1
- {
2
- "name": "friend-vrm-frontend",
3
- "version": "0.1.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "friend-vrm-frontend",
9
- "version": "0.1.0",
10
- "dependencies": {
11
- "@pixiv/three-vrm": "^3.4.0",
12
- "@pixiv/three-vrm-animation": "^3.5.0",
13
- "@pixiv/three-vrm-core": "^3.5.1",
14
- "@ricky0123/vad-web": "^0.0.30",
15
- "@tauri-apps/api": "^2.11.1",
16
- "lucide-react": "^0.577.0",
17
- "marked": "^18.0.5",
18
- "mmd-parser": "^1.0.4",
19
- "react": "^18.3.1",
20
- "react-dom": "^18.3.1",
21
- "three": "^0.175.0",
22
- "vite-plugin-static-copy": "^4.1.1",
23
- "wlipsync": "^1.3.0"
24
- },
25
- "devDependencies": {
26
- "@tauri-apps/cli": "^2.11.3",
27
- "@types/react": "^18.3.23",
28
- "@types/react-dom": "^18.3.7",
29
- "@types/three": "^0.175.0",
30
- "@vitejs/plugin-react": "^4.5.0",
31
- "typescript": "^5.8.3",
32
- "vite": "^6.3.2"
33
- }
34
- },
35
- "node_modules/@babel/code-frame": {
36
- "version": "7.29.7",
37
- "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz",
38
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
39
- "dev": true,
40
- "license": "MIT",
41
- "dependencies": {
42
- "@babel/helper-validator-identifier": "^7.29.7",
43
- "js-tokens": "^4.0.0",
44
- "picocolors": "^1.1.1"
45
- },
46
- "engines": {
47
- "node": ">=6.9.0"
48
- }
49
- },
50
- "node_modules/@babel/compat-data": {
51
- "version": "7.29.7",
52
- "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz",
53
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
54
- "dev": true,
55
- "license": "MIT",
56
- "engines": {
57
- "node": ">=6.9.0"
58
- }
59
- },
60
- "node_modules/@babel/core": {
61
- "version": "7.29.7",
62
- "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz",
63
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
64
- "dev": true,
65
- "license": "MIT",
66
- "dependencies": {
67
- "@babel/code-frame": "^7.29.7",
68
- "@babel/generator": "^7.29.7",
69
- "@babel/helper-compilation-targets": "^7.29.7",
70
- "@babel/helper-module-transforms": "^7.29.7",
71
- "@babel/helpers": "^7.29.7",
72
- "@babel/parser": "^7.29.7",
73
- "@babel/template": "^7.29.7",
74
- "@babel/traverse": "^7.29.7",
75
- "@babel/types": "^7.29.7",
76
- "@jridgewell/remapping": "^2.3.5",
77
- "convert-source-map": "^2.0.0",
78
- "debug": "^4.1.0",
79
- "gensync": "^1.0.0-beta.2",
80
- "json5": "^2.2.3",
81
- "semver": "^6.3.1"
82
- },
83
- "engines": {
84
- "node": ">=6.9.0"
85
- },
86
- "funding": {
87
- "type": "opencollective",
88
- "url": "https://opencollective.com/babel"
89
- }
90
- },
91
- "node_modules/@babel/generator": {
92
- "version": "7.29.7",
93
- "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz",
94
- "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
95
- "dev": true,
96
- "license": "MIT",
97
- "dependencies": {
98
- "@babel/parser": "^7.29.7",
99
- "@babel/types": "^7.29.7",
100
- "@jridgewell/gen-mapping": "^0.3.12",
101
- "@jridgewell/trace-mapping": "^0.3.28",
102
- "jsesc": "^3.0.2"
103
- },
104
- "engines": {
105
- "node": ">=6.9.0"
106
- }
107
- },
108
- "node_modules/@babel/helper-compilation-targets": {
109
- "version": "7.29.7",
110
- "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
111
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
112
- "dev": true,
113
- "license": "MIT",
114
- "dependencies": {
115
- "@babel/compat-data": "^7.29.7",
116
- "@babel/helper-validator-option": "^7.29.7",
117
- "browserslist": "^4.24.0",
118
- "lru-cache": "^5.1.1",
119
- "semver": "^6.3.1"
120
- },
121
- "engines": {
122
- "node": ">=6.9.0"
123
- }
124
- },
125
- "node_modules/@babel/helper-globals": {
126
- "version": "7.29.7",
127
- "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
128
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
129
- "dev": true,
130
- "license": "MIT",
131
- "engines": {
132
- "node": ">=6.9.0"
133
- }
134
- },
135
- "node_modules/@babel/helper-module-imports": {
136
- "version": "7.29.7",
137
- "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
138
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
139
- "dev": true,
140
- "license": "MIT",
141
- "dependencies": {
142
- "@babel/traverse": "^7.29.7",
143
- "@babel/types": "^7.29.7"
144
- },
145
- "engines": {
146
- "node": ">=6.9.0"
147
- }
148
- },
149
- "node_modules/@babel/helper-module-transforms": {
150
- "version": "7.29.7",
151
- "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
152
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
153
- "dev": true,
154
- "license": "MIT",
155
- "dependencies": {
156
- "@babel/helper-module-imports": "^7.29.7",
157
- "@babel/helper-validator-identifier": "^7.29.7",
158
- "@babel/traverse": "^7.29.7"
159
- },
160
- "engines": {
161
- "node": ">=6.9.0"
162
- },
163
- "peerDependencies": {
164
- "@babel/core": "^7.0.0"
165
- }
166
- },
167
- "node_modules/@babel/helper-plugin-utils": {
168
- "version": "7.29.7",
169
- "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
170
- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
171
- "dev": true,
172
- "license": "MIT",
173
- "engines": {
174
- "node": ">=6.9.0"
175
- }
176
- },
177
- "node_modules/@babel/helper-string-parser": {
178
- "version": "7.29.7",
179
- "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
180
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
181
- "dev": true,
182
- "license": "MIT",
183
- "engines": {
184
- "node": ">=6.9.0"
185
- }
186
- },
187
- "node_modules/@babel/helper-validator-identifier": {
188
- "version": "7.29.7",
189
- "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
190
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
191
- "dev": true,
192
- "license": "MIT",
193
- "engines": {
194
- "node": ">=6.9.0"
195
- }
196
- },
197
- "node_modules/@babel/helper-validator-option": {
198
- "version": "7.29.7",
199
- "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
200
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
201
- "dev": true,
202
- "license": "MIT",
203
- "engines": {
204
- "node": ">=6.9.0"
205
- }
206
- },
207
- "node_modules/@babel/helpers": {
208
- "version": "7.29.7",
209
- "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz",
210
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
211
- "dev": true,
212
- "license": "MIT",
213
- "dependencies": {
214
- "@babel/template": "^7.29.7",
215
- "@babel/types": "^7.29.7"
216
- },
217
- "engines": {
218
- "node": ">=6.9.0"
219
- }
220
- },
221
- "node_modules/@babel/parser": {
222
- "version": "7.29.7",
223
- "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz",
224
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
225
- "dev": true,
226
- "license": "MIT",
227
- "dependencies": {
228
- "@babel/types": "^7.29.7"
229
- },
230
- "bin": {
231
- "parser": "bin/babel-parser.js"
232
- },
233
- "engines": {
234
- "node": ">=6.0.0"
235
- }
236
- },
237
- "node_modules/@babel/plugin-transform-react-jsx-self": {
238
- "version": "7.29.7",
239
- "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
240
- "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
241
- "dev": true,
242
- "license": "MIT",
243
- "dependencies": {
244
- "@babel/helper-plugin-utils": "^7.29.7"
245
- },
246
- "engines": {
247
- "node": ">=6.9.0"
248
- },
249
- "peerDependencies": {
250
- "@babel/core": "^7.0.0-0"
251
- }
252
- },
253
- "node_modules/@babel/plugin-transform-react-jsx-source": {
254
- "version": "7.29.7",
255
- "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
256
- "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
257
- "dev": true,
258
- "license": "MIT",
259
- "dependencies": {
260
- "@babel/helper-plugin-utils": "^7.29.7"
261
- },
262
- "engines": {
263
- "node": ">=6.9.0"
264
- },
265
- "peerDependencies": {
266
- "@babel/core": "^7.0.0-0"
267
- }
268
- },
269
- "node_modules/@babel/template": {
270
- "version": "7.29.7",
271
- "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz",
272
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
273
- "dev": true,
274
- "license": "MIT",
275
- "dependencies": {
276
- "@babel/code-frame": "^7.29.7",
277
- "@babel/parser": "^7.29.7",
278
- "@babel/types": "^7.29.7"
279
- },
280
- "engines": {
281
- "node": ">=6.9.0"
282
- }
283
- },
284
- "node_modules/@babel/traverse": {
285
- "version": "7.29.7",
286
- "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz",
287
- "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
288
- "dev": true,
289
- "license": "MIT",
290
- "dependencies": {
291
- "@babel/code-frame": "^7.29.7",
292
- "@babel/generator": "^7.29.7",
293
- "@babel/helper-globals": "^7.29.7",
294
- "@babel/parser": "^7.29.7",
295
- "@babel/template": "^7.29.7",
296
- "@babel/types": "^7.29.7",
297
- "debug": "^4.3.1"
298
- },
299
- "engines": {
300
- "node": ">=6.9.0"
301
- }
302
- },
303
- "node_modules/@babel/types": {
304
- "version": "7.29.7",
305
- "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz",
306
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
307
- "dev": true,
308
- "license": "MIT",
309
- "dependencies": {
310
- "@babel/helper-string-parser": "^7.29.7",
311
- "@babel/helper-validator-identifier": "^7.29.7"
312
- },
313
- "engines": {
314
- "node": ">=6.9.0"
315
- }
316
- },
317
- "node_modules/@esbuild/aix-ppc64": {
318
- "version": "0.25.12",
319
- "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
320
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
321
- "cpu": [
322
- "ppc64"
323
- ],
324
- "license": "MIT",
325
- "optional": true,
326
- "os": [
327
- "aix"
328
- ],
329
- "engines": {
330
- "node": ">=18"
331
- }
332
- },
333
- "node_modules/@esbuild/android-arm": {
334
- "version": "0.25.12",
335
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
336
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
337
- "cpu": [
338
- "arm"
339
- ],
340
- "license": "MIT",
341
- "optional": true,
342
- "os": [
343
- "android"
344
- ],
345
- "engines": {
346
- "node": ">=18"
347
- }
348
- },
349
- "node_modules/@esbuild/android-arm64": {
350
- "version": "0.25.12",
351
- "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
352
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
353
- "cpu": [
354
- "arm64"
355
- ],
356
- "license": "MIT",
357
- "optional": true,
358
- "os": [
359
- "android"
360
- ],
361
- "engines": {
362
- "node": ">=18"
363
- }
364
- },
365
- "node_modules/@esbuild/android-x64": {
366
- "version": "0.25.12",
367
- "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
368
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
369
- "cpu": [
370
- "x64"
371
- ],
372
- "license": "MIT",
373
- "optional": true,
374
- "os": [
375
- "android"
376
- ],
377
- "engines": {
378
- "node": ">=18"
379
- }
380
- },
381
- "node_modules/@esbuild/darwin-arm64": {
382
- "version": "0.25.12",
383
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
384
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
385
- "cpu": [
386
- "arm64"
387
- ],
388
- "license": "MIT",
389
- "optional": true,
390
- "os": [
391
- "darwin"
392
- ],
393
- "engines": {
394
- "node": ">=18"
395
- }
396
- },
397
- "node_modules/@esbuild/darwin-x64": {
398
- "version": "0.25.12",
399
- "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
400
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
401
- "cpu": [
402
- "x64"
403
- ],
404
- "license": "MIT",
405
- "optional": true,
406
- "os": [
407
- "darwin"
408
- ],
409
- "engines": {
410
- "node": ">=18"
411
- }
412
- },
413
- "node_modules/@esbuild/freebsd-arm64": {
414
- "version": "0.25.12",
415
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
416
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
417
- "cpu": [
418
- "arm64"
419
- ],
420
- "license": "MIT",
421
- "optional": true,
422
- "os": [
423
- "freebsd"
424
- ],
425
- "engines": {
426
- "node": ">=18"
427
- }
428
- },
429
- "node_modules/@esbuild/freebsd-x64": {
430
- "version": "0.25.12",
431
- "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
432
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
433
- "cpu": [
434
- "x64"
435
- ],
436
- "license": "MIT",
437
- "optional": true,
438
- "os": [
439
- "freebsd"
440
- ],
441
- "engines": {
442
- "node": ">=18"
443
- }
444
- },
445
- "node_modules/@esbuild/linux-arm": {
446
- "version": "0.25.12",
447
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
448
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
449
- "cpu": [
450
- "arm"
451
- ],
452
- "license": "MIT",
453
- "optional": true,
454
- "os": [
455
- "linux"
456
- ],
457
- "engines": {
458
- "node": ">=18"
459
- }
460
- },
461
- "node_modules/@esbuild/linux-arm64": {
462
- "version": "0.25.12",
463
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
464
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
465
- "cpu": [
466
- "arm64"
467
- ],
468
- "license": "MIT",
469
- "optional": true,
470
- "os": [
471
- "linux"
472
- ],
473
- "engines": {
474
- "node": ">=18"
475
- }
476
- },
477
- "node_modules/@esbuild/linux-ia32": {
478
- "version": "0.25.12",
479
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
480
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
481
- "cpu": [
482
- "ia32"
483
- ],
484
- "license": "MIT",
485
- "optional": true,
486
- "os": [
487
- "linux"
488
- ],
489
- "engines": {
490
- "node": ">=18"
491
- }
492
- },
493
- "node_modules/@esbuild/linux-loong64": {
494
- "version": "0.25.12",
495
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
496
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
497
- "cpu": [
498
- "loong64"
499
- ],
500
- "license": "MIT",
501
- "optional": true,
502
- "os": [
503
- "linux"
504
- ],
505
- "engines": {
506
- "node": ">=18"
507
- }
508
- },
509
- "node_modules/@esbuild/linux-mips64el": {
510
- "version": "0.25.12",
511
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
512
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
513
- "cpu": [
514
- "mips64el"
515
- ],
516
- "license": "MIT",
517
- "optional": true,
518
- "os": [
519
- "linux"
520
- ],
521
- "engines": {
522
- "node": ">=18"
523
- }
524
- },
525
- "node_modules/@esbuild/linux-ppc64": {
526
- "version": "0.25.12",
527
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
528
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
529
- "cpu": [
530
- "ppc64"
531
- ],
532
- "license": "MIT",
533
- "optional": true,
534
- "os": [
535
- "linux"
536
- ],
537
- "engines": {
538
- "node": ">=18"
539
- }
540
- },
541
- "node_modules/@esbuild/linux-riscv64": {
542
- "version": "0.25.12",
543
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
544
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
545
- "cpu": [
546
- "riscv64"
547
- ],
548
- "license": "MIT",
549
- "optional": true,
550
- "os": [
551
- "linux"
552
- ],
553
- "engines": {
554
- "node": ">=18"
555
- }
556
- },
557
- "node_modules/@esbuild/linux-s390x": {
558
- "version": "0.25.12",
559
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
560
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
561
- "cpu": [
562
- "s390x"
563
- ],
564
- "license": "MIT",
565
- "optional": true,
566
- "os": [
567
- "linux"
568
- ],
569
- "engines": {
570
- "node": ">=18"
571
- }
572
- },
573
- "node_modules/@esbuild/linux-x64": {
574
- "version": "0.25.12",
575
- "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
576
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
577
- "cpu": [
578
- "x64"
579
- ],
580
- "license": "MIT",
581
- "optional": true,
582
- "os": [
583
- "linux"
584
- ],
585
- "engines": {
586
- "node": ">=18"
587
- }
588
- },
589
- "node_modules/@esbuild/netbsd-arm64": {
590
- "version": "0.25.12",
591
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
592
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
593
- "cpu": [
594
- "arm64"
595
- ],
596
- "license": "MIT",
597
- "optional": true,
598
- "os": [
599
- "netbsd"
600
- ],
601
- "engines": {
602
- "node": ">=18"
603
- }
604
- },
605
- "node_modules/@esbuild/netbsd-x64": {
606
- "version": "0.25.12",
607
- "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
608
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
609
- "cpu": [
610
- "x64"
611
- ],
612
- "license": "MIT",
613
- "optional": true,
614
- "os": [
615
- "netbsd"
616
- ],
617
- "engines": {
618
- "node": ">=18"
619
- }
620
- },
621
- "node_modules/@esbuild/openbsd-arm64": {
622
- "version": "0.25.12",
623
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
624
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
625
- "cpu": [
626
- "arm64"
627
- ],
628
- "license": "MIT",
629
- "optional": true,
630
- "os": [
631
- "openbsd"
632
- ],
633
- "engines": {
634
- "node": ">=18"
635
- }
636
- },
637
- "node_modules/@esbuild/openbsd-x64": {
638
- "version": "0.25.12",
639
- "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
640
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
641
- "cpu": [
642
- "x64"
643
- ],
644
- "license": "MIT",
645
- "optional": true,
646
- "os": [
647
- "openbsd"
648
- ],
649
- "engines": {
650
- "node": ">=18"
651
- }
652
- },
653
- "node_modules/@esbuild/openharmony-arm64": {
654
- "version": "0.25.12",
655
- "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
656
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
657
- "cpu": [
658
- "arm64"
659
- ],
660
- "license": "MIT",
661
- "optional": true,
662
- "os": [
663
- "openharmony"
664
- ],
665
- "engines": {
666
- "node": ">=18"
667
- }
668
- },
669
- "node_modules/@esbuild/sunos-x64": {
670
- "version": "0.25.12",
671
- "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
672
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
673
- "cpu": [
674
- "x64"
675
- ],
676
- "license": "MIT",
677
- "optional": true,
678
- "os": [
679
- "sunos"
680
- ],
681
- "engines": {
682
- "node": ">=18"
683
- }
684
- },
685
- "node_modules/@esbuild/win32-arm64": {
686
- "version": "0.25.12",
687
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
688
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
689
- "cpu": [
690
- "arm64"
691
- ],
692
- "license": "MIT",
693
- "optional": true,
694
- "os": [
695
- "win32"
696
- ],
697
- "engines": {
698
- "node": ">=18"
699
- }
700
- },
701
- "node_modules/@esbuild/win32-ia32": {
702
- "version": "0.25.12",
703
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
704
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
705
- "cpu": [
706
- "ia32"
707
- ],
708
- "license": "MIT",
709
- "optional": true,
710
- "os": [
711
- "win32"
712
- ],
713
- "engines": {
714
- "node": ">=18"
715
- }
716
- },
717
- "node_modules/@esbuild/win32-x64": {
718
- "version": "0.25.12",
719
- "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
720
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
721
- "cpu": [
722
- "x64"
723
- ],
724
- "license": "MIT",
725
- "optional": true,
726
- "os": [
727
- "win32"
728
- ],
729
- "engines": {
730
- "node": ">=18"
731
- }
732
- },
733
- "node_modules/@jridgewell/gen-mapping": {
734
- "version": "0.3.13",
735
- "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
736
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
737
- "dev": true,
738
- "license": "MIT",
739
- "dependencies": {
740
- "@jridgewell/sourcemap-codec": "^1.5.0",
741
- "@jridgewell/trace-mapping": "^0.3.24"
742
- }
743
- },
744
- "node_modules/@jridgewell/remapping": {
745
- "version": "2.3.5",
746
- "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
747
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
748
- "dev": true,
749
- "license": "MIT",
750
- "dependencies": {
751
- "@jridgewell/gen-mapping": "^0.3.5",
752
- "@jridgewell/trace-mapping": "^0.3.24"
753
- }
754
- },
755
- "node_modules/@jridgewell/resolve-uri": {
756
- "version": "3.1.2",
757
- "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
758
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
759
- "dev": true,
760
- "license": "MIT",
761
- "engines": {
762
- "node": ">=6.0.0"
763
- }
764
- },
765
- "node_modules/@jridgewell/sourcemap-codec": {
766
- "version": "1.5.5",
767
- "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
768
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
769
- "dev": true,
770
- "license": "MIT"
771
- },
772
- "node_modules/@jridgewell/trace-mapping": {
773
- "version": "0.3.31",
774
- "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
775
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
776
- "dev": true,
777
- "license": "MIT",
778
- "dependencies": {
779
- "@jridgewell/resolve-uri": "^3.1.0",
780
- "@jridgewell/sourcemap-codec": "^1.4.14"
781
- }
782
- },
783
- "node_modules/@pixiv/three-vrm": {
784
- "version": "3.5.4",
785
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm/-/three-vrm-3.5.4.tgz",
786
- "integrity": "sha512-hY0MnmKVLUebFR9QN9vCRXo0rZmLpxjLVZAupQv5qDDGCKZuNxlOts9PqTkWlzhkMLLdhB48AF+P4PCy/DFOzA==",
787
- "license": "MIT",
788
- "dependencies": {
789
- "@pixiv/three-vrm-core": "3.5.4",
790
- "@pixiv/three-vrm-materials-hdr-emissive-multiplier": "3.5.4",
791
- "@pixiv/three-vrm-materials-mtoon": "3.5.4",
792
- "@pixiv/three-vrm-materials-v0compat": "3.5.4",
793
- "@pixiv/three-vrm-node-constraint": "3.5.4",
794
- "@pixiv/three-vrm-springbone": "3.5.4"
795
- },
796
- "peerDependencies": {
797
- "three": ">=0.137"
798
- }
799
- },
800
- "node_modules/@pixiv/three-vrm-animation": {
801
- "version": "3.5.4",
802
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-animation/-/three-vrm-animation-3.5.4.tgz",
803
- "integrity": "sha512-yVJL7LoVwZ0fsW2SJhfa0bqIMcfTzBKyc1q/LeKqAtEv63BEQ9VUudx5ikd4zIkgvXwjxw7vGl2zvedrexFisg==",
804
- "license": "MIT",
805
- "dependencies": {
806
- "@pixiv/three-vrm-core": "3.5.4",
807
- "@pixiv/types-vrmc-vrm-1.0": "3.5.4",
808
- "@pixiv/types-vrmc-vrm-animation-1.0": "3.5.4"
809
- },
810
- "peerDependencies": {
811
- "three": ">=0.137"
812
- }
813
- },
814
- "node_modules/@pixiv/three-vrm-core": {
815
- "version": "3.5.4",
816
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-core/-/three-vrm-core-3.5.4.tgz",
817
- "integrity": "sha512-CgaxZ4qX6JmE2oKsOVGGlheT011qP4bBZGUaMJJ5iQINI999+cveIdISe459B+AMYD1dU5h1+xCtDthLQNH1Bg==",
818
- "license": "MIT",
819
- "dependencies": {
820
- "@pixiv/types-vrm-0.0": "3.5.4",
821
- "@pixiv/types-vrmc-vrm-1.0": "3.5.4"
822
- },
823
- "peerDependencies": {
824
- "three": ">=0.137"
825
- }
826
- },
827
- "node_modules/@pixiv/three-vrm-materials-hdr-emissive-multiplier": {
828
- "version": "3.5.4",
829
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-hdr-emissive-multiplier/-/three-vrm-materials-hdr-emissive-multiplier-3.5.4.tgz",
830
- "integrity": "sha512-3BPZ42qW38cHhP+imqEnqTFsltvYkHA/t4VzWFdQ9sngWt0NiFAOotEsv8gZn1ZgKE9VRQezXne6s5aRWQlQqA==",
831
- "license": "MIT",
832
- "dependencies": {
833
- "@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": "3.5.4"
834
- },
835
- "peerDependencies": {
836
- "three": ">=0.137"
837
- }
838
- },
839
- "node_modules/@pixiv/three-vrm-materials-mtoon": {
840
- "version": "3.5.4",
841
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-mtoon/-/three-vrm-materials-mtoon-3.5.4.tgz",
842
- "integrity": "sha512-vLHt7IYZxlijbCMa5TRRf6gaQjA65F/d7oZBNfZW3XC8sBg57ZV0M/xBAJm2d6UwVmTbBd9NG9li/Y9yuupDQw==",
843
- "license": "MIT",
844
- "dependencies": {
845
- "@pixiv/types-vrm-0.0": "3.5.4",
846
- "@pixiv/types-vrmc-materials-mtoon-1.0": "3.5.4"
847
- },
848
- "peerDependencies": {
849
- "three": ">=0.137"
850
- }
851
- },
852
- "node_modules/@pixiv/three-vrm-materials-v0compat": {
853
- "version": "3.5.4",
854
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-v0compat/-/three-vrm-materials-v0compat-3.5.4.tgz",
855
- "integrity": "sha512-qRRtg8vYFBRJpsa3evruWqmXr0Gwd8uJXwxxUKuKtrIN0Hw0hJsRLHkg9E238Q8xRTnoycdkSgX4VvVknvtXBA==",
856
- "license": "MIT",
857
- "dependencies": {
858
- "@pixiv/types-vrm-0.0": "3.5.4",
859
- "@pixiv/types-vrmc-materials-mtoon-1.0": "3.5.4"
860
- },
861
- "peerDependencies": {
862
- "three": ">=0.137"
863
- }
864
- },
865
- "node_modules/@pixiv/three-vrm-node-constraint": {
866
- "version": "3.5.4",
867
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-node-constraint/-/three-vrm-node-constraint-3.5.4.tgz",
868
- "integrity": "sha512-nyAghDrYNp0Z2siEaY2+th+FzZdDs9EJy8K576Mpw9cy3jwtCJuTHBQmGzKkrgoLaBfrso+3Tdfgr62AJV0m1w==",
869
- "license": "MIT",
870
- "dependencies": {
871
- "@pixiv/types-vrmc-node-constraint-1.0": "3.5.4"
872
- },
873
- "peerDependencies": {
874
- "three": ">=0.137"
875
- }
876
- },
877
- "node_modules/@pixiv/three-vrm-springbone": {
878
- "version": "3.5.4",
879
- "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-springbone/-/three-vrm-springbone-3.5.4.tgz",
880
- "integrity": "sha512-8KWA7vHU+OnW6XcZPnDuUrjjPFRzmv2yxcTMNCYQvBS547/Iqo0RLbQvpO/ppceLTNJFY2TGXAWJU67aeazWjw==",
881
- "license": "MIT",
882
- "dependencies": {
883
- "@pixiv/types-vrm-0.0": "3.5.4",
884
- "@pixiv/types-vrmc-springbone-1.0": "3.5.4",
885
- "@pixiv/types-vrmc-springbone-extended-collider-1.0": "3.5.4"
886
- },
887
- "peerDependencies": {
888
- "three": ">=0.137"
889
- }
890
- },
891
- "node_modules/@pixiv/types-vrm-0.0": {
892
- "version": "3.5.4",
893
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrm-0.0/-/types-vrm-0.0-3.5.4.tgz",
894
- "integrity": "sha512-g9VMPikJxKJ/XgnhXxnqj6ejVhQ9WwOEUp7KyyZHsqRnHcQfSqyZFTs9wkAMCbwCqHXX/lg9+9Ogj8KhxCkbXw==",
895
- "license": "MIT"
896
- },
897
- "node_modules/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": {
898
- "version": "3.5.4",
899
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0/-/types-vrmc-materials-hdr-emissive-multiplier-1.0-3.5.4.tgz",
900
- "integrity": "sha512-h9GEQ3q1VTylL/P40kJ8uoQhfUYY54NhTG6Xsnl4X0jf7oHh8MKXYnMMn903hXml6082Yjx1C6x3DKckdnophg==",
901
- "license": "MIT"
902
- },
903
- "node_modules/@pixiv/types-vrmc-materials-mtoon-1.0": {
904
- "version": "3.5.4",
905
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-materials-mtoon-1.0/-/types-vrmc-materials-mtoon-1.0-3.5.4.tgz",
906
- "integrity": "sha512-AU5sOcsbmcnzRPfgfk7fzKhk5lNm2MqrbvtAsSlhylVMdlzrmXqIfRuexABcB9k+ysTp49qIFioot9KRlQcDUw==",
907
- "license": "MIT"
908
- },
909
- "node_modules/@pixiv/types-vrmc-node-constraint-1.0": {
910
- "version": "3.5.4",
911
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-node-constraint-1.0/-/types-vrmc-node-constraint-1.0-3.5.4.tgz",
912
- "integrity": "sha512-RRbK5NNvZv4ewRELezueCiDB11FGkt4pdZR/UJ027DJPaNzd6rls2OEJ8weQ3OmgHZpbx/BtdpX2JWX5WZZd/Q==",
913
- "license": "MIT"
914
- },
915
- "node_modules/@pixiv/types-vrmc-springbone-1.0": {
916
- "version": "3.5.4",
917
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-springbone-1.0/-/types-vrmc-springbone-1.0-3.5.4.tgz",
918
- "integrity": "sha512-NO7HTRBuWEe89Wo9BRI5hX1kWVkZzA4YWg9XALSTLdIT8HMiArL4NRse/CF91jNuLcTGfPEWlBJyq9SciAQxAg==",
919
- "license": "MIT"
920
- },
921
- "node_modules/@pixiv/types-vrmc-springbone-extended-collider-1.0": {
922
- "version": "3.5.4",
923
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-springbone-extended-collider-1.0/-/types-vrmc-springbone-extended-collider-1.0-3.5.4.tgz",
924
- "integrity": "sha512-uzJmcRh/iHYnZPtnq6N1+jb+T/ptYNCyFEoArkWGNXHSfPjTkMcheTQkL5iMNvTE6hqhyMRVMha9OmbvyLmmUQ==",
925
- "license": "MIT"
926
- },
927
- "node_modules/@pixiv/types-vrmc-vrm-1.0": {
928
- "version": "3.5.4",
929
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-vrm-1.0/-/types-vrmc-vrm-1.0-3.5.4.tgz",
930
- "integrity": "sha512-pkjT4QXT/Hp6rcq8J8EFEHIldbtPGYWOTaWdoRGMA8/KzJI5PdekZ6AUYbnSP6ElfJ3BSZ01DxxpQZa/UNsFqA==",
931
- "license": "MIT"
932
- },
933
- "node_modules/@pixiv/types-vrmc-vrm-animation-1.0": {
934
- "version": "3.5.4",
935
- "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-vrm-animation-1.0/-/types-vrmc-vrm-animation-1.0-3.5.4.tgz",
936
- "integrity": "sha512-qIl/RO+WnWcBlu/PdTpPtm+YRum8RNtNNigndd/V9dJ9mWeFVI6TThc5i+qbFQjTF/I5ka0HojfeujesIYc1IA==",
937
- "license": "MIT",
938
- "dependencies": {
939
- "@pixiv/types-vrmc-vrm-1.0": "3.5.4"
940
- }
941
- },
942
- "node_modules/@protobufjs/aspromise": {
943
- "version": "1.1.2",
944
- "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
945
- "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
946
- "license": "BSD-3-Clause"
947
- },
948
- "node_modules/@protobufjs/base64": {
949
- "version": "1.1.2",
950
- "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
951
- "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
952
- "license": "BSD-3-Clause"
953
- },
954
- "node_modules/@protobufjs/codegen": {
955
- "version": "2.0.5",
956
- "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.5.tgz",
957
- "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
958
- "license": "BSD-3-Clause"
959
- },
960
- "node_modules/@protobufjs/eventemitter": {
961
- "version": "1.1.1",
962
- "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
963
- "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
964
- "license": "BSD-3-Clause"
965
- },
966
- "node_modules/@protobufjs/fetch": {
967
- "version": "1.1.1",
968
- "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.1.tgz",
969
- "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
970
- "license": "BSD-3-Clause",
971
- "dependencies": {
972
- "@protobufjs/aspromise": "^1.1.1"
973
- }
974
- },
975
- "node_modules/@protobufjs/float": {
976
- "version": "1.0.2",
977
- "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
978
- "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
979
- "license": "BSD-3-Clause"
980
- },
981
- "node_modules/@protobufjs/path": {
982
- "version": "1.1.2",
983
- "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
984
- "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
985
- "license": "BSD-3-Clause"
986
- },
987
- "node_modules/@protobufjs/pool": {
988
- "version": "1.1.0",
989
- "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
990
- "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
991
- "license": "BSD-3-Clause"
992
- },
993
- "node_modules/@protobufjs/utf8": {
994
- "version": "1.1.1",
995
- "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.1.tgz",
996
- "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
997
- "license": "BSD-3-Clause"
998
- },
999
- "node_modules/@ricky0123/vad-web": {
1000
- "version": "0.0.30",
1001
- "resolved": "https://registry.npmmirror.com/@ricky0123/vad-web/-/vad-web-0.0.30.tgz",
1002
- "integrity": "sha512-cJyYrh4YeeUBJcbR9Bic/bFDyB9qBkAepvpuWM3vLxnAi7bC3VHzf51UeNdT+OtY4D7MLAgV8iJMc4z41ZnaWg==",
1003
- "license": "ISC",
1004
- "dependencies": {
1005
- "onnxruntime-web": "^1.17.0"
1006
- }
1007
- },
1008
- "node_modules/@rolldown/pluginutils": {
1009
- "version": "1.0.0-beta.27",
1010
- "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
1011
- "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
1012
- "dev": true,
1013
- "license": "MIT"
1014
- },
1015
- "node_modules/@rollup/rollup-android-arm-eabi": {
1016
- "version": "4.62.2",
1017
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
1018
- "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
1019
- "cpu": [
1020
- "arm"
1021
- ],
1022
- "license": "MIT",
1023
- "optional": true,
1024
- "os": [
1025
- "android"
1026
- ]
1027
- },
1028
- "node_modules/@rollup/rollup-android-arm64": {
1029
- "version": "4.62.2",
1030
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
1031
- "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
1032
- "cpu": [
1033
- "arm64"
1034
- ],
1035
- "license": "MIT",
1036
- "optional": true,
1037
- "os": [
1038
- "android"
1039
- ]
1040
- },
1041
- "node_modules/@rollup/rollup-darwin-arm64": {
1042
- "version": "4.62.2",
1043
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
1044
- "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
1045
- "cpu": [
1046
- "arm64"
1047
- ],
1048
- "license": "MIT",
1049
- "optional": true,
1050
- "os": [
1051
- "darwin"
1052
- ]
1053
- },
1054
- "node_modules/@rollup/rollup-darwin-x64": {
1055
- "version": "4.62.2",
1056
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
1057
- "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
1058
- "cpu": [
1059
- "x64"
1060
- ],
1061
- "license": "MIT",
1062
- "optional": true,
1063
- "os": [
1064
- "darwin"
1065
- ]
1066
- },
1067
- "node_modules/@rollup/rollup-freebsd-arm64": {
1068
- "version": "4.62.2",
1069
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
1070
- "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
1071
- "cpu": [
1072
- "arm64"
1073
- ],
1074
- "license": "MIT",
1075
- "optional": true,
1076
- "os": [
1077
- "freebsd"
1078
- ]
1079
- },
1080
- "node_modules/@rollup/rollup-freebsd-x64": {
1081
- "version": "4.62.2",
1082
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
1083
- "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
1084
- "cpu": [
1085
- "x64"
1086
- ],
1087
- "license": "MIT",
1088
- "optional": true,
1089
- "os": [
1090
- "freebsd"
1091
- ]
1092
- },
1093
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1094
- "version": "4.62.2",
1095
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
1096
- "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
1097
- "cpu": [
1098
- "arm"
1099
- ],
1100
- "libc": [
1101
- "glibc"
1102
- ],
1103
- "license": "MIT",
1104
- "optional": true,
1105
- "os": [
1106
- "linux"
1107
- ]
1108
- },
1109
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1110
- "version": "4.62.2",
1111
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
1112
- "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
1113
- "cpu": [
1114
- "arm"
1115
- ],
1116
- "libc": [
1117
- "musl"
1118
- ],
1119
- "license": "MIT",
1120
- "optional": true,
1121
- "os": [
1122
- "linux"
1123
- ]
1124
- },
1125
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
1126
- "version": "4.62.2",
1127
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
1128
- "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
1129
- "cpu": [
1130
- "arm64"
1131
- ],
1132
- "libc": [
1133
- "glibc"
1134
- ],
1135
- "license": "MIT",
1136
- "optional": true,
1137
- "os": [
1138
- "linux"
1139
- ]
1140
- },
1141
- "node_modules/@rollup/rollup-linux-arm64-musl": {
1142
- "version": "4.62.2",
1143
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
1144
- "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
1145
- "cpu": [
1146
- "arm64"
1147
- ],
1148
- "libc": [
1149
- "musl"
1150
- ],
1151
- "license": "MIT",
1152
- "optional": true,
1153
- "os": [
1154
- "linux"
1155
- ]
1156
- },
1157
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
1158
- "version": "4.62.2",
1159
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
1160
- "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
1161
- "cpu": [
1162
- "loong64"
1163
- ],
1164
- "libc": [
1165
- "glibc"
1166
- ],
1167
- "license": "MIT",
1168
- "optional": true,
1169
- "os": [
1170
- "linux"
1171
- ]
1172
- },
1173
- "node_modules/@rollup/rollup-linux-loong64-musl": {
1174
- "version": "4.62.2",
1175
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
1176
- "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
1177
- "cpu": [
1178
- "loong64"
1179
- ],
1180
- "libc": [
1181
- "musl"
1182
- ],
1183
- "license": "MIT",
1184
- "optional": true,
1185
- "os": [
1186
- "linux"
1187
- ]
1188
- },
1189
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1190
- "version": "4.62.2",
1191
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
1192
- "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
1193
- "cpu": [
1194
- "ppc64"
1195
- ],
1196
- "libc": [
1197
- "glibc"
1198
- ],
1199
- "license": "MIT",
1200
- "optional": true,
1201
- "os": [
1202
- "linux"
1203
- ]
1204
- },
1205
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
1206
- "version": "4.62.2",
1207
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
1208
- "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
1209
- "cpu": [
1210
- "ppc64"
1211
- ],
1212
- "libc": [
1213
- "musl"
1214
- ],
1215
- "license": "MIT",
1216
- "optional": true,
1217
- "os": [
1218
- "linux"
1219
- ]
1220
- },
1221
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1222
- "version": "4.62.2",
1223
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
1224
- "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
1225
- "cpu": [
1226
- "riscv64"
1227
- ],
1228
- "libc": [
1229
- "glibc"
1230
- ],
1231
- "license": "MIT",
1232
- "optional": true,
1233
- "os": [
1234
- "linux"
1235
- ]
1236
- },
1237
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
1238
- "version": "4.62.2",
1239
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
1240
- "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
1241
- "cpu": [
1242
- "riscv64"
1243
- ],
1244
- "libc": [
1245
- "musl"
1246
- ],
1247
- "license": "MIT",
1248
- "optional": true,
1249
- "os": [
1250
- "linux"
1251
- ]
1252
- },
1253
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
1254
- "version": "4.62.2",
1255
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
1256
- "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
1257
- "cpu": [
1258
- "s390x"
1259
- ],
1260
- "libc": [
1261
- "glibc"
1262
- ],
1263
- "license": "MIT",
1264
- "optional": true,
1265
- "os": [
1266
- "linux"
1267
- ]
1268
- },
1269
- "node_modules/@rollup/rollup-linux-x64-gnu": {
1270
- "version": "4.62.2",
1271
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
1272
- "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
1273
- "cpu": [
1274
- "x64"
1275
- ],
1276
- "libc": [
1277
- "glibc"
1278
- ],
1279
- "license": "MIT",
1280
- "optional": true,
1281
- "os": [
1282
- "linux"
1283
- ]
1284
- },
1285
- "node_modules/@rollup/rollup-linux-x64-musl": {
1286
- "version": "4.62.2",
1287
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
1288
- "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
1289
- "cpu": [
1290
- "x64"
1291
- ],
1292
- "libc": [
1293
- "musl"
1294
- ],
1295
- "license": "MIT",
1296
- "optional": true,
1297
- "os": [
1298
- "linux"
1299
- ]
1300
- },
1301
- "node_modules/@rollup/rollup-openbsd-x64": {
1302
- "version": "4.62.2",
1303
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
1304
- "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
1305
- "cpu": [
1306
- "x64"
1307
- ],
1308
- "license": "MIT",
1309
- "optional": true,
1310
- "os": [
1311
- "openbsd"
1312
- ]
1313
- },
1314
- "node_modules/@rollup/rollup-openharmony-arm64": {
1315
- "version": "4.62.2",
1316
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
1317
- "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
1318
- "cpu": [
1319
- "arm64"
1320
- ],
1321
- "license": "MIT",
1322
- "optional": true,
1323
- "os": [
1324
- "openharmony"
1325
- ]
1326
- },
1327
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
1328
- "version": "4.62.2",
1329
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
1330
- "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
1331
- "cpu": [
1332
- "arm64"
1333
- ],
1334
- "license": "MIT",
1335
- "optional": true,
1336
- "os": [
1337
- "win32"
1338
- ]
1339
- },
1340
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
1341
- "version": "4.62.2",
1342
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
1343
- "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
1344
- "cpu": [
1345
- "ia32"
1346
- ],
1347
- "license": "MIT",
1348
- "optional": true,
1349
- "os": [
1350
- "win32"
1351
- ]
1352
- },
1353
- "node_modules/@rollup/rollup-win32-x64-gnu": {
1354
- "version": "4.62.2",
1355
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
1356
- "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
1357
- "cpu": [
1358
- "x64"
1359
- ],
1360
- "license": "MIT",
1361
- "optional": true,
1362
- "os": [
1363
- "win32"
1364
- ]
1365
- },
1366
- "node_modules/@rollup/rollup-win32-x64-msvc": {
1367
- "version": "4.62.2",
1368
- "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
1369
- "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
1370
- "cpu": [
1371
- "x64"
1372
- ],
1373
- "license": "MIT",
1374
- "optional": true,
1375
- "os": [
1376
- "win32"
1377
- ]
1378
- },
1379
- "node_modules/@tauri-apps/api": {
1380
- "version": "2.11.1",
1381
- "resolved": "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.11.1.tgz",
1382
- "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==",
1383
- "license": "Apache-2.0 OR MIT",
1384
- "funding": {
1385
- "type": "opencollective",
1386
- "url": "https://opencollective.com/tauri"
1387
- }
1388
- },
1389
- "node_modules/@tauri-apps/cli": {
1390
- "version": "2.11.3",
1391
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-2.11.3.tgz",
1392
- "integrity": "sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ==",
1393
- "dev": true,
1394
- "license": "Apache-2.0 OR MIT",
1395
- "bin": {
1396
- "tauri": "tauri.js"
1397
- },
1398
- "engines": {
1399
- "node": ">= 10"
1400
- },
1401
- "funding": {
1402
- "type": "opencollective",
1403
- "url": "https://opencollective.com/tauri"
1404
- },
1405
- "optionalDependencies": {
1406
- "@tauri-apps/cli-darwin-arm64": "2.11.3",
1407
- "@tauri-apps/cli-darwin-x64": "2.11.3",
1408
- "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.3",
1409
- "@tauri-apps/cli-linux-arm64-gnu": "2.11.3",
1410
- "@tauri-apps/cli-linux-arm64-musl": "2.11.3",
1411
- "@tauri-apps/cli-linux-riscv64-gnu": "2.11.3",
1412
- "@tauri-apps/cli-linux-x64-gnu": "2.11.3",
1413
- "@tauri-apps/cli-linux-x64-musl": "2.11.3",
1414
- "@tauri-apps/cli-win32-arm64-msvc": "2.11.3",
1415
- "@tauri-apps/cli-win32-ia32-msvc": "2.11.3",
1416
- "@tauri-apps/cli-win32-x64-msvc": "2.11.3"
1417
- }
1418
- },
1419
- "node_modules/@tauri-apps/cli-darwin-arm64": {
1420
- "version": "2.11.3",
1421
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.3.tgz",
1422
- "integrity": "sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew==",
1423
- "cpu": [
1424
- "arm64"
1425
- ],
1426
- "dev": true,
1427
- "license": "Apache-2.0 OR MIT",
1428
- "optional": true,
1429
- "os": [
1430
- "darwin"
1431
- ],
1432
- "engines": {
1433
- "node": ">= 10"
1434
- }
1435
- },
1436
- "node_modules/@tauri-apps/cli-darwin-x64": {
1437
- "version": "2.11.3",
1438
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.3.tgz",
1439
- "integrity": "sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg==",
1440
- "cpu": [
1441
- "x64"
1442
- ],
1443
- "dev": true,
1444
- "license": "Apache-2.0 OR MIT",
1445
- "optional": true,
1446
- "os": [
1447
- "darwin"
1448
- ],
1449
- "engines": {
1450
- "node": ">= 10"
1451
- }
1452
- },
1453
- "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
1454
- "version": "2.11.3",
1455
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.3.tgz",
1456
- "integrity": "sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA==",
1457
- "cpu": [
1458
- "arm"
1459
- ],
1460
- "dev": true,
1461
- "license": "Apache-2.0 OR MIT",
1462
- "optional": true,
1463
- "os": [
1464
- "linux"
1465
- ],
1466
- "engines": {
1467
- "node": ">= 10"
1468
- }
1469
- },
1470
- "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
1471
- "version": "2.11.3",
1472
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.3.tgz",
1473
- "integrity": "sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA==",
1474
- "cpu": [
1475
- "arm64"
1476
- ],
1477
- "dev": true,
1478
- "libc": [
1479
- "glibc"
1480
- ],
1481
- "license": "Apache-2.0 OR MIT",
1482
- "optional": true,
1483
- "os": [
1484
- "linux"
1485
- ],
1486
- "engines": {
1487
- "node": ">= 10"
1488
- }
1489
- },
1490
- "node_modules/@tauri-apps/cli-linux-arm64-musl": {
1491
- "version": "2.11.3",
1492
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.3.tgz",
1493
- "integrity": "sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA==",
1494
- "cpu": [
1495
- "arm64"
1496
- ],
1497
- "dev": true,
1498
- "libc": [
1499
- "musl"
1500
- ],
1501
- "license": "Apache-2.0 OR MIT",
1502
- "optional": true,
1503
- "os": [
1504
- "linux"
1505
- ],
1506
- "engines": {
1507
- "node": ">= 10"
1508
- }
1509
- },
1510
- "node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
1511
- "version": "2.11.3",
1512
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.3.tgz",
1513
- "integrity": "sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw==",
1514
- "cpu": [
1515
- "riscv64"
1516
- ],
1517
- "dev": true,
1518
- "libc": [
1519
- "glibc"
1520
- ],
1521
- "license": "Apache-2.0 OR MIT",
1522
- "optional": true,
1523
- "os": [
1524
- "linux"
1525
- ],
1526
- "engines": {
1527
- "node": ">= 10"
1528
- }
1529
- },
1530
- "node_modules/@tauri-apps/cli-linux-x64-gnu": {
1531
- "version": "2.11.3",
1532
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.3.tgz",
1533
- "integrity": "sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA==",
1534
- "cpu": [
1535
- "x64"
1536
- ],
1537
- "dev": true,
1538
- "libc": [
1539
- "glibc"
1540
- ],
1541
- "license": "Apache-2.0 OR MIT",
1542
- "optional": true,
1543
- "os": [
1544
- "linux"
1545
- ],
1546
- "engines": {
1547
- "node": ">= 10"
1548
- }
1549
- },
1550
- "node_modules/@tauri-apps/cli-linux-x64-musl": {
1551
- "version": "2.11.3",
1552
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.3.tgz",
1553
- "integrity": "sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g==",
1554
- "cpu": [
1555
- "x64"
1556
- ],
1557
- "dev": true,
1558
- "libc": [
1559
- "musl"
1560
- ],
1561
- "license": "Apache-2.0 OR MIT",
1562
- "optional": true,
1563
- "os": [
1564
- "linux"
1565
- ],
1566
- "engines": {
1567
- "node": ">= 10"
1568
- }
1569
- },
1570
- "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
1571
- "version": "2.11.3",
1572
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.3.tgz",
1573
- "integrity": "sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w==",
1574
- "cpu": [
1575
- "arm64"
1576
- ],
1577
- "dev": true,
1578
- "license": "Apache-2.0 OR MIT",
1579
- "optional": true,
1580
- "os": [
1581
- "win32"
1582
- ],
1583
- "engines": {
1584
- "node": ">= 10"
1585
- }
1586
- },
1587
- "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
1588
- "version": "2.11.3",
1589
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.3.tgz",
1590
- "integrity": "sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg==",
1591
- "cpu": [
1592
- "ia32"
1593
- ],
1594
- "dev": true,
1595
- "license": "Apache-2.0 OR MIT",
1596
- "optional": true,
1597
- "os": [
1598
- "win32"
1599
- ],
1600
- "engines": {
1601
- "node": ">= 10"
1602
- }
1603
- },
1604
- "node_modules/@tauri-apps/cli-win32-x64-msvc": {
1605
- "version": "2.11.3",
1606
- "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.3.tgz",
1607
- "integrity": "sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww==",
1608
- "cpu": [
1609
- "x64"
1610
- ],
1611
- "dev": true,
1612
- "license": "Apache-2.0 OR MIT",
1613
- "optional": true,
1614
- "os": [
1615
- "win32"
1616
- ],
1617
- "engines": {
1618
- "node": ">= 10"
1619
- }
1620
- },
1621
- "node_modules/@tweenjs/tween.js": {
1622
- "version": "23.1.3",
1623
- "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
1624
- "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
1625
- "dev": true,
1626
- "license": "MIT"
1627
- },
1628
- "node_modules/@types/babel__core": {
1629
- "version": "7.20.5",
1630
- "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
1631
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1632
- "dev": true,
1633
- "license": "MIT",
1634
- "dependencies": {
1635
- "@babel/parser": "^7.20.7",
1636
- "@babel/types": "^7.20.7",
1637
- "@types/babel__generator": "*",
1638
- "@types/babel__template": "*",
1639
- "@types/babel__traverse": "*"
1640
- }
1641
- },
1642
- "node_modules/@types/babel__generator": {
1643
- "version": "7.27.0",
1644
- "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1645
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1646
- "dev": true,
1647
- "license": "MIT",
1648
- "dependencies": {
1649
- "@babel/types": "^7.0.0"
1650
- }
1651
- },
1652
- "node_modules/@types/babel__template": {
1653
- "version": "7.4.4",
1654
- "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz",
1655
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1656
- "dev": true,
1657
- "license": "MIT",
1658
- "dependencies": {
1659
- "@babel/parser": "^7.1.0",
1660
- "@babel/types": "^7.0.0"
1661
- }
1662
- },
1663
- "node_modules/@types/babel__traverse": {
1664
- "version": "7.28.0",
1665
- "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1666
- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1667
- "dev": true,
1668
- "license": "MIT",
1669
- "dependencies": {
1670
- "@babel/types": "^7.28.2"
1671
- }
1672
- },
1673
- "node_modules/@types/estree": {
1674
- "version": "1.0.9",
1675
- "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
1676
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1677
- "license": "MIT"
1678
- },
1679
- "node_modules/@types/node": {
1680
- "version": "26.0.0",
1681
- "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.0.0.tgz",
1682
- "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
1683
- "license": "MIT",
1684
- "dependencies": {
1685
- "undici-types": "~8.3.0"
1686
- }
1687
- },
1688
- "node_modules/@types/prop-types": {
1689
- "version": "15.7.15",
1690
- "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
1691
- "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1692
- "dev": true,
1693
- "license": "MIT"
1694
- },
1695
- "node_modules/@types/react": {
1696
- "version": "18.3.31",
1697
- "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
1698
- "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
1699
- "dev": true,
1700
- "license": "MIT",
1701
- "dependencies": {
1702
- "@types/prop-types": "*",
1703
- "csstype": "^3.2.2"
1704
- }
1705
- },
1706
- "node_modules/@types/react-dom": {
1707
- "version": "18.3.7",
1708
- "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
1709
- "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1710
- "dev": true,
1711
- "license": "MIT",
1712
- "peerDependencies": {
1713
- "@types/react": "^18.0.0"
1714
- }
1715
- },
1716
- "node_modules/@types/stats.js": {
1717
- "version": "0.17.4",
1718
- "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
1719
- "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
1720
- "dev": true,
1721
- "license": "MIT"
1722
- },
1723
- "node_modules/@types/three": {
1724
- "version": "0.175.0",
1725
- "resolved": "https://registry.npmmirror.com/@types/three/-/three-0.175.0.tgz",
1726
- "integrity": "sha512-ldMSBgtZOZ3g9kJ3kOZSEtZIEITmJOzu8eKVpkhf036GuNkM4mt0NXecrjCn5tMm1OblOF7dZehlaDypBfNokw==",
1727
- "dev": true,
1728
- "license": "MIT",
1729
- "dependencies": {
1730
- "@tweenjs/tween.js": "~23.1.3",
1731
- "@types/stats.js": "*",
1732
- "@types/webxr": "*",
1733
- "@webgpu/types": "*",
1734
- "fflate": "~0.8.2",
1735
- "meshoptimizer": "~0.18.1"
1736
- }
1737
- },
1738
- "node_modules/@types/webxr": {
1739
- "version": "0.5.24",
1740
- "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz",
1741
- "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
1742
- "dev": true,
1743
- "license": "MIT"
1744
- },
1745
- "node_modules/@vitejs/plugin-react": {
1746
- "version": "4.7.0",
1747
- "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1748
- "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1749
- "dev": true,
1750
- "license": "MIT",
1751
- "dependencies": {
1752
- "@babel/core": "^7.28.0",
1753
- "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1754
- "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1755
- "@rolldown/pluginutils": "1.0.0-beta.27",
1756
- "@types/babel__core": "^7.20.5",
1757
- "react-refresh": "^0.17.0"
1758
- },
1759
- "engines": {
1760
- "node": "^14.18.0 || >=16.0.0"
1761
- },
1762
- "peerDependencies": {
1763
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1764
- }
1765
- },
1766
- "node_modules/@webgpu/types": {
1767
- "version": "0.1.70",
1768
- "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.70.tgz",
1769
- "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==",
1770
- "dev": true,
1771
- "license": "BSD-3-Clause"
1772
- },
1773
- "node_modules/anymatch": {
1774
- "version": "3.1.3",
1775
- "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
1776
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
1777
- "license": "ISC",
1778
- "dependencies": {
1779
- "normalize-path": "^3.0.0",
1780
- "picomatch": "^2.0.4"
1781
- },
1782
- "engines": {
1783
- "node": ">= 8"
1784
- }
1785
- },
1786
- "node_modules/anymatch/node_modules/picomatch": {
1787
- "version": "2.3.2",
1788
- "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
1789
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1790
- "license": "MIT",
1791
- "engines": {
1792
- "node": ">=8.6"
1793
- },
1794
- "funding": {
1795
- "url": "https://github.com/sponsors/jonschlinkert"
1796
- }
1797
- },
1798
- "node_modules/baseline-browser-mapping": {
1799
- "version": "2.10.38",
1800
- "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
1801
- "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
1802
- "dev": true,
1803
- "license": "Apache-2.0",
1804
- "bin": {
1805
- "baseline-browser-mapping": "dist/cli.cjs"
1806
- },
1807
- "engines": {
1808
- "node": ">=6.0.0"
1809
- }
1810
- },
1811
- "node_modules/binary-extensions": {
1812
- "version": "2.3.0",
1813
- "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
1814
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
1815
- "license": "MIT",
1816
- "engines": {
1817
- "node": ">=8"
1818
- },
1819
- "funding": {
1820
- "url": "https://github.com/sponsors/sindresorhus"
1821
- }
1822
- },
1823
- "node_modules/braces": {
1824
- "version": "3.0.3",
1825
- "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
1826
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
1827
- "license": "MIT",
1828
- "dependencies": {
1829
- "fill-range": "^7.1.1"
1830
- },
1831
- "engines": {
1832
- "node": ">=8"
1833
- }
1834
- },
1835
- "node_modules/browserslist": {
1836
- "version": "4.28.2",
1837
- "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz",
1838
- "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1839
- "dev": true,
1840
- "funding": [
1841
- {
1842
- "type": "opencollective",
1843
- "url": "https://opencollective.com/browserslist"
1844
- },
1845
- {
1846
- "type": "tidelift",
1847
- "url": "https://tidelift.com/funding/github/npm/browserslist"
1848
- },
1849
- {
1850
- "type": "github",
1851
- "url": "https://github.com/sponsors/ai"
1852
- }
1853
- ],
1854
- "license": "MIT",
1855
- "dependencies": {
1856
- "baseline-browser-mapping": "^2.10.12",
1857
- "caniuse-lite": "^1.0.30001782",
1858
- "electron-to-chromium": "^1.5.328",
1859
- "node-releases": "^2.0.36",
1860
- "update-browserslist-db": "^1.2.3"
1861
- },
1862
- "bin": {
1863
- "browserslist": "cli.js"
1864
- },
1865
- "engines": {
1866
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1867
- }
1868
- },
1869
- "node_modules/caniuse-lite": {
1870
- "version": "1.0.30001799",
1871
- "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
1872
- "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
1873
- "dev": true,
1874
- "funding": [
1875
- {
1876
- "type": "opencollective",
1877
- "url": "https://opencollective.com/browserslist"
1878
- },
1879
- {
1880
- "type": "tidelift",
1881
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1882
- },
1883
- {
1884
- "type": "github",
1885
- "url": "https://github.com/sponsors/ai"
1886
- }
1887
- ],
1888
- "license": "CC-BY-4.0"
1889
- },
1890
- "node_modules/chokidar": {
1891
- "version": "3.6.0",
1892
- "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
1893
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
1894
- "license": "MIT",
1895
- "dependencies": {
1896
- "anymatch": "~3.1.2",
1897
- "braces": "~3.0.2",
1898
- "glob-parent": "~5.1.2",
1899
- "is-binary-path": "~2.1.0",
1900
- "is-glob": "~4.0.1",
1901
- "normalize-path": "~3.0.0",
1902
- "readdirp": "~3.6.0"
1903
- },
1904
- "engines": {
1905
- "node": ">= 8.10.0"
1906
- },
1907
- "funding": {
1908
- "url": "https://paulmillr.com/funding/"
1909
- },
1910
- "optionalDependencies": {
1911
- "fsevents": "~2.3.2"
1912
- }
1913
- },
1914
- "node_modules/convert-source-map": {
1915
- "version": "2.0.0",
1916
- "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
1917
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1918
- "dev": true,
1919
- "license": "MIT"
1920
- },
1921
- "node_modules/csstype": {
1922
- "version": "3.2.3",
1923
- "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
1924
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1925
- "dev": true,
1926
- "license": "MIT"
1927
- },
1928
- "node_modules/debug": {
1929
- "version": "4.4.3",
1930
- "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
1931
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1932
- "dev": true,
1933
- "license": "MIT",
1934
- "dependencies": {
1935
- "ms": "^2.1.3"
1936
- },
1937
- "engines": {
1938
- "node": ">=6.0"
1939
- },
1940
- "peerDependenciesMeta": {
1941
- "supports-color": {
1942
- "optional": true
1943
- }
1944
- }
1945
- },
1946
- "node_modules/electron-to-chromium": {
1947
- "version": "1.5.376",
1948
- "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz",
1949
- "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==",
1950
- "dev": true,
1951
- "license": "ISC"
1952
- },
1953
- "node_modules/esbuild": {
1954
- "version": "0.25.12",
1955
- "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
1956
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1957
- "hasInstallScript": true,
1958
- "license": "MIT",
1959
- "bin": {
1960
- "esbuild": "bin/esbuild"
1961
- },
1962
- "engines": {
1963
- "node": ">=18"
1964
- },
1965
- "optionalDependencies": {
1966
- "@esbuild/aix-ppc64": "0.25.12",
1967
- "@esbuild/android-arm": "0.25.12",
1968
- "@esbuild/android-arm64": "0.25.12",
1969
- "@esbuild/android-x64": "0.25.12",
1970
- "@esbuild/darwin-arm64": "0.25.12",
1971
- "@esbuild/darwin-x64": "0.25.12",
1972
- "@esbuild/freebsd-arm64": "0.25.12",
1973
- "@esbuild/freebsd-x64": "0.25.12",
1974
- "@esbuild/linux-arm": "0.25.12",
1975
- "@esbuild/linux-arm64": "0.25.12",
1976
- "@esbuild/linux-ia32": "0.25.12",
1977
- "@esbuild/linux-loong64": "0.25.12",
1978
- "@esbuild/linux-mips64el": "0.25.12",
1979
- "@esbuild/linux-ppc64": "0.25.12",
1980
- "@esbuild/linux-riscv64": "0.25.12",
1981
- "@esbuild/linux-s390x": "0.25.12",
1982
- "@esbuild/linux-x64": "0.25.12",
1983
- "@esbuild/netbsd-arm64": "0.25.12",
1984
- "@esbuild/netbsd-x64": "0.25.12",
1985
- "@esbuild/openbsd-arm64": "0.25.12",
1986
- "@esbuild/openbsd-x64": "0.25.12",
1987
- "@esbuild/openharmony-arm64": "0.25.12",
1988
- "@esbuild/sunos-x64": "0.25.12",
1989
- "@esbuild/win32-arm64": "0.25.12",
1990
- "@esbuild/win32-ia32": "0.25.12",
1991
- "@esbuild/win32-x64": "0.25.12"
1992
- }
1993
- },
1994
- "node_modules/escalade": {
1995
- "version": "3.2.0",
1996
- "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
1997
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1998
- "dev": true,
1999
- "license": "MIT",
2000
- "engines": {
2001
- "node": ">=6"
2002
- }
2003
- },
2004
- "node_modules/fdir": {
2005
- "version": "6.5.0",
2006
- "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
2007
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
2008
- "license": "MIT",
2009
- "engines": {
2010
- "node": ">=12.0.0"
2011
- },
2012
- "peerDependencies": {
2013
- "picomatch": "^3 || ^4"
2014
- },
2015
- "peerDependenciesMeta": {
2016
- "picomatch": {
2017
- "optional": true
2018
- }
2019
- }
2020
- },
2021
- "node_modules/fflate": {
2022
- "version": "0.8.3",
2023
- "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz",
2024
- "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
2025
- "dev": true,
2026
- "license": "MIT"
2027
- },
2028
- "node_modules/fill-range": {
2029
- "version": "7.1.1",
2030
- "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
2031
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
2032
- "license": "MIT",
2033
- "dependencies": {
2034
- "to-regex-range": "^5.0.1"
2035
- },
2036
- "engines": {
2037
- "node": ">=8"
2038
- }
2039
- },
2040
- "node_modules/flatbuffers": {
2041
- "version": "25.9.23",
2042
- "resolved": "https://registry.npmmirror.com/flatbuffers/-/flatbuffers-25.9.23.tgz",
2043
- "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
2044
- "license": "Apache-2.0"
2045
- },
2046
- "node_modules/fsevents": {
2047
- "version": "2.3.3",
2048
- "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
2049
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
2050
- "hasInstallScript": true,
2051
- "license": "MIT",
2052
- "optional": true,
2053
- "os": [
2054
- "darwin"
2055
- ],
2056
- "engines": {
2057
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
2058
- }
2059
- },
2060
- "node_modules/gensync": {
2061
- "version": "1.0.0-beta.2",
2062
- "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
2063
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
2064
- "dev": true,
2065
- "license": "MIT",
2066
- "engines": {
2067
- "node": ">=6.9.0"
2068
- }
2069
- },
2070
- "node_modules/glob-parent": {
2071
- "version": "5.1.2",
2072
- "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
2073
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
2074
- "license": "ISC",
2075
- "dependencies": {
2076
- "is-glob": "^4.0.1"
2077
- },
2078
- "engines": {
2079
- "node": ">= 6"
2080
- }
2081
- },
2082
- "node_modules/guid-typescript": {
2083
- "version": "1.0.9",
2084
- "resolved": "https://registry.npmmirror.com/guid-typescript/-/guid-typescript-1.0.9.tgz",
2085
- "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
2086
- "license": "ISC"
2087
- },
2088
- "node_modules/is-binary-path": {
2089
- "version": "2.1.0",
2090
- "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
2091
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
2092
- "license": "MIT",
2093
- "dependencies": {
2094
- "binary-extensions": "^2.0.0"
2095
- },
2096
- "engines": {
2097
- "node": ">=8"
2098
- }
2099
- },
2100
- "node_modules/is-extglob": {
2101
- "version": "2.1.1",
2102
- "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
2103
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
2104
- "license": "MIT",
2105
- "engines": {
2106
- "node": ">=0.10.0"
2107
- }
2108
- },
2109
- "node_modules/is-glob": {
2110
- "version": "4.0.3",
2111
- "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
2112
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
2113
- "license": "MIT",
2114
- "dependencies": {
2115
- "is-extglob": "^2.1.1"
2116
- },
2117
- "engines": {
2118
- "node": ">=0.10.0"
2119
- }
2120
- },
2121
- "node_modules/is-number": {
2122
- "version": "7.0.0",
2123
- "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
2124
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
2125
- "license": "MIT",
2126
- "engines": {
2127
- "node": ">=0.12.0"
2128
- }
2129
- },
2130
- "node_modules/js-tokens": {
2131
- "version": "4.0.0",
2132
- "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
2133
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
2134
- "license": "MIT"
2135
- },
2136
- "node_modules/jsesc": {
2137
- "version": "3.1.0",
2138
- "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
2139
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
2140
- "dev": true,
2141
- "license": "MIT",
2142
- "bin": {
2143
- "jsesc": "bin/jsesc"
2144
- },
2145
- "engines": {
2146
- "node": ">=6"
2147
- }
2148
- },
2149
- "node_modules/json5": {
2150
- "version": "2.2.3",
2151
- "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
2152
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
2153
- "dev": true,
2154
- "license": "MIT",
2155
- "bin": {
2156
- "json5": "lib/cli.js"
2157
- },
2158
- "engines": {
2159
- "node": ">=6"
2160
- }
2161
- },
2162
- "node_modules/long": {
2163
- "version": "5.3.2",
2164
- "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
2165
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
2166
- "license": "Apache-2.0"
2167
- },
2168
- "node_modules/loose-envify": {
2169
- "version": "1.4.0",
2170
- "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
2171
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
2172
- "license": "MIT",
2173
- "dependencies": {
2174
- "js-tokens": "^3.0.0 || ^4.0.0"
2175
- },
2176
- "bin": {
2177
- "loose-envify": "cli.js"
2178
- }
2179
- },
2180
- "node_modules/lru-cache": {
2181
- "version": "5.1.1",
2182
- "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
2183
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2184
- "dev": true,
2185
- "license": "ISC",
2186
- "dependencies": {
2187
- "yallist": "^3.0.2"
2188
- }
2189
- },
2190
- "node_modules/lucide-react": {
2191
- "version": "0.577.0",
2192
- "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.577.0.tgz",
2193
- "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
2194
- "license": "ISC",
2195
- "peerDependencies": {
2196
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2197
- }
2198
- },
2199
- "node_modules/marked": {
2200
- "version": "18.0.5",
2201
- "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.5.tgz",
2202
- "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
2203
- "license": "MIT",
2204
- "bin": {
2205
- "marked": "bin/marked.js"
2206
- },
2207
- "engines": {
2208
- "node": ">= 20"
2209
- }
2210
- },
2211
- "node_modules/meshoptimizer": {
2212
- "version": "0.18.1",
2213
- "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
2214
- "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
2215
- "dev": true,
2216
- "license": "MIT"
2217
- },
2218
- "node_modules/mmd-parser": {
2219
- "version": "1.0.4",
2220
- "resolved": "https://registry.npmmirror.com/mmd-parser/-/mmd-parser-1.0.4.tgz",
2221
- "integrity": "sha512-Qi0VCU46t2IwfGv5KF0+D/t9cizcDug7qnNoy9Ggk7aucp0tssV8IwTMkBlDbm+VqAf3cdQHTCARKSsuS2MYFg==",
2222
- "license": "MIT"
2223
- },
2224
- "node_modules/ms": {
2225
- "version": "2.1.3",
2226
- "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
2227
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2228
- "dev": true,
2229
- "license": "MIT"
2230
- },
2231
- "node_modules/nanoid": {
2232
- "version": "3.3.13",
2233
- "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.13.tgz",
2234
- "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==",
2235
- "funding": [
2236
- {
2237
- "type": "github",
2238
- "url": "https://github.com/sponsors/ai"
2239
- }
2240
- ],
2241
- "license": "MIT",
2242
- "bin": {
2243
- "nanoid": "bin/nanoid.cjs"
2244
- },
2245
- "engines": {
2246
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2247
- }
2248
- },
2249
- "node_modules/node-releases": {
2250
- "version": "2.0.48",
2251
- "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.48.tgz",
2252
- "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
2253
- "dev": true,
2254
- "license": "MIT",
2255
- "engines": {
2256
- "node": ">=18"
2257
- }
2258
- },
2259
- "node_modules/normalize-path": {
2260
- "version": "3.0.0",
2261
- "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
2262
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
2263
- "license": "MIT",
2264
- "engines": {
2265
- "node": ">=0.10.0"
2266
- }
2267
- },
2268
- "node_modules/onnxruntime-common": {
2269
- "version": "1.27.0",
2270
- "resolved": "https://registry.npmmirror.com/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz",
2271
- "integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==",
2272
- "license": "MIT"
2273
- },
2274
- "node_modules/onnxruntime-web": {
2275
- "version": "1.27.0",
2276
- "resolved": "https://registry.npmmirror.com/onnxruntime-web/-/onnxruntime-web-1.27.0.tgz",
2277
- "integrity": "sha512-ogDLsqIozHZwifPuN37OproAo0byX6t43/bP8GzeZWBWD6MOGExswFAx3up4NS/vvWBOg2u2PXomDt3rMmdQSg==",
2278
- "license": "MIT",
2279
- "dependencies": {
2280
- "flatbuffers": "^25.1.24",
2281
- "guid-typescript": "^1.0.9",
2282
- "long": "^5.2.3",
2283
- "onnxruntime-common": "1.27.0",
2284
- "platform": "^1.3.6",
2285
- "protobufjs": "^7.2.4"
2286
- }
2287
- },
2288
- "node_modules/p-map": {
2289
- "version": "7.0.4",
2290
- "resolved": "https://registry.npmmirror.com/p-map/-/p-map-7.0.4.tgz",
2291
- "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
2292
- "license": "MIT",
2293
- "engines": {
2294
- "node": ">=18"
2295
- },
2296
- "funding": {
2297
- "url": "https://github.com/sponsors/sindresorhus"
2298
- }
2299
- },
2300
- "node_modules/picocolors": {
2301
- "version": "1.1.1",
2302
- "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
2303
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2304
- "license": "ISC"
2305
- },
2306
- "node_modules/picomatch": {
2307
- "version": "4.0.4",
2308
- "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
2309
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2310
- "license": "MIT",
2311
- "engines": {
2312
- "node": ">=12"
2313
- },
2314
- "funding": {
2315
- "url": "https://github.com/sponsors/jonschlinkert"
2316
- }
2317
- },
2318
- "node_modules/platform": {
2319
- "version": "1.3.6",
2320
- "resolved": "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz",
2321
- "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
2322
- "license": "MIT"
2323
- },
2324
- "node_modules/postcss": {
2325
- "version": "8.5.15",
2326
- "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
2327
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
2328
- "funding": [
2329
- {
2330
- "type": "opencollective",
2331
- "url": "https://opencollective.com/postcss/"
2332
- },
2333
- {
2334
- "type": "tidelift",
2335
- "url": "https://tidelift.com/funding/github/npm/postcss"
2336
- },
2337
- {
2338
- "type": "github",
2339
- "url": "https://github.com/sponsors/ai"
2340
- }
2341
- ],
2342
- "license": "MIT",
2343
- "dependencies": {
2344
- "nanoid": "^3.3.12",
2345
- "picocolors": "^1.1.1",
2346
- "source-map-js": "^1.2.1"
2347
- },
2348
- "engines": {
2349
- "node": "^10 || ^12 || >=14"
2350
- }
2351
- },
2352
- "node_modules/protobufjs": {
2353
- "version": "7.6.4",
2354
- "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.6.4.tgz",
2355
- "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==",
2356
- "hasInstallScript": true,
2357
- "license": "BSD-3-Clause",
2358
- "dependencies": {
2359
- "@protobufjs/aspromise": "^1.1.2",
2360
- "@protobufjs/base64": "^1.1.2",
2361
- "@protobufjs/codegen": "^2.0.5",
2362
- "@protobufjs/eventemitter": "^1.1.1",
2363
- "@protobufjs/fetch": "^1.1.1",
2364
- "@protobufjs/float": "^1.0.2",
2365
- "@protobufjs/path": "^1.1.2",
2366
- "@protobufjs/pool": "^1.1.0",
2367
- "@protobufjs/utf8": "^1.1.1",
2368
- "@types/node": ">=13.7.0",
2369
- "long": "^5.3.2"
2370
- },
2371
- "engines": {
2372
- "node": ">=12.0.0"
2373
- }
2374
- },
2375
- "node_modules/react": {
2376
- "version": "18.3.1",
2377
- "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
2378
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
2379
- "license": "MIT",
2380
- "dependencies": {
2381
- "loose-envify": "^1.1.0"
2382
- },
2383
- "engines": {
2384
- "node": ">=0.10.0"
2385
- }
2386
- },
2387
- "node_modules/react-dom": {
2388
- "version": "18.3.1",
2389
- "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
2390
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
2391
- "license": "MIT",
2392
- "dependencies": {
2393
- "loose-envify": "^1.1.0",
2394
- "scheduler": "^0.23.2"
2395
- },
2396
- "peerDependencies": {
2397
- "react": "^18.3.1"
2398
- }
2399
- },
2400
- "node_modules/react-refresh": {
2401
- "version": "0.17.0",
2402
- "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
2403
- "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
2404
- "dev": true,
2405
- "license": "MIT",
2406
- "engines": {
2407
- "node": ">=0.10.0"
2408
- }
2409
- },
2410
- "node_modules/readdirp": {
2411
- "version": "3.6.0",
2412
- "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
2413
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
2414
- "license": "MIT",
2415
- "dependencies": {
2416
- "picomatch": "^2.2.1"
2417
- },
2418
- "engines": {
2419
- "node": ">=8.10.0"
2420
- }
2421
- },
2422
- "node_modules/readdirp/node_modules/picomatch": {
2423
- "version": "2.3.2",
2424
- "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
2425
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
2426
- "license": "MIT",
2427
- "engines": {
2428
- "node": ">=8.6"
2429
- },
2430
- "funding": {
2431
- "url": "https://github.com/sponsors/jonschlinkert"
2432
- }
2433
- },
2434
- "node_modules/rollup": {
2435
- "version": "4.62.2",
2436
- "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz",
2437
- "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
2438
- "license": "MIT",
2439
- "dependencies": {
2440
- "@types/estree": "1.0.9"
2441
- },
2442
- "bin": {
2443
- "rollup": "dist/bin/rollup"
2444
- },
2445
- "engines": {
2446
- "node": ">=18.0.0",
2447
- "npm": ">=8.0.0"
2448
- },
2449
- "optionalDependencies": {
2450
- "@rollup/rollup-android-arm-eabi": "4.62.2",
2451
- "@rollup/rollup-android-arm64": "4.62.2",
2452
- "@rollup/rollup-darwin-arm64": "4.62.2",
2453
- "@rollup/rollup-darwin-x64": "4.62.2",
2454
- "@rollup/rollup-freebsd-arm64": "4.62.2",
2455
- "@rollup/rollup-freebsd-x64": "4.62.2",
2456
- "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
2457
- "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
2458
- "@rollup/rollup-linux-arm64-gnu": "4.62.2",
2459
- "@rollup/rollup-linux-arm64-musl": "4.62.2",
2460
- "@rollup/rollup-linux-loong64-gnu": "4.62.2",
2461
- "@rollup/rollup-linux-loong64-musl": "4.62.2",
2462
- "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
2463
- "@rollup/rollup-linux-ppc64-musl": "4.62.2",
2464
- "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
2465
- "@rollup/rollup-linux-riscv64-musl": "4.62.2",
2466
- "@rollup/rollup-linux-s390x-gnu": "4.62.2",
2467
- "@rollup/rollup-linux-x64-gnu": "4.62.2",
2468
- "@rollup/rollup-linux-x64-musl": "4.62.2",
2469
- "@rollup/rollup-openbsd-x64": "4.62.2",
2470
- "@rollup/rollup-openharmony-arm64": "4.62.2",
2471
- "@rollup/rollup-win32-arm64-msvc": "4.62.2",
2472
- "@rollup/rollup-win32-ia32-msvc": "4.62.2",
2473
- "@rollup/rollup-win32-x64-gnu": "4.62.2",
2474
- "@rollup/rollup-win32-x64-msvc": "4.62.2",
2475
- "fsevents": "~2.3.2"
2476
- }
2477
- },
2478
- "node_modules/scheduler": {
2479
- "version": "0.23.2",
2480
- "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
2481
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
2482
- "license": "MIT",
2483
- "dependencies": {
2484
- "loose-envify": "^1.1.0"
2485
- }
2486
- },
2487
- "node_modules/semver": {
2488
- "version": "6.3.1",
2489
- "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
2490
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2491
- "dev": true,
2492
- "license": "ISC",
2493
- "bin": {
2494
- "semver": "bin/semver.js"
2495
- }
2496
- },
2497
- "node_modules/source-map-js": {
2498
- "version": "1.2.1",
2499
- "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
2500
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2501
- "license": "BSD-3-Clause",
2502
- "engines": {
2503
- "node": ">=0.10.0"
2504
- }
2505
- },
2506
- "node_modules/three": {
2507
- "version": "0.175.0",
2508
- "resolved": "https://registry.npmmirror.com/three/-/three-0.175.0.tgz",
2509
- "integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==",
2510
- "license": "MIT"
2511
- },
2512
- "node_modules/tinyglobby": {
2513
- "version": "0.2.17",
2514
- "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
2515
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
2516
- "license": "MIT",
2517
- "dependencies": {
2518
- "fdir": "^6.5.0",
2519
- "picomatch": "^4.0.4"
2520
- },
2521
- "engines": {
2522
- "node": ">=12.0.0"
2523
- },
2524
- "funding": {
2525
- "url": "https://github.com/sponsors/SuperchupuDev"
2526
- }
2527
- },
2528
- "node_modules/to-regex-range": {
2529
- "version": "5.0.1",
2530
- "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
2531
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
2532
- "license": "MIT",
2533
- "dependencies": {
2534
- "is-number": "^7.0.0"
2535
- },
2536
- "engines": {
2537
- "node": ">=8.0"
2538
- }
2539
- },
2540
- "node_modules/typescript": {
2541
- "version": "5.9.3",
2542
- "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
2543
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
2544
- "dev": true,
2545
- "license": "Apache-2.0",
2546
- "bin": {
2547
- "tsc": "bin/tsc",
2548
- "tsserver": "bin/tsserver"
2549
- },
2550
- "engines": {
2551
- "node": ">=14.17"
2552
- }
2553
- },
2554
- "node_modules/undici-types": {
2555
- "version": "8.3.0",
2556
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.3.0.tgz",
2557
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
2558
- "license": "MIT"
2559
- },
2560
- "node_modules/update-browserslist-db": {
2561
- "version": "1.2.3",
2562
- "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2563
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2564
- "dev": true,
2565
- "funding": [
2566
- {
2567
- "type": "opencollective",
2568
- "url": "https://opencollective.com/browserslist"
2569
- },
2570
- {
2571
- "type": "tidelift",
2572
- "url": "https://tidelift.com/funding/github/npm/browserslist"
2573
- },
2574
- {
2575
- "type": "github",
2576
- "url": "https://github.com/sponsors/ai"
2577
- }
2578
- ],
2579
- "license": "MIT",
2580
- "dependencies": {
2581
- "escalade": "^3.2.0",
2582
- "picocolors": "^1.1.1"
2583
- },
2584
- "bin": {
2585
- "update-browserslist-db": "cli.js"
2586
- },
2587
- "peerDependencies": {
2588
- "browserslist": ">= 4.21.0"
2589
- }
2590
- },
2591
- "node_modules/vite": {
2592
- "version": "6.4.3",
2593
- "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz",
2594
- "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
2595
- "license": "MIT",
2596
- "dependencies": {
2597
- "esbuild": "^0.25.0",
2598
- "fdir": "^6.4.4",
2599
- "picomatch": "^4.0.2",
2600
- "postcss": "^8.5.3",
2601
- "rollup": "^4.34.9",
2602
- "tinyglobby": "^0.2.13"
2603
- },
2604
- "bin": {
2605
- "vite": "bin/vite.js"
2606
- },
2607
- "engines": {
2608
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2609
- },
2610
- "funding": {
2611
- "url": "https://github.com/vitejs/vite?sponsor=1"
2612
- },
2613
- "optionalDependencies": {
2614
- "fsevents": "~2.3.3"
2615
- },
2616
- "peerDependencies": {
2617
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2618
- "jiti": ">=1.21.0",
2619
- "less": "*",
2620
- "lightningcss": "^1.21.0",
2621
- "sass": "*",
2622
- "sass-embedded": "*",
2623
- "stylus": "*",
2624
- "sugarss": "*",
2625
- "terser": "^5.16.0",
2626
- "tsx": "^4.8.1",
2627
- "yaml": "^2.4.2"
2628
- },
2629
- "peerDependenciesMeta": {
2630
- "@types/node": {
2631
- "optional": true
2632
- },
2633
- "jiti": {
2634
- "optional": true
2635
- },
2636
- "less": {
2637
- "optional": true
2638
- },
2639
- "lightningcss": {
2640
- "optional": true
2641
- },
2642
- "sass": {
2643
- "optional": true
2644
- },
2645
- "sass-embedded": {
2646
- "optional": true
2647
- },
2648
- "stylus": {
2649
- "optional": true
2650
- },
2651
- "sugarss": {
2652
- "optional": true
2653
- },
2654
- "terser": {
2655
- "optional": true
2656
- },
2657
- "tsx": {
2658
- "optional": true
2659
- },
2660
- "yaml": {
2661
- "optional": true
2662
- }
2663
- }
2664
- },
2665
- "node_modules/vite-plugin-static-copy": {
2666
- "version": "4.1.1",
2667
- "resolved": "https://registry.npmmirror.com/vite-plugin-static-copy/-/vite-plugin-static-copy-4.1.1.tgz",
2668
- "integrity": "sha512-GrlA8YklrAfSyxJ4M3fdQLOo9oNkp56IM9FYgX/WtEgeIFkPwhu4wzpufBCIuNKCa6Fn77FkRdYxkHqV0FwjAw==",
2669
- "license": "MIT",
2670
- "dependencies": {
2671
- "chokidar": "^3.6.0",
2672
- "p-map": "^7.0.4",
2673
- "picocolors": "^1.1.1",
2674
- "tinyglobby": "^0.2.17"
2675
- },
2676
- "engines": {
2677
- "node": "^22.0.0 || >=24.0.0"
2678
- },
2679
- "funding": {
2680
- "type": "github",
2681
- "url": "https://github.com/sponsors/sapphi-red"
2682
- },
2683
- "peerDependencies": {
2684
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
2685
- }
2686
- },
2687
- "node_modules/wlipsync": {
2688
- "version": "1.3.0",
2689
- "resolved": "https://registry.npmmirror.com/wlipsync/-/wlipsync-1.3.0.tgz",
2690
- "integrity": "sha512-wYF85QAWCQE2rma9qoXok5we77XouCKxNh0HvbzRAtSIhc/QZbSh2rh5vKgWYHD4jrTE8+CwaxvU9oEUqYO9Zw==",
2691
- "license": "MIT"
2692
- },
2693
- "node_modules/yallist": {
2694
- "version": "3.1.1",
2695
- "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
2696
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2697
- "dev": true,
2698
- "license": "ISC"
2699
- }
2700
- }
2701
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/package.json DELETED
@@ -1,34 +0,0 @@
1
- {
2
- "name": "friend-vrm-frontend",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "vite build"
9
- },
10
- "dependencies": {
11
- "@pixiv/three-vrm": "^3.4.0",
12
- "@pixiv/three-vrm-animation": "^3.5.0",
13
- "@pixiv/three-vrm-core": "^3.5.1",
14
- "@ricky0123/vad-web": "^0.0.30",
15
- "@tauri-apps/api": "^2.11.1",
16
- "lucide-react": "^0.577.0",
17
- "marked": "^18.0.5",
18
- "mmd-parser": "^1.0.4",
19
- "react": "^18.3.1",
20
- "react-dom": "^18.3.1",
21
- "three": "^0.175.0",
22
- "vite-plugin-static-copy": "^4.1.1",
23
- "wlipsync": "^1.3.0"
24
- },
25
- "devDependencies": {
26
- "@tauri-apps/cli": "^2.11.3",
27
- "@types/react": "^18.3.23",
28
- "@types/react-dom": "^18.3.7",
29
- "@types/three": "^0.175.0",
30
- "@vitejs/plugin-react": "^4.5.0",
31
- "typescript": "^5.8.3",
32
- "vite": "^6.3.2"
33
- }
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/components/friend/frontend/public/akimbo.vrma DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:6c9f7f0cbb37a9404800583dccfdd8e03b66b73b806c3d80265c5c7690710324
3
- size 289136
 
 
 
 
src/components/friend/frontend/public/angry.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:bfc3a0d10d3e1918636a1151216098651680d9f9be41550fd0d725a5d639d884
3
- size 1688752
 
 
 
 
src/components/friend/frontend/public/angryPump.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:04e7ec62c958914f69df388a3ed8bd8e11cf14d48bbace28eda47e764374654f
3
- size 632864
 
 
 
 
src/components/friend/frontend/public/excited.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:cdd3f99b18177a12b49bfd3731091d981bea0863282391fbd3f1229f08d16cda
3
- size 826000
 
 
 
 
src/components/friend/frontend/public/greeting.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8a345411dd77faa101e6ac1bc977f6362be05382af6c811643ce7f2fc2c5d169
3
- size 711008
 
 
 
 
src/components/friend/frontend/public/happy.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a656a5da7309bde2e1159ef9e5bf35f3ce0d8088b3aadbfbb5eb99923bbe2c94
3
- size 1095680
 
 
 
 
src/components/friend/frontend/public/idle_loop.vrma DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:ace95ba6dcc0bdf2ed1081c002332b4184441117c8d543b6f642b3d2c5cf99be
3
- size 157664
 
 
 
 
src/components/friend/frontend/public/jile.vmd DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:5963cad3d5dd6ec43b1974816903694b3214a89443e06afc4b9633c898a5b42f
3
- size 1994161
 
 
 
 
src/components/friend/frontend/public/love.vmd DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:a6aff9a97ab4e5073c4fa28db84422dcce6276e3027127bd9c98bc8ad797c766
3
- size 3116347
 
 
 
 
src/components/friend/frontend/public/playFingers.vrma DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f2ef8e98d7c4ea4f4d0b5f0973b5ae14a11f006981575913671416f4349a9caa
3
- size 142692
 
 
 
 
src/components/friend/frontend/public/point.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:863d7bf87428bbcb7d6a4d77e1b4fe1b9d7263cf3cde5ba5679fe431028ec5b4
3
- size 538352
 
 
 
 
src/components/friend/frontend/public/salute.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b9fd250487f99bb0bc39cfa0a2d83e0891b8ada4939437050b15bb4e6c33fd49
3
- size 595056
 
 
 
 
src/components/friend/frontend/public/scratchHead.vrma DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:31ef24bc1bb2766a609a292157c3685e79dc844e532a4fa499bc1975650e2e4e
3
- size 183472
 
 
 
 
src/components/friend/frontend/public/shy.fbx DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:6f3dd889a485efcb9b549f3c060600e329baf259e0971b16bc711cca898b2718
3
- size 489216
 
 
 
 
src/components/friend/frontend/public/stretch.vrma DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:4f5ab38707bc04802888644af6d4e8935c7cf23160ac129153f8c6b6f9ff9176
3
- size 136032
 
 
 
 
src/components/friend/frontend/src-tauri/Cargo.lock DELETED
The diff for this file is too large to render. See raw diff