refactor: remove friend feature, local whisper, doubao & anthropic STT; consolidate voice to Groq
Browse filesRemove 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>
- assets/friend/friend_use.png +0 -3
- assets/friend/prompt_format.png +0 -3
- docs/friend/architecture.md +0 -419
- docs/friend/data-flow.md +0 -369
- docs/friend/emotion-map.md +0 -195
- docs/friend/frontend-3d.md +0 -573
- docs/friend/overview.md +0 -138
- docs/friend/voice-vad.md +0 -527
- package.json +1 -3
- scripts/build.ts +0 -27
- scripts/speak.py +0 -42
- scripts/transcribe.py +0 -81
- scripts/whisper_server.py +0 -80
- src/commands.ts +0 -2
- src/commands/friend/friend.tsx +0 -156
- src/commands/friend/index.ts +0 -11
- src/components/friend/frontend/App.tsx +0 -352
- src/components/friend/frontend/api.ts +0 -51
- src/components/friend/frontend/assets/lip-sync-profile.json +0 -1
- src/components/friend/frontend/components/ChatInput.tsx +0 -215
- src/components/friend/frontend/components/ResizeHandles.tsx +0 -7
- src/components/friend/frontend/components/SettingsPanel.tsx +0 -657
- src/components/friend/frontend/components/TextBubble.tsx +0 -609
- src/components/friend/frontend/components/VRMScene.tsx +0 -697
- src/components/friend/frontend/emote.ts +0 -206
- src/components/friend/frontend/hooks/usePassThrough.ts +0 -12
- src/components/friend/frontend/hooks/useServerStt.ts +0 -137
- src/components/friend/frontend/index.html +0 -63
- src/components/friend/frontend/lip-sync.ts +0 -192
- src/components/friend/frontend/main.tsx +0 -15
- src/components/friend/frontend/mixamo-loader.ts +0 -141
- src/components/friend/frontend/motion-controller.ts +0 -331
- src/components/friend/frontend/package-lock.json +0 -2701
- src/components/friend/frontend/package.json +0 -34
- src/components/friend/frontend/public/akimbo.vrma +0 -3
- src/components/friend/frontend/public/angry.fbx +0 -3
- src/components/friend/frontend/public/angryPump.fbx +0 -3
- src/components/friend/frontend/public/excited.fbx +0 -3
- src/components/friend/frontend/public/greeting.fbx +0 -3
- src/components/friend/frontend/public/happy.fbx +0 -3
- src/components/friend/frontend/public/idle_loop.vrma +0 -3
- src/components/friend/frontend/public/jile.vmd +0 -3
- src/components/friend/frontend/public/love.vmd +0 -3
- src/components/friend/frontend/public/playFingers.vrma +0 -3
- src/components/friend/frontend/public/point.fbx +0 -3
- src/components/friend/frontend/public/salute.fbx +0 -3
- src/components/friend/frontend/public/scratchHead.vrma +0 -3
- src/components/friend/frontend/public/shy.fbx +0 -3
- src/components/friend/frontend/public/stretch.vrma +0 -3
- src/components/friend/frontend/src-tauri/Cargo.lock +0 -0
Git LFS Details
|
Git LFS Details
|
|
@@ -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 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
此系统防止角色长时间静止不动,增加生动感。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 条消息,支持拖拽移动位置。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
| 文字输入 | 不适用 | 不适用 | 键盘 | 不适用 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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",
|
|
@@ -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()
|
|
@@ -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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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,
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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] }] }] }
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:6c9f7f0cbb37a9404800583dccfdd8e03b66b73b806c3d80265c5c7690710324
|
| 3 |
-
size 289136
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:bfc3a0d10d3e1918636a1151216098651680d9f9be41550fd0d725a5d639d884
|
| 3 |
-
size 1688752
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:04e7ec62c958914f69df388a3ed8bd8e11cf14d48bbace28eda47e764374654f
|
| 3 |
-
size 632864
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:cdd3f99b18177a12b49bfd3731091d981bea0863282391fbd3f1229f08d16cda
|
| 3 |
-
size 826000
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:8a345411dd77faa101e6ac1bc977f6362be05382af6c811643ce7f2fc2c5d169
|
| 3 |
-
size 711008
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:a656a5da7309bde2e1159ef9e5bf35f3ce0d8088b3aadbfbb5eb99923bbe2c94
|
| 3 |
-
size 1095680
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:ace95ba6dcc0bdf2ed1081c002332b4184441117c8d543b6f642b3d2c5cf99be
|
| 3 |
-
size 157664
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:5963cad3d5dd6ec43b1974816903694b3214a89443e06afc4b9633c898a5b42f
|
| 3 |
-
size 1994161
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:a6aff9a97ab4e5073c4fa28db84422dcce6276e3027127bd9c98bc8ad797c766
|
| 3 |
-
size 3116347
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:f2ef8e98d7c4ea4f4d0b5f0973b5ae14a11f006981575913671416f4349a9caa
|
| 3 |
-
size 142692
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:863d7bf87428bbcb7d6a4d77e1b4fe1b9d7263cf3cde5ba5679fe431028ec5b4
|
| 3 |
-
size 538352
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:b9fd250487f99bb0bc39cfa0a2d83e0891b8ada4939437050b15bb4e6c33fd49
|
| 3 |
-
size 595056
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:31ef24bc1bb2766a609a292157c3685e79dc844e532a4fa499bc1975650e2e4e
|
| 3 |
-
size 183472
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:6f3dd889a485efcb9b549f3c060600e329baf259e0971b16bc711cca898b2718
|
| 3 |
-
size 489216
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:4f5ab38707bc04802888644af6d4e8935c7cf23160ac129153f8c6b6f9ff9176
|
| 3 |
-
size 136032
|
|
|
|
|
|
|
|
|
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|