diff --git a/assets/friend/friend_use.png b/assets/friend/friend_use.png deleted file mode 100644 index b7316517f6289d24865b2328e778e90341f5e352..0000000000000000000000000000000000000000 --- a/assets/friend/friend_use.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4cbec71c829ec3f5d408c15f142e0cb91f5057aeaee6ea51a88c2e1d752505f7 -size 5650465 diff --git a/assets/friend/prompt_format.png b/assets/friend/prompt_format.png deleted file mode 100644 index 0cc8df85ccc99797687c2ccf75e0ee289d31c96e..0000000000000000000000000000000000000000 --- a/assets/friend/prompt_format.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfcb31e3ef93a8bf621f551c016237529a494c52fb6d59f9910d16a5bba4b215 -size 3171882 diff --git a/docs/friend/architecture.md b/docs/friend/architecture.md deleted file mode 100644 index df02ba3b0a30a57fea5931a9b61b8a7947c0ef3e..0000000000000000000000000000000000000000 --- a/docs/friend/architecture.md +++ /dev/null @@ -1,419 +0,0 @@ -# Friend 系统架构 - -本文档详细描述 Friend VRM 桌面伴侣系统的各个组件架构。 - ---- - -## 1. FriendService — 核心编排器 - -**文件**: `src/friend/FriendService.ts` (~31KB, 897 行) - -### 1.1 生命周期管理 - -``` -start() ──► 状态: 'starting' - │ - ├── 初始化 Silero VAD (非致命失败,失败后语音捕获降级为 F2-only) - │ └── 配置: threshold 0.75, preSpeechTriggerFrames 10, redemptionFrames 20 - │ - └── 状态: 'running' - -stop() ──► 停止语音捕获 → 清理 STT 连接 → 状态: 'stopped' -``` - -### 1.2 React external store 接口 - -FriendService 实现了类似 React `useSyncExternalStore` 的接口: - -```typescript -subscribe(listener: Listener): () => void // 添加状态监听 -subscribeToInbound(listener): () => void // 监听用户输入事件 -getStateSnapshot(): FriendServiceState // 获取当前状态快照 -``` - -`FriendServiceState` 包含: -- `status`: `'stopped' | 'starting' | 'running' | 'error'` -- `lastError`: 可选错误信息 -- `displayClientCount`: SSE 显示器客户端数量 -- `captureStatus`: 语音捕获状态(`capturing` + `interimText`) - -### 1.3 文本中继 - -```typescript -sendText(text: string): void -``` - -流程: -1. 通知所有 `inboundListeners`(bridge hook 用于 turn tracking) -2. 动态导入 `messageQueueManager.enqueue()`(避免循环依赖) -3. 使用 `bridgeOrigin: true` 和 `origin: { kind: 'channel', server: 'friend' }` 标记来源 - -### 1.4 语音捕获 - -参见 [语音捕获与 VAD 策略](voice-vad.md) 详细文档。 - -``` -startVoiceCapture() - │ - ├── 检测 STT provider(自动降级) - ├── 创建 STT 连接(8s 超时) - ├── 启动 arecord/parecord 子进程(500ms 验证窗口) - └── 启动 VAD 检测 -``` - -### 1.5 静音系统 - -AI 处理全过程阻止麦克风采音进入 STT/VAD,防止 TTS 播放时的回声。 - -``` -startAiTurnMute() - │ muted = true, VAD pause - │ 设置 30s 安全性计时器 - │ - ├── AI 处理 → TTS 生成 - │ - └── extendMuteForTts(audioId) - │ 解析 MP3 精确时长 - │ 重置计时器为精确播放时长 - │ - └── unmute() - │ muted = false, VAD resume -``` - -### 1.6 STT 自动检测 - -```typescript -detectAvailableSttProvider() - │ - ├── 1. Groq Whisper API (最快, REST 调用) - ├── 2. Local Whisper (pip install openai-whisper) - ├── 3. Anthropic Voice Stream - ├── 4. Doubao ASR (检查 ~/.claude/tts/doubao/credentials.json) - │ - └── 全不可用则抛出错误 -``` - -### 1.7 TTS 生成 - -```typescript -generateTts(text: string) - │ - ├── prefs.provider === 'qwen' + prefs.qwenKey 存在 - │ └── Qwen DashScope TTS (qwen3-tts-flash) - │ - └── else - └── Edge TTS (node-edge-tts, 默认 zh-CN-XiaoxiaoNeural) -``` - -### 1.8 SSE 广播 - -```typescript -broadcastResponse(text: string) - │ - ├── broadcastToVrm({ text }) // 发送文字到前端 TextBubble - ├── if TTS enabled: - │ ├── generateTts(text) - │ ├── broadcastToVrm({ audioUrl, sendFirstTts: true }) - │ └── extendMuteForTts(audioId) - └── broadcastToVrm({ replyDone: true }) // 信号回复完成 -``` - -### 1.9 MP3 时长解析 - -`getMp3DurationMs()` 方法使用帧同步头扫描法精确计算 MP3 时长: - -1. 跳过 ID3v2 标签头 -2. 查找前两个帧同步字(0xFF + 0xE0) -3. 计算实际帧间隔(CBR 模式) -4. 解析帧头获取采样率和每帧采样数 -5. 按步长计数帧数 -6. 计算 `(帧数 * 每帧采样数 / 采样率) * 1000` - ---- - -## 2. SSE 模块 - -**文件**: `src/friend/sse.ts` - -### 2.1 客户端注册表 - -```typescript -Set // 全局 SSE 客户端集合 -``` - -- `addSseClient(client)`: 注册新客户端 -- `removeSseClient(client)`: 移除客户端 -- `getSseClientCount()`: 获取活跃客户端数 -- `createSseClientId()`: 生成唯一 ID (`sse-{counter}-{timestamp}`) - -### 2.2 VrmBroadcastPayload 类型 - -```typescript -type VrmBroadcastPayload = { - text?: string; // 回复文字 - emotion?: string; // 表情名 - emotionIntensity?: number; // 表情强度 0-1 - audioUrl?: string; // TTS 音频 URL - audioIndex?: number; // 音频索引(多句排序) - clearText?: boolean; // 清空气泡文字 - imageUrl?: string; // 显示图片 - moodDelta?: number; // 心情变化 - moodIndex?: number; // 当前心情指数 0-100 - sendFirstTts?: boolean; // 开始 TTS 播放信号 - appendText?: boolean; // 追加文字(后续句子) - replyDone?: boolean; // 回复结束信号 -}; -``` - -### 2.3 广播机制 - -```typescript -broadcastToVrm(payload: VrmBroadcastPayload) - ├── 序列化为 SSE data 格式: `data: {json}\n\n` - ├── 遍历所有客户端,逐个写入 - └── 写入失败的客户端自动移除 -``` - -### 2.4 连接建立 - -`createSseResponse()` 创建 Bun ReadableStream,返回 `text/event-stream` 响应。 -初始发送空行以确认连接建立。客户端断开时自动取消注册。 - ---- - -## 3. HTTP 服务器 - -**文件**: `src/friend/server.ts` - -### 3.1 Bun.serve() 配置 - -- 端口: 3456 -- 主机: 127.0.0.1 -- `idleTimeout`: 60s(容纳慢速 STT 初始化) - -### 3.2 端口管理 - -`freePort()` 方法在启动时尝试释放被占用的端口: -1. 使用 `ss -tlnp` 查找端口占用进程 -2. 验证进程是否为 `bun`/`Codev`/`claude-*`/`node` -3. 发送 SIGTERM,等待 3s,失败则 SIGKILL - -### 3.3 路由 - -| 路径 | 方法 | 功能 | -|------|------|------| -| `/plugins/friend/events` | GET | SSE 事件流 | -| `/plugins/friend/*` | ANY | Friend API 路由(见下文) | -| `/friend/*` | GET | 静态文件 | -| WebSocket 升级 | ANY | 返回 426(不支持) | - ---- - -## 4. Friend API 路由 - -**文件**: `src/server/api/friend.ts` (~793 行) - -### 4.1 完整路由表 - -| 端点 | 方法 | 功能 | -|------|------|------| -| `/plugins/friend/events` | GET | SSE 事件流 | -| `/plugins/friend/audio/:id` | GET | 提供 TTS 音频文件 | -| `/plugins/friend/media/:id` | GET | 提供媒体文件 | -| `/plugins/friend/chat` | POST | 文字聊天消息 | -| `/plugins/friend/voice/stt-segment` | POST | 浏览器 VAD 语音片段 | -| `/plugins/friend/voice/start` | POST | 开始服务器端语音捕获 | -| `/plugins/friend/voice/stop` | POST | 停止语音捕获 | -| `/plugins/friend/voice/status` | POST | 获取捕获状态 | -| `/plugins/friend/touch` | POST | 触摸交互事件 | -| `/plugins/friend/voice` | GET/POST | 语音设置 | -| `/plugins/friend/stt/config` | GET | STT 配置 | -| `/plugins/friend/stt/file` | POST | 文件转录 | -| `/plugins/friend/preview` | POST | TTS 预览 | -| `/plugins/friend/settings` | GET/POST | 通用设置 | -| `/plugins/friend/persona` | GET/POST | 角色设定 | -| `/plugins/friend/model/list` | GET | 模型列表 | -| `/plugins/friend/model/serve/:file` | GET | 提供 VRM 模型文件 | -| `/plugins/friend/model/import` | POST | 导入 VRM 模型 | -| `/plugins/friend/history` | GET | 对话历史 | -| `/plugins/friend/context/clear` | POST | 清空上下文 | -| `/plugins/friend/mood/adjust` | POST | 调整心情 | -| `/plugins/friend/session/memo` | POST | 记录会话备注 | -| `/plugins/friend/dance/list` | GET | 舞蹈列表 | -| `/plugins/friend/dance/import` | POST | 导入舞蹈 VMD/MP3 | -| `/plugins/friend/dance/delete` | POST | 删除舞蹈 | -| `/plugins/friend/dance/serve/:file` | GET | 提供舞蹈文件 | -| `/plugins/friend/persona/screenshot` | POST | 保存 VRM 截图 | -| `/plugins/friend/persona/generate` | POST | AI 生成角色设定 | -| `/plugins/friend/screen/observe` | POST | 屏观察触发 | -| `/friend/api/window-close` | POST | Tauri 窗口关闭事件 | - -### 4.2 MIME 类型支持 - -- 音频: `.mp3`, `.opus`, `.ogg`, `.wav`, `.webm` -- 图片: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp` - ---- - -## 5. TTS 服务 - -**文件**: `src/friend/tts.ts` - -### 5.1 Edge TTS - -```typescript -edgeTts({ text, voice? }) - ├── 使用 node-edge-tts 库 - ├── 默认语音: zh-CN-XiaoxiaoNeural - ├── 输出: 临时目录中的 MP3 文件 - └── 返回: { success, audioPath, error } -``` - -### 5.2 Qwen DashScope TTS - -```typescript -qwenTts({ text, apiKey, voice?, model?, language? }) - ├── 端点: dashscope.aliyuncs.com / dashscope-intl.aliyuncs.com - ├── 默认模型: qwen3-tts-flash - ├── 默认语音: Cherry - ├── 超时: 30s - └── 返回: { success, audioPath, error } -``` - -### 5.3 音频文件注册表 - -```typescript -audioFiles = Map // id → 文件路径 - -registerAudioFile(filePath) // 注册文件,5 分钟后自动过期 -getAudioFile(id) // 获取文件路径 -``` - ---- - -## 6. STT 服务 - -**文件**: `src/friend/stt-service.ts` - -提供基于文件的语音转录,用于 REST 端点。流式/进程内捕获由 FriendService 处理。 - -支持 Provider: -- `anthropic`: Anthropic Voice Stream -- `local`: Local Whisper (openai-whisper) -- `doubao`: Doubao ASR -- `browser`: 浏览器 VAD (仅前端的占位符) - ---- - -## 7. VAD 服务 - -**文件**: `src/friend/voice/vad-service.ts` - -参见 [语音捕获与 VAD 策略](voice-vad.md) 详细文档。 - -### 7.1 核心架构 - -- **模型**: Silero VAD legacy ONNX (来自 `@ericedouard/vad-node-realtime`) -- **运行时**: onnxruntime-web WASM 后端(Bun 不兼容 onnxruntime-node) -- **帧大小**: 512 采样 @ 16kHz = 32ms/帧 -- **预处理**: RMS 能量预过滤 (<0.004 跳过推理) - -### 7.2 状态机 - -``` -pre-speech phase (preSpeechCount < preSpeechTriggerFrames) - │ ├── 非语音帧 → 重置计数 - │ └── 连续语音帧达到阈值 → 进入 speaking - ▼ -speaking (confirmed speech segment) - │ ├── 语音帧 → 重置 redemptionCounter - │ └── 静音帧 → redemptionCounter++ - │ └── redemptionCounter >= redemptionFrames → endSpeech() - ▼ -silence redemption (grace period) - └── endSpeech() → onSpeechEnd 回调 -``` - ---- - -## 8. 偏好设置 - -**文件**: `src/friend/prefs.ts` - -```typescript -interface FriendPrefs { - enabled?: boolean; - voice?: string; // TTS 语音 - provider?: string; // TTS provider (edge | qwen) - qwenKey?: string; // Qwen API Key - qwenModel?: string; // Qwen 模型名 - modelPath?: string; // VRM 模型路径 - ttsEnabled?: boolean; // TTS 开启 - showText?: boolean; // 显示文字气泡 - hideUI?: boolean; // 隐藏 UI - tracking?: 'mouse' | 'camera'; // 眼球追踪模式 - volume?: number; // 音量 0-1 - uiAlign?: 'left' | 'right'; // UI 对齐 - screenObserve?: boolean; // 屏幕观察 - screenObserveInterval?: number; // 观察间隔(秒) - language?: 'zh' | 'en'; // 语言 - currentDance?: string; // 当前舞蹈 - hideMood?: boolean; // 隐藏心情 - sttProvider?: string; // STT provider - sttLanguage?: string; // STT 语言 - groqApiKey?: string; // Groq API Key -} -``` - -持久化路径: `~/.config/Codev/friend.json` - ---- - -## 9. Tauri Launcher - -**文件**: `src/friend/tauri-launcher.ts` - -- 查找 Tauri 二进制文件(release → debug) -- 启动为 detached 子进程 -- 管道 stdout/stderr 到主进程日志 -- 退出时自动清理 - -启动路径: `src/components/friend/frontend/src-tauri/target/{release|debug}/codev-friend` - ---- - -## 10. 常量 - -**文件**: `src/friend/constants.ts` - -```typescript -GATEWAY_URL = 'http://127.0.0.1:3456' -FRIEND_SESSION_KEY = 'agent:main:main' -CHANNEL_ID = 'friend' -VALID_EMOTIONS = ['happy', 'sad', 'angry', 'surprised', 'think', 'awkward', - 'question', 'curious', 'neutral', 'love', 'flirty', - 'greeting', 'relaxed'] -``` - ---- - -## 依赖关系图 - -``` -server.ts ──┬── sse.ts ──────────► FriendService.ts ──┬── tts.ts - │ │ ├── vad-service.ts - │ │ ├── prefs.ts - │ │ └── text-utils.ts - │ │ - └── api/friend.ts ───┤ - ├── sse.ts - ├── prefs.ts - ├── tts.ts - ├── stt-service.ts - └── FriendService.ts - -FriendEmotionTool.ts ──► sse.ts, prefs.ts -FriendScreenObserveTool.ts ──► sse.ts - -tauri-launcher.ts (独立启动) -``` diff --git a/docs/friend/data-flow.md b/docs/friend/data-flow.md deleted file mode 100644 index d44cd12a1c810bf94eab708b2975fd1e800c1f79..0000000000000000000000000000000000000000 --- a/docs/friend/data-flow.md +++ /dev/null @@ -1,369 +0,0 @@ -# Friend 数据流 - -本文档详细描述 Friend 系统中的四种核心数据流,以及 SSE 事件格式规范。 - ---- - -## 1. 文字对话流 - -用户在前端输入框输入文字,按下 Enter 发送。 - -``` -用户输入文字 - │ - ▼ -ChatInput.tsx ── POST /plugins/friend/chat ──────────────────┐ - │ { message: "你好" } │ - │ │ - ▼ │ -handleFriendApi() ── friendService.start() │ - │ friendService.sendText(message) │ - ▼ │ -FriendService.sendText() │ - │ │ - ├── 通知 inboundListeners (bridge hook turn tracking) │ - │ │ - └── messageQueueManager.enqueue() │ - │ { mode: 'prompt', skipSlashCommands: true, │ - │ bridgeOrigin: true, origin: { server: 'friend' }} │ - │ │ - ▼ │ - AI Provider (Anthropic/NVIDIA/OpenAI) │ - │ │ - ├── 处理消息 │ - ├── 可调用 friend_emotion 工具设置表情 │ - ├── 可调用 friend_screen_observe 观察屏幕 │ - │ │ - ▼ │ - AI 回复流回 (通过 useFriendBridge / REPL) │ - │ │ - ▼ │ - FriendService.broadcastResponse(text) │ - │ │ - ├── broadcastToVrm({ text }) │ - │ │ │ - │ ▼ SSE data │ - │ TextBubble 收到 │ - │ │ │ - │ ├── 重置气泡,显示文字 │ - │ ├── 启动打字机效果 (逐字符显示) │ - │ │ - CJK: 200ms/char (TTS开启) / 80ms (关闭) │ - │ │ - English: 60ms/char (TTS开启) / 30ms (关闭)│ - │ ├── 打字机完成后渲染 Markdown │ - │ └── 1秒后触发 onMessage → VRMScene 表情动作 │ - │ │ - ├── if TTS enabled: │ - │ ├── generateTts(text) │ - │ │ ├── stripForTts() 清洗文本 │ - │ │ └── EdgeTTS 或 QwenTTS 生成 MP3 │ - │ │ │ - │ ├── broadcastToVrm({ audioUrl, sendFirstTts }) │ - │ │ │ │ - │ │ ▼ SSE data │ - │ │ TextBubble 开始音频队列播放 │ - │ │ │ │ - │ │ └── LipSync.playAudio(url) │ - │ │ ├── fetch 音频文件 │ - │ │ ├── decodeAudioData │ - │ │ ├── 连接到 lipSyncNode (分析) + gainNode (扬声器) │ - │ │ └── 播放时实时更新 VRM 嘴形 │ - │ │ │ - │ └── extendMuteForTts(audioId) │ - │ └── 精确计算 MP3 时长,更新静音定时器 │ - │ │ - └── broadcastToVrm({ replyDone: true }) │ - │ │ - ▼ SSE data │ - TextBubble 调度气泡隐藏 (2s 延迟) │ - 或等待后续 appendText 消息 │ -``` - ---- - -## 2. 语音捕获流 (F2 通话模式) - -用户按下 F2 进入连续语音通话模式,再次按下 F2 结束通话。 - -### 2.1 启动通话 - -``` -用户按 F2 - │ - ▼ -ChatInput.startVoiceCall() - │ - ├── setVoiceCallActive(true) - │ - └── useServerStt.startStreaming() - │ - └── POST /plugins/friend/voice/start - │ - ▼ - handleFriendApi() - │ - └── friendService.startVoiceCapture() - │ - ├── 检测 STT provider (Groq→Whisper→Anthropic→Doubao) - │ - ├── startSttConnection() (8s 超时) - │ ├── Groq: connectGroqStream() - │ ├── Local: connectLocalWhisperStream() - │ ├── Anthropic: connectVoiceStream() - │ └── Doubao: connectDoubaoStream() - │ - ├── loadAudioCapture() - │ ├── 尝试 arecord (ALSA) - │ │ args: -D default -r 16000 -f S16_LE -c 1 -t raw -q - │ ├── 失败则尝试 parecord (PulseAudio) - │ │ args: --raw --rate=16000 --format=s16le --channels=1 - │ └── 500ms 验证窗口: 确认子进程输出音频数据 - │ - ├── arecord 数据回调: - │ ├── if not muted → 转发到 STT connection.send(chunk) - │ └── if not muted → 转发到 VAD processAudio(float32) - │ - └── vadInstance.start() -``` - -### 2.2 语音检测与转录 - -``` -麦克风音频流 (16kHz S16LE) - │ - ├──► STT Connection.send(chunk) (实时流式转录) - │ - └──► SileroVad.processAudio(float32) - │ - ├── RMS 预过滤 (阈值 0.004) - │ ├── < 阈值 → 概率 = 0 (跳过推理) - │ └── >= 阈值 → ONNX 推理 - │ - ├── 状态机 - │ ├── pre-speech: 需要连续 10 帧 (~320ms) 确认说话 - │ ├── speaking: 语音持续中 - │ └── silence redemption: 连续 20 帧 (~640ms) 静音触发 endSpeech - │ - └── onSpeechEnd callback - │ - ▼ - FriendService._flushVadSegment() - │ - ├── 创建新的 STT 连接 (旧的连接继续处理) - │ - ├── 等待旧连接 finalize() - │ └── 获取转录文本推入 captureTranscripts - │ - ├── if 有转录文本: - │ ├── this.sendText(transcript) - │ │ │ - │ │ ▼ - │ │ messageQueueManager.enqueue() → AI 开始处理 - │ │ - │ └── this.startAiTurnMute() - │ ├── muted = true - │ ├── VAD pause - │ └── 30s 超时安全性定时器 - │ - └── 循环继续监听下一段语音 -``` - -### 2.3 AI 回复与静音解除 - -``` -AI 处理完成 - │ - ▼ -FriendService.broadcastResponse(text) - │ - ├── broadcastToVrm({ text }) // 显示文字 - │ - ├── if TTS enabled: - │ ├── generateTts(text) - │ │ │ - │ │ ▼ - │ ├── broadcastToVrm({ audioUrl, sendFirstTts: true }) - │ │ - │ └── extendMuteForTts(audioId) - │ ├── getMp3DurationMs() 精确计算 - │ ├── 取消 30s 安全性定时器 - │ └── 设定精确的播放时长定时器 - │ - └── broadcastToVrm({ replyDone: true }) - │ - ▼ - 播放完成后 → unmute() - ├── muted = false - └── VAD resume (可继续接收语音) -``` - -### 2.4 结束通话 - -``` -用户按 F2 (再次) - │ - ▼ -ChatInput.endVoiceCall() - │ - └── useServerStt.stopStreaming() - │ - └── POST /plugins/friend/voice/stop - │ - ▼ - friendService.stopVoiceCapture() - │ - ├── arecord.kill('SIGTERM') → 2s 后 SIGKILL - ├── capturing = false - ├── clearMute() - ├── VAD reset() - ├── STT connection.finalize() + close() - ├── 发送剩余转录文本 - └── 返回完整转录 -``` - ---- - -## 3. 情绪表情流 - -LLM 调用 `friend_emotion` 工具触发情绪更新。 - -``` -LLM 处理完成,调用 friend_emotion 工具 - │ - ▼ -FriendEmotionTool.call({ emotion: 'happy', intensity: 0.8, mood_delta: 2 }) - │ - ├── broadcastToVrm({ emotion: 'happy', emotionIntensity: 0.8 }) - │ │ - │ ▼ SSE data - │ App.tsx handleVrmMessage - │ │ - │ ├── emotionActionMap['happy'] = 'happy' - │ │ - │ ├── sceneRef.current.setEmotionWithReset('happy', 5000, 0.8) - │ │ │ - │ │ ▼ - │ │ VRMScene.setEmotionWithReset (via forwardRef) - │ │ │ - │ │ └── EmoteController.setEmotionWithReset('happy', 5000, 0.8) - │ │ ├── setEmotion('happy', 0.8) - │ │ │ ├── 获取 happy 的 blend shapes: [{name:'happy', val:0.2}, {name:'aa', val:0.8}] - │ │ │ ├── 应用 intensity: aa = 0.8*0.8 = 0.64, happy = 0.2*0.8 = 0.16 - │ │ │ ├── isTransitioning = true - │ │ │ └── 记录目标 blendshape 值 - │ │ │ - │ │ └── setTimeout(5000ms → setEmotion('neutral')) - │ │ - │ └── sceneRef.current.playAction('happy') - │ │ - │ ▼ - │ MotionController.playAction('happy') - │ ├── loadClip('happy.fbx') - │ ├── crossFadeTo(clip, 0.3s) - │ ├── LoopOnce + clampWhenFinished - │ └── 完成后 crossFade 回 idle 动画 - │ - ├── 处理 mood_delta - │ ├── 读取当前 moodIndex = 60 - │ ├── newMood = clamp(60 + 2, 0, 100) = 62 - │ ├── 持久化到 prefs - │ └── broadcastToVrm({ moodDelta: 2, moodIndex: 62 }) - │ │ - │ ▼ SSE data - │ MoodIndicator 收到 - │ ├── 显示心情数值变化气泡 (+2) - │ ├── Canvas 动画: displayPercent 从 60 → 62 渐变 - │ └── 5s 后自动隐藏 - │ - └── 返回 tool result -``` - -### 每帧更新循环 (VRMScene animate) - -``` -requestAnimationFrame 循环 (约 60fps) - │ - ├── 1. MotionController.update(delta) - │ └── AnimationMixer.update(delta) - │ - ├── 2. 应用 Relaxed Hand Pose (非舞蹈状态) - │ └── 手指自然弯曲 + 微妙颤动 - │ - ├── 3. Humanoid.update() - │ - ├── 4. 眼球追踪 (camera 模式) - │ └── lookAtTarget = camera.position - │ - ├── 5. LookAt.update(delta) - │ - ├── 6. Eye Saccades Controller.update() - │ └── 每隔 400-1200ms 添加随机眼球微动偏移 - │ - ├── 7. Blink State Machine.update() - │ └── 随机眨眼 (间隔 1-6s, 时长 150ms, sin 曲线) - │ - ├── 8. EmoteController.update(delta) - │ └── cubic ease 过渡到目标 blendshape 值 - │ - ├── 9. LipSync.update(vrm, delta) - │ ├── 读取 wlipsync 音频分析节点的音素权重 - │ ├── 选择胜者/亚军音素 - │ ├── Attack/Release 平滑 (50/30) - │ └── 设置 VRM 嘴形 blendshapes (aa, ee, ih, oh, ou) - │ - ├── 10. ExpressionManager.update() - │ - └── 11. SpringBoneManager.update(delta) - └── 物理头发/衣服/饰品模拟 -``` - ---- - -## 4. SSE 事件格式 - -所有前端 SSE 事件通过 `GET /plugins/friend/events` 接收,格式为标准 SSE (`data: {json}\n\n`)。 - -### 4.1 VrmBroadcastPayload 字段说明 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `text` | string | 否 | AI 回复文字,TextBubble 显示并启动打字机效果 | -| `emotion` | string | 否 | VRM 表情名,触发 EmoteController 切换 blend shapes | -| `emotionIntensity` | number | 否 | 表情强度 0-1,默认 1 | -| `audioUrl` | string | 否 | TTS 音频 URL,TextBubble 触发 LipSync 播放 | -| `audioIndex` | number | 否 | 音频播放顺序索引,用于多句排序 | -| `clearText` | boolean | 否 | 清空气泡文字和音频队列 | -| `imageUrl` | string | 否 | 图片 URL,在气泡中显示 | -| `moodDelta` | number | 否 | 心情变化量,MoodIndicator 显示浮动气泡 | -| `moodIndex` | number | 否 | 当前心情指数 0-100,MoodIndicator Canvas 更新 | -| `sendFirstTts` | boolean | 否 | 开始 TTS 播放的信号,重置音频队列 | -| `appendText` | boolean | 否 | 追加文字模式,后续句子的文字和音频配对 | -| `replyDone` | boolean | 否 | 回复完成信号,TextBubble 调度气泡隐藏 | - -### 4.2 典型回复序列 - -``` -1. { text: "你好!今天心情不错啊!", replyDone: true } - → 显示文字,打字机效果,1s 后触发表情 - -2. { text: "一起玩吧!" } - { audioUrl: "http://127.0.0.1:3456/plugins/friend/audio/123-1", sendFirstTts: true } - { replyDone: true } - → 显示文字 + 播放 TTS + 语音结束后隐藏气泡 - -3. { text: "今天天气真好。" } - { audioUrl: "...", sendFirstTts: true, emotion: "happy", emotionIntensity: 0.8 } - { appendText: true, text: "要不要出去走走?", audioUrl: "...", audioIndex: 1 } - { appendText: true, text: "我知道一个好地方。", audioUrl: "...", audioIndex: 2 } - { replyDone: true } - → 多句子回复,每句独立音频,按索引顺序播放 - → 第一句发送时触发 happy 表情 - -4. { emotion: "think", emotionIntensity: 0.7 } - → 思考阶段的表情更新(LLM 处理中) - -5. { clearText: true } - → 清空气泡(新会话) - -6. { moodDelta: 3, moodIndex: 63 } - → 心情更新,显示 +3 浮动气泡,Canvas 液态填充变化 -``` diff --git a/docs/friend/emotion-map.md b/docs/friend/emotion-map.md deleted file mode 100644 index a77f8228e7f178386e1bf0003444d6fdc650d8bf..0000000000000000000000000000000000000000 --- a/docs/friend/emotion-map.md +++ /dev/null @@ -1,195 +0,0 @@ -# 情绪 → 3D 表情映射 - -Friend 系统支持 13 种情绪,每种映射到 VRM blend shapes 组合、过渡时间和肢体动作。 - ---- - -## 完整映射表 - -数据来源: `src/components/friend/frontend/emote.ts` - -| 情绪 | Blend Shapes 组合 | 过渡时间 | 肢体动作 | -|------|------------------|---------|---------| -| `happy` | happy(0.2) + aa(0.8) | 0.4s | `happy` (开心.fbx) | -| `sad` | sad(0.7) + oh(0.15) | 0.4s | `shy` (害羞.fbx) | -| `angry` | angry(0.7) + ee(0.3) | 0.3s | `angry` (生气.fbx) | -| `surprised` | surprised(0.8) + oh(0.4) | 0.15s | `excited` (兴奋.fbx) | -| `think` | think(0.7) | 0.5s | `scratchHead` (挠头.vrma) | -| `awkward` | sad(0.3) + ee(0.2) | 0.5s | `playFingers` (搓手.vrma) | -| `question` | surprised(0.4) + think(0.3) | 0.4s | `point` (指点.fbx) | -| `curious` | think(0.5) + surprised(0.2) | 0.4s | `scratchHead` (挠头.vrma) | -| `neutral` | neutral(1.0) | 0.6s | `salute` (敬礼.fbx) | -| `love` | happy(0.2) + relaxed(0.4) | 0.4s | `shy` (害羞.fbx) | -| `flirty` | happy(0.2) + relaxed(0.3) + aa(0.15) | 0.4s | `shy` (害羞.fbx) | -| `greeting` | happy(0.2) + aa(0.3) | 0.3s | `greeting` (招呼.fbx) | -| `relaxed` | relaxed(0.8) | 0.5s | `salute` (敬礼.fbx) | - -### 使用的 VRM Blend Shapes - -VRM 标准 blendshape 名称及其对应的面部区域: - -| Blend Shape | 面部区域 | -|-------------|---------| -| `happy` | 嘴角上扬 (smile) | -| `sad` | 嘴角下垂 | -| `angry` | 皱眉 | -| `surprised` | 眉毛上抬 | -| `think` | 思考表情 | -| `neutral` | 自然表情 | -| `relaxed` | 放松表情 | -| `aa` | 张嘴 (A 音) | -| `ee` | 露齿 (E 音) | -| `oh` | 嘟嘴 (O 音) | -| `ih` | 微张嘴 (I 音, 主要用于唇同步) | -| `ou` | 收唇 (U 音, 主要用于唇同步) | -| `blink` | 闭眼 (由眨眼系统独立控制) | - ---- - -## EmoteController 过渡系统 - -**文件**: `src/components/friend/frontend/emote.ts` - -### 过渡算法 - -```typescript -// cubic ease 缓动函数 -setEmotion('happy', intensity = 0.8) - │ - ├── 1. 获取情绪定义 - │ happy: [{name:'happy', value:0.2}, {name:'aa', value:0.8}] - │ - ├── 2. 乘以 intensity - │ happy = 0.2 * 0.8 = 0.16 - │ aa = 0.8 * 0.8 = 0.64 - │ - ├── 3. 记录当前 blendshape 值 (起始值) - │ currentValues = { happy: 0.05, aa: 0, ... } - │ - ├── 4. 设置目标值 - │ targetValues = { happy: 0.16, aa: 0.64 } - │ - ├── 5. 开始过渡 (isTransitioning = true) - │ - └── 每帧 update(deltaTime): - │ - ├── transitionProgress += deltaTime / blendDuration(0.4s) - │ - ├── if progress >= 1: transition end - │ - ├── cubic ease: - │ t < 0.5 → 4 * t³ - │ t >= 0.5 → 1 - (-2t + 2)³ / 2 - │ - └── value = start + (target - start) * ease(t) -``` - -### 自动回中系统 - -```typescript -setEmotionWithReset('happy', durationMs = 5000, intensity = 0.8) - ├── setEmotion('happy', 0.8) // 立即开始过渡 - └── setTimeout(5000ms) - └── setEmotion('neutral') // 自动回中到自然表情 -``` - -### 完整重置 - -```typescript -resetAll() - ├── 清除 resetTimer - ├── isTransitioning = false - ├── 将所有 blendshape 值设为 0 - └── 清空 currentValues / targetValues -``` - ---- - -## 情绪 → 动作映射 - -**文件**: `src/components/friend/frontend/App.tsx` - -App.tsx 中的 `emotionActionMap` 定义了情绪与动作的关联: - -```typescript -const emotionActionMap: Record = { - think: 'scratchHead', // 挠头 - question: 'point', // 指 - curious: 'scratchHead', // 挠头 - happy: 'happy', // 开心 - surprised: 'excited', // 兴奋 - angry: 'angry', // 生气 - awkward: 'playFingers', // 搓手指 - sad: 'shy', // 害羞 - love: 'shy', // 害羞 - flirty: 'shy', // 害羞 - greeting: 'greeting', // 招呼 - relaxed: 'salute', // 敬礼 - neutral: 'salute', // 敬礼 -} -``` - -动作文件类型: -- `.vrma`: VRM Animation 格式 (挠头、搓手、伸展、叉腰) -- `.fbx`: Mixamo FBX 格式 (开心、生气、招呼、兴奋、害羞、指点、敬礼、暴怒) - ---- - -## 心情指数系统 - -除了即时表情,Friend 还有持续的心情指数系统: - -``` -moodIndex: 0-100 - ├── 0-29: 低 (灰色, rgb(160,168,180)) - ├── 30-49: 偏低 (蓝色, rgb(78,168,222)) - ├── 50-69: 中等 (绿色, rgb(72,199,142)) - ├── 70-89: 良好 (橙色, rgb(255,165,70)) - └── 90-100: 优秀 (粉色, rgb(255,107,157)) -``` - -- LLM 通过 `friend_emotion` 工具的 `mood_delta` 参数调整 (-3 到 +3) -- 前端 MoodIndicator 组件以液态填充柱状图 + 爱心图标可视化 -- Canvas 动画使用贝塞尔波浪动画和颜色渐变 - ---- - -## 触摸交互反应系统 - -**文件**: `src/components/friend/frontend/App.tsx` - -6 个触摸区域各自有多个可能的反应: - -| 区域 | 可能的反应 (情绪 + 动作) | -|------|------------------------| -| head (头) | relaxed+happy, relaxed+shy, angry+angryPump, relaxed+excited | -| arm (手臂) | surprised+excited, happy+happy, relaxed+greeting, relaxed+akimbo | -| chest (胸) | angry+angryPump, angry+angry, angry+point | -| belly (肚子) | angry+angryPump, angry+angry, awkward+playFingers | -| buttocks (屁股) | angry+angryPump, angry+point, sad+shy | -| leg (腿) | sad+shy, angry+angry, awkward+playFingers | - -触摸交互逻辑: -1. 双击模型触发区域检测(射线检测 + 最近骨骼匹配) -2. 随机选择该区域的一个反应组合 -3. 立即播放表情 + 动作 -4. 3s 冷却期写入 session memo([用户摸了摸你的xx]) -5. 60s 冷却期 + 50% 概率发送文字回复到 AI(POST /plugins/friend/touch) - ---- - -## 空闲小动作系统 - -当用户 30 秒无活动时,VRM 角色会自动做一些小动作: - -``` -idleMs >= 30,000ms → 每 15s 检查一次 - │ - ├── 50% 概率触发 - ├── 随机选择 13 种情绪之一 - ├── 随机选择 12 种动作之一 - ├── 强度: 0.4-0.8 (随机) - └── 持续: 3-5s (随机) -``` - -此系统防止角色长时间静止不动,增加生动感。 diff --git a/docs/friend/frontend-3d.md b/docs/friend/frontend-3d.md deleted file mode 100644 index 5ff563a77fa7fe5bbb50a3063cf667e6b381c149..0000000000000000000000000000000000000000 --- a/docs/friend/frontend-3d.md +++ /dev/null @@ -1,573 +0,0 @@ -# 前端 3D 渲染 - -本文档描述 Friend VRM 桌面伴侣的前端 3D 渲染系统。 - ---- - -## 技术栈 - -| 技术 | 用途 | -|------|------| -| **Three.js** | 3D 渲染引擎 | -| **@pixiv/three-vrm** | VRM 模型加载与控制 | -| **@pixiv/three-vrm-animation** | VRM 动画加载与 LookAt | -| **@pixiv/three-vrm-animation** | VRMA 动画支持 | -| **Tauri (WebKitGTK)** | 桌面窗口外壳 | -| **React** | UI 框架 | -| **Vite** | 前端构建工具 | -| **Bun** | 构建运行环境 | -| **CSS-in-JS** (内联 style) | 组件样式 | -| **Lucide React** | 图标库 | -| **marked** | Markdown 渲染 | -| **wlipsync** | WebAudio 唇形同步 | -| **Intl.Segmenter** | Unicode 字符分割 | - ---- - -## VRMScene.tsx — 核心 3D 场景 - -**文件**: `src/components/friend/frontend/components/VRMScene.tsx` (~871 行) - -### 组件接口 - -通过 `forwardRef` 暴露的操作句柄: - -```typescript -interface VRMSceneHandle { - setEmotion(emotion: string, intensity?: number): void - setEmotionWithReset(emotion: string, durationMs: number, intensity?: number): void - resetCamera(): void - setTrackingMode(mode: 'mouse' | 'camera'): void - playAction(name: string, hold?: boolean): void - captureScreenshot(): string | null - panCamera(dx: number, dy: number): void - rotateCamera(dx: number, dy: number): void - playDance(nameOrPreset: string | DancePreset): void - stopDance(): void - isDancing(): boolean - setBgmVolume(v: number): void - reset(): void -} -``` - -### 场景初始化 - -```typescript -// VRMScene 组件创建时 -const renderer = new THREE.WebGLRenderer({ - canvas, - alpha: true, // 透明背景 - antialias: true, // 抗锯齿 - preserveDrawingBuffer: true, // 截图支持 -}) -renderer.setClearColor(0x000000, 0) // 完全透明 - -// 透视相机 -const FOV = 40 -const camera = new THREE.PerspectiveCamera(FOV, aspect, 0.1, 100) -// 轨道控制: 围绕 pivot 点的球面坐标 - -// 光照 -const ambientLight = new THREE.AmbientLight(0xffffff, 0.6) -const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2) // 主光 -const fillLight = new THREE.DirectionalLight(0xffffff, 0.4) // 补光 -``` - -### 模型加载 - -```typescript -loader.load(modelPath, async (gltf) => { - // 1. 获取 VRM 数据 - const loadedVrm = gltf.userData.vrm - - // 2. 优化: 移除冗余顶点 + 合并骨架 - VRMUtils.removeUnnecessaryVertices(loadedVrm.scene) - VRMUtils.combineSkeletons(loadedVrm.scene) - - // 3. 添加 LookAt 四元数代理 - const lookAtQuatProxy = new VRMLookAtQuaternionProxy(loadedVrm.lookAt) - loadedVrm.scene.add(lookAtQuatProxy) - - // 4. 标准化 VRM 0.x → 1.0 姿态 - VRMUtils.rotateVRM0(loadedVrm) - - // 5. 计算自动相机位置 (根据模型包围盒) - const box = new THREE.Box3().setFromObject(loadedVrm.scene) - // pivot 在颈部高度 - // orbitRadius = modelSize.y / 4.2 / tan(FOV/2) - - // 6. 初始化控制器 - emote = new EmoteController(loadedVrm) - motion = new MotionController(loadedVrm) - handPoseCache = buildHandPoseCache(loadedVrm) - - // 7. 加载空闲动画 - motion.loadIdle('/friend/idle_loop.vrma') -}) -``` - -### 每帧更新循环 (11 步) - -```typescript -function animate() { - // 1. Animation Mixer (MotionController) - motion.update(delta) - - // 2. 放松手部姿态 (非舞蹈时) - if (handPose && !motion.isDancing) - applyRelaxedHandPose(handPose, elapsedTime) - - // 3. Humanoid 骨骼更新 - vrm.humanoid.update() - - // 4. Camera tracking mode → lookAt - if (trackingMode === 'camera') - saccades.instantUpdate(vrm, camera.position) - - // 5. LookAt 更新 - vrm.lookAt.update(delta) - - // 6. 眼球微动 (saccades) - saccadesController.update(vrm, lookAtTarget, delta) - - // 7. 眨眼 (blink) - updateBlink(vrm, delta, blinkState) - - // 8. 表情过渡 (EmoteController) - emote.update(delta) - - // 9. 唇形同步 (LipSync) - lipSync.update(vrm, delta) - - // 10. Expression Manager - vrm.expressionManager.update() - - // 11. Spring Bone 物理 - vrm.springBoneManager.update(delta) - - renderer.render(scene, camera) -} -``` - -### 相机控制系统 - -球面坐标相机控制: - -```typescript -// 参数: pivot(旋转中心), orbitRadius(距离), orbitTheta(水平角), orbitPhi(垂直角) -camera.position.set( - pivot.x + radius * sin(phi) * sin(theta), - pivot.y + radius * cos(phi), - pivot.z + radius * sin(phi) * cos(theta), -) -camera.lookAt(pivot) -``` - -- 鼠标滚轮: 缩放 (0.8-5.0) -- 左键拖拽 VRM 模型: 双击触发射线碰撞检测身体区域 -- 中键拖拽: 推拉 (dolly) -- 右键拖拽: 旋转视角 -- 菜单栏拖拽按钮: 平移 / 旋转 视角 - -### 眼球追踪系统 - -两种模式: -- `mouse`: 鼠标在屏幕上的位置决定 VRM 视线的交点(通过射线平面求交) -- `camera`: VRM 始终看向相机位置 - -眼球微动控制器 (`EyeSaccadeController`): -- 每 400-1200ms 添加随机偏移 (-0.25 到 0.25 units) -- 瞬间更新 + 每帧 lerp 平滑 - -### 眨眼系统 - -- 随机间隔: 1-6s -- 眨眼时长: 150ms -- 使用 sin(π * progress) 曲线实现自然闭合 -- 每帧更新 `blink` blendshape - -### 放松手部姿态 - -当 VRM 模型没有手指动画轨道时,手指会保持 T-pose 僵硬状态。 -解决方案:每帧手动设置手指旋转。 - -```typescript -// 手指自然弯曲 (从拇指到小指递增) -const curlMap = { - Thumb: [0.25, 0.15, 0.10], - Index: [0.20, 0.30, 0.20], - Middle: [0.25, 0.35, 0.25], - Ring: [0.30, 0.40, 0.30], - Little: [0.35, 0.45, 0.30], -} -// 手指自然张开 -const spreadMap = { Thumb: 0.15, Index: 0.04, Middle: 0, Ring: -0.04, Little: -0.08 } -// 微妙颤动: sin(time * freq + seed) * 0.02 -``` - -### 触摸区域检测 - -射线检测 + 最近骨骼匹配: - -```typescript -const boneRegionMap: [string, TouchRegion][] = [ - ['head', 'head'], ['neck', 'head'], - ['leftShoulder', 'arm'], /* ...所有手臂骨骼... */ - ['chest', 'chest'], ['spine', 'belly'], - ['hips', 'buttocks'], - ['leftUpperLeg', 'leg'], /* ...所有腿部骨骼... */ -] -// 计算点击点与所有骨骼的世界坐标距离 -// 选择最近的骨骼对应的区域 -``` - -双击确认(500ms 窗口)+ 5s 冷却。 - -### 窗口穿透点击检测 - -```typescript -// 渲染到 1x1 offscreen render target -// 读取光标位置的 alpha 通道 -// alpha > 10 → 点击在模型上 → 不穿透 -// alpha <= 10 → 点击在透明背景 → 穿透窗口 -``` - ---- - -## EmoteController — Blend Shape 动画系统 - -**文件**: `src/components/friend/frontend/emote.ts` (~207 行) - -详细情绪映射见 [情绪 → 3D 表情映射](emotion-map.md)。 - -### 核心机制 - -``` -EmoteController - ├── emotionStates: Map - │ 13 种情绪的 blend shapes 组合定义 - │ - ├── setEmotion(name, intensity) - │ 开始过渡:记录起始值、设置目标值、启动过渡 - │ - ├── setEmotionWithReset(name, durationMs, intensity) - │ 设置表情 + 定时自动回中到 neutral - │ - ├── update(deltaTime) - │ 每帧计算 cubic ease 过渡 - │ - └── resetAll() - 立即清零所有 blendshape -``` - ---- - -## MotionController — 动画系统 - -**文件**: `src/components/friend/frontend/motion-controller.ts` (~421 行) - -### 支持的文件格式 - -| 格式 | 描述 | 来源 | -|------|------|------| -| VRMA | VRM Animation 格式 | 标准动画文件 | -| VMD | MikuMikuDance 格式 | 舞蹈动画 (极乐净土/恋爱循环) | -| FBX | Autodesk FBX 格式 | Mixamo 动画 (开心/生气等) | - -### 动作预设 - -**短动作** (单次触发,完成后回归 idle): - -```typescript -akimbo: { label: '叉腰', type: 'vrma' } -playFingers: { label: '搓手', type: 'vrma' } -scratchHead: { label: '挠头', type: 'vrma' } -stretch: { label: '伸展', type: 'vrma' } -happy: { label: '开心', type: 'fbx' } -angry: { label: '生气', type: 'fbx' } -greeting: { label: '招呼', type: 'fbx' } -excited: { label: '兴奋', type: 'fbx' } -shy: { label: '害羞', type: 'fbx' } -point: { label: '指点', type: 'fbx' } -salute: { label: '敬礼', type: 'fbx' } -angryPump: { label: '暴怒', type: 'fbx' } -``` - -**舞蹈** (循环播放,支持 BGM): - -```typescript -jile: { label: '极乐净土', type: 'vmd', bgm: '/friend/jile.mp3' } -love: { label: '恋爱循环', type: 'vmd', bgm: '/friend/love.mp3' } -``` - -### 动画过渡系统 - -使用单个持久的 `AnimationMixer` 配合 `crossFade` 过渡,避免 T-pose 闪烁: - -```typescript -private crossFadeTo(newAction, duration = 0.3s) { - newAction.reset().setEffectiveWeight(1).play() - const prev = this.currentAction ?? this.idleAction - if (prev && prev !== newAction) { - prev.crossFadeTo(newAction, duration, false) - } - this.currentAction = newAction -} -``` - -### 动作生命周期 - -``` -playAction('happy') - │ - ├── 检查并发锁 (_actionPlaying / _isDancing) - │ - ├── 异步加载动画文件 (带 generation 标记) - │ ├── VRMA: GLTFLoader + VRMAnimationLoaderPlugin - │ ├── FBX: loadMixamoAnimation - │ └── VMD: parseVMDAnimation + bindVMDToVRM + IK - │ - ├── crossFadeTo(action, 0.3s) - │ - ├── LoopOnce + clampWhenFinished - │ - ├── finished 事件 → 回归 idle - │ └── hold 模式: 保持 10s 后回归 idle - │ - └── 安全性超时: (duration + 1s) 后强制释放 -``` - -### VMD 舞蹈系统 - -特殊处理 VMD 格式: - -```typescript -loadVMDWithIK(url) - ├── 1. 解析 VMD 文件 (parseVMDAnimation) - │ - 解析 VMD 二进制格式 - │ - 从 VRM 骨骼映射到关键帧 - │ - 缓存解析结果 (解析开销大) - │ - ├── 2. 绑定到 VRM (bindVMDToVRM) - │ - 生成 Three.js AnimationClip - │ - 创建 IK 目标对象 - │ - 启用 IK 处理器 - │ - └── 3. 建立复用的 IK 处理器 (VRMIKHandler) - - 使用 FABRIK 算法 - - 支持手臂 IK - - 每帧在 mixer.update 后运行 -``` - -舞蹈启动时自动切换相机视角到合适位置(臀部高度为中心)。 - -### BGM 系统 - -```typescript -// 舞蹈开始时 -this.bgmAudio = new Audio(preset.bgm) -this.bgmAudio.loop = true -this.bgmAudio.volume = this._volume -this.bgmAudio.play() - -// 舞蹈停止时: 淡出 (每隔 50ms 降低 0.1) -const fadeInterval = setInterval(() => { - audio.volume = Math.max(0, audio.volume - 0.1) - if (audio.volume <= 0) { clearInterval(fadeInterval); audio.pause() } -}, 50) -``` - ---- - -## LipSync — 唇形同步 - -**文件**: `src/components/friend/frontend/lip-sync.ts` (~187 行) - -### 技术实现 - -使用 `wlipsync` 库的 WebAudio 音频分析节点: - -```typescript -// 初始化 -this.lipSyncNode = await createWLipSyncNode(audioContext, profile) -// lipSyncNode 只分析音频,不连接扬声器 -// gainNode 连接扬声器 - -// 播放音频时,同时连接到 lipSyncNode 和 gainNode -source.connect(this.lipSyncNode) // 分析 -source.connect(this.gainNode) // 扬声器 -``` - -### 音素 → VRM Blend Shape 映射 - -| wlipsync 分析键 | VRM Blend Shape | -|----------------|----------------| -| `A` | `aa` (张嘴) | -| `E` | `ee` (露齿) | -| `I` | `ih` (微张嘴) | -| `O` | `oh` (嘟嘴) | -| `U` | `ou` (收唇) | -| `S` | 映射到 `I`/`ih` | - -双胜者策略:取概率最高的两个音素,胜者 cap 0.7,亚军 cap 0.35。 - -### 平滑参数 - -- `ATTACK`: 50 (上升速率) -- `RELEASE`: 30 (衰减速率) -- `CAP`: 0.7 (最大权重) -- `SILENCE_VOL`: 0.04 (静音音量阈值) -- `SILENCE_GAIN`: 0.05 (静音增益阈值) -- `IDLE_MS`: 160 (静音判定窗口) - -公式: `smoothed = from + (to - from) * (1 - exp(-rate * delta))` - -静音时完全跳过 blendshape 设置(让 EmoteController 控制嘴部)。 - ---- - -## TextBubble.tsx — 文字气泡 - -**文件**: `src/components/friend/frontend/components/TextBubble.tsx` (~608 行) - -### 核心功能 - -1. **SSE 驱动**:通过 `EventSource` 实时接收 `VrmBroadcastPayload` -2. **打字机效果**:逐字符显示,CJK/English 自适应速率 -3. **音频队列**:多句子音频按索引顺序播放,支持 `appendText` 配对 -4. **Markdown 渲染**:打字机完成后使用 `marked` 渲染 -5. **发送首 TTS 队列**:支持排队 `sendFirstTts` 信号 -6. **看门狗**:30s 超时自动隐藏(防止卡死) - -### 打字机速率计算 - -```typescript -function getCharRate(text: string, ttsEnabled: boolean): number { - const ratio = cjkRatio(text) - if (ttsEnabled) { - return Math.round(200 * ratio + 60 * (1 - ratio)) // CJK: 200ms, EN: 60ms - } - return Math.round(80 * ratio + 30 * (1 - ratio)) // CJK: 80ms, EN: 30ms -} -``` - -### 消息处理逻辑 - -``` -handleMessage(msg) - │ - ├── Audio-only: 直接入音频队列 - │ - ├── appendText: 配对文字和音频索引,等待播放时揭示文字 - │ - ├── Image-only: 显示图片,15s 自动隐藏 - │ - ├── Emotion-only: 转发 onMessage 回调 (不改变气泡) - │ - └── Text message: - ├── sendFirstTts: 重置音频队列,播放首句 TTS - ├── 设置文字 → 启动打字机 - ├── 打字机完成后隐藏调度 (2s) - └── 如果没有 TTS → 打字机完成后直接调度隐藏 -``` - -### 隐藏调度逻辑 - -``` -tryScheduleHide() - ├── 打字机完成? (typewriterRef === null) - ├── 音频播放完毕? (!audioPlaying && queue empty) - ├── sendFirstTts 队列为空? - ├── replyDone 已收到? - │ - └── 全部满足 → setTimeout(hideBubble, 2000ms) -``` - ---- - -## ChatInput.tsx — 输入栏 - -**文件**: `src/components/friend/frontend/components/ChatInput.tsx` (~475 行) - -### 三种模式 - -1. **文字输入模式**: 输入框 + 发送按钮 + 回车发送 -2. **PTT 模式**: 按住麦克风按钮录音,松开停止并转录 -3. **语音通话模式** (F2): 连续语音,VAD 自动分段 - -### 全局快捷键 - -- `Enter`: 打开输入栏 / 发送消息 -- `Escape`: 关闭输入栏 -- `F2`: 切换语音通话 -- `F4`: 设置面板 -- `F5`: 刷新前端 -- `Tab`: 折叠/展开菜单 -- `Ctrl+D`: 清空输入 - -### 语音通话 TTS 中断 - -```typescript -const scheduleInterrupt = useCallback(() => { - // 1s 延迟中断 TTS 播放 - // 当用户开始说话时,延迟 1s 后中断当前 TTS - // 避免用户的"嗯"等短暂声音打断对话 - setTimeout(() => { - ;(window as any).__clawInterruptAudio?.() - }, 1000) -}, []) -``` - ---- - -## MoodIndicator.tsx — 心情指示器 - -**文件**: `src/components/friend/frontend/components/MoodIndicator.tsx` (~373 行) - -### Canvas 动画 - -- **液态填充柱状图**: 使用二次贝塞尔波浪动画 + Canvas clip -- **爱心图标**: 同样的波浪填充,经典心形路径 -- **双波浪层**: 不同速度和透明度叠加,产生液态流动效果 -- **颜色分级**: 90+ 粉色 → 70+ 橙色 → 50+ 绿色 → 30+ 蓝色 → 0+ 灰色 -- **浮动气泡**: 心情变化时显示 `❤️+3` 或 `🩶-2` 浮动动画 - -### 交互 - -- 正常状态: 半透明 (opacity 0.5) -- 鼠标悬停: 全透明 (opacity 1.0) -- 拖拽: 可自由移动到任意位置 -- 自动隐藏: 5s 无交互后恢复半透明 - -### 波浪参数 - -```typescript -WAVE_LENGTH = 8 // 波长 -WAVE_HEIGHT = 2.5 // 波高 (px) - -// 双波浪叠加 -drawWave(scrollDir: -1, alpha: 0.55) // 底层波浪,慢速反向 -drawWave(scrollDir: 1, alpha: 1.0) // 顶层波浪,快速正向 -``` - ---- - -## 其他组件 - -### ResizeHandles -窗口大小调整手柄,支持拖拽调整窗口尺寸。 - -### SettingsPanel -设置面板 (F4 打开),包含: -- VRM 模型选择/导入 -- TTS 开关 + 语音选择 -- 显示设置 (文字气泡、UI、心情) -- 追踪模式 (鼠标/相机) -- 音量控制 -- 屏幕观察设置 -- 语言设置 -- STT Provider 选择 -- 舞蹈选择 - -### HistoryPanel -对话历史面板,显示最近 100 条消息,支持拖拽移动位置。 diff --git a/docs/friend/overview.md b/docs/friend/overview.md deleted file mode 100644 index f3db3094674609086756353a505503ba5f77d2de..0000000000000000000000000000000000000000 --- a/docs/friend/overview.md +++ /dev/null @@ -1,138 +0,0 @@ -# VRM 桌面伴侣系统 - -## 定位 - -Friend 是 Codev 的 VRM 3D 桌面伙伴系统,与 CLI 共享同一进程运行。它通过 SSE + HTTP 与 Tauri 前端通信,无需独立的子进程或外部服务。用户可以与 VRM 角色进行文字聊天、语音对话,角色会通过 3D 表情、肢体动作和语音进行反馈。 - -核心设计理念:**同进程集成** — FriendService 作为单例运行在 CLI 主进程中,消息通过 `messageQueueManager.enqueue()` 直接注入到对话流程中,AI 回复通过 SSE 实时广播到前端显示。 - ---- - -## 核心组件 - -| 组件 | 文件 | 职责 | -|------|------|------| -| **FriendService** | `src/friend/FriendService.ts` (~31KB) | 核心编排器,管理生命周期、语音捕获、STT/TTS、SSE 广播、静音系统 | -| **SSE 模块** | `src/friend/sse.ts` | 客户端注册表,类型化广播 `VrmBroadcastPayload` | -| **HTTP 服务器** | `src/friend/server.ts` | Bun.serve() 在 3456 端口,处理静态文件、API 路由、SSE 连接 | -| **API 路由** | `src/server/api/friend.ts` | `/plugins/friend/*` 的所有 REST 端点 | -| **TTS 服务** | `src/friend/tts.ts` | Edge TTS + Qwen DashScope TTS,音频文件注册表 | -| **STT 服务** | `src/friend/stt-service.ts` | 基于文件的语音转录(REST 端点) | -| **VAD 服务** | `src/friend/voice/vad-service.ts` | Silero VAD ONNX 模型,onnxruntime-web WASM 后端 | -| **偏好设置** | `src/friend/prefs.ts` | 持久化到 `~/.config/Codev/friend.json` | -| **Tauri 启动器** | `src/friend/tauri-launcher.ts` | 启动 Tauri 桌面窗口 | -| **前端应用** | `src/components/friend/frontend/` | React + Three.js + @pixiv/three-vrm | - ---- - -## LLM 集成 - -Friend 通过三种方式与 LLM 深度集成: - -### 1. `friend_emotion` 工具 -- 定义在 `src/tools/FriendEmotionTool.ts` -- LLM 可在每次回复后调用,设置角色表情和心情 -- 参数:`emotion` (13种情绪之一)、`intensity` (0-1)、`mood_delta` (-3 到 +3) -- 通过 `broadcastToVrm()` 向 SSE 客户端广播表情切换 -- `mood_delta` 会持久化到 prefs 中的 `_moodIndex`,并广播给前端心情指示器 - -### 2. `friend_screen_observe` 工具 -- 定义在 `src/tools/FriendScreenObserveTool.ts` -- 捕获桌面截图,返回图片路径 -- LLM 使用 Read 工具查看截图后,以同伴身份回应 -- 自动广播 `think` 表情到前端 - -### 3. friendPrompt 技能注入 -- 定义在 `src/skills/bundled/friendPrompt.ts` -- 自动注入系统提示,告知 LLM 拥有 VRM 虚拟形象 -- 包含情绪列表、心情指数、对话风格指引 - ---- - -## 情绪系统 - -Friend 支持 13 种情绪,映射到 VRM blend shapes 和骨骼动画: - -- `happy`, `sad`, `angry`, `surprised`, `think`, `awkward`, `question`, `curious`, `neutral`, `love`, `flirty`, `greeting`, `relaxed` - -每种情绪在 `src/components/friend/frontend/emote.ts` 中定义了: -- VRM blend shapes 组合及权重 -- 过渡时间(cubic ease 缓动) -- 自动回中到 neutral 的定时器 - -情绪与肢体动作在 `App.tsx` 的 `emotionActionMap` 中关联。 - ---- - -## 交互模式 - -### 文字聊天 -用户在输入框输入文字,通过 `POST /plugins/friend/chat` 发送到后端,FriendService 调用 `sendText()` 将消息通过 `messageQueueManager.enqueue()` 注入 CLI 对话流程。 - -### PTT 按键通话 -按住麦克风按钮进行语音录制,松开后语音数据被发送到 STT 服务转录为文字,然后提交到对话流程。 - -### F2 语音通话 -按下 F2 进入连续语音通话模式。服务器端通过 `arecord`/`parecord` 持续捕获麦克风音频,Silero VAD 自动检测语音段落边界,转录后自动发送给 AI 处理。AI 回复通过 TTS 播放,期间静音系统阻止回声。 - ---- - -## 简要架构图 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Codev 主进程 (Bun) │ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────┐ │ -│ │ CLI TUI │ │ LLM Provider │ │ Bridge API │ │ -│ │ (React Ink) │◄──►│ (Anthropic/NIM) │◄──►│ (SSE/HTTP) │ │ -│ └──────┬───────┘ └────────┬─────────┘ └───────────────┘ │ -│ │ │ │ -│ ┌──────▼─────────────────────▼──────────────────────────────┐ │ -│ │ FriendService (单例) │ │ -│ │ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌───────┐ │ │ -│ │ │ STT 连接器 │ │ TTS 生成器│ │ VAD 检测 │ │静音系统│ │ │ -│ │ │(Groq/等) │ │(Edge/Qwen)│ │(Silero) │ │ │ │ │ -│ │ └────────────┘ └───────────┘ └───────────┘ └───────┘ │ │ -│ └───────────────────────┬────────────────────────────────────┘ │ -│ │ │ -│ ┌───────────────────────▼────────────────────────────────────┐ │ -│ │ HTTP 服务器 (Bun.serve :3456) │ │ -│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ -│ │ │ SSE /events │ │ REST API │ │ 静态文件服务 │ │ │ -│ │ │ │ │ /chat /voice │ │ /friend/* │ │ │ -│ │ └─────────────┘ └──────────────┘ └──────────────────┘ │ │ -│ └───────────────────────┬────────────────────────────────────┘ │ -└──────────────────────────┼────────────────────────────────────────┘ - │ SSE + HTTP - ▼ -┌──────────────────────────────────────────────────────────────────┐ -│ Tauri 桌面窗口 (WebKitGTK) │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ App.tsx (React 应用) │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ │ -│ │ │ VRMScene │ │TextBubble│ │ChatInput│ │ MoodIndicator│ │ │ -│ │ │ Three.js │ │SSE驱动 │ │PTT/通话│ │ Canvas 动画 │ │ │ -│ │ └──────────┘ └──────────┘ └────────┘ └──────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ 核心 3D 子系统: │ -│ ┌─────────────┐ ┌────────────────┐ ┌──────────┐ ┌──────────┐ │ -│ │EmoteController│ MotionController │ LipSync │ TextBubble │ │ -│ │blend shapes │ VRMA/VMD/FBX │ WebAudio │ 打字机效果 │ │ -│ │13种情绪映射 │ 舞蹈系统 │ 唇形同步 │ Markdown │ │ -│ └─────────────┘ └────────────────┘ └──────────┘ └──────────┘ │ -└──────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 文件位置 - -- 后端: `src/friend/` — FriendService, SSE, TTS, STT, VAD, prefs, server, launcher -- 前端: `src/components/friend/frontend/` — React app, Three.js 3D 场景 -- API 路由: `src/server/api/friend.ts` — 所有 HTTP 端点 -- LLM 工具: `src/tools/FriendEmotionTool.ts`, `src/tools/FriendScreenObserveTool.ts` -- 技能注入: `src/skills/bundled/friendPrompt.ts` -- 配置: `~/.config/Codev/friend.json` diff --git a/docs/friend/voice-vad.md b/docs/friend/voice-vad.md deleted file mode 100644 index a0d4529c80ae2f79663f577cf6277e6e0ab688c4..0000000000000000000000000000000000000000 --- a/docs/friend/voice-vad.md +++ /dev/null @@ -1,527 +0,0 @@ -# 语音捕获与 VAD 策略 - -本文档详细描述 Friend 系统的音频捕获、语音活动检测 (VAD)、语音转文字 (STT) 和静音管理策略。 - ---- - -## 1. 音频捕获 - -### 1.1 子进程架构 - -Friend 使用子进程方式进行音频捕获,而非原生 NAPI 绑定。这是因为 cpal 的同步 NAPI 调用在 ALSA 初始化卡顿时会阻塞事件循环,且无法从 JS 侧超时。 - -**文件**: `src/friend/FriendService.ts` — `loadAudioCapture()` 方法 - -```typescript -private async loadAudioCapture(): Promise { - // 使用 arecord / parecord 子进程 - // 尝试顺序: arecord (ALSA) → parecord (PulseAudio) -} -``` - -### 1.2 工具选择与参数 - -**arecord** (ALSA): -``` --D default # 默认设备 --r 16000 # 采样率 16kHz --f S16_LE # 16位有符号小端 PCM --c 1 # 单声道 --t raw # 原始 PCM 格式 --q # 静默模式 -``` - -**parecord** (PulseAudio): -``` ---raw # 原始 PCM ---rate=16000 # 16kHz ---format=s16le # 16位有符号小端 ---channels=1 # 单声道 ---latency-msec=20 # 低延迟 -``` - -### 1.3 验证机制 (500ms 窗口) - -子进程启动后有 500ms 验证窗口: - -```typescript -// 启动子进程后等待 500ms -// 若子进程在此时间内产生了音频数据 → 验证通过 -// 若子进程退出且未产出数据 → 验证失败,尝试下一个工具 -// 若 500ms 无数据 → 验证失败 -``` - -这防止了 `parecord` 在 PulseAudio 不可用时静默失败的问题(进程启动但立即退出)。 - -### 1.4 数据回调 - -```typescript -const feedAudio = (chunk: Buffer) => { - // 1. 静音过滤: muted 时不转发(防止 TTS 回声) - if (this.muted) return; - - // 2. 转发到 STT 连接 - onData(chunk); - - // 3. 转发到 VAD 检测 - if (this.vadInstance) { - const float32 = new Float32Array(chunk.length / 2); - for (let i = 0; i < float32.length; i++) { - float32[i] = chunk.readInt16LE(i * 2) / 32768; - } - this.vadInstance.processAudio(float32).catch(() => {}); - } -}; -``` - -### 1.5 停止逻辑 - -```typescript -stopRecording: async () => { - if (captureProc) { - captureProc.kill('SIGTERM'); - // 2s 后强制 SIGKILL(防止僵进程) - setTimeout(() => { - try { captureProc?.kill('SIGKILL'); } catch {} - }, 2000); - captureProc = null; - } -}, -``` - ---- - -## 2. STT Provider 检测与降级 - -### 2.1 自动检测链 - -**文件**: `src/friend/FriendService.ts` — `detectAvailableSttProvider()` 方法 - -```typescript -detectAvailableSttProvider() - │ - ├── 1. Groq Whisper (isGroqAvailable) - │ 最快 - REST API 调用,无需 Python - │ 检测: 检查 API key 是否存在 - │ - ├── 2. Local Whisper (checkLocalWhisperAvailable) - │ 本地运行,无需网络 - │ 检测: 导入 connectLocalWhisperStream 检查 - │ - ├── 3. Anthropic Voice Stream (isVoiceStreamAvailable) - │ 通过 Anthropic API 的流式语音识别 - │ 检测: 检查登录状态和 API key - │ - ├── 4. Doubao ASR - │ 通过豆包 API - │ 检测: 检查 ~/.claude/tts/doubao/credentials.json - │ - └── 全不可用 → 抛出错误 - "No STT provider available. Install local Whisper: pip install openai-whisper" -``` - -### 2.2 STT Provider 特性对比 - -| Provider | 延迟 | 依赖 | 是否需要网络 | 支持语言 | -|----------|------|------|-------------|---------| -| **Groq Whisper** | 低 | API key | 是 | 多语言 | -| **Local Whisper** | 中 | Python + pip | 否 | 多语言 | -| **Anthropic Voice Stream** | 低 | Anthropic 登录 | 是 | 多语言 (keyterms支持) | -| **Doubao ASR** | 中 | 凭据文件 | 是 | 中文最佳 | - -### 2.3 STT 连接超时 - -所有 provider 连接都有 8 秒超时: - -```typescript -startSttConnectionWithTimeout(provider, language) - ├── 超时 8s - └── 超时错误提示: "STT provider '{provider}' timed out after 8s." - └── provider === 'local' 时附带 pip 安装提示 -``` - -### 2.4 连接工厂 - -```typescript -startSttConnection(provider, language) - │ - ├── anthropic: connectVoiceStream(callbacks, { language, keyterms }) - │ keyterms: ['code', 'codev'] 提高相关词汇识别率 - │ - ├── local: preloadWhisperModel + connectLocalWhisperStream - │ 需预加载模型(首次加载较慢) - │ - ├── doubao: connectDoubaoStream(callbacks, { language }) - │ - └── groq: connectGroqStream(callbacks, { language }) - 最快,纯 REST 流式调用 -``` - -### 2.5 回调接口 - -```typescript -const callbacks = { - onTranscript: (text: string, isFinal: boolean) => { - if (isFinal) { - this.captureTranscripts.push(text); // 最终文本入队列 - this.captureInterimText = ''; - } else { - this.captureInterimText = text; // 临时文本(前端轮询显示) - } - // 更新状态供前端轮询 - this.setState({ - captureStatus: { capturing: true, interimText: this.captureInterimText }, - }); - }, - onError: (_error: string) => {}, - onClose: () => {}, - onReady: (_conn: any) => {}, -}; -``` - ---- - -## 3. Silero VAD 架构 - -**文件**: `src/friend/voice/vad-service.ts` (~322 行) - -### 3.1 为什么选择 onnxruntime-web WASM - -Bun 不支持 onnxruntime-node 原生插件(会触发 segfault),因此使用 onnxruntime-web 的 WASM 后端。WASM 二进制文件来自 `onnxruntime-web/dist`。 - -### 3.2 模型规格 - -- **模型**: Silero VAD legacy ONNX (来自 `@ericedouard/vad-node-realtime`) -- **模型文件**: `silero_vad_legacy.onnx` -- **输入**: 512 采样帧 @ 16kHz (32ms) -- **输出**: 语音概率 (0-1) -- **LSTM 状态**: h=[2,1,64], c=[2,1,64] - -### 3.3 配置参数 - -```typescript -this.opts = { - // 说话判定阈值 - positiveSpeechThreshold: 0.75, // 超过此值判定为语音帧 - negativeSpeechThreshold: 0.50, // 低于此值判定为静音帧 - - // 触发条件 - preSpeechTriggerFrames: 10, // 需要连续 10 帧 (320ms) 确认说话 - minSpeechFrames: 6, // 最少 6 帧 (192ms) 有效语音 - - // 静音消音 - redemptionFrames: 20, // 连续 20 帧 (640ms) 静音结束段落 - - // 前置填充 - preSpeechPadFrames: 10, // 段落开头包含 10 帧前置音频 - - // 能量过滤 - rmsThreshold: 0.004, // RMS 能量阈值 (-48dBFS 噪声底限) - - // 采样率 - sampleRate: 16000, // 16kHz -}; -``` - -### 3.4 RMS 能量预过滤(噪声抑制) - -RMS 预过滤是 VAD 的**第一道噪声防线**。在运行 ONNX 推理之前,先计算帧的 RMS 能量: - -```typescript -let sumSq = 0; -for (let i = 0; i < frame.length; i++) { - sumSq += frame[i] * frame[i]; -} -const rms = Math.sqrt(sumSq / frame.length); - -if (rms < this.opts.rmsThreshold) { - prob = 0; // 低于噪声底限 → 跳过 ONNX 推理 -} else { - // 执行 ONNX 推理 - prob = await this.session.run({ input, sr, h, c }); -} -``` - -作用: -- 节省 CPU 资源(大量帧无语音信号) -- 过滤机械噪声/麦克风碰撞/环境静音(噪声抑制) -- 降低误触发率 - -注:FriendService 中使用更严格的阈值 `0.01`(-40dBFS)而非 SileroVAD 的默认 `0.004`,以减少非语音噪音造成的误触发。 - -### 3.5 状态机详解 - -``` - ┌──────────────────────────────────────────────────┐ - │ pre-speech phase │ - │ preSpeechCount < preSpeechTriggerFrames (10) │ - │ │ - │ 语音帧 → preSpeechCount++ │ - │ 非语音帧 → preSpeechCount = 0 │ - │ │ - │ 当 preSpeechCount >= 10: │ - │ → speaking = true │ - │ → speechFrameCount = preSpeechCount │ - │ → onSpeechStart() │ - └──────────────────────┬───────────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────────────────┐ - │ speaking phase │ - │ │ - │ 语音帧 → redemptionCounter = 0 │ - │ 静音帧 → redemptionCounter++ │ - │ │ - │ 当 redemptionCounter >= 20 (640ms): │ - │ → endSpeech() │ - │ → onSpeechEnd(audioSegment) │ - └──────────────────────────────────────────────────┘ -``` - -### 3.6 段落构建 - -当 onSpeechEnd 触发时,构建包含前置填充的音频段: - -```typescript -private endSpeech(): void { - // 1. 检查最小语音帧数 (防止误触发) - if (this.speechFrameCount < this.opts.minSpeechFrames) { - this.callbacks.onVADMisfire(); // 误触发回调 - return; - } - - // 2. 构建音频段 (含前置填充) - const total = this.frameHistory.length; - const prePad = Math.min(this.opts.preSpeechPadFrames, total); - const segFrames = this.frameHistory.slice( - total - prePad - this.speechFrameCount, - total - ); - // 合并所有帧为一个 Float32Array - const segment = new Float32Array(totalSamples); - for (const f of segFrames) { segment.set(f.frame, offset); offset += ... } - - // 3. 回调 - this.callbacks.onSpeechEnd(segment); -} -``` - -### 3.7 VAD 生命周期 - -```typescript -class SileroVad { - async init() // 加载 ONNX 模型 (初始化) - start() // 激活 VAD 处理 - pause() // 暂停 + 结束当前语音段 - processAudio() // 处理 PCM 音频帧 - flush() // 刷新剩余缓冲区 + 结束段 - reset() // 重置全部状态 (保留 session) - destroy() // 清理资源 -} -``` - -### 3.8 VAD 初始化失败的处理 - -VAD 初始化失败是非致命的: - -```typescript -vad.init() - .then(() => { this.vadInstance = vad; }) - .catch((e) => { - console.warn('[FriendService] VAD init failed (non-fatal, voice capture falls back to F2-only):', e); - }); -``` - -当 VAD 不可用时,F2 语音通话模式降级为手动分段(仍可通过 PTT 模式使用语音)。 - ---- - -## 4. 静音系统 - -### 4.1 为什么需要静音 - -当 AI 回复通过 TTS 播放时,扬声器声音会被麦克风捕获,如果不做静音处理会产生两种问题: -1. **TTS 回声**: 自己的语音进入 STT 造成重复识别 -2. **打断 AI 回复**: 用户未说话但环境噪声导致 VAD 误触发 - -### 4.2 静音策略 - -``` -startAiTurnMute() ──── 在语音片段提交时立即静音 - │ - ├── muted = true - ├── vadInstance.pause() - └── 30s 超时定时器 (安全性保障) - │ - ▼ -AI 处理 (工具调用、深度思考) - │ - ▼ -broadcastResponse() → generateTts() - │ - ├── TTS 成功: - │ └── extendMuteForTts(audioId) - │ ├── 取消 30s 定时器 - │ └── 设置精确的 TTS 播放时长定时器 - │ - ├── TTS 失败: - │ └── unmute() (立即解除静音) - │ - ▼ -TTS 播放完毕 → unmute() - ├── muted = false - ├── muteTimer = null - └── vadInstance.start() (恢复 VAD 监听) -``` - -### 4.3 定时器精确控制 - -- **初始静音**: 30s(覆盖几乎所有 AI 响应周期) -- **精确调整**: TTS 生成后通过 MP3 时长解析精确控制 -- **安全性保障**: 任何情况下都不会永久静音 - -### 4.4 清除静音 (紧急情况) - -```typescript -stopVoiceCapture() → _stopCapture() - ├── 停止音频捕获 - ├── clearMute() (立即解除静音) - └── VAD reset() -``` - ---- - -## 5. MP3 时长解析 - -**文件**: `src/friend/FriendService.ts` — `getMp3DurationMs()` 方法 - -### 5.1 帧同步头扫描法 - -不使用 bitrate 查找表(容易出错),而是通过实际帧间隔计算: - -```typescript -private getMp3DurationMs(audioId: string): number { - // 1. 获取音频文件路径 - const filePath = getAudioFile(audioId); - const buf = readFileSync(filePath); - - // 2. 跳过 ID3v2 标签 (如果存在) - if (buf[0..2] === 'ID3') { - offset = 10 + syncsafe_int(buf[6..9]); - } - - // 3. 找到前两个帧同步字 - // 帧同步: 0xFF 字节 + 0xE0 掩码 - for (let i = offset; i < buf.length - 3; i++) { - if (isSync(i)) { - if (firstSync === -1) firstSync = i; - else { secondSync = i; break; } - } - } - - // 4. 计算帧间隔 (CBR 模式) - const frameSize = secondSync - firstSync; - - // 5. 解析帧头获取采样率 - const h = read32BE(firstSync); - const version = (h >> 19) & 0x3; - const sampleRateIdx = (h >> 10) & 0x3; - // MPEG1 → 1152 samples/frame, MPEG2/2.5 → 576 - - // 6. 按步长计数帧数 (帧损坏时扫描到下一个同步字) - for (let pos = firstSync; pos + 3 < buf.length; pos += frameSize) { - if (!isSync(pos)) { - // 损坏帧: 扫描到下一个同步字 - while (pos < buf.length - 3 && !isSync(pos)) pos++; - } - frames++; - } - - // 7. 计算时长 - return Math.round((frames * spf) / sampleRate * 1000); -} -``` - -### 5.2 为什么自实现 MP3 解析 - -- Edge TTS 输出 CBR MP3 -- 不依赖外部库(减少依赖项) -- 帧同步头扫描法对 CBR 精确且鲁棒 -- 处理文件损坏的帧 (向前扫描下一个同步字) - ---- - -## 6. 前端语音捕获 (useServerStt hook) - -**文件**: `src/components/friend/frontend/hooks/useServerStt.ts` - -### 6.1 设计原因 - -Tauri 使用 WebKitGTK,不兼容 `onnxruntime-web` WASM 后端,因此浏览器 VAD (`@ricky0123/vad-web`) 不可用。前端将语音捕获完全委托给后端。 - -### 6.2 两种模式 - -**Push-to-Talk (PTT)**: -```typescript -startPushToTalk() → POST /voice/start // 后端开始捕获 -stopPushToTalk() → POST /voice/stop // 后端停止并返回转录 -``` - -**Voice Call (F2 模式)**: -```typescript -startStreaming(onTranscript, onError) → POST /voice/start - │ - ├── 后端持续捕获麦克风音频 - ├── 后端自动分段 (VAD / 定时 5s) 并转录 - ├── 前端每 1s 轮询 POST /voice/status 获取 interimText - └── 显示在输入栏中 - -stopStreaming() → POST /voice/stop - └── 结束后端捕获,返回完整转录 -``` - -### 6.3 前端语音通话交互 - -```typescript -// TTS 中断: 用户说话时延迟 1s 中断当前 TTS -const scheduleInterrupt = useCallback(() => { - setTimeout(() => { - ;(window as any).__clawInterruptAudio?.() - }, 1000) -}, []) - -// 显示控制: 转录文字显示 3s 后自动清除 -setTimeout(() => { - if (voiceCallActiveRef.current) { - setText('') - } -}, 3000) -``` - ---- - -## 7. 浏览器 VAD 片段转录 - -前端浏览器 VAD(非 WebKitGTK 环境)检测到语音段落后,通过 HTTP 发送到后端: - -```typescript -// POST /plugins/friend/voice/stt-segment -// Body: raw PCM/WAV buffer -handleFriendApi → friendService.transcribeAudioSegment(audioBuffer) - │ - ├── 检测 STT provider (自动降级) - ├── 创建临时 STT 连接 - ├── 发送音频 → 等待 finalize - └── 发送转录文本到 AI 对话 -``` - ---- - -## 总结: 语音路径选择 - -| 使用场景 | VAD | STT Provider | 音频来源 | 静音需求 | -|---------|-----|------------|---------|---------| -| Push-to-Talk (PTT) | 否 (人工分段) | 自动检测 | arecord/parecord | 否 | -| F2 语音通话 | Silero VAD | 自动检测 → 分段 flush | arecord/parecord | 是 | -| 浏览器 VAD (非 Tauri) | 浏览器 VAD | STT segment API | getUserMedia | 否 | -| 文字输入 | 不适用 | 不适用 | 键盘 | 不适用 | diff --git a/package.json b/package.json index 9be261f074de24ca4b864e199b7519bd7f84a470..8652ea22bc618bd494b95d134b2428d2d8abaa4f 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,7 @@ }, "scripts": { "build": "bun run ./scripts/build.ts", - "dev": "bun run ./scripts/dev.ts", - "friend:build": "cd src/components/friend/frontend && npx vite build", - "friend:dev": "cd src/components/friend/frontend && npx vite" + "dev": "bun run ./scripts/dev.ts" }, "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/scripts/build.ts b/scripts/build.ts index 4b46f6faed1334f366067103a373e49f1485b324..3da8e2ac6b41332db9a71d69dc538fdd43ce54b1 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -51,33 +51,6 @@ const features = [...featureSet] const outfile = join('dist', 'codev') -// ── Pre-step: build Friend VRM frontend ────────────────────────────────── -function buildFriendFrontend(): boolean { - const frontendDir = join(process.cwd(), 'src', 'components', 'friend', 'frontend') - const distIndex = join(frontendDir, 'dist', 'index.html') - if (existsSync(distIndex)) { - console.log('Friend frontend already built, skipping.') - return true - } - console.log('Building Friend VRM frontend...') - const proc = Bun.spawnSync({ - cmd: ['npm', 'run', 'build'], - cwd: frontendDir, - stdout: 'inherit', - stderr: 'inherit', - }) - if (proc.exitCode !== 0) { - console.error('Friend frontend build failed.') - return false - } - console.log('Friend frontend built.') - return true -} - -if (!buildFriendFrontend()) { - process.exit(1) -} - // ────────────────────────────────────────────────────────────────────────── const buildTime = new Date().toISOString() diff --git a/scripts/speak.py b/scripts/speak.py deleted file mode 100644 index 556aa90c4ae702e7291668dd933a1d7931d593f7..0000000000000000000000000000000000000000 --- a/scripts/speak.py +++ /dev/null @@ -1,42 +0,0 @@ -import json, os, sys, tempfile, asyncio - -async def speak(text: str, voice: str = "en-US-JennyNeural", output_path: str | None = None) -> dict: - try: - import edge_tts - communicate = edge_tts.Communicate(text, voice) - if output_path: - await communicate.save(output_path) - return {"success": True, "audio_path": output_path} - tmp = tempfile.mktemp(suffix=".mp3") - await communicate.save(tmp) - return {"success": True, "audio_path": tmp} - except ImportError: - pass - - return {"success": False, "error": "edge-tts is not installed. Run: pip install edge-tts"} - -def main(): - if len(sys.argv) < 2: - print(json.dumps({"success": False, "error": "Usage: speak.py [--voice ] [--output ]"})) - sys.exit(1) - - text = sys.argv[1] - voice = "en-US-JennyNeural" - output_path = None - - i = 2 - while i < len(sys.argv): - if sys.argv[i] == "--voice" and i + 1 < len(sys.argv): - voice = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--output" and i + 1 < len(sys.argv): - output_path = sys.argv[i + 1] - i += 2 - else: - i += 1 - - result = asyncio.run(speak(text, voice, output_path)) - print(json.dumps(result)) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/transcribe.py b/scripts/transcribe.py deleted file mode 100644 index f640b380e02617241219d651c874eb5c333b1122..0000000000000000000000000000000000000000 --- a/scripts/transcribe.py +++ /dev/null @@ -1,81 +0,0 @@ -import json, os, sys, tempfile, wave - -WAV_HEADER_SIZE = 44 - -WHISPER_CACHE = os.path.expanduser("~/.cache/whisper") - -def write_wav(path: str, raw_pcm: bytes, sample_rate: int = 16000): - with wave.open(path, 'wb') as w: - w.setnchannels(1) - w.setsampwidth(2) - w.setframerate(sample_rate) - w.writeframes(raw_pcm) - -def get_model_path(name): - lp = os.path.expanduser(f"~/.cache/huggingface/hub/models--Systran--faster-whisper-{name}") - if os.path.isdir(lp): - return lp - return name - -def transcribe(wav_path: str, model_size: str = "small", language: str | None = None) -> dict: - try: - from faster_whisper import WhisperModel - model_path = get_model_path(model_size) - model = WhisperModel(model_path, device="cuda", compute_type="int8_float16") - opts = {"beam_size": 5} - if language: - opts["language"] = language - segments, info = model.transcribe(wav_path, **opts) - text = " ".join(seg.text for seg in segments) - return {"success": True, "text": text.strip(), "language": info.language} - except ImportError: - pass - - try: - import whisper - model = whisper.load_model(model_size, device="cuda", download_root=WHISPER_CACHE) - opts = {} - if language: - opts["language"] = language - result = model.transcribe(wav_path, **opts) - return {"success": True, "text": result["text"].strip(), "language": result.get("language", "")} - except ImportError: - pass - - return {"success": False, "error": "Neither faster-whisper nor openai-whisper is installed. Run: pip install faster-whisper"} - -def main(): - if len(sys.argv) < 2: - print(json.dumps({"success": False, "error": "Usage: transcribe.py [--model ] [--language ]"})) - sys.exit(1) - - wav_path = sys.argv[1] - model_size = "small" - language = None - - i = 2 - while i < len(sys.argv): - if sys.argv[i] == "--model" and i + 1 < len(sys.argv): - model_size = sys.argv[i + 1] - i += 2 - elif sys.argv[i] == "--language" and i + 1 < len(sys.argv): - language = sys.argv[i + 1] - i += 2 - else: - i += 1 - - if sys.argv[1] == "--stdin-pcm": - sample_rate = int(sys.argv[2]) if len(sys.argv) > 2 else 16000 - raw = sys.stdin.buffer.read() - tmp = tempfile.mktemp(suffix=".wav") - write_wav(tmp, raw, sample_rate) - wav_path = tmp - result = transcribe(wav_path, model_size, language) - os.unlink(tmp) - else: - result = transcribe(wav_path, model_size, language) - - print(json.dumps(result)) - -if __name__ == "__main__": - main() diff --git a/scripts/whisper_server.py b/scripts/whisper_server.py deleted file mode 100644 index a705a4122b0a4d4ad7527f4f315fe01919e7d715..0000000000000000000000000000000000000000 --- a/scripts/whisper_server.py +++ /dev/null @@ -1,80 +0,0 @@ -import json, os, sys, wave - -WHISPER_CACHE = os.path.expanduser("~/.cache/whisper") - -def write_wav(path, raw_pcm, sample_rate=16000): - with wave.open(path, 'wb') as w: - w.setnchannels(1) - w.setsampwidth(2) - w.setframerate(sample_rate) - w.writeframes(raw_pcm) - -def get_model_path(name): - lp = os.path.expanduser(f'~/.cache/huggingface/hub/models--Systran--faster-whisper-{name}') - if os.path.isdir(lp): - return lp - return name - -def main(): - model = None - model_name = None - - for line in sys.stdin: - line = line.strip() - if not line: - continue - try: - msg = json.loads(line) - except: - continue - - msg_type = msg.get('type') - - if msg_type == 'load': - name = msg.get('model', 'small') - print(json.dumps({'type': 'ready', 'model': name}), flush=True) - sys.stdout.flush() - - elif msg_type == 'transcribe': - wav_path = msg.get('wav') - language = msg.get('language') - - if not model or model_name != msg.get('model'): - model_name = msg.get('model', 'base') - print(json.dumps({'type': 'status', 'message': f'Loading model {model_name}...'}), flush=True) - try: - import whisper - model = whisper.load_model(model_name, device='cuda', download_root=WHISPER_CACHE) - print(json.dumps({'type': 'status', 'message': 'Model loaded'}), flush=True) - except ImportError: - try: - from faster_whisper import WhisperModel - mp = get_model_path(model_name) - model = WhisperModel(mp, device='cuda', compute_type='int8_float16') - print(json.dumps({'type': 'status', 'message': 'Model loaded'}), flush=True) - except (ImportError, RuntimeError) as e: - print(json.dumps({'type': 'error', 'message': f'No whisper library available: {e}'}), flush=True) - sys.stdout.flush() - continue - - if not wav_path or not os.path.exists(wav_path): - print(json.dumps({'type': 'error', 'message': 'WAV file not found'}), flush=True) - sys.stdout.flush() - continue - - try: - opts = {} - if language: - opts['language'] = language - - result = model.transcribe(wav_path, **opts) - text = result['text'].strip() if isinstance(result, dict) else '' - lang = result.get('language', language or 'en') if isinstance(result, dict) else (language or 'en') - - print(json.dumps({'type': 'result', 'text': text, 'language': lang}), flush=True) - except Exception as e: - print(json.dumps({'type': 'error', 'message': str(e)}), flush=True) - sys.stdout.flush() - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 242ff46a4134f0bbadabddc25215f183fc492826..9b2d1376fc41bfb329d369125a71ef4e9a8a8ab6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -44,7 +44,6 @@ import skills from './commands/skills/index.js' import status from './commands/status/index.js' import tasks from './commands/tasks/index.js' import feishu from './commands/feishu/index.js' -import friend from './commands/friend/index.js' import goals from './commands/goal/index.js' import telegram from './commands/telegram/index.js' import teleport from './commands/teleport/index.js' @@ -310,7 +309,6 @@ const COMMANDS = memoize((): Command[] => [ theme, feedback, feishu, - friend, goals, review, ultrareview, diff --git a/src/commands/friend/friend.tsx b/src/commands/friend/friend.tsx deleted file mode 100644 index 674003567300750840c8d7d84f18bd40819e0408..0000000000000000000000000000000000000000 --- a/src/commands/friend/friend.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/** - * /friend TUI command — manage VRM desktop pet companion. - * - * Ink-based terminal UI that shows status and controls for the - * Friend desktop pet feature. - */ -import React, { useEffect } from 'react' -import { Box, Text, useInput } from '../../ink.js' -import { getPrefs, updatePrefs } from '../../friend/prefs.js' -import { - launchTauri, - stopTauri, -} from '../../friend/tauri-launcher.js' -import { startFriendServer, stopFriendServer, getServerPort } from '../../friend/server.js' -import { friendService } from '../../friend/FriendService.js' -import type { LocalJSXCommandOnDone, CommandResultDisplay } from '../../types/command.js' - -const FRIEND_URL = 'http://127.0.0.1:3456/friend/' - -const logger = () => ({ - info: (msg: string) => console.log(`[Friend] ${msg}`), - warn: (msg: string) => console.warn(`[Friend] ${msg}`), -}) - -export async function call( - onDone: (result?: string, options?: { display?: CommandResultDisplay }) => void, - _context: unknown, - args?: string, -): Promise { - const trimmed = args?.trim().toLowerCase() - - // /friend (no args) or /friend start → start the service - if (!trimmed || trimmed === 'start') { - updatePrefs({ enabled: true }) - // Start in-process HTTP server for friend API and SSE - try { - startFriendServer(3456, '127.0.0.1') - } catch (err) { - console.warn(`[Friend] HTTP server start failed: ${err}`) - console.warn('[Friend] The Tauri app may need the main server on port 3456.') - } - // FriendService runs in-process; messages enqueue into the CLI queue - await friendService.start().catch((err) => { - console.warn(`[Friend] Service start failed: ${err}`) - }) - // Launch Tauri display window (thin client) - launchTauri(logger()) - return - } - - if (trimmed === 'stop') { - updatePrefs({ enabled: false }) - await friendService.stop().catch(() => {}) - stopFriendServer() - stopTauri(logger()) - return - } - - // /friend help → show manager/status view - return -} - -function FriendManager({ onDone }: { onDone: LocalJSXCommandOnDone }) { - useInput((_input, key) => { - if (key.escape || key.return) { - onDone(undefined, { display: 'skip' }) - } - }) - - return ( - - - - Friend VRM Desktop Pet - - - - - - - Usage: - /friend Interactive status & controls - /friend start Launch friend window - /friend stop Stop friend window - - - - Press Esc or Enter to close. - - - ) -} - -function FriendStopView({ onDone }: { onDone: LocalJSXCommandOnDone }) { - useEffect(() => { - const t = setTimeout(() => onDone(undefined, { display: 'skip' }), 1500) - return () => clearTimeout(t) - }, [onDone]) - - return ( - - Friend companion stopped. - - ) -} - -function FriendStartView({ onDone }: { onDone: LocalJSXCommandOnDone }) { - useEffect(() => { - const t = setTimeout(() => onDone(undefined, { display: 'skip' }), 1500) - return () => clearTimeout(t) - }, [onDone]) - - return ( - - Starting Friend VRM companion... - A Tauri window will open. - Also available at: {FRIEND_URL} - - ) -} - -function FriendStatus({ - prefs, -}: { - prefs: ReturnType -}) { - const statusDot = prefs.enabled ? '●' : '○' - const statusColor = prefs.enabled ? 'green' : 'gray' - - return ( - - - {statusDot} Status: - - {prefs.enabled ? 'Enabled' : 'Disabled'} - - - - URL: - {FRIEND_URL} - - - Voice: - {prefs.voice ?? 'default'} - - - TTS: - {prefs.ttsEnabled ? 'on' : 'off'} - - - Tracking: - {prefs.tracking ?? 'mouse'} - - - ) -} \ No newline at end of file diff --git a/src/commands/friend/index.ts b/src/commands/friend/index.ts deleted file mode 100644 index ffce28932490b9a7545f02d7e26209f1396bf21d..0000000000000000000000000000000000000000 --- a/src/commands/friend/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Command } from '../../commands.js' - -const friend = { - type: 'local-jsx', - name: 'friend', - description: 'Manage the VRM desktop pet companion — start/stop the 3D avatar window and adjust settings', - aliases: ['vrm'], - load: () => import('./friend.js'), -} satisfies Command - -export default friend diff --git a/src/components/friend/frontend/App.tsx b/src/components/friend/frontend/App.tsx deleted file mode 100644 index 22dc5c7d7bd2eb304b8469fbe2a83a73de854050..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/App.tsx +++ /dev/null @@ -1,352 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from 'react' -import { VRMScene } from './components/VRMScene' -import type { VRMSceneHandle } from './components/VRMScene' -import { TextBubble } from './components/TextBubble' -import type { OnVrmMessage } from './components/TextBubble' -import { ChatInput } from './components/ChatInput' -import { ResizeHandles } from './components/ResizeHandles' -import { SettingsPanel } from './components/SettingsPanel' -import { usePassThrough } from './hooks/usePassThrough' -import { LipSync } from './lip-sync' -import { FRIEND_API, bindScene } from './api' -import { Menu, Move, Rotate3D, EyeOff, Settings, RefreshCw, Pin } from 'lucide-react' - -const DEFAULT_MODEL = '/friend/model1.vrm' - -// 情绪 → 动作映射 -// 只有有明确肢体动作关联的情绪才映射;neutral/relaxed 不绑动作, -// 避免角色在无明确意图时做出违和姿势。 -const emotionActionMap: Record = { - think: 'scratchHead', - question: 'point', - curious: 'scratchHead', - happy: 'happy', - surprised: 'excited', - angry: 'angry', - awkward: 'playFingers', - love: 'shy', - flirty: 'shy', - greeting: 'greeting', - sad: '', - relaxed: '', - neutral: '', -} - -const btnStyle: React.CSSProperties = { - width: 32, - height: 32, - border: 'none', - borderRadius: 6, - background: 'rgba(125, 125, 125, 0.28)', - backdropFilter: 'blur(6px)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: 'rgba(255, 255, 255, 0.8)', - fontSize: 16, - cursor: 'pointer', -} - -export default function App() { - const sceneRef = useRef(null) - const [pinned, setPinned] = useState(true) - const [tracking, setTracking] = useState<'mouse' | 'camera'>('mouse') - const [showText, setShowText] = useState(true) - const [collapsed, setCollapsed] = useState(false) - const [ttsEnabled, setTtsEnabled] = useState(true) - const [modelPath, setModelPath] = useState(DEFAULT_MODEL) - const [settingsOpen, setSettingsOpen] = useState(false) - const [hideUI, setHideUI] = useState(false) - const [volume, setVolume] = useState(0.5) - const [uiAlign, setUiAlign] = useState<'left' | 'right'>('right') - const [language, setLanguage] = useState<'zh' | 'en'>(() => navigator.language.startsWith('zh') ? 'zh' : 'en') - const [sttProvider, setSttProvider] = useState<'browser' | 'groq' | 'anthropic' | 'local' | 'doubao'>('browser') - const t = (zh: string, en: string) => language === 'en' ? en : zh - usePassThrough(!settingsOpen) - - // Load persisted settings on mount - useEffect(() => { - fetch(`${FRIEND_API}/settings`) - .then((r) => r.json()) - .then((s) => { - if (s.modelPath) setModelPath(s.modelPath) - if (s.ttsEnabled !== undefined) setTtsEnabled(s.ttsEnabled) - if (s.showText !== undefined) setShowText(s.showText) - if (s.hideUI !== undefined) setHideUI(s.hideUI) - if (s.tracking) { setTracking(s.tracking); sceneRef.current?.setTrackingMode(s.tracking) } - if (s.volume !== undefined) { setVolume(s.volume); LipSync.getInstance().setVolume(s.volume) } - if (s.uiAlign) setUiAlign(s.uiAlign) - if (s.sttProvider) setSttProvider(s.sttProvider) - if (s.language) { - setLanguage(s.language) - } else { - const detected = navigator.language.startsWith('zh') ? 'zh' : 'en' - fetch(`${FRIEND_API}/settings`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ language: detected }), - }).catch(() => {}) - } - }) - .catch(() => {}) - }, []) - - const saveSettings = (patch: Record) => { - fetch(`${FRIEND_API}/settings`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(patch), - }).catch(() => {}) - } - - useEffect(() => { - bindScene(sceneRef.current) - return () => bindScene(null) - }) - - const handleVolumeChange = useCallback((v: number) => { - setVolume(v) - LipSync.getInstance().setVolume(v) - saveSettings({ volume: v }) - }, []) - - const handleTrackingChange = useCallback((mode: 'mouse' | 'camera') => { - sceneRef.current?.setTrackingMode(mode) - setTracking(mode) - saveSettings({ tracking: mode }) - }, []) - - const handleVrmMessage: OnVrmMessage = useCallback((msg) => { - if (msg.emotion && sceneRef.current) { - const action = msg.action || emotionActionMap[msg.emotion] - if (msg.text) { - sceneRef.current.setEmotionWithReset(msg.emotion, msg.emotionDuration ?? 5000, msg.emotionIntensity) - if (action) sceneRef.current.playAction(action) - } else { - sceneRef.current.setEmotionWithReset(msg.emotion, msg.emotionDuration ?? 10000, msg.emotionIntensity) - // No hold — action plays once, emotion timer handles duration. - // Hold would lock _actionPlaying for 10s, blocking text-message actions - // when friend_emotion tool fires before broadcastResponse. - if (action) sceneRef.current.playAction(action) - } - } - }, []) - - // ── Idle fidget: natural micro-movements when no activity ───────────────── - const lastActivityRef = useRef(Date.now()) - const originalHandleVrmMessage = handleVrmMessage - const handleVrmMessageWithActivity: OnVrmMessage = useCallback((msg) => { - lastActivityRef.current = Date.now() - originalHandleVrmMessage(msg) - }, [originalHandleVrmMessage]) - - useEffect(() => { - const allEmotions = [ - 'happy', 'sad', 'angry', 'surprised', 'think', 'awkward', - 'question', 'curious', 'neutral', 'love', 'flirty', 'greeting', 'relaxed', - ] - const idleTimers: Array<{ emotion?: string; action?: string; minIdle: number }> = [ - // Emotion-only (subtle facial changes, no body action) - { emotion: 'think', minIdle: 8 }, - { emotion: 'curious', minIdle: 12 }, - { emotion: 'relaxed', minIdle: 15 }, - { emotion: 'happy', minIdle: 20 }, - // Small body actions (no strong emotion) - { action: 'stretch', minIdle: 25 }, - { action: 'scratchHead', minIdle: 10 }, - { action: 'playFingers', minIdle: 15 }, - { action: 'akimbo', minIdle: 30 }, - // Combined emotion + action - { emotion: 'think', action: 'scratchHead', minIdle: 18 }, - { emotion: 'relaxed', action: 'akimbo', minIdle: 35 }, - { emotion: 'curious', action: 'point', minIdle: 22 }, - { emotion: 'happy', action: 'shy', minIdle: 28 }, - { emotion: 'surprised', action: 'excited', minIdle: 40 }, - ] - const IDLE_THRESHOLD_MS = 15_000 - const FIDGET_CHECK_MS = 8_000 - - const timer = setInterval(() => { - const idleMs = Date.now() - lastActivityRef.current - if (idleMs < IDLE_THRESHOLD_MS) return - if (Math.random() > 0.55) return - - // Pick a weighted-random fidget based on how long we've been idle - const candidates = idleTimers.filter((t) => idleMs >= t.minIdle * 1000) - if (candidates.length === 0) return - const pick = candidates[Math.floor(Math.random() * candidates.length)] - - if (pick.emotion) { - const intensity = 0.3 + Math.random() * 0.5 - const duration = 2000 + Math.random() * 3000 - sceneRef.current?.setEmotionWithReset(pick.emotion, duration, intensity) - } - if (pick.action) { - sceneRef.current?.playAction(pick.action) - } - lastActivityRef.current = Date.now() - }, FIDGET_CHECK_MS) - - return () => clearInterval(timer) - }, []) - - // 全局快捷键 - useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return - if (e.key === 'Tab') { - e.preventDefault() - setCollapsed((v) => !v) - } - if (e.key === 'F4') { - e.preventDefault() - setSettingsOpen((v) => !v) - } - if (e.key === 'F5') { - e.preventDefault() - window.location.reload() - } - } - window.addEventListener('keydown', onKeyDown) - return () => window.removeEventListener('keydown', onKeyDown) - }, []) - - const togglePin = async () => { - setPinned((v) => !v) - } - - return ( -
- - - - {!hideUI && } - setSettingsOpen(false)} - currentModel={modelPath} - onModelChange={(m) => { setModelPath(m); saveSettings({ modelPath: m }) }} - hideUI={hideUI} - onHideUIChange={(v) => { setHideUI(v); saveSettings({ hideUI: v }) }} - showText={showText} - onShowTextChange={(v) => { setShowText(v); saveSettings({ showText: v }) }} - ttsEnabled={ttsEnabled} - onTtsEnabledChange={(v) => { setTtsEnabled(v); saveSettings({ ttsEnabled: v }) }} - tracking={tracking} - onTrackingChange={handleTrackingChange} - volume={volume} - onVolumeChange={handleVolumeChange} - uiAlign={uiAlign} - onUiAlignChange={(v) => { setUiAlign(v); saveSettings({ uiAlign: v }) }} - language={language} - onLanguageChange={(v) => { setLanguage(v); saveSettings({ language: v }) }} - sttProvider={sttProvider} - onSttProviderChange={(v) => { setSttProvider(v); saveSettings({ sttProvider: v }) }} - /> - {!hideUI &&
- - {!collapsed && <> - - - - - - - } -
} -
- ) -} diff --git a/src/components/friend/frontend/api.ts b/src/components/friend/frontend/api.ts deleted file mode 100644 index 885ca3bd531cec8b053179a0fc2060d4ec4c4476..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/api.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { VRMSceneHandle } from './components/VRMScene' - -/** Codev server base URL */ -export const SERVER_URL = 'http://127.0.0.1:3456' - -/** Base path for the Friend VRM plugin API */ -export const FRIEND_API = `${SERVER_URL}/plugins/friend` - -let sceneHandle: VRMSceneHandle | null = null - -export function bindScene(handle: VRMSceneHandle | null) { - sceneHandle = handle -} - -/** - * POST to a server API. If the request body contains an `emotion` field, - * the VRM expression will automatically change to match. - * - * Body example: - * { emotion: "happy", message: "hello", ... } - * - * Supported emotions: happy, sad, angry, surprised, think, neutral - * - * Optional fields: - * emotionDuration — ms, auto-reset to neutral after this time - * emotionIntensity — 0~1, defaults to 1 - */ -export async function postApi(url: string, body: Record): Promise { - // Trigger emotion from the request body - if (body.emotion && sceneHandle) { - const duration = body.emotionDuration as number | undefined - const intensity = body.emotionIntensity as number | undefined - if (duration) { - sceneHandle.setEmotionWithReset(body.emotion, duration, intensity) - } else { - sceneHandle.setEmotion(body.emotion, intensity) - } - } - - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - - if (!res.ok) { - throw new Error(`API error: ${res.status} ${res.statusText}`) - } - - return res.json() -} diff --git a/src/components/friend/frontend/assets/lip-sync-profile.json b/src/components/friend/frontend/assets/lip-sync-profile.json deleted file mode 100644 index 879765368aaff39d42610bce83600069ace5c229..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/assets/lip-sync-profile.json +++ /dev/null @@ -1 +0,0 @@ -{ "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] }] }] } diff --git a/src/components/friend/frontend/components/ChatInput.tsx b/src/components/friend/frontend/components/ChatInput.tsx deleted file mode 100644 index 2d3d444c1789f0d88da7b3b175756f4df50e3f72..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/components/ChatInput.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useState, useRef, useCallback, useEffect } from 'react' -import { Phone, PhoneOff, Mic } from 'lucide-react' -import { FRIEND_API } from '../api' -import { useServerStt, type SttProvider } from '../hooks/useServerStt' - -export function ChatInput({ uiAlign = 'right', language = 'zh', sttProvider = 'browser' }: { uiAlign?: 'left' | 'right'; language?: 'zh' | 'en'; sttProvider?: SttProvider }) { - const t = (zh: string, en: string) => language === 'en' ? en : zh - const [voiceCallActive, setVoiceCallActive] = useState(false) - const [text, setText] = useState('') - const [recording, setRecording] = useState(false) - const voiceCallActiveRef = useRef(false) - const serverStt = useServerStt() - const displayTimerRef = useRef | null>(null) - - // --- Voice Call --- - const startVoiceCall = useCallback(async () => { - setVoiceCallActive(true) - voiceCallActiveRef.current = true - setText('') - - serverStt.startStreaming( - sttProvider, - (partialText, _isFinal) => { - if (partialText.trim()) { - setText(partialText) - if (displayTimerRef.current) clearTimeout(displayTimerRef.current) - displayTimerRef.current = setTimeout(() => { - if (voiceCallActiveRef.current) { - setText('') - } - }, 3000) - } - }, - (err) => { - console.error('Voice call error:', err) - if (voiceCallActiveRef.current) { - setText(`\u8bed\u97f3\u542f\u52a8\u5931\u8d25: ${err}`) - setTimeout(() => { - setText('') - endVoiceCall() - }, 3000) - } - }, - language === 'en' ? 'en' : 'zh', - ) - setRecording(true) - }, [sttProvider, serverStt, language]) - - const endVoiceCall = useCallback(async () => { - if (!voiceCallActiveRef.current) return - voiceCallActiveRef.current = false - setVoiceCallActive(false) - setRecording(false) - const finalText = await serverStt.stopStreaming() - if (finalText.trim()) { - // Send text via chat endpoint - fetch(`${FRIEND_API}/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: finalText }), - }).catch(() => {}) - } - setText('') - }, [serverStt]) - - // Global keyboard shortcuts - useEffect(() => { - const onGlobalKeyDown = (e: KeyboardEvent) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return - - if (e.key === 'F2') { - e.preventDefault() - if (voiceCallActive) { - endVoiceCall() - } else { - startVoiceCall() - } - } - } - window.addEventListener('keydown', onGlobalKeyDown) - return () => window.removeEventListener('keydown', onGlobalKeyDown) - }, [voiceCallActive, startVoiceCall, endVoiceCall]) - - // Broadcast recording state for VRM "listening" response - useEffect(() => { - (window as any).__userRecording = recording - return () => { (window as any).__userRecording = false } - }, [recording]) - - // Cleanup display timer - useEffect(() => { - return () => { - if (displayTimerRef.current) clearTimeout(displayTimerRef.current) - } - }, []) - - if (voiceCallActive) { - return ( -
-
-
- - {text || (recording ? t('正在听...', 'Listening...') : t('等待说话...', 'Waiting to speak...'))} - -
- - - - -
-
- ) - } - - return ( -
- -
- ) -} - -const barStyle: React.CSSProperties = { - position: 'absolute', - bottom: 16, - left: 12, - right: 12, - display: 'flex', - gap: 4, - zIndex: 300, - pointerEvents: 'auto', -} - -const inputStyle: React.CSSProperties = { - width: '100%', - height: 50, - boxSizing: 'border-box', - border: '1px solid rgba(255, 255, 255, 0.2)', - borderRadius: 25, - background: 'rgba(0, 0, 0, 0.4)', - backdropFilter: 'blur(6px)', - color: '#fff', - fontSize: 18, - padding: '0 10px', - outline: 'none', - fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif', -} diff --git a/src/components/friend/frontend/components/ResizeHandles.tsx b/src/components/friend/frontend/components/ResizeHandles.tsx deleted file mode 100644 index e4942c48a5d210c0ce4f4994d7591a8d0f9a1dea..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/components/ResizeHandles.tsx +++ /dev/null @@ -1,7 +0,0 @@ -/** - * ResizeHandles — stub for Tauri window resize. - * Not applicable in web mode (browser window cannot be resized from content). - */ -export function ResizeHandles() { - return null -} diff --git a/src/components/friend/frontend/components/SettingsPanel.tsx b/src/components/friend/frontend/components/SettingsPanel.tsx deleted file mode 100644 index 067cd32c5a8d828a6da03aaba4803013cb503a12..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/components/SettingsPanel.tsx +++ /dev/null @@ -1,657 +0,0 @@ -import { useState, useEffect, useRef, useCallback } from 'react' -import { X, Play, Loader } from 'lucide-react' -import { FRIEND_API } from '../api' - -type SttProvider = 'browser' | 'groq' | 'anthropic' | 'local' | 'doubao' - -interface SettingsPanelProps { - visible: boolean - onClose: () => void - currentModel: string - onModelChange: (path: string) => void - hideUI: boolean - onHideUIChange: (v: boolean) => void - showText: boolean - onShowTextChange: (v: boolean) => void - ttsEnabled: boolean - onTtsEnabledChange: (v: boolean) => void - tracking: 'mouse' | 'camera' - onTrackingChange: (v: 'mouse' | 'camera') => void - volume: number - onVolumeChange: (v: number) => void - uiAlign: 'left' | 'right' - onUiAlignChange: (v: 'left' | 'right') => void - language: 'zh' | 'en' - onLanguageChange: (v: 'zh' | 'en') => void - sttProvider?: SttProvider - onSttProviderChange?: (v: SttProvider) => void -} - -type Tab = 'general' | 'voice' | 'model' - -const BUILTIN_MODELS = ['/friend/model1.vrm', '/friend/model2.vrm', '/friend/model3.vrm', '/friend/model4.vrm', '/friend/model5.vrm'] - -const EDGE_VOICES = [ - { id: 'zh-CN-XiaoxiaoNeural', label: '晓晓 (女)' }, - { id: 'zh-CN-XiaoyiNeural', label: '晓依 (女)' }, - { id: 'zh-CN-YunxiNeural', label: '云希 (男)' }, - { id: 'zh-CN-YunjianNeural', label: '云健 (男)' }, - { id: 'zh-CN-XiaohanNeural', label: '晓涵 (女)' }, - { id: 'zh-CN-XiaomoNeural', label: '晓墨 (女)' }, - { id: 'zh-CN-XiaoxuanNeural', label: '晓萱 (女)' }, - { id: 'zh-CN-YunyangNeural', label: '云扬 (男)' }, - { id: 'zh-TW-HsiaoChenNeural', label: '曉臻 (女)' }, - { id: 'ja-JP-NanamiNeural', label: 'Nanami (女)' }, - { id: 'en-US-MichelleNeural', label: 'Michelle (F)' }, - { id: 'en-US-GuyNeural', label: 'Guy (M)' }, -] - -const QWEN_VOICES = [ - { id: 'Cherry', label: '芊悦 - 阳光亲切 (女)' }, - { id: 'Serena', label: '苏瑶 - 温柔 (女)' }, - { id: 'Ethan', label: '晨煦 - 阳光温暖 (男)' }, - { id: 'Chelsie', label: '千雪 - 二次元 (女)' }, - { id: 'Momo', label: '茉兔 - 撒娇搞怪 (女)' }, - { id: 'Vivian', label: '十三 - 可爱小暴躁 (女)' }, - { id: 'Moon', label: '月白 - 率性帅气 (男)' }, - { id: 'Maia', label: '四月 - 知性温柔 (女)' }, - { id: 'Kai', label: '凯 - 耳朵SPA (男)' }, - { id: 'Nofish', label: '不吃鱼 - 设计师 (男)' }, - { id: 'Bella', label: '萌宝 - 小萝莉 (女)' }, - { id: 'Mia', label: '乖小妹 - 温顺乖巧 (女)' }, - { id: 'Mochi', label: '沙小弥 - 童真小大人 (男)' }, - { id: 'Bunny', label: '萌小姬 - 萌属性 (女)' }, - { id: 'Nini', label: '邻家妹妹 - 软糯甜蜜 (女)' }, - { id: 'Stella', label: '少女阿月 - 迷糊少女 (女)' }, - { id: 'Pip', label: '顽屁小孩 - 调皮捣蛋 (男)' }, - { id: 'Neil', label: '阿闻 - 新闻主持 (男)' }, - { id: 'Eldric Sage', label: '沧明子 - 沉稳老者 (男)' }, - { id: 'Vincent', label: '田叔 - 沙哑烟嗓 (男)' }, - { id: 'Bellona', label: '燕铮莺 - 有声书 (女)' }, - { id: 'Seren', label: '小婉 - 温柔助眠 (女)' }, -] - -const QWEN_MODELS = [ - { id: 'qwen3-tts-flash', label: 'Qwen3 TTS Flash' }, -] - -export function SettingsPanel({ - visible, onClose, currentModel, onModelChange, - hideUI, onHideUIChange, - showText, onShowTextChange, - ttsEnabled, onTtsEnabledChange, - tracking, onTrackingChange, - volume, onVolumeChange, - uiAlign, onUiAlignChange, - language, onLanguageChange, - sttProvider = 'browser', onSttProviderChange, -}: SettingsPanelProps) { - const t = (zh: string, en: string) => language === 'en' ? en : zh - - const [tab, setTab] = useState('general') - const [currentVoice, setCurrentVoice] = useState('') - const [currentProvider, setCurrentProvider] = useState('edge') - const [qwenKey, setQwenKey] = useState('') - const [qwenModel, setQwenModel] = useState('qwen3-tts-flash') - const [previewingId, setPreviewingId] = useState(null) - const audioRef = useRef(null) - - // Drag state - const [panelPos, setPanelPos] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) - const dragRef = useRef<{ dragging: boolean; startX: number; startY: number; origX: number; origY: number }>({ - dragging: false, startX: 0, startY: 0, origX: 0, origY: 0, - }) - - const onDragStart = useCallback((e: React.MouseEvent) => { - dragRef.current = { dragging: true, startX: e.clientX, startY: e.clientY, origX: panelPos.x, origY: panelPos.y } - const onMove = (ev: MouseEvent) => { - if (!dragRef.current.dragging) return - setPanelPos({ - x: dragRef.current.origX + ev.clientX - dragRef.current.startX, - y: dragRef.current.origY + ev.clientY - dragRef.current.startY, - }) - } - const onUp = () => { - dragRef.current.dragging = false - window.removeEventListener('mousemove', onMove) - window.removeEventListener('mouseup', onUp) - } - window.addEventListener('mousemove', onMove) - window.addEventListener('mouseup', onUp) - }, [panelPos]) - - useEffect(() => { - if (visible) setPanelPos({ x: 0, y: 0 }) - }, [visible]) - - useEffect(() => { - if (!visible) return - fetch(`${FRIEND_API}/voice`) - .then((r) => r.json()) - .then((data) => { - setCurrentVoice(data.voice || '') - setCurrentProvider(data.provider || 'edge') - if (data.qwenKey) setQwenKey(data.qwenKey) - if (data.qwenModel) setQwenModel(data.qwenModel) - }) - .catch(() => {}) - }, [visible]) - - const postVoiceSettings = (body: Record) => { - return fetch(`${FRIEND_API}/voice`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - } - - const saveModelPath = (modelPath: string) => { - fetch(`${FRIEND_API}/settings`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modelPath }), - }).catch(() => {}) - } - - const setVoice = (voice: string) => { - postVoiceSettings({ voice }).then(() => setCurrentVoice(voice)).catch(() => {}) - } - - const setProvider = (provider: string) => { - postVoiceSettings({ provider }).then(() => setCurrentProvider(provider)).catch(() => {}) - } - - const saveQwenKey = (key: string) => { - postVoiceSettings({ qwenKey: key }).catch(() => {}) - } - - const saveQwenModel = (model: string) => { - postVoiceSettings({ qwenModel: model }).then(() => setQwenModel(model)).catch(() => {}) - } - - const stopPreview = useCallback(() => { - if (audioRef.current) { - audioRef.current.onended = null - audioRef.current.onerror = null - audioRef.current.pause() - audioRef.current = null - } - setPreviewingId(null) - }, []) - - const preview = (voiceId: string) => { - stopPreview() - setPreviewingId(voiceId) - fetch(`${FRIEND_API}/preview`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ voice: voiceId, provider: currentProvider }), - }) - .then((r) => r.json()) - .then((data) => { - if (data.audioUrl) { - const audio = new Audio(data.audioUrl) - audioRef.current = audio - audio.onended = () => stopPreview() - audio.onerror = () => stopPreview() - audio.play().catch(() => stopPreview()) - } else { - if (data.error) console.warn('TTS preview error:', data.error) - stopPreview() - } - }) - .catch(() => stopPreview()) - } - - if (!visible) return null - - const voices = currentProvider === 'qwen' ? QWEN_VOICES : EDGE_VOICES - - return ( -
-
e.stopPropagation()}> -
- {t('设置', 'Settings')} - -
- - {/* Tabs */} -
- {(['general', 'voice', 'model'] as const).map((tb) => ( - - ))} -
- - {/* Tab content */} -
- {tab === 'general' && ( -
- - - - - - - -
- )} - - {tab === 'voice' && ( -
-
{t('TTS 服务', 'TTS Provider')}
-
- {(['edge', 'qwen'] as const).map((p) => ( - - ))} -
- - {currentProvider === 'qwen' && ( -
-
-
{t('阿里云 API Key', 'Alibaba Cloud API Key')}
- setQwenKey(e.target.value)} - onBlur={() => saveQwenKey(qwenKey)} - onKeyDown={(e) => { if (e.key === 'Enter') saveQwenKey(qwenKey) }} - placeholder="sk-..." - style={{ ...inputStyle, width: '100%' }} - /> -
- {t('从阿里云百炼控制台获取 API Key', 'Get API Key from Alibaba Cloud console')} -
-
-
-
{t('语音模型', 'Voice Model')}
- -
-
- )} - -
-
{t('语音识别 (STT)', 'Speech Recognition (STT)')}
-
- {(['browser', 'groq', 'anthropic', 'local', 'doubao'] as const).map((p) => ( - - ))} -
-
- -
-
{currentProvider === 'qwen' ? t('千问语音', 'Qwen Voice') : t('Edge TTS 语音', 'Edge TTS Voice')}
-
- {voices.map((v) => ( -
setVoice(v.id)} - style={{ - ...modelBtnStyle, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '6px 6px 6px 10px', - fontSize: 13, - background: v.id === currentVoice ? 'rgba(100, 160, 255, 0.4)' : 'rgba(255, 255, 255, 0.08)', - borderColor: v.id === currentVoice ? 'rgba(100, 160, 255, 0.6)' : 'rgba(255, 255, 255, 0.15)', - }} - > -
-
{v.label}
-
{v.id}
-
-
{ e.stopPropagation(); preview(v.id) }} - style={{ - width: 28, - height: 28, - borderRadius: 6, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - background: previewingId === v.id ? 'rgba(100, 160, 255, 0.3)' : 'rgba(255, 255, 255, 0.08)', - cursor: previewingId !== null ? 'default' : 'pointer', - flexShrink: 0, - opacity: previewingId !== null && previewingId !== v.id ? 0.3 : 0.7, - }} - title={t('试听', 'Preview')} - > - {previewingId === v.id ? : } -
-
- ))} -
-
-
- )} - - {tab === 'model' && ( -
-
{t('内置VRM模型', 'Built-in VRM Models')}
- -
- )} -
-
-
- ) -} - -function LangToggle({ language, onChange, t }: { language: 'zh' | 'en'; onChange: (v: 'zh' | 'en') => void; t: (zh: string, en: string) => string }) { - return ( -
- {t('语言', 'Language')} -
- {(['zh', 'en'] as const).map((l) => ( - - ))} -
-
- ) -} - -function VolumeControl({ volume, onChange, t }: { volume: number; onChange: (v: number) => void; t: (zh: string, en: string) => string }) { - return ( -
- {t('音量', 'Volume')} -
- onChange(Number(e.target.value) / 100)} - style={{ width: 100, accentColor: 'rgba(100, 160, 255, 0.8)' }} - /> - {Math.round(volume * 100)} -
-
- ) -} - -function TrackingControl({ tracking, onChange, t }: { tracking: 'mouse' | 'camera'; onChange: (v: 'mouse' | 'camera') => void; t: (zh: string, en: string) => string }) { - return ( -
- {t('视线跟随', 'Eye Tracking')} -
- {(['mouse', 'camera'] as const).map((m) => ( - - ))} -
-
- ) -} - -function UIAlignControl({ uiAlign, onChange, t }: { uiAlign: 'left' | 'right'; onChange: (v: 'left' | 'right') => void; t: (zh: string, en: string) => string }) { - return ( -
- {t('UI位置', 'UI Position')} -
- {(['left', 'right'] as const).map((a) => ( - - ))} -
-
- ) -} - -function ToggleRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) { - return ( -
- {label} - -
- ) -} - -const overlayStyle: React.CSSProperties = { - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - background: 'rgba(0, 0, 0, 0.5)', - zIndex: 500, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - pointerEvents: 'auto', -} - -const panelStyle: React.CSSProperties = { - width: 320, - background: 'rgba(30, 30, 40, 0.95)', - backdropFilter: 'blur(12px)', - borderRadius: 12, - border: '1px solid rgba(255, 255, 255, 0.15)', - boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5)', - padding: 16, - color: '#fff', - fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif', -} - -const headerStyle: React.CSSProperties = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - cursor: 'grab', - userSelect: 'none', -} - -const closeBtnStyle: React.CSSProperties = { - width: 28, - height: 28, - border: 'none', - borderRadius: 6, - background: 'rgba(255, 255, 255, 0.1)', - color: 'rgba(255, 255, 255, 0.7)', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -} - -const tabBarStyle: React.CSSProperties = { - display: 'flex', - gap: 2, - marginBottom: 16, - background: 'rgba(255, 255, 255, 0.06)', - borderRadius: 8, - padding: 2, -} - -const tabStyle: React.CSSProperties = { - flex: 1, - height: 32, - border: 'none', - borderRadius: 6, - background: 'transparent', - color: 'rgba(255, 255, 255, 0.6)', - fontSize: 13, - fontWeight: 500, - cursor: 'pointer', -} - -const activeTabStyle: React.CSSProperties = { - background: 'rgba(255, 255, 255, 0.12)', - color: '#fff', -} - -const contentStyle: React.CSSProperties = { - minHeight: 120, -} - -const sectionStyle: React.CSSProperties = { - display: 'flex', - flexDirection: 'column', - gap: 8, -} - -const labelStyle: React.CSSProperties = { - fontSize: 13, - color: 'rgba(255, 255, 255, 0.6)', - marginBottom: 2, -} - -const toggleStyle: React.CSSProperties = { - width: 40, - height: 22, - borderRadius: 11, - border: 'none', - cursor: 'pointer', - position: 'relative', - transition: 'background 0.2s', - padding: 0, -} - -const toggleKnobStyle: React.CSSProperties = { - width: 18, - height: 18, - borderRadius: 9, - background: '#fff', - transition: 'transform 0.2s', - position: 'absolute', - top: 2, -} - -const smallBtnStyle: React.CSSProperties = { - padding: '4px 10px', - border: '1px solid', - borderRadius: 6, - color: '#fff', - fontSize: 12, - cursor: 'pointer', -} - -const modelBtnStyle: React.CSSProperties = { - padding: '8px 12px', - border: '1px solid', - borderRadius: 8, - color: '#fff', - fontSize: 14, - cursor: 'pointer', - textAlign: 'left', -} - -const inputStyle: React.CSSProperties = { - flex: 1, - height: 32, - boxSizing: 'border-box', - border: '1px solid rgba(255, 255, 255, 0.2)', - borderRadius: 6, - background: 'rgba(0, 0, 0, 0.3)', - color: '#fff', - fontSize: 13, - padding: '0 8px', - outline: 'none', - fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif', -} - -const selectStyle: React.CSSProperties = { - width: '100%', - height: 32, - boxSizing: 'border-box', - border: '1px solid rgba(255, 255, 255, 0.2)', - borderRadius: 6, - background: 'rgba(0, 0, 0, 0.3)', - color: '#fff', - fontSize: 13, - padding: '0 8px', - outline: 'none', - fontFamily: '"Segoe UI", "Microsoft YaHei", sans-serif', -} diff --git a/src/components/friend/frontend/components/TextBubble.tsx b/src/components/friend/frontend/components/TextBubble.tsx deleted file mode 100644 index d332caed08002e8057971bc7615782fd9b020ef6..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/components/TextBubble.tsx +++ /dev/null @@ -1,609 +0,0 @@ -import { useEffect, useState, useRef, useCallback } from 'react' -import { marked } from 'marked' -import { FRIEND_API } from '../api' -import { LipSync } from '../lip-sync' - -interface VrmMessage { - text?: string - emotion?: string - emotionDuration?: number - emotionIntensity?: number - duration?: number - audioUrl?: string - audioIndex?: number - imageUrl?: string - action?: string - sendFirstTts?: boolean - appendText?: boolean - replyDone?: boolean -} - -export type OnVrmMessage = (msg: VrmMessage) => void - -// CJK detection: check proportion of CJK characters in text -function cjkRatio(text: string): number { - const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef\u3040-\u309f\u30a0-\u30ff]/g - const matches = text.match(CJK_RE) - return matches ? matches.length / text.length : 0 -} - -// Dynamic rate based on CJK proportion -function getCharRate(text: string, ttsEnabled: boolean): number { - const ratio = cjkRatio(text) - if (ttsEnabled) { - // CJK: 200ms/char, English: 60ms/char, interpolate - return Math.round(200 * ratio + 60 * (1 - ratio)) - } - // CJK: 80ms/char, English: 30ms/char, interpolate - return Math.round(80 * ratio + 30 * (1 - ratio)) -} -const HIDE_DELAY_MS = 2000 // delay after everything is done before hiding -const POP_DURATION_MS = 300 - -// Grapheme segmenter singleton -const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }) - -// Keyframes (claw-pop-in) are in index.html -
- - ) : ( - chars.current.map((ch, i) => ( - i < charCount ? ( - {ch === '\n' ?
: ch}
- ) : null - )) - )} -
- )} - - - )} - {zoomedSrc && ( -
-
- setZoomedSrc(null)} - /> - -
-
- )} - - ) -} - -const imageThumbStyle: React.CSSProperties = { - maxWidth: '80%', - maxHeight: 120, - borderRadius: 6, - border: '1px solid rgba(255, 255, 255, 0.3)', - objectFit: 'contain' as const, - marginBottom: 4, - display: 'block', - cursor: 'pointer', -} - -const overlayStyle: React.CSSProperties = { - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - background: 'rgba(0, 0, 0, 0.7)', - backdropFilter: 'blur(8px)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 10000, - pointerEvents: 'auto', -} - -const closeButtonStyle: React.CSSProperties = { - position: 'absolute', - top: 8, - right: 8, - width: 32, - height: 32, - border: 'none', - borderRadius: 6, - background: 'rgba(125, 125, 125, 0.28)', - backdropFilter: 'blur(6px)', - color: 'rgba(255, 255, 255, 0.8)', - fontSize: 16, - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 10001, -} - -const zoomedImageStyle: React.CSSProperties = { - maxWidth: '100%', - maxHeight: '100%', - borderRadius: 8, - border: '1px solid rgba(255, 255, 255, 0.3)', - boxShadow: '0 0 24px rgba(100, 160, 255, 0.4)', - objectFit: 'contain' as const, - display: 'block', -} - -const popCharStyle: React.CSSProperties = { - display: 'inline-block', - animation: `claw-pop-in ${POP_DURATION_MS}ms ease-out both`, - whiteSpace: 'pre', -} - -const containerStyle: React.CSSProperties = { - position: 'absolute', - bottom: 80, - left: 0, - width: '100%', - zIndex: 200, - pointerEvents: 'none', - padding: 8, - boxSizing: 'border-box', -} - -const boxStyle: React.CSSProperties = { - background: 'rgba(0, 0, 0, 0.35)', - backdropFilter: 'blur(6px)', - borderRadius: 12, - border: '1px solid rgba(255, 255, 255, 0.15)', - boxShadow: '0 0 12px rgba(100, 160, 255, 0.25), 0 0 24px rgba(100, 160, 255, 0.1)', - padding: '8px 12px', - height: 140, - overflowY: 'auto' as const, - pointerEvents: 'auto', - userSelect: 'text', - cursor: 'text', -} - -const textStyle: React.CSSProperties = { - color: '#fff', - fontSize: 22, - lineHeight: 1.6, - wordBreak: 'break-word', - fontFamily: '"Segoe UI", "Microsoft YaHei", "PingFang SC", sans-serif', - textShadow: '0 0 6px rgba(255,255,255,0.5)', -} diff --git a/src/components/friend/frontend/components/VRMScene.tsx b/src/components/friend/frontend/components/VRMScene.tsx deleted file mode 100644 index 75a4a383790f3050ef99041e47fdec377f567c8b..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/components/VRMScene.tsx +++ /dev/null @@ -1,697 +0,0 @@ -import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react' -import * as THREE from 'three' -import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' -import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm' -import { VRMLookAtQuaternionProxy } from '@pixiv/three-vrm-animation' -import type { VRM } from '@pixiv/three-vrm' -import { EmoteController } from '../emote' -import { LipSync } from '../lip-sync' -import { MotionController } from '../motion-controller' - -interface VRMSceneProps { - modelPath: string - idleAnimationPath?: string -} - -export type TrackingMode = 'mouse' | 'camera' - -export interface VRMSceneHandle { - setEmotion: (emotion: string, intensity?: number) => void - setEmotionWithReset: (emotion: string, durationMs: number, intensity?: number) => void - resetCamera: () => void - setTrackingMode: (mode: TrackingMode) => void - playAction: (name: string, hold?: boolean) => void - panCamera: (dx: number, dy: number) => void - rotateCamera: (dx: number, dy: number) => void - /** Unified reset: camera + resetToIdle + expressions to zero */ - reset: () => void -} - -// ── Blink state ─────────────────────────────────────────────────────────────── -interface BlinkState { - isBlinking: boolean - blinkProgress: number - timeSinceLastBlink: number - nextBlinkTime: number - blinkDuration: number - doubleBlink: boolean - doubleBlinkCount: number -} - -function createBlinkState(): BlinkState { - return { - isBlinking: false, - blinkProgress: 0, - timeSinceLastBlink: 0, - nextBlinkTime: Math.random() * 5 + 2, - blinkDuration: 0.1 + Math.random() * 0.08, - doubleBlink: false, - doubleBlinkCount: 0, - } -} - -function updateBlink(vrm: VRM, delta: number, state: BlinkState) { - if (!vrm.expressionManager) return - - state.timeSinceLastBlink += delta - - if (!state.isBlinking && state.timeSinceLastBlink >= state.nextBlinkTime) { - state.isBlinking = true - state.blinkProgress = 0 - state.blinkDuration = 0.08 + Math.random() * 0.12 - // ~8% chance of double blink (more natural) - state.doubleBlink = Math.random() < 0.08 - state.doubleBlinkCount = 0 - } - - if (state.isBlinking) { - state.blinkProgress += delta / state.blinkDuration - const blinkValue = Math.sin(Math.PI * state.blinkProgress) - vrm.expressionManager.setValue('blink', blinkValue) - - if (state.blinkProgress >= 1) { - state.blinkProgress = 0 - state.doubleBlinkCount++ - - if (state.doubleBlink && state.doubleBlinkCount < 2) { - // Quick reopen then re-blink - vrm.expressionManager.setValue('blink', 0) - state.blinkDuration = 0.06 + Math.random() * 0.06 - } else { - state.isBlinking = false - state.timeSinceLastBlink = 0 - vrm.expressionManager.setValue('blink', 0) - state.doubleBlink = false - state.nextBlinkTime = Math.random() * 6 + 1.5 - } - } - } -} - -// ── Relaxed hand pose ───────────────────────────────────────────────────────── - -interface HandPoseCache { - bones: { bone: THREE.Object3D; z: number; y: number }[] -} - -function buildHandPoseCache(vrm: VRM): HandPoseCache { - const humanoid = vrm.humanoid - const bones: HandPoseCache['bones'] = [] - if (!humanoid) return { bones } - - const fingers = ['Thumb', 'Index', 'Middle', 'Ring', 'Little'] as const - const segments = ['Proximal', 'Intermediate', 'Distal'] as const - const sides = ['left', 'right'] as const - - const curlMap: Record = { - Thumb: [0.25, 0.15, 0.10], - Index: [0.20, 0.30, 0.20], - Middle: [0.25, 0.35, 0.25], - Ring: [0.30, 0.40, 0.30], - Little: [0.35, 0.45, 0.30], - } - - const spreadMap: Record = { - Thumb: 0.15, - Index: 0.04, - Middle: 0.0, - Ring: -0.04, - Little: -0.08, - } - - for (const side of sides) { - const sign = side === 'left' ? 1 : -1 - - for (const finger of fingers) { - const curls = curlMap[finger] - const spread = spreadMap[finger] - - for (let s = 0; s < segments.length; s++) { - const boneName = `${side}${finger}${segments[s]}` as any - const bone = humanoid.getNormalizedBoneNode(boneName) - if (!bone) continue - - const z = sign * curls[s] - const y = s === 0 ? sign * spread : 0 - - bones.push({ bone, z, y }) - } - } - } - - return { bones } -} - -function applyRelaxedHandPose(cache: HandPoseCache, time: number) { - // Slow hand tension cycle (~30s period): hands subtly change posture over time - const tensionCycle = 0.5 + 0.5 * Math.sin(time * 0.035) - const zScale = 0.85 + tensionCycle * 0.3 // 0.85-1.15 - - for (const { bone, z, y } of cache.bones) { - const freq = 0.3 + Math.abs(z) * 2 - const micro = Math.sin(time * freq + z * 50) * 0.02 - bone.rotation.z = z * zScale + micro - if (y !== 0) bone.rotation.y = y - } -} - -export const VRMScene = forwardRef(function VRMScene({ - modelPath, - idleAnimationPath = '/friend/idle_loop.vrma', -}, ref) { - const canvasRef = useRef(null) - - const emoteRef = useRef(null) - const resetCameraRef = useRef<(() => void) | null>(null) - const trackingModeRef = useRef('mouse') - const motionRef = useRef(null) - const panCameraRef = useRef<((dx: number, dy: number) => void) | null>(null) - const rotateCameraRef = useRef<((dx: number, dy: number) => void) | null>(null) - const lipSyncRef = useRef(LipSync.getInstance()) - - useImperativeHandle(ref, () => ({ - setEmotion(emotion: string, intensity?: number) { - emoteRef.current?.setEmotion(emotion, intensity) - }, - setEmotionWithReset(emotion: string, durationMs: number, intensity?: number) { - emoteRef.current?.setEmotionWithReset(emotion, durationMs, intensity) - }, - resetCamera() { - resetCameraRef.current?.() - }, - setTrackingMode(mode: TrackingMode) { - trackingModeRef.current = mode - }, - playAction(name: string, hold?: boolean) { - motionRef.current?.playAction(name, hold) - }, - panCamera(dx: number, dy: number) { - panCameraRef.current?.(dx, dy) - }, - rotateCamera(dx: number, dy: number) { - rotateCameraRef.current?.(dx, dy) - }, - reset() { - resetCameraRef.current?.() - motionRef.current?.resetToIdle() - emoteRef.current?.resetAll() - }, - })) - - useEffect(() => { - const canvas = canvasRef.current - if (!canvas) return - - // ── Renderer ────────────────────────────────────────────────────────────── - const renderer = new THREE.WebGLRenderer({ - canvas, - alpha: true, - antialias: true, - preserveDrawingBuffer: true, - }) - renderer.setSize(window.innerWidth, window.innerHeight) - renderer.setPixelRatio(window.devicePixelRatio) - renderer.setClearColor(0x000000, 0) - - // ── Scene ───────────────────────────────────────────────────────────────── - const scene = new THREE.Scene() - - // ── Camera ──────────────────────────────────────────────────────────────── - const FOV = 40 - const camera = new THREE.PerspectiveCamera( - FOV, - window.innerWidth / window.innerHeight, - 0.1, - 100, - ) - const pivot = new THREE.Vector3(0, 0, 0) - let orbitRadius = 2.0 - let orbitTheta = 0 - let orbitPhi = Math.PI / 2 - - function updateCameraOrbit() { - camera.position.set( - pivot.x + orbitRadius * Math.sin(orbitPhi) * Math.sin(orbitTheta), - pivot.y + orbitRadius * Math.cos(orbitPhi), - pivot.z + orbitRadius * Math.sin(orbitPhi) * Math.cos(orbitTheta), - ) - camera.lookAt(pivot) - } - updateCameraOrbit() - - // ── Lights ──────────────────────────────────────────────────────────────── - scene.add(new THREE.AmbientLight(0xffffff, 0.6)) - const dirLight = new THREE.DirectionalLight(0xffffff, 1.2) - dirLight.position.set(1, 2, 3) - scene.add(dirLight) - const fillLight = new THREE.DirectionalLight(0xffffff, 0.4) - fillLight.position.set(-2, 1, -1) - scene.add(fillLight) - - // ── Loader ─────────────────────────────────────────────────────────────── - const loader = new GLTFLoader() - loader.register((parser) => new VRMLoaderPlugin(parser)) - - // ── State ───────────────────────────────────────────────────────────────── - let vrm: VRM | null = null - let motion: MotionController | null = null - let emote: EmoteController | null = null - let handPose: HandPoseCache | null = null - const blinkState = createBlinkState() - const saccades = new EyeSaccadeController() - const lookAtTarget = { x: 0, y: 0, z: -100 } - - // ── Aliveness system state: breathing, sway, speech micro-movements ───── - let alivenessBones = { chestBone: null as THREE.Object3D | null, spineBone: null as THREE.Object3D | null, neckBone: null as THREE.Object3D | null } - let breathPhase = Math.random() * Math.PI * 2 - let swayPhase = Math.random() * Math.PI * 2 - let speechBlend = 0 - let userListenBlend = 0 - // Micro-expression state - let microTimer = 5 + Math.random() * 10 - let microActive = false - let microPhase = 0 - let microShape = '' - - // ── Load VRM model, then load idle animation ───────────────────────────── - loader.load( - modelPath, - async (gltf) => { - const loadedVrm = gltf.userData.vrm as VRM - if (!loadedVrm) { - console.error('No VRM data found in GLTF') - return - } - - VRMUtils.removeUnnecessaryVertices(loadedVrm.scene) - VRMUtils.combineSkeletons(loadedVrm.scene) - loadedVrm.scene.traverse((obj) => { - obj.frustumCulled = false - }) - - if (loadedVrm.lookAt) { - const lookAtQuatProxy = new VRMLookAtQuaternionProxy(loadedVrm.lookAt) - lookAtQuatProxy.name = 'lookAtQuaternionProxy' - loadedVrm.scene.add(lookAtQuatProxy) - } - - VRMUtils.rotateVRM0(loadedVrm) - - scene.add(loadedVrm.scene) - vrm = loadedVrm - - // ── Compute camera from model bounds ─────────────────── - const box = new THREE.Box3().setFromObject(loadedVrm.scene) - const modelSize = new THREE.Vector3() - const modelCenter = new THREE.Vector3() - box.getSize(modelSize) - box.getCenter(modelCenter) - modelCenter.y += modelSize.y / 3.2 - - const radians = (FOV / 2 * Math.PI) / 180 - const offsetX = modelSize.x / 16 - const offsetY = modelSize.y / 10 - const offsetZ = (modelSize.y / 4.2) / Math.tan(radians) - - pivot.copy(modelCenter) - orbitRadius = offsetZ - orbitTheta = Math.atan2(offsetX, offsetZ) - orbitPhi = Math.PI / 2 - Math.atan2(offsetY, offsetZ) - updateCameraOrbit() - - const initPivot = pivot.clone() - const initRadius = orbitRadius - const initTheta = orbitTheta - const initPhi = orbitPhi - resetCameraRef.current = () => { - pivot.copy(initPivot) - orbitRadius = initRadius - orbitTheta = initTheta - orbitPhi = initPhi - updateCameraOrbit() - } - - panCameraRef.current = (dx: number, dy: number) => { - const right = new THREE.Vector3() - const up = new THREE.Vector3() - camera.getWorldDirection(new THREE.Vector3()) - right.setFromMatrixColumn(camera.matrixWorld, 0) - up.setFromMatrixColumn(camera.matrixWorld, 1) - pivot.addScaledVector(right, -dx * 0.003) - pivot.addScaledVector(up, dy * 0.003) - updateCameraOrbit() - } - - rotateCameraRef.current = (dx: number, dy: number) => { - orbitTheta -= dx * 0.005 - orbitPhi = THREE.MathUtils.clamp( - orbitPhi - dy * 0.005, - 0.1, - Math.PI - 0.1, - ) - updateCameraOrbit() - } - - handPose = buildHandPoseCache(loadedVrm) - - // ── Initialize aliveness bone references ── - const h = loadedVrm.humanoid - alivenessBones = { - chestBone: h?.getNormalizedBoneNode('chest') ?? null, - spineBone: h?.getNormalizedBoneNode('spine') ?? null, - neckBone: h?.getNormalizedBoneNode('neck') ?? null, - } - - emote = new EmoteController(loadedVrm) - emoteRef.current = emote - - motion = new MotionController(loadedVrm) - motionRef.current = motion - - motion.loadIdle(idleAnimationPath).catch((err) => - console.warn('Failed to load idle animation:', err), - ) - - loadedVrm.springBoneManager?.reset() - }, - () => {}, - (err) => { - console.error('Failed to load VRM:', err) - }, - ) - - // ── Mouse tracking ──────────────────────────────────────────────────────── - const mouse = new THREE.Vector2(0, 0) - const _raycaster = new THREE.Raycaster() - const _mouseVec = new THREE.Vector2() - - function onMouseMove(e: MouseEvent) { - mouse.x = (e.clientX / window.innerWidth) * 2 - 1 - mouse.y = -(e.clientY / window.innerHeight) * 2 + 1 - - if (trackingModeRef.current !== 'mouse') return - - _mouseVec.set(mouse.x, mouse.y) - _raycaster.setFromCamera(_mouseVec, camera) - const camDir = new THREE.Vector3() - camera.getWorldDirection(camDir) - const plane = new THREE.Plane() - plane.setFromNormalAndCoplanarPoint( - camDir, - camera.position.clone().add(camDir.multiplyScalar(1)), - ) - const intersection = new THREE.Vector3() - if (_raycaster.ray.intersectPlane(plane, intersection)) { - lookAtTarget.x = intersection.x - lookAtTarget.y = intersection.y - lookAtTarget.z = intersection.z - if (vrm) { - saccades.instantUpdate(vrm, lookAtTarget) - } - } - } - window.addEventListener('mousemove', onMouseMove) - document.addEventListener('mousemove', onMouseMove) - - // ── Scroll zoom ────────────────────────────────────────────────────────── - const MIN_RADIUS = 0.8 - const MAX_RADIUS = 5.0 - const ZOOM_SPEED = 0.002 - - function onWheel(e: WheelEvent) { - e.preventDefault() - orbitRadius = THREE.MathUtils.clamp( - orbitRadius + e.deltaY * ZOOM_SPEED, - MIN_RADIUS, - MAX_RADIUS, - ) - updateCameraOrbit() - } - canvas.addEventListener('wheel', onWheel, { passive: false }) - - // ── Camera drag controls ────────────────────────────────────────────── - let dragMode: 'rotate' | 'dolly' | null = null - let prevX = 0 - let prevY = 0 - const ROTATE_SPEED = 0.005 - const DOLLY_SPEED = 0.01 - - function onPointerDown(e: PointerEvent) { - if (e.button === 1) { - dragMode = 'dolly' - e.preventDefault() - } else if (e.button === 2) { - dragMode = 'rotate' - } else { - return - } - prevX = e.clientX - prevY = e.clientY - canvas!.setPointerCapture(e.pointerId) - } - - function onPointerMove(e: PointerEvent) { - if (!dragMode) return - const dx = e.clientX - prevX - const dy = e.clientY - prevY - prevX = e.clientX - prevY = e.clientY - - if (dragMode === 'rotate') { - orbitTheta -= dx * ROTATE_SPEED - orbitPhi = THREE.MathUtils.clamp( - orbitPhi - dy * ROTATE_SPEED, - 0.1, - Math.PI - 0.1, - ) - } else if (dragMode === 'dolly') { - orbitRadius = THREE.MathUtils.clamp( - orbitRadius + dy * DOLLY_SPEED, - MIN_RADIUS, - MAX_RADIUS, - ) - } - updateCameraOrbit() - } - - function onPointerUp() { - if (dragMode) { - dragMode = null - } - } - - function onContextMenu(e: Event) { - e.preventDefault() - } - - canvas.addEventListener('pointerdown', onPointerDown) - canvas.addEventListener('pointermove', onPointerMove) - canvas.addEventListener('pointerup', onPointerUp) - canvas.addEventListener('contextmenu', onContextMenu) - - // ── Resize ──────────────────────────────────────────────────────────────── - function onResize() { - camera.aspect = window.innerWidth / window.innerHeight - camera.updateProjectionMatrix() - renderer.setSize(window.innerWidth, window.innerHeight) - } - window.addEventListener('resize', onResize) - - // ── Animation loop ──────────────────────────────────────────────────────── - const clock = new THREE.Clock() - let animFrameId: number - - function animate() { - animFrameId = requestAnimationFrame(animate) - const delta = clock.getDelta() - - if (vrm) { - motion?.update(delta) - - if (handPose) applyRelaxedHandPose(handPose, clock.elapsedTime) - - vrm.humanoid?.update() - - if (trackingModeRef.current === 'camera') { - lookAtTarget.x = camera.position.x - lookAtTarget.y = camera.position.y - lookAtTarget.z = camera.position.z - saccades.instantUpdate(vrm, lookAtTarget) - } - - vrm.lookAt?.update(delta) - saccades.update(vrm, lookAtTarget, delta) - updateBlink(vrm, delta, blinkState) - emote?.update(delta) - lipSyncRef.current.update(vrm, delta) - - // ── Aliveness: micro-expressions (asymmetric blink, subtle morphs) ── - microTimer -= delta - if (microTimer <= 0 && !microActive) { - microShape = Math.random() > 0.5 ? 'blinkLeft' : 'blinkRight' - microActive = true - microPhase = 0 - } - if (microActive && vrm.expressionManager) { - microPhase += delta * 4 - const val = Math.sin(Math.PI * Math.min(microPhase, 1)) - vrm.expressionManager.setValue(microShape, val * 0.3) - if (microPhase >= 2) { - microActive = false - vrm.expressionManager.setValue(microShape, 0) - microTimer = 8 + Math.random() * 16 - } - } - - vrm.expressionManager?.update() - vrm.springBoneManager?.update(delta) - - // ── Aliveness: breathing (chest rise/fall, post-mixer) ── - // Breathing rate varies naturally, amplitude tuned for VRM scale - const breathRate = 1.8 + Math.sin(breathPhase * 0.05) * 0.4 + Math.sin(clock.elapsedTime * 0.1) * 0.3 - breathPhase += delta * breathRate - const breathVal = Math.sin(breathPhase) * 0.006 - if (alivenessBones.chestBone) { - alivenessBones.chestBone.position.y += breathVal - } - - // ── Aliveness: postural sway ── - swayPhase += delta * 0.35 - const swayZ = Math.sin(swayPhase) * 0.005 - if (alivenessBones.spineBone) { - alivenessBones.spineBone.rotation.z += swayZ - } - - // ── Aliveness: speech-driven micro-movements ── - const isSpeaking = lipSyncRef.current.isActive() - const targetBlend = isSpeaking ? 1 : 0 - speechBlend += (targetBlend - speechBlend) * Math.min(1, delta * 3) - if (speechBlend > 0.01) { - const t = clock.elapsedTime - const headX = Math.sin(t * 3.7 + 1.2) * 0.02 * speechBlend - const headZ = Math.sin(t * 2.3 + 0.7) * 0.015 * speechBlend - const headY = Math.sin(t * 1.5 + 3.8) * 0.008 * speechBlend // slight rotation (looking around while talking) - const spineSway = Math.sin(t * 1.8 + 0.3) * 0.006 * speechBlend - if (alivenessBones.neckBone) { - alivenessBones.neckBone.rotation.x += headX - alivenessBones.neckBone.rotation.z += headZ - alivenessBones.neckBone.rotation.y += headY - } - if (alivenessBones.spineBone) { - alivenessBones.spineBone.rotation.x += spineSway - } - } - - // ── Aliveness: listening response when user is speaking ── - const isUserSpeaking = !!(window as any).__userRecording - userListenBlend += ((isUserSpeaking ? 1 : 0) - userListenBlend) * Math.min(1, delta * 2) - if (userListenBlend > 0.01) { - const t = clock.elapsedTime - const listenTilt = Math.sin(t * 0.9 + 0.5) * 0.012 * userListenBlend // head tilt - const listenNod = Math.sin(t * 2.3 + 3.1) * 0.008 * userListenBlend // subtle nodding - if (alivenessBones.neckBone) { - alivenessBones.neckBone.rotation.z += listenTilt - alivenessBones.neckBone.rotation.x += listenNod - } - } - } - - renderer.render(scene, camera) - } - - animate() - - // ── Cleanup ─────────────────────────────────────────────────────────────── - return () => { - cancelAnimationFrame(animFrameId) - window.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mousemove', onMouseMove) - canvas.removeEventListener('wheel', onWheel) - canvas.removeEventListener('pointerdown', onPointerDown) - canvas.removeEventListener('pointermove', onPointerMove) - canvas.removeEventListener('pointerup', onPointerUp) - canvas.removeEventListener('contextmenu', onContextMenu) - window.removeEventListener('resize', onResize) - emote?.dispose() - emoteRef.current = null - motion?.dispose() - motionRef.current = null - delete (window as any).__clawHitTest - renderer.dispose() - } - }, [modelPath, idleAnimationPath]) - - return ( -
- -
- ) -}) - -// ── Eye saccade interval ───────────────────────────────────────── -const EYE_SACCADE_INT_STEP = 400 -const EYE_SACCADE_INT_P: number[][] = [ - [0.075, 800], [0.110, 0], [0.125, 0], [0.140, 0], [0.125, 0], - [0.050, 0], [0.040, 0], [0.030, 0], [0.020, 0], [1.000, 0], -] -for (let i = 1; i < EYE_SACCADE_INT_P.length; i++) { - EYE_SACCADE_INT_P[i][0] += EYE_SACCADE_INT_P[i - 1][0] - EYE_SACCADE_INT_P[i][1] = EYE_SACCADE_INT_P[i - 1][1] + EYE_SACCADE_INT_STEP -} - -function randomSaccadeInterval(): number { - const r = Math.random() - for (let i = 0; i < EYE_SACCADE_INT_P.length; i++) { - if (r <= EYE_SACCADE_INT_P[i][0]) { - return EYE_SACCADE_INT_P[i][1] + Math.random() * EYE_SACCADE_INT_STEP - } - } - return EYE_SACCADE_INT_P[EYE_SACCADE_INT_P.length - 1][1] + Math.random() * EYE_SACCADE_INT_STEP -} - -class EyeSaccadeController { - private nextSaccadeAfter = -1 - private timeSinceLastSaccade = 0 - private fixationTarget = new THREE.Vector3() - - instantUpdate(vrm: VRM, target: { x: number; y: number; z: number }) { - this.fixationTarget.set(target.x, target.y, target.z) - if (!vrm.lookAt) return - if (!vrm.lookAt.target) { - vrm.lookAt.target = new THREE.Object3D() - } - vrm.lookAt.target.position.copy(this.fixationTarget) - vrm.lookAt.update(0.016) - } - - update(vrm: VRM, lookAtTarget: { x: number; y: number; z: number }, delta: number) { - if (!vrm.expressionManager || !vrm.lookAt) return - - if (this.timeSinceLastSaccade >= this.nextSaccadeAfter) { - this.fixationTarget.set( - lookAtTarget.x + THREE.MathUtils.randFloat(-0.25, 0.25), - lookAtTarget.y + THREE.MathUtils.randFloat(-0.25, 0.25), - lookAtTarget.z, - ) - this.timeSinceLastSaccade = 0 - this.nextSaccadeAfter = randomSaccadeInterval() / 1000 - } - - if (!vrm.lookAt.target) { - vrm.lookAt.target = new THREE.Object3D() - } - vrm.lookAt.target.position.lerp(this.fixationTarget, 1) - vrm.lookAt.update(delta) - - this.timeSinceLastSaccade += delta - } -} diff --git a/src/components/friend/frontend/emote.ts b/src/components/friend/frontend/emote.ts deleted file mode 100644 index 29963dabcd6c1398b5dca9e85e55aa537ab4a277..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/emote.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { VRM } from '@pixiv/three-vrm' - -interface EmotionExpr { - name: string - value: number -} - -interface EmotionState { - expression: EmotionExpr[] - blendDuration: number -} - -const emotionStates = new Map([ - ['happy', { - expression: [ - { name: 'happy', value: 0.2 }, - { name: 'aa', value: 0.8 }, - ], - blendDuration: 0.4, - }], - ['sad', { - expression: [ - { name: 'sad', value: 0.7 }, - { name: 'oh', value: 0.15 }, - ], - blendDuration: 0.4, - }], - ['angry', { - expression: [ - { name: 'angry', value: 0.7 }, - { name: 'ee', value: 0.3 }, - ], - blendDuration: 0.3, - }], - ['surprised', { - expression: [ - { name: 'surprised', value: 0.8 }, - { name: 'oh', value: 0.4 }, - ], - blendDuration: 0.15, - }], - ['think', { - expression: [ - { name: 'think', value: 0.7 }, - ], - blendDuration: 0.5, - }], - ['awkward', { - expression: [ - { name: 'sad', value: 0.3 }, - { name: 'ee', value: 0.2 }, - ], - blendDuration: 0.5, - }], - ['question', { - expression: [ - { name: 'surprised', value: 0.4 }, - { name: 'think', value: 0.3 }, - ], - blendDuration: 0.4, - }], - ['curious', { - expression: [ - { name: 'think', value: 0.5 }, - { name: 'surprised', value: 0.2 }, - ], - blendDuration: 0.4, - }], - ['neutral', { - expression: [ - { name: 'neutral', value: 1.0 }, - ], - blendDuration: 0.6, - }], - ['love', { - expression: [ - { name: 'happy', value: 0.2 }, - { name: 'relaxed', value: 0.4 }, - ], - blendDuration: 0.4, - }], - ['flirty', { - expression: [ - { name: 'happy', value: 0.2 }, - { name: 'relaxed', value: 0.3 }, - { name: 'aa', value: 0.15 }, - ], - blendDuration: 0.4, - }], - ['greeting', { - expression: [ - { name: 'happy', value: 0.2 }, - { name: 'aa', value: 0.3 }, - ], - blendDuration: 0.3, - }], - ['relaxed', { - expression: [ - { name: 'relaxed', value: 0.8 }, - ], - blendDuration: 0.5, - }], -]) - -export class EmoteController { - private vrm: VRM - private currentEmotion: string | null = null - private isTransitioning = false - private transitionProgress = 0 - private currentValues = new Map() - private targetValues = new Map() - private resetTimer: ReturnType | null = null - - constructor(vrm: VRM) { - this.vrm = vrm - } - - setEmotion(emotionName: string, intensity = 1) { - if (this.resetTimer) { - clearTimeout(this.resetTimer) - this.resetTimer = null - } - - const state = emotionStates.get(emotionName) - if (!state) { - console.warn(`Emotion "${emotionName}" not found`) - return - } - - this.currentEmotion = emotionName - this.isTransitioning = true - this.transitionProgress = 0 - this.currentValues.clear() - this.targetValues.clear() - - const clampedIntensity = Math.min(1, Math.max(0, intensity)) - - // Capture current expression values as start point - if (this.vrm.expressionManager) { - const names = Object.keys(this.vrm.expressionManager.expressionMap) - for (const name of names) { - this.currentValues.set(name, this.vrm.expressionManager.getValue(name) || 0) - this.targetValues.set(name, 0) - } - } - - // Set targets for this emotion - for (const expr of state.expression) { - this.targetValues.set(expr.name, expr.value * clampedIntensity) - } - } - - setEmotionWithReset(emotionName: string, durationMs: number, intensity = 1) { - this.setEmotion(emotionName, intensity) - this.resetTimer = setTimeout(() => { - this.setEmotion('neutral') - this.resetTimer = null - }, durationMs) - } - - update(deltaTime: number) { - if (!this.isTransitioning || !this.currentEmotion) return - - const state = emotionStates.get(this.currentEmotion)! - this.transitionProgress += deltaTime / state.blendDuration - - if (this.transitionProgress >= 1) { - this.transitionProgress = 1 - this.isTransitioning = false - } - - const t = this.transitionProgress - const ease = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2 - - for (const [name, target] of this.targetValues) { - const start = this.currentValues.get(name) || 0 - const value = start + (target - start) * ease - this.vrm.expressionManager?.setValue(name, value) - } - } - - /** Reset all expressions to zero (idle state) without transition. */ - resetAll() { - if (this.resetTimer) { - clearTimeout(this.resetTimer) - this.resetTimer = null - } - this.isTransitioning = false - this.currentEmotion = null - if (this.vrm.expressionManager) { - const names = Object.keys(this.vrm.expressionManager.expressionMap) - for (const name of names) { - this.vrm.expressionManager.setValue(name, 0) - } - } - this.currentValues.clear() - this.targetValues.clear() - } - - dispose() { - if (this.resetTimer) { - clearTimeout(this.resetTimer) - this.resetTimer = null - } - } -} diff --git a/src/components/friend/frontend/hooks/usePassThrough.ts b/src/components/friend/frontend/hooks/usePassThrough.ts deleted file mode 100644 index 8c360a480d99faec84f3addb9cd97088b2c0274a..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/hooks/usePassThrough.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Web fallback for Tauri window pass-through. - * In the web version, cursor pass-through is not available. - * This is a no-op. - */ -import { useEffect } from 'react' - -export function usePassThrough(_enabled: boolean) { - useEffect(() => { - // No-op in web mode — pass-through is a Tauri-only feature - }, [_enabled]) -} diff --git a/src/components/friend/frontend/hooks/useServerStt.ts b/src/components/friend/frontend/hooks/useServerStt.ts deleted file mode 100644 index 22dcd95da29b439075ad64dd6c23ebbba81b416f..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/hooks/useServerStt.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Hook for server-side STT in the Friend Tauri app. - * - * Two modes: - * - push-to-talk: POST /voice/start → capture → POST /voice/stop → get text - * - voice-call: POST /voice/start → server-side periodic segmentation → - * POST /voice/stop → get text - * - * Browser VAD (@ricky0123/vad-web) is NOT used — WebKitGTK (Tauri on Linux) - * does not support onnxruntime-web WASM. Instead, the server segments audio - * every ~5s and sends text to the CLI conversation automatically. - */ -import { useRef, useCallback, useState } from 'react' - -const FRIEND_API_BASE = 'http://127.0.0.1:3456/plugins/friend' - -export type SttProvider = 'browser' | 'groq' | 'anthropic' | 'local' | 'doubao' - -export function useServerStt() { - const [connected, setConnected] = useState(false) - const [interimText, setInterimText] = useState('') - const pollTimerRef = useRef | null>(null) - const onTranscriptRef = useRef<((text: string, isFinal: boolean) => void) | null>(null) - const onErrorRef = useRef<((err: string) => void) | null>(null) - - // ── Push-to-talk ────────────────────────────────────────────────────── - - /** Start push-to-talk: tell backend to start audio capture. */ - const startPushToTalk = useCallback( - async (_provider: SttProvider, _language: string): Promise => { - const res = await fetch(`${FRIEND_API_BASE}/voice/start`, { - method: 'POST', - }) - if (!res.ok) { - const err = await res.json().catch(() => ({ error: 'Unknown error' })) - throw new Error(err.error || 'STT start failed') - } - setConnected(true) - }, - [], - ) - - /** Stop push-to-talk: stop capture and return transcript text. */ - const stopPushToTalk = useCallback(async (): Promise => { - const res = await fetch(`${FRIEND_API_BASE}/voice/stop`, { - method: 'POST', - }) - setConnected(false) - if (!res.ok) return '' - const data = await res.json() - return data.text || '' - }, []) - - // ── Voice call (server-side segmentation, no browser VAD) ───────────── - - /** - * Start voice call mode. - * - * Server captures microphone audio via cpal/arecord and segments it - * every ~5 seconds, sending each segment to STT (Groq Whisper). - * The transcript is enqueued to the CLI conversation automatically. - */ - const startStreaming = useCallback( - ( - _provider: SttProvider, - onTranscript: (text: string, isFinal: boolean) => void, - onError: (err: string) => void, - _language = 'zh', - ) => { - onTranscriptRef.current = onTranscript - onErrorRef.current = onError - - // Start server-side capture - fetch(`${FRIEND_API_BASE}/voice/start`, { method: 'POST' }) - .then(async (res) => { - if (!res.ok) { - const err = await res.json().catch(() => ({ error: 'Unknown error' })) - throw new Error(err.error || 'STT start failed') - } - setConnected(true) - - // Poll interim status every 1s for display - const lastTextRef = { current: '' } - pollTimerRef.current = setInterval(async () => { - try { - const statusRes = await fetch(`${FRIEND_API_BASE}/voice/status`, { - method: 'POST', - }) - if (!statusRes.ok) return - const status = await statusRes.json() - const text = status.interimText || '' - if (text && text !== lastTextRef.current) { - lastTextRef.current = text - setInterimText(text) - onTranscriptRef.current?.(text, false) - } - } catch { - // Polling errors are non-fatal - } - }, 1000) - }) - .catch((err) => { - console.error('[VoiceCall] start failed:', err) - onErrorRef.current?.(String(err)) - }) - }, - [], - ) - - /** Stop voice call mode. */ - const stopStreaming = useCallback(async (): Promise => { - // Stop polling - if (pollTimerRef.current) { - clearInterval(pollTimerRef.current) - pollTimerRef.current = null - } - setInterimText('') - setConnected(false) - - // Stop server-side capture - const res = await fetch(`${FRIEND_API_BASE}/voice/stop`, { - method: 'POST', - }) - if (!res.ok) return '' - const data = await res.json() - return data.text || '' - }, []) - - return { - connected, - interimText, - startPushToTalk, - stopPushToTalk, - startStreaming, - stopStreaming, - } -} diff --git a/src/components/friend/frontend/index.html b/src/components/friend/frontend/index.html deleted file mode 100644 index 599718b24900659380c7710f89adfc10fe8d3268..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/index.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - Claw Sama - - - - -
- - - - diff --git a/src/components/friend/frontend/lip-sync.ts b/src/components/friend/frontend/lip-sync.ts deleted file mode 100644 index 16ed15d28f5afe425f8136e076a439e41ea5c0c9..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/lip-sync.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { VRM } from '@pixiv/three-vrm' -import type { Profile } from 'wlipsync' -import { createWLipSyncNode } from 'wlipsync' -import profile from './assets/lip-sync-profile.json' - -type LipKey = 'A' | 'E' | 'I' | 'O' | 'U' -const RAW_KEYS = ['A', 'E', 'I', 'O', 'U', 'S'] as const -const LIP_KEYS: LipKey[] = ['A', 'E', 'I', 'O', 'U'] -const BLENDSHAPE_MAP: Record = { - A: 'aa', - E: 'ee', - I: 'ih', - O: 'oh', - U: 'ou', -} -const RAW_TO_LIP: Record = { - A: 'A', E: 'E', I: 'I', O: 'O', U: 'U', S: 'I', -} - -const ATTACK = 50 -const RELEASE = 30 -const CAP = 0.7 -const SILENCE_VOL = 0.04 -const SILENCE_GAIN = 0.05 -const IDLE_MS = 160 - -let singleton: LipSync | null = null - -export class LipSync { - private audioContext: AudioContext | null = null - private lipSyncNode: any = null - private gainNode: GainNode | null = null - private smoothState: Record = { A: 0, E: 0, I: 0, O: 0, U: 0 } - private lastActiveAt = 0 - private currentSource: AudioBufferSourceNode | null = null - private ready = false - private initPromise: Promise | null = null - - static getInstance(): LipSync { - if (!singleton) singleton = new LipSync() - return singleton - } - - private constructor() {} - - private async ensureReady() { - if (this.ready) return - if (this.initPromise) return this.initPromise - this.initPromise = (async () => { - this.audioContext = new AudioContext() - this.lipSyncNode = await createWLipSyncNode(this.audioContext, profile as Profile) - // lipSyncNode is analysis-only, no need to connect to destination - this.gainNode = this.audioContext.createGain() - this.gainNode.connect(this.audioContext.destination) - this.ready = true - })() - return this.initPromise - } - - /** - * Fetch audio from URL, decode it, and play through Web Audio API. - * Returns duration (ms) once playback starts. Sound goes to both - * lipSyncNode (for mouth analysis) and destination (for speakers). - */ - async playAudio(url: string): Promise { - await this.ensureReady() - if (!this.lipSyncNode || !this.audioContext) return 0 - - // Stop previous source - if (this.currentSource) { - try { this.currentSource.stop() } catch {} - try { this.currentSource.disconnect() } catch {} - this.currentSource = null - } - - if (this.audioContext.state === 'suspended') { - await this.audioContext.resume() - } - - let response: Response - try { - response = await fetch(url) - } catch (err) { - console.error('[LipSync] fetch failed:', url, err) - return 0 - } - if (!response.ok) { - console.error('[LipSync] fetch error:', url, response.status) - return 0 - } - const arrayBuffer = await response.arrayBuffer() - if (arrayBuffer.byteLength === 0) { - console.error('[LipSync] empty response:', url) - return 0 - } - const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer) - - const source = this.audioContext.createBufferSource() - source.buffer = audioBuffer - source.connect(this.lipSyncNode) - source.connect(this.gainNode!) - this.currentSource = source - source.start() - - return audioBuffer.duration * 1000 - } - - setVolume(value: number) { - if (this.gainNode) { - this.gainNode.gain.value = value - } - } - - update(vrm: VRM, delta: number) { - const node = this.lipSyncNode - if (!vrm.expressionManager || !node || !this.ready) return - - const vol = node.volume ?? 0 - const amp = Math.min(vol * 0.9, 1) ** 0.7 - - const projected: Record = { A: 0, E: 0, I: 0, O: 0, U: 0 } - for (const raw of RAW_KEYS) { - const lip = RAW_TO_LIP[raw] - const rawVal = node.weights[raw] ?? 0 - projected[lip] = Math.max(projected[lip], rawVal * amp) - } - - let winner: LipKey = 'I' - let runner: LipKey = 'E' - let winnerVal = -Infinity - let runnerVal = -Infinity - for (const key of LIP_KEYS) { - const val = projected[key] - if (val > winnerVal) { - runnerVal = winnerVal - runner = winner - winnerVal = val - winner = key - } else if (val > runnerVal) { - runnerVal = val - runner = key - } - } - - const now = performance.now() - let silent = amp < SILENCE_VOL || winnerVal < SILENCE_GAIN - if (!silent) this.lastActiveAt = now - if (now - this.lastActiveAt > IDLE_MS) silent = true - - const target: Record = { A: 0, E: 0, I: 0, O: 0, U: 0 } - if (!silent) { - target[winner] = Math.min(CAP, winnerVal) - target[runner] = Math.min(CAP * 0.5, runnerVal * 0.6) - } - - for (const key of LIP_KEYS) { - const from = this.smoothState[key] - const to = target[key] - const rate = 1 - Math.exp(-(to > from ? ATTACK : RELEASE) * delta) - this.smoothState[key] = from + (to - from) * rate - - // When fully silent and decayed, skip writing so emote can control mouth morphs - if (silent && this.smoothState[key] <= 0.01) continue - - const weight = (this.smoothState[key] <= 0.01 ? 0 : this.smoothState[key]) * 0.7 - vrm.expressionManager.setValue(BLENDSHAPE_MAP[key], weight) - } - } - - /** Check if currently playing audio (within last 300ms). */ - isActive(): boolean { - return this.currentSource !== null && performance.now() - this.lastActiveAt < 300 - } - - /** Stop current audio playback immediately. */ - stopAudio() { - if (this.currentSource) { - try { this.currentSource.stop() } catch {} - try { this.currentSource.disconnect() } catch {} - this.currentSource = null - } - } - - dispose() { - this.stopAudio() - if (this.lipSyncNode) { - try { this.lipSyncNode.disconnect() } catch {} - } - this.audioContext?.close() - singleton = null - } -} diff --git a/src/components/friend/frontend/main.tsx b/src/components/friend/frontend/main.tsx deleted file mode 100644 index da9b1052499f0696c898a582c85e67e38c74548f..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/main.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import { listen } from '@tauri-apps/api/event' - -// Listen for Tauri close event and notify backend -listen('friend-window-close', () => { - fetch('http://127.0.0.1:3456/friend/api/window-close', { method: 'POST' }).catch(() => {}) -}) - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) \ No newline at end of file diff --git a/src/components/friend/frontend/mixamo-loader.ts b/src/components/friend/frontend/mixamo-loader.ts deleted file mode 100644 index 0694508ae7b62aaf869cea23e5305b38551a6679..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/mixamo-loader.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Mixamo FBX animation loader for VRM models. - * Ported from lobe-vidol's loadMixamoAnimation. - */ - -import type { VRM } from '@pixiv/three-vrm' -import type { VRMHumanBoneName } from '@pixiv/three-vrm-core' -import * as THREE from 'three' -import { FBXLoader } from 'three/addons/loaders/FBXLoader.js' - -const mixamoVRMRigMap: Record = { - mixamorigHips: 'hips', - mixamorigSpine: 'spine', - mixamorigSpine1: 'chest', - mixamorigSpine2: 'upperChest', - mixamorigNeck: 'neck', - mixamorigHead: 'head', - mixamorigLeftShoulder: 'leftShoulder', - mixamorigLeftArm: 'leftUpperArm', - mixamorigLeftForeArm: 'leftLowerArm', - mixamorigLeftHand: 'leftHand', - mixamorigLeftHandThumb1: 'leftThumbMetacarpal', - mixamorigLeftHandThumb2: 'leftThumbProximal', - mixamorigLeftHandThumb3: 'leftThumbDistal', - mixamorigLeftHandIndex1: 'leftIndexProximal', - mixamorigLeftHandIndex2: 'leftIndexIntermediate', - mixamorigLeftHandIndex3: 'leftIndexDistal', - mixamorigLeftHandMiddle1: 'leftMiddleProximal', - mixamorigLeftHandMiddle2: 'leftMiddleIntermediate', - mixamorigLeftHandMiddle3: 'leftMiddleDistal', - mixamorigLeftHandRing1: 'leftRingProximal', - mixamorigLeftHandRing2: 'leftRingIntermediate', - mixamorigLeftHandRing3: 'leftRingDistal', - mixamorigLeftHandPinky1: 'leftLittleProximal', - mixamorigLeftHandPinky2: 'leftLittleIntermediate', - mixamorigLeftHandPinky3: 'leftLittleDistal', - mixamorigRightShoulder: 'rightShoulder', - mixamorigRightArm: 'rightUpperArm', - mixamorigRightForeArm: 'rightLowerArm', - mixamorigRightHand: 'rightHand', - mixamorigRightHandPinky1: 'rightLittleProximal', - mixamorigRightHandPinky2: 'rightLittleIntermediate', - mixamorigRightHandPinky3: 'rightLittleDistal', - mixamorigRightHandRing1: 'rightRingProximal', - mixamorigRightHandRing2: 'rightRingIntermediate', - mixamorigRightHandRing3: 'rightRingDistal', - mixamorigRightHandMiddle1: 'rightMiddleProximal', - mixamorigRightHandMiddle2: 'rightMiddleIntermediate', - mixamorigRightHandMiddle3: 'rightMiddleDistal', - mixamorigRightHandIndex1: 'rightIndexProximal', - mixamorigRightHandIndex2: 'rightIndexIntermediate', - mixamorigRightHandIndex3: 'rightIndexDistal', - mixamorigRightHandThumb1: 'rightThumbMetacarpal', - mixamorigRightHandThumb2: 'rightThumbProximal', - mixamorigRightHandThumb3: 'rightThumbDistal', - mixamorigLeftUpLeg: 'leftUpperLeg', - mixamorigLeftLeg: 'leftLowerLeg', - mixamorigLeftFoot: 'leftFoot', - mixamorigLeftToeBase: 'leftToes', - mixamorigRightUpLeg: 'rightUpperLeg', - mixamorigRightLeg: 'rightLowerLeg', - mixamorigRightFoot: 'rightFoot', - mixamorigRightToeBase: 'rightToes', -} - -export async function loadMixamoAnimation(url: string, vrm: VRM): Promise { - const loader = new FBXLoader() - let asset: THREE.Group - try { - asset = await loader.loadAsync(url) as THREE.Group - } catch (err) { - console.error('[MixamoLoader] FBXLoader.loadAsync failed:', url, err) - throw err - } - // Try 'mixamo.com' first, fall back to first available animation - const clip = THREE.AnimationClip.findByName(asset.animations, 'mixamo.com') - ?? asset.animations[0] - if (!clip) throw new Error('No animation clip found in FBX') - - const tracks: THREE.KeyframeTrack[] = [] - const restRotationInverse = new THREE.Quaternion() - const parentRestWorldRotation = new THREE.Quaternion() - const _quatA = new THREE.Quaternion() - const _vec3 = new THREE.Vector3() - - // Scale based on hips height ratio - const hipsObj = asset.getObjectByName('mixamorigHips') - if (!hipsObj) throw new Error('No mixamorigHips bone found in FBX') - const motionHipsHeight = hipsObj.position.y - const vrmHipsY = vrm.humanoid?.getNormalizedBoneNode('hips')?.getWorldPosition(_vec3).y || 0 - const vrmRootY = vrm.scene.getWorldPosition(_vec3).y - const vrmHipsHeight = Math.abs(vrmHipsY - vrmRootY) - const hipsPositionScale = vrmHipsHeight / motionHipsHeight - - clip.tracks.forEach((track) => { - const trackSplitted = track.name.split('.') - const mixamoRigName = trackSplitted[0] - const vrmBoneName = mixamoVRMRigMap[mixamoRigName] - const vrmNodeName = vrm.humanoid?.getNormalizedBoneNode(vrmBoneName)?.name - const mixamoRigNode = asset.getObjectByName(mixamoRigName) - - if (vrmNodeName != null) { - const propertyName = trackSplitted[1] - - if (mixamoRigNode) { - mixamoRigNode.getWorldQuaternion(restRotationInverse).invert() - if (mixamoRigNode.parent) - mixamoRigNode.parent.getWorldQuaternion(parentRestWorldRotation) - } - - if (track instanceof THREE.QuaternionKeyframeTrack) { - for (let i = 0; i < track.values.length; i += 4) { - const flatQuaternion = track.values.slice(i, i + 4) - _quatA.fromArray(flatQuaternion) - _quatA.premultiply(parentRestWorldRotation).multiply(restRotationInverse) - _quatA.toArray(flatQuaternion) - flatQuaternion.forEach((v, index) => { - track.values[index + i] = v - }) - } - - tracks.push( - new THREE.QuaternionKeyframeTrack( - `${vrmNodeName}.${propertyName}`, - Array.from(track.times), - Array.from(track.values).map((v, i) => (vrm.meta?.metaVersion === '0' && i % 2 === 0 ? -v : v)), - ), - ) - } else if (track instanceof THREE.VectorKeyframeTrack) { - const value = Array.from(track.values).map( - (v, i) => (vrm.meta?.metaVersion === '0' && i % 3 !== 1 ? -v : v) * hipsPositionScale, - ) - tracks.push( - new THREE.VectorKeyframeTrack(`${vrmNodeName}.${propertyName}`, Array.from(track.times), value), - ) - } - } - }) - - return new THREE.AnimationClip('mixamoAnimation', clip.duration, tracks) -} diff --git a/src/components/friend/frontend/motion-controller.ts b/src/components/friend/frontend/motion-controller.ts deleted file mode 100644 index a39f51489ea2855df776d60a27d36c03ea9e8b51..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/motion-controller.ts +++ /dev/null @@ -1,331 +0,0 @@ -/** - * MotionController — unified animation system supporting VRMA and FBX actions. - * - * Uses a single persistent AnimationMixer with crossFade transitions to avoid - * T-pose flickering between animations. - * - * Simplified: removed dance system, kept action presets for human-like behavior. - */ - -import * as THREE from 'three' -import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js' -import { VRMAnimationLoaderPlugin, createVRMAnimationClip } from '@pixiv/three-vrm-animation' -import type { VRMAnimation } from '@pixiv/three-vrm-animation' -import type { VRM } from '@pixiv/three-vrm' -import { loadMixamoAnimation } from './mixamo-loader' - -// ── Motion file types ─────────────────────────────────────────────────────── - -export type MotionFileType = 'vrma' | 'fbx' - -// ── Motion presets ────────────────────────────────────────────────────────── - -export interface MotionPreset { - label: string - type: MotionFileType - url: string -} - -// Actions: short one-shot gestures triggered by emotions / interactions -export const actionPresets: Record = { - akimbo: { label: '叉腰', type: 'vrma', url: '/friend/akimbo.vrma' }, - playFingers: { label: '搓手', type: 'vrma', url: '/friend/playFingers.vrma' }, - scratchHead: { label: '挠头', type: 'vrma', url: '/friend/scratchHead.vrma' }, - stretch: { label: '伸展', type: 'vrma', url: '/friend/stretch.vrma' }, - - happy: { label: '开心', type: 'fbx', url: '/friend/happy.fbx' }, - angry: { label: '生气', type: 'fbx', url: '/friend/angry.fbx' }, - greeting: { label: '招呼', type: 'fbx', url: '/friend/greeting.fbx' }, - excited: { label: '兴奋', type: 'fbx', url: '/friend/excited.fbx' }, - shy: { label: '害羞', type: 'fbx', url: '/friend/shy.fbx' }, - point: { label: '指点', type: 'fbx', url: '/friend/point.fbx' }, - salute: { label: '敬礼', type: 'fbx', url: '/friend/salute.fbx' }, - angryPump: { label: '暴怒', type: 'fbx', url: '/friend/angryPump.fbx' }, -} - -// ── Utility: re-anchor root position ──────────────────────────────────────── - -function reAnchorRootPositionTrack(clip: THREE.AnimationClip, vrm: VRM) { - const hipNode = vrm.humanoid?.getNormalizedBoneNode('hips') - if (!hipNode) return - - hipNode.updateMatrixWorld(true) - const defaultHipPos = new THREE.Vector3() - hipNode.getWorldPosition(defaultHipPos) - - const hipsTrack = clip.tracks.find( - (t) => - t instanceof THREE.VectorKeyframeTrack && - t.name === `${hipNode.name}.position`, - ) - if (!(hipsTrack instanceof THREE.VectorKeyframeTrack)) return - - const animeHipPos = new THREE.Vector3( - hipsTrack.values[0], - hipsTrack.values[1], - hipsTrack.values[2], - ) - const delta = new THREE.Vector3().subVectors(animeHipPos, defaultHipPos) - - clip.tracks.forEach((track) => { - if ( - track.name.endsWith('.position') && - track instanceof THREE.VectorKeyframeTrack - ) { - for (let i = 0; i < track.values.length; i += 3) { - track.values[i] -= delta.x - track.values[i + 1] -= delta.y - track.values[i + 2] -= delta.z - } - } - }) -} - -// ── MotionController ──────────────────────────────────────────────────────── - -export class MotionController { - private vrm: VRM - private mixer: THREE.AnimationMixer | null = null - private idleClip: THREE.AnimationClip | null = null - private idleAction: THREE.AnimationAction | null = null - private currentAction: THREE.AnimationAction | null = null - private clipCache = new Map() - private gltfLoader: GLTFLoader - private _actionPlaying = false - private holdTimer: ReturnType | null = null - private _actionSafetyTimer: ReturnType | null = null - private actionQueue: Array<{ name: string; hold: boolean }> = [] - private _settleGen = 0 // generation at which the current settle is valid - private _settleHold = false // hold flag for current settle - - constructor(vrm: VRM) { - this.vrm = vrm - this.mixer = new THREE.AnimationMixer(vrm.scene) - this.gltfLoader = new GLTFLoader() - this.gltfLoader.register((parser) => new VRMAnimationLoaderPlugin(parser)) - } - - get actionPlaying() { return this._actionPlaying } - - update(delta: number) { - if (this.mixer) { - this.mixer.update(delta) - this.checkActionCompletion() - } - } - - /** - * Frame-accurate action completion detection. - * THREE.AnimationMixer 'finished' event only fires when ALL actions finish, - * which never happens with a looping idle. So we check per-frame instead. - */ - private checkActionCompletion() { - if (!this._actionPlaying || !this.currentAction) return - const clip = this.currentAction.getClip() - if (!clip || clip.duration <= 0) return - - // Allow a 1-frame epsilon (≈16ms at 60fps) to avoid precision issues - if (this.currentAction.time >= clip.duration - 0.02) { - this.finishCurrentAction() - } - } - - // ── CrossFade helper ───────────────────────────────────────────────────── - - private crossFadeTo(newAction: THREE.AnimationAction, duration = 0.3) { - newAction.reset().setEffectiveWeight(1).play() - const prev = this.currentAction ?? this.idleAction - if (prev && prev !== newAction) { - prev.crossFadeTo(newAction, duration, false) - } - this.currentAction = newAction - } - - // ── Load & play idle animation ─────────────────────────────────────────── - - async loadIdle(path: string) { - const clip = await this.loadVRMA(path) - if (!clip) return - reAnchorRootPositionTrack(clip, this.vrm) - this.idleClip = clip - this.startIdle() - } - - /** (Re)start idle via crossFade. */ - private startIdle() { - if (!this.idleClip || !this.mixer) return - this.idleAction = this.mixer.clipAction(this.idleClip) - this.crossFadeTo(this.idleAction) - } - - // ── Clear current action (private) ────────────────────────────────────── - - private clearTimers() { - if (this.holdTimer) { clearTimeout(this.holdTimer); this.holdTimer = null } - if (this._actionSafetyTimer) { clearTimeout(this._actionSafetyTimer); this._actionSafetyTimer = null } - } - - // ── Reset to idle (public) ────────────────────────────────────────────── - - resetToIdle() { - this.clearTimers() - this._actionPlaying = false - this._actionGeneration++ - - // CrossFade back to idle - if (this.mixer && this.idleClip) { - this.idleAction = this.mixer.clipAction(this.idleClip) - this.crossFadeTo(this.idleAction) - } - } - - // ── Play a one-shot action ────────────────────────────────────────────── - - private _actionGeneration = 0 - - async playAction(name: string, hold = false) { - const preset = actionPresets[name] - if (!preset) { console.warn('[Motion] unknown action:', name); return } - - // If currently holding (pose frozen, not actively animating), interrupt hold - // and play the new action immediately. This prevents LLM text-message actions - // from being blocked by a stale 10s hold timer. - if (this._actionPlaying && this.holdTimer !== null) { - this.clearTimers() - this._actionPlaying = false - // fall through to play new action - } else if (this._actionPlaying) { - // Normal queue: only when an action is actively animating (e.g. two rapid - // LLM tool calls). Hold does NOT queue — it interrupts. - const last = this.actionQueue[this.actionQueue.length - 1] - if (!last || last.name !== name) { - this.actionQueue.push({ name, hold }) - } - return - } - - // Set lock BEFORE async load to prevent concurrent playAction calls - this._actionPlaying = true - const gen = ++this._actionGeneration - this._settleGen = gen - this._settleHold = hold - - const clip = await this.loadClip(preset) - - // Check if state was reset or another action started during await - if (gen !== this._actionGeneration) return - if (!clip) { console.warn('[Motion] clip load failed for:', name); this._actionPlaying = false; return } - if (!this.mixer) { this._actionPlaying = false; return } - - this.clearTimers() - - const action = this.mixer.clipAction(clip) - action.setLoop(THREE.LoopOnce, 1) - action.clampWhenFinished = true - this.crossFadeTo(action) - - // Frame-accurate completion is handled in checkActionCompletion() via update() - // Safety timer: generous fallback (should never fire if checkActionCompletion works) - const safeDuration = Math.max(clip.duration, 3) + 5 - this._actionSafetyTimer = setTimeout(() => { - if (gen === this._actionGeneration && this._actionPlaying) { - this.finishCurrentAction() - } - }, safeDuration * 1000) - } - - /** Called by checkActionCompletion() or safety timer when current action ends. */ - private finishCurrentAction() { - if (!this._actionPlaying) return - this.clearTimers() - const gen = this._settleGen - const hold = this._settleHold - - if (gen !== this._actionGeneration) return - - // Process next queued action - const next = this.actionQueue.shift() - if (next) { - this._actionPlaying = false - this.playAction(next.name, next.hold) - return - } - - if (hold) { - this.holdTimer = setTimeout(() => { - if (gen !== this._actionGeneration) return - this.clearTimers() - // Drain any actions queued during hold before going idle — - // prevents the queue leak where LLM actions are silently dropped. - const queued = this.actionQueue.shift() - if (queued) { - this._actionPlaying = false - this.playAction(queued.name, queued.hold) - return - } - this._actionPlaying = false - this.startIdle() - }, 10000) - } else { - this._actionPlaying = false - this.startIdle() - } - } - - /** Cleanup when controller is being destroyed (model reload etc.) */ - dispose() { - this.clearTimers() - this._actionPlaying = false - this._actionGeneration++ - if (this.mixer) { - this.mixer.stopAllAction() - this.mixer.uncacheRoot(this.vrm.scene) - this.mixer = null - } - this.idleAction = null - this.currentAction = null - } - - // ── Internal ──────────────────────────────────────────────────────────── - - private async loadClip(preset: MotionPreset): Promise { - const cached = this.clipCache.get(preset.url) - if (cached) return cached - - let clip: THREE.AnimationClip | null = null - - try { - switch (preset.type) { - case 'vrma': - clip = await this.loadVRMA(preset.url) - if (clip) reAnchorRootPositionTrack(clip, this.vrm) - break - case 'fbx': - clip = await loadMixamoAnimation(preset.url, this.vrm) - break - } - } catch (err) { - console.error('Failed to load clip:', preset.url, err) - return null - } - - if (clip) { - clip.name = preset.url - this.clipCache.set(preset.url, clip) - } - return clip - } - - private async loadVRMA(url: string): Promise { - try { - const gltf = await this.gltfLoader.loadAsync(url) - const anims = gltf.userData.vrmAnimations as VRMAnimation[] - if (anims?.length) { - return createVRMAnimationClip(anims[0], this.vrm) - } - } catch (err) { - console.warn(`Failed to load VRMA: ${url}`, err) - } - return null - } -} diff --git a/src/components/friend/frontend/package-lock.json b/src/components/friend/frontend/package-lock.json deleted file mode 100644 index f4cf974c102958b0c4d139252d7ebfc51c014c83..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/package-lock.json +++ /dev/null @@ -1,2701 +0,0 @@ -{ - "name": "friend-vrm-frontend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "friend-vrm-frontend", - "version": "0.1.0", - "dependencies": { - "@pixiv/three-vrm": "^3.4.0", - "@pixiv/three-vrm-animation": "^3.5.0", - "@pixiv/three-vrm-core": "^3.5.1", - "@ricky0123/vad-web": "^0.0.30", - "@tauri-apps/api": "^2.11.1", - "lucide-react": "^0.577.0", - "marked": "^18.0.5", - "mmd-parser": "^1.0.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "three": "^0.175.0", - "vite-plugin-static-copy": "^4.1.1", - "wlipsync": "^1.3.0" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.11.3", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@types/three": "^0.175.0", - "@vitejs/plugin-react": "^4.5.0", - "typescript": "^5.8.3", - "vite": "^6.3.2" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@pixiv/three-vrm": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm/-/three-vrm-3.5.4.tgz", - "integrity": "sha512-hY0MnmKVLUebFR9QN9vCRXo0rZmLpxjLVZAupQv5qDDGCKZuNxlOts9PqTkWlzhkMLLdhB48AF+P4PCy/DFOzA==", - "license": "MIT", - "dependencies": { - "@pixiv/three-vrm-core": "3.5.4", - "@pixiv/three-vrm-materials-hdr-emissive-multiplier": "3.5.4", - "@pixiv/three-vrm-materials-mtoon": "3.5.4", - "@pixiv/three-vrm-materials-v0compat": "3.5.4", - "@pixiv/three-vrm-node-constraint": "3.5.4", - "@pixiv/three-vrm-springbone": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-animation": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-animation/-/three-vrm-animation-3.5.4.tgz", - "integrity": "sha512-yVJL7LoVwZ0fsW2SJhfa0bqIMcfTzBKyc1q/LeKqAtEv63BEQ9VUudx5ikd4zIkgvXwjxw7vGl2zvedrexFisg==", - "license": "MIT", - "dependencies": { - "@pixiv/three-vrm-core": "3.5.4", - "@pixiv/types-vrmc-vrm-1.0": "3.5.4", - "@pixiv/types-vrmc-vrm-animation-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-core": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-core/-/three-vrm-core-3.5.4.tgz", - "integrity": "sha512-CgaxZ4qX6JmE2oKsOVGGlheT011qP4bBZGUaMJJ5iQINI999+cveIdISe459B+AMYD1dU5h1+xCtDthLQNH1Bg==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrm-0.0": "3.5.4", - "@pixiv/types-vrmc-vrm-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-materials-hdr-emissive-multiplier": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-hdr-emissive-multiplier/-/three-vrm-materials-hdr-emissive-multiplier-3.5.4.tgz", - "integrity": "sha512-3BPZ42qW38cHhP+imqEnqTFsltvYkHA/t4VzWFdQ9sngWt0NiFAOotEsv8gZn1ZgKE9VRQezXne6s5aRWQlQqA==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-materials-mtoon": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-mtoon/-/three-vrm-materials-mtoon-3.5.4.tgz", - "integrity": "sha512-vLHt7IYZxlijbCMa5TRRf6gaQjA65F/d7oZBNfZW3XC8sBg57ZV0M/xBAJm2d6UwVmTbBd9NG9li/Y9yuupDQw==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrm-0.0": "3.5.4", - "@pixiv/types-vrmc-materials-mtoon-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-materials-v0compat": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-materials-v0compat/-/three-vrm-materials-v0compat-3.5.4.tgz", - "integrity": "sha512-qRRtg8vYFBRJpsa3evruWqmXr0Gwd8uJXwxxUKuKtrIN0Hw0hJsRLHkg9E238Q8xRTnoycdkSgX4VvVknvtXBA==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrm-0.0": "3.5.4", - "@pixiv/types-vrmc-materials-mtoon-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-node-constraint": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-node-constraint/-/three-vrm-node-constraint-3.5.4.tgz", - "integrity": "sha512-nyAghDrYNp0Z2siEaY2+th+FzZdDs9EJy8K576Mpw9cy3jwtCJuTHBQmGzKkrgoLaBfrso+3Tdfgr62AJV0m1w==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrmc-node-constraint-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/three-vrm-springbone": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/three-vrm-springbone/-/three-vrm-springbone-3.5.4.tgz", - "integrity": "sha512-8KWA7vHU+OnW6XcZPnDuUrjjPFRzmv2yxcTMNCYQvBS547/Iqo0RLbQvpO/ppceLTNJFY2TGXAWJU67aeazWjw==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrm-0.0": "3.5.4", - "@pixiv/types-vrmc-springbone-1.0": "3.5.4", - "@pixiv/types-vrmc-springbone-extended-collider-1.0": "3.5.4" - }, - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/@pixiv/types-vrm-0.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrm-0.0/-/types-vrm-0.0-3.5.4.tgz", - "integrity": "sha512-g9VMPikJxKJ/XgnhXxnqj6ejVhQ9WwOEUp7KyyZHsqRnHcQfSqyZFTs9wkAMCbwCqHXX/lg9+9Ogj8KhxCkbXw==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-materials-hdr-emissive-multiplier-1.0": { - "version": "3.5.4", - "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", - "integrity": "sha512-h9GEQ3q1VTylL/P40kJ8uoQhfUYY54NhTG6Xsnl4X0jf7oHh8MKXYnMMn903hXml6082Yjx1C6x3DKckdnophg==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-materials-mtoon-1.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-materials-mtoon-1.0/-/types-vrmc-materials-mtoon-1.0-3.5.4.tgz", - "integrity": "sha512-AU5sOcsbmcnzRPfgfk7fzKhk5lNm2MqrbvtAsSlhylVMdlzrmXqIfRuexABcB9k+ysTp49qIFioot9KRlQcDUw==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-node-constraint-1.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-node-constraint-1.0/-/types-vrmc-node-constraint-1.0-3.5.4.tgz", - "integrity": "sha512-RRbK5NNvZv4ewRELezueCiDB11FGkt4pdZR/UJ027DJPaNzd6rls2OEJ8weQ3OmgHZpbx/BtdpX2JWX5WZZd/Q==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-springbone-1.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-springbone-1.0/-/types-vrmc-springbone-1.0-3.5.4.tgz", - "integrity": "sha512-NO7HTRBuWEe89Wo9BRI5hX1kWVkZzA4YWg9XALSTLdIT8HMiArL4NRse/CF91jNuLcTGfPEWlBJyq9SciAQxAg==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-springbone-extended-collider-1.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-springbone-extended-collider-1.0/-/types-vrmc-springbone-extended-collider-1.0-3.5.4.tgz", - "integrity": "sha512-uzJmcRh/iHYnZPtnq6N1+jb+T/ptYNCyFEoArkWGNXHSfPjTkMcheTQkL5iMNvTE6hqhyMRVMha9OmbvyLmmUQ==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-vrm-1.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-vrm-1.0/-/types-vrmc-vrm-1.0-3.5.4.tgz", - "integrity": "sha512-pkjT4QXT/Hp6rcq8J8EFEHIldbtPGYWOTaWdoRGMA8/KzJI5PdekZ6AUYbnSP6ElfJ3BSZ01DxxpQZa/UNsFqA==", - "license": "MIT" - }, - "node_modules/@pixiv/types-vrmc-vrm-animation-1.0": { - "version": "3.5.4", - "resolved": "https://registry.npmmirror.com/@pixiv/types-vrmc-vrm-animation-1.0/-/types-vrmc-vrm-animation-1.0-3.5.4.tgz", - "integrity": "sha512-qIl/RO+WnWcBlu/PdTpPtm+YRum8RNtNNigndd/V9dJ9mWeFVI6TThc5i+qbFQjTF/I5ka0HojfeujesIYc1IA==", - "license": "MIT", - "dependencies": { - "@pixiv/types-vrmc-vrm-1.0": "3.5.4" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" - }, - "node_modules/@ricky0123/vad-web": { - "version": "0.0.30", - "resolved": "https://registry.npmmirror.com/@ricky0123/vad-web/-/vad-web-0.0.30.tgz", - "integrity": "sha512-cJyYrh4YeeUBJcbR9Bic/bFDyB9qBkAepvpuWM3vLxnAi7bC3VHzf51UeNdT+OtY4D7MLAgV8iJMc4z41ZnaWg==", - "license": "ISC", - "dependencies": { - "onnxruntime-web": "^1.17.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-2.11.3.tgz", - "integrity": "sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.3", - "@tauri-apps/cli-darwin-x64": "2.11.3", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.3", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.3", - "@tauri-apps/cli-linux-arm64-musl": "2.11.3", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.3", - "@tauri-apps/cli-linux-x64-gnu": "2.11.3", - "@tauri-apps/cli-linux-x64-musl": "2.11.3", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.3", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.3", - "@tauri-apps/cli-win32-x64-msvc": "2.11.3" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.3.tgz", - "integrity": "sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.3.tgz", - "integrity": "sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.3.tgz", - "integrity": "sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.3.tgz", - "integrity": "sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.3.tgz", - "integrity": "sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.3.tgz", - "integrity": "sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.3.tgz", - "integrity": "sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.3.tgz", - "integrity": "sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.3.tgz", - "integrity": "sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.3.tgz", - "integrity": "sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.3.tgz", - "integrity": "sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.175.0", - "resolved": "https://registry.npmmirror.com/@types/three/-/three-0.175.0.tgz", - "integrity": "sha512-ldMSBgtZOZ3g9kJ3kOZSEtZIEITmJOzu8eKVpkhf036GuNkM4mt0NXecrjCn5tMm1OblOF7dZehlaDypBfNokw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": "*", - "@webgpu/types": "*", - "fflate": "~0.8.2", - "meshoptimizer": "~0.18.1" - } - }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@webgpu/types": { - "version": "0.1.70", - "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.70.tgz", - "integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.38", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.376", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", - "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flatbuffers": { - "version": "25.9.23", - "resolved": "https://registry.npmmirror.com/flatbuffers/-/flatbuffers-25.9.23.tgz", - "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", - "license": "Apache-2.0" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/guid-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmmirror.com/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", - "license": "ISC" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/marked": { - "version": "18.0.5", - "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.5.tgz", - "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/meshoptimizer": { - "version": "0.18.1", - "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz", - "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/mmd-parser": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/mmd-parser/-/mmd-parser-1.0.4.tgz", - "integrity": "sha512-Qi0VCU46t2IwfGv5KF0+D/t9cizcDug7qnNoy9Ggk7aucp0tssV8IwTMkBlDbm+VqAf3cdQHTCARKSsuS2MYFg==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.13", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.13.tgz", - "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.48", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.48.tgz", - "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/onnxruntime-common": { - "version": "1.27.0", - "resolved": "https://registry.npmmirror.com/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz", - "integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==", - "license": "MIT" - }, - "node_modules/onnxruntime-web": { - "version": "1.27.0", - "resolved": "https://registry.npmmirror.com/onnxruntime-web/-/onnxruntime-web-1.27.0.tgz", - "integrity": "sha512-ogDLsqIozHZwifPuN37OproAo0byX6t43/bP8GzeZWBWD6MOGExswFAx3up4NS/vvWBOg2u2PXomDt3rMmdQSg==", - "license": "MIT", - "dependencies": { - "flatbuffers": "^25.1.24", - "guid-typescript": "^1.0.9", - "long": "^5.2.3", - "onnxruntime-common": "1.27.0", - "platform": "^1.3.6", - "protobufjs": "^7.2.4" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/platform": { - "version": "1.3.6", - "resolved": "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", - "license": "MIT" - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/three": { - "version": "0.175.0", - "resolved": "https://registry.npmmirror.com/three/-/three-0.175.0.tgz", - "integrity": "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-plugin-static-copy": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/vite-plugin-static-copy/-/vite-plugin-static-copy-4.1.1.tgz", - "integrity": "sha512-GrlA8YklrAfSyxJ4M3fdQLOo9oNkp56IM9FYgX/WtEgeIFkPwhu4wzpufBCIuNKCa6Fn77FkRdYxkHqV0FwjAw==", - "license": "MIT", - "dependencies": { - "chokidar": "^3.6.0", - "p-map": "^7.0.4", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.17" - }, - "engines": { - "node": "^22.0.0 || >=24.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/sapphi-red" - }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/wlipsync": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/wlipsync/-/wlipsync-1.3.0.tgz", - "integrity": "sha512-wYF85QAWCQE2rma9qoXok5we77XouCKxNh0HvbzRAtSIhc/QZbSh2rh5vKgWYHD4jrTE8+CwaxvU9oEUqYO9Zw==", - "license": "MIT" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/src/components/friend/frontend/package.json b/src/components/friend/frontend/package.json deleted file mode 100644 index 53bc9d09fb2ddc286e34b3f5fba380e703c9e61f..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "friend-vrm-frontend", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build" - }, - "dependencies": { - "@pixiv/three-vrm": "^3.4.0", - "@pixiv/three-vrm-animation": "^3.5.0", - "@pixiv/three-vrm-core": "^3.5.1", - "@ricky0123/vad-web": "^0.0.30", - "@tauri-apps/api": "^2.11.1", - "lucide-react": "^0.577.0", - "marked": "^18.0.5", - "mmd-parser": "^1.0.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "three": "^0.175.0", - "vite-plugin-static-copy": "^4.1.1", - "wlipsync": "^1.3.0" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.11.3", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@types/three": "^0.175.0", - "@vitejs/plugin-react": "^4.5.0", - "typescript": "^5.8.3", - "vite": "^6.3.2" - } -} diff --git a/src/components/friend/frontend/public/akimbo.vrma b/src/components/friend/frontend/public/akimbo.vrma deleted file mode 100644 index 9914da8cf9c2661dc654f354d49cb3a293b2210d..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/akimbo.vrma +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c9f7f0cbb37a9404800583dccfdd8e03b66b73b806c3d80265c5c7690710324 -size 289136 diff --git a/src/components/friend/frontend/public/angry.fbx b/src/components/friend/frontend/public/angry.fbx deleted file mode 100644 index 63aff29af379eff1ca06d9d3cd32b13967978f6d..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/angry.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfc3a0d10d3e1918636a1151216098651680d9f9be41550fd0d725a5d639d884 -size 1688752 diff --git a/src/components/friend/frontend/public/angryPump.fbx b/src/components/friend/frontend/public/angryPump.fbx deleted file mode 100644 index ef639a13aa035d3f73cc51f319127c2741ad3a4e..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/angryPump.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:04e7ec62c958914f69df388a3ed8bd8e11cf14d48bbace28eda47e764374654f -size 632864 diff --git a/src/components/friend/frontend/public/excited.fbx b/src/components/friend/frontend/public/excited.fbx deleted file mode 100644 index 4705d0d938fdb2f0c59bd2c08b9f9cd1151d0f09..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/excited.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cdd3f99b18177a12b49bfd3731091d981bea0863282391fbd3f1229f08d16cda -size 826000 diff --git a/src/components/friend/frontend/public/greeting.fbx b/src/components/friend/frontend/public/greeting.fbx deleted file mode 100644 index 347489fcd81b1952c795299df23f427a1b6008cf..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/greeting.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a345411dd77faa101e6ac1bc977f6362be05382af6c811643ce7f2fc2c5d169 -size 711008 diff --git a/src/components/friend/frontend/public/happy.fbx b/src/components/friend/frontend/public/happy.fbx deleted file mode 100644 index f3c3c0d8fc6e29b5d5134e3e0875c4b7731ff230..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/happy.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a656a5da7309bde2e1159ef9e5bf35f3ce0d8088b3aadbfbb5eb99923bbe2c94 -size 1095680 diff --git a/src/components/friend/frontend/public/idle_loop.vrma b/src/components/friend/frontend/public/idle_loop.vrma deleted file mode 100644 index 7891422101c17f715650c4dccd94b2635ee72794..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/idle_loop.vrma +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ace95ba6dcc0bdf2ed1081c002332b4184441117c8d543b6f642b3d2c5cf99be -size 157664 diff --git a/src/components/friend/frontend/public/jile.vmd b/src/components/friend/frontend/public/jile.vmd deleted file mode 100644 index 860ba087600e552c178ac8ed1360c91bc97b8a9d..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/jile.vmd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5963cad3d5dd6ec43b1974816903694b3214a89443e06afc4b9633c898a5b42f -size 1994161 diff --git a/src/components/friend/frontend/public/love.vmd b/src/components/friend/frontend/public/love.vmd deleted file mode 100644 index a5ac24382db77b9a28439a3dd4e9a1bb2dcc087d..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/love.vmd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6aff9a97ab4e5073c4fa28db84422dcce6276e3027127bd9c98bc8ad797c766 -size 3116347 diff --git a/src/components/friend/frontend/public/playFingers.vrma b/src/components/friend/frontend/public/playFingers.vrma deleted file mode 100644 index 0b8a69848b1c546105200b7f4f82f6476620aa18..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/playFingers.vrma +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f2ef8e98d7c4ea4f4d0b5f0973b5ae14a11f006981575913671416f4349a9caa -size 142692 diff --git a/src/components/friend/frontend/public/point.fbx b/src/components/friend/frontend/public/point.fbx deleted file mode 100644 index 9ecd815b140c55785b1f09c3eb11f2de5d6fa0cc..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/point.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:863d7bf87428bbcb7d6a4d77e1b4fe1b9d7263cf3cde5ba5679fe431028ec5b4 -size 538352 diff --git a/src/components/friend/frontend/public/salute.fbx b/src/components/friend/frontend/public/salute.fbx deleted file mode 100644 index 8ab9c0df0857ca7348bb148d0b553917ecd4abc2..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/salute.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b9fd250487f99bb0bc39cfa0a2d83e0891b8ada4939437050b15bb4e6c33fd49 -size 595056 diff --git a/src/components/friend/frontend/public/scratchHead.vrma b/src/components/friend/frontend/public/scratchHead.vrma deleted file mode 100644 index e762b1f5fd09d4e63850ece8d34ee4bfd711a479..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/scratchHead.vrma +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31ef24bc1bb2766a609a292157c3685e79dc844e532a4fa499bc1975650e2e4e -size 183472 diff --git a/src/components/friend/frontend/public/shy.fbx b/src/components/friend/frontend/public/shy.fbx deleted file mode 100644 index c321272941c77ae1da2b300409071b54fdc334cb..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/shy.fbx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f3dd889a485efcb9b549f3c060600e329baf259e0971b16bc711cca898b2718 -size 489216 diff --git a/src/components/friend/frontend/public/stretch.vrma b/src/components/friend/frontend/public/stretch.vrma deleted file mode 100644 index 5b214c6c97646caa2720fec92de45a9fc7f3c641..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/public/stretch.vrma +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4f5ab38707bc04802888644af6d4e8935c7cf23160ac129153f8c6b6f9ff9176 -size 136032 diff --git a/src/components/friend/frontend/src-tauri/Cargo.lock b/src/components/friend/frontend/src-tauri/Cargo.lock deleted file mode 100644 index 970e25582773eb89b2a1b3c7d148bb4c2b221859..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/Cargo.lock +++ /dev/null @@ -1,4904 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.13.0", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.0", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "ctor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dbus" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.118", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "dom_query" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" -dependencies = [ - "bit-set", - "cssparser", - "foldhash", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 1.1.2+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" -dependencies = [ - "log", - "markup5ever", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" -dependencies = [ - "byteorder", - "png 0.17.16", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.118", -] - -[[package]] -name = "js-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.13.0", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" -dependencies = [ - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" - -[[package]] -name = "markup5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "muda" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.0", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.0", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.0", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.13.0", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "open" -version = "5.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plist" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" -dependencies = [ - "base64 0.22.1", - "indexmap 2.14.0", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "regex" -version = "1.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.118", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" -dependencies = [ - "bitflags 2.13.0", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_with" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" -dependencies = [ - "base64 0.22.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.35.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" -dependencies = [ - "bitflags 2.13.0", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dbus", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "libc", - "log", - "ndk", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "once_cell", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.18", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png 0.17.16", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.118", - "tauri-utils", - "thiserror 2.0.18", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "walkdir", -] - -[[package]] -name = "tauri-plugin-opener" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" -dependencies = [ - "dunce", - "glob", - "objc2-app-kit", - "objc2-foundation", - "open", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "url", - "windows", - "zbus", -] - -[[package]] -name = "tauri-runtime" -version = "2.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dom_query", - "dunce", - "glob", - "http", - "infer", - "json-patch", - "log", - "memchr", - "phf", - "plist", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" -dependencies = [ - "dunce", - "embed-resource", - "toml 1.1.2+spec-1.1.0", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" -dependencies = [ - "new_debug_unreachable", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "time" -version = "0.3.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.3", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.14.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.3", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.0", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.18", - "windows-sys 0.61.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "codev-friend" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-opener", - "webkit2gtk", -] - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" -dependencies = [ - "thiserror 2.0.18", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wry" -version = "0.55.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.18", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 1.0.3", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" -dependencies = [ - "serde", - "winnow 1.0.3", - "zvariant", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zvariant" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow 1.0.3", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.118", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.118", - "winnow 1.0.3", -] diff --git a/src/components/friend/frontend/src-tauri/Cargo.toml b/src/components/friend/frontend/src-tauri/Cargo.toml deleted file mode 100644 index 335f6fb0f0807f1695c2bc3b80eeb874d152e396..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "codev-friend" -version = "0.1.0" -description = "Codev VRM desktop pet" -authors = ["Codev"] -edition = "2021" - -[lib] -name = "codev_friend_lib" -crate-type = ["lib", "cdylib", "staticlib"] - -[build-dependencies] -tauri-build = { version = "2", features = [] } - -[dependencies] -tauri = { version = "2", features = [] } -tauri-plugin-opener = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -webkit2gtk = "2.0.2" - -[features] -default = ["custom-protocol"] -custom-protocol = ["tauri/custom-protocol"] diff --git a/src/components/friend/frontend/src-tauri/build.rs b/src/components/friend/frontend/src-tauri/build.rs deleted file mode 100644 index d860e1e6a7cac333c3cc0bc9cb67faf286b07d69..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build() -} diff --git a/src/components/friend/frontend/src-tauri/capabilities/default.json b/src/components/friend/frontend/src-tauri/capabilities/default.json deleted file mode 100644 index 541373c5d14b53a5c55c8c5e027f1bf159d69f4b..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/capabilities/default.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "identifier": "default", - "description": "Default capabilities for Codev Friend", - "windows": ["main"], - "permissions": [ - "core:default", - "opener:default" - ] -} diff --git a/src/components/friend/frontend/src-tauri/icons/128x128.png b/src/components/friend/frontend/src-tauri/icons/128x128.png deleted file mode 100644 index d9cf023bac12d71daf19f44dd56bb8f72d407f8d..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/icons/128x128.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:abed9d6d7efb363e64f404b9773c9aabbab7fc2e91039f31f117ef54be59262a -size 2013 diff --git a/src/components/friend/frontend/src-tauri/icons/256x256.png b/src/components/friend/frontend/src-tauri/icons/256x256.png deleted file mode 100644 index 56f5cce6af0dc601bb1a34b8e92603ebe2a8080c..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/icons/256x256.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1498c13855dc0290dec4e0a2df4065073ddcc3e6c844b3c121ff420aaf8aadb -size 3657 diff --git a/src/components/friend/frontend/src-tauri/icons/32x32.png b/src/components/friend/frontend/src-tauri/icons/32x32.png deleted file mode 100644 index 06b713856629eba843591d14c45e22264fafaedb..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/icons/32x32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:463ca82091efa940a0172fa29c918228ef20e88cba2ad8561708e5158afd843b -size 886 diff --git a/src/components/friend/frontend/src-tauri/src/lib.rs b/src/components/friend/frontend/src-tauri/src/lib.rs deleted file mode 100644 index ebdae29010be120f427ff6f47d85620845f89c60..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -use tauri::{Emitter, Manager, WindowEvent}; - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - let builder = tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) - .setup(|app| { - let window = app.get_webview_window("main").unwrap(); - - // Allow microphone/camera access for browser VAD (getUserMedia) - #[cfg(target_os = "linux")] - { - use webkit2gtk::{PermissionRequestExt, WebViewExt}; - let _ = window.with_webview(|webview| { - let platform = webview.inner(); - platform.connect_permission_request(|_webview, request| { - // Allow all permission requests (media, etc.) - request.allow(); - true - }); - }); - } - - let _ = window.set_focus(); - Ok(()) - }) - .on_window_event(|window, event| { - if let WindowEvent::CloseRequested { .. } = event { - window.emit("friend-window-close", ()).ok(); - } - }); - - builder.run(tauri::generate_context!()) - .expect("error while running tauri application"); -} diff --git a/src/components/friend/frontend/src-tauri/src/main.rs b/src/components/friend/frontend/src-tauri/src/main.rs deleted file mode 100644 index 022d0327f8c997e4a0ba27c197d76875f2f3518b..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Prevents additional console window on Windows in release -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - codev_friend_lib::run() -} diff --git a/src/components/friend/frontend/src-tauri/tauri.conf.json b/src/components/friend/frontend/src-tauri/tauri.conf.json deleted file mode 100644 index 047db4aa7cd2adad72138383680a95419463abf4..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/src-tauri/tauri.conf.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/nicedoc/tauri/dev/crates/tauri-config-schema/schema.json", - "productName": "Codev Friend", - "version": "0.1.0", - "identifier": "com.codev.friend", - "build": { - "devUrl": "http://127.0.0.1:3456/friend/" - }, - "bundle": { - "active": true, - "targets": [ - "deb", - "rpm" - ], - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/256x256.png" - ] - }, - "app": { - "windows": [ - { - "title": "Codev Friend", - "label": "main", - "url": "http://127.0.0.1:3456/friend/", - "transparent": true, - "decorations": false, - "alwaysOnTop": true, - "width": 400, - "height": 600, - "minWidth": 200, - "minHeight": 300, - "resizable": true, - "skipTaskbar": false, - "center": true, - "visible": true - } - ], - "security": { - "csp": null - } - } -} diff --git a/src/components/friend/frontend/three-helpers.ts b/src/components/friend/frontend/three-helpers.ts deleted file mode 100644 index 4abe800bd4596e8462d13f0cff4271994142a9f2..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/three-helpers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Euler, MathUtils, Vector3 } from 'three' - -const PI2 = Math.PI * 2 - -export function clampByRadian( - v: number, - min = Number.NEGATIVE_INFINITY, - max = Number.POSITIVE_INFINITY, -) { - const hasMin = Number.isFinite(min) - const hasMax = Number.isFinite(max) - if (hasMin && hasMax && min === max) return min - - const newMin = hasMin ? MathUtils.euclideanModulo(min, PI2) : min - let newMax = hasMax ? MathUtils.euclideanModulo(max, PI2) : max - let newV = MathUtils.euclideanModulo(v, PI2) - - if (hasMin && hasMax && newMin >= newMax) { - newMax += PI2 - if (newV < Math.PI) newV += PI2 - } - if (hasMax && newV > newMax) newV = newMax - else if (hasMin && newV < newMin) newV = newMin - return MathUtils.euclideanModulo(newV, PI2) -} - -export function clampVector3ByRadian(v: Vector3 | Euler, min?: Vector3, max?: Vector3) { - return v.set( - clampByRadian(v.x, min?.x, max?.x), - clampByRadian(v.y, min?.y, max?.y), - clampByRadian(v.z, min?.z, max?.z), - ) -} diff --git a/src/components/friend/frontend/tsconfig.json b/src/components/friend/frontend/tsconfig.json deleted file mode 100644 index 5cdd4e20dc8f50fae915e988821be58cb25e2fad..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "useDefineForClassFields": true, - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noFallthroughCasesInSwitch": true - }, - "include": ["."] -} diff --git a/src/components/friend/frontend/vite.config.ts b/src/components/friend/frontend/vite.config.ts deleted file mode 100644 index bdb8fd70b85d33e3c832eefa8ea174f033ee3fc7..0000000000000000000000000000000000000000 --- a/src/components/friend/frontend/vite.config.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { viteStaticCopy } from 'vite-plugin-static-copy' - -export default defineConfig({ - plugins: [ - react(), - viteStaticCopy({ - targets: [ - { - // Silero VAD model files (used by @ricky0123/vad-web) - src: 'node_modules/@ricky0123/vad-web/dist/silero_*.onnx', - dest: '.', - rename: { stripBase: true }, - }, - { - // Audio worklet bundle (used by @ricky0123/vad-web for AudioWorkletNode) - src: 'node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js', - dest: '.', - rename: { stripBase: true }, - }, - { - // ONNX Runtime Web WASM binary files (used by onnxruntime-web) - src: 'node_modules/onnxruntime-web/dist/ort-wasm*.wasm', - dest: '.', - rename: { stripBase: true }, - }, - { - // ONNX Runtime Web ESM glue file (used by onnxruntime-web/wasm dynamic import) - src: 'node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.mjs', - dest: '.', - rename: { stripBase: true }, - }, - ], - }), - ], - base: './', - server: { - port: 1420, - strictPort: false, - }, - optimizeDeps: { - esbuildOptions: { - target: 'es2022', - }, - }, - build: { - target: ['es2022', 'chrome100', 'safari16'], - minify: 'esbuild', - sourcemap: false, - }, -}) diff --git a/src/friend/FriendService.ts b/src/friend/FriendService.ts deleted file mode 100644 index f737a5b6108311d9247fab6e6f2b609e65c6cd83..0000000000000000000000000000000000000000 --- a/src/friend/FriendService.ts +++ /dev/null @@ -1,908 +0,0 @@ -/** - * FriendService — in-process VRM companion brain service. - * - * Mirror of FeishuService pattern: - * - Singleton, runs in the main CLI process - * - subscribe()/subscribeToInbound() for React external store sync - * - enqueue() with origin tracking for message submission - * - In-process audio capture via cpal (src/services/voice.ts) - * - SSE broadcast for VRM display commands - * - * Eliminates the need for: - * - A separate background server subprocess (port 3456) - * - A CLI SDK subprocess (conversationService.startSession) - * - Server-side arecord/parecord audio capture - */ - -import { broadcastToVrm, type VrmBroadcastPayload } from './sse.js'; -import { getPrefs } from './prefs.js'; -import { stripForTts } from './text-utils.js'; -import { edgeTts, qwenTts, registerAudioFile, getAudioFile } from './tts.js'; -import { splitSentences } from './text-utils.js'; -import { SileroVad } from './voice/vad-service.js'; -import { HighPassFilter } from './voice/highpass-filter.js'; -import { readFileSync } from 'node:fs'; -import { buildVrmSystemPrompt } from '../skills/bundled/friendPrompt.js'; - -// ── Types ────────────────────────────────────────────────────────────── - -export type FriendServiceState = { - status: 'stopped' | 'starting' | 'running' | 'error'; - lastError?: string; - /** Number of active SSE display clients */ - displayClientCount?: number; - /** Current capture status (for voice call interim polling) */ - captureStatus?: { capturing: boolean; interimText?: string }; -}; - -type Listener = () => void; - -/** Inbound event for bridge hook consumption */ -export type FriendInboundEvent = { - text: string; -}; - -type InboundListener = (event: FriendInboundEvent) => void; - -// ── Audio capture types (cpal wrapper) ───────────────────────────────── - -type AudioCaptureProvider = { - startRecording( - onData: (chunk: Buffer) => void, - onEnd: () => void, - ): Promise; - stopRecording(): Promise; - isRecording(): boolean; -}; - -// ── Service implementation ───────────────────────────────────────────── - -class FriendService { - private listeners = new Set(); - private inboundListeners = new Set(); - private state: FriendServiceState = { status: 'stopped' }; - /** Audio capture in progress? */ - private capturing = false; - /** Accumulated STT text chunks during capture */ - private captureTranscripts: string[] = []; - /** Interim (non-final) text during active capture */ - private captureInterimText = ''; - /** Resolver for the current stopVoiceCapture() call */ - private captureResolver: ((text: string) => void) | null = null; - /** Lazy-loaded cpal audio capture module */ - private audioCapture: AudioCaptureProvider | null = null; - /** Active STT connection (Anthropic/Doubao/Whisper) during capture */ - private sttConnection: { send: (chunk: Buffer) => void; finalize: () => Promise; close: () => void } | null = null; - /** STT provider/language cached for connection re-creation during segmentation */ - private captureProvider = ''; - private captureLanguage = ''; - /** Guard against concurrent flush calls */ - private _flushing = false; - /** Silero VAD instance (real ML-based voice activity detection) */ - private vadInstance: SileroVad | null = null; - /** High-pass filter to remove low-frequency noise (fan/AC hum) before VAD */ - private highpassFilter = new HighPassFilter(80); - /** When muted, audio from arecord is not forwarded to STT or VAD (prevents echo) */ - private muted = false; - /** Timer to automatically unmute after estimated TTS playback */ - private muteTimer: ReturnType | null = null; - - // ── React sync external store interface ────────────────────────────── - - subscribe(listener: Listener): () => void { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - subscribeToInbound(listener: InboundListener): () => void { - this.inboundListeners.add(listener); - return () => this.inboundListeners.delete(listener); - } - - getStateSnapshot(): FriendServiceState { - return this.state; - } - - // ── Lifecycle ──────────────────────────────────────────────────────── - - async start(): Promise { - if (this.state.status === 'running') return; - - this.setState({ status: 'starting', lastError: undefined }); - - try { - // Pre-warm Silero VAD (loads ONNX model + onnxruntime-web WASM) - if (!this.vadInstance) { - const vad = new SileroVad({ - onSpeechStart: () => {}, - onSpeechEnd: (_audio) => { - this._flushVadSegment().catch((e) => - console.error('[FriendService] VAD segment flush error:', e), - ); - }, - }, { - // Strict thresholds for near-field mic — rejects keyboard/ambient noise - positiveSpeechThreshold: 0.90, // need 90% confidence (was 80%) - negativeSpeechThreshold: 0.40, // drop below 40% to end (was 50%) - preSpeechTriggerFrames: 16, // require ~512ms sustained speech (was 480ms) - minSpeechFrames: 8, // ~256ms min speech (was 192ms) - redemptionFrames: 15, // ~480ms silence before segment end (was 640ms) - rmsThreshold: 0.015, // -36dBFS noise floor (was -40dBFS) - }); - vad.init().then(() => { - this.vadInstance = vad; - }).catch((e) => { - console.warn('[FriendService] VAD init failed (non-fatal, voice capture falls back to F2-only):', e); - }); - } - - this.setState({ status: 'running' }); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - this.setState({ status: 'error', lastError: msg }); - throw err; - } - } - - async stop(): Promise { - // Stop any active capture - if (this.capturing) { - await this.stopVoiceCapture().catch(() => {}); - } - - this.audioCapture = null; - this.sttConnection = null; - - if (this.state.status !== 'stopped') { - this.setState({ status: 'stopped' }); - } - } - - // ── Text relay via messageQueueManager.enqueue() ───────────────────── - - /** - * Send text through the main CLI conversation. - * Mirrors FeishuService's enqueue() pattern with origin tracking. - */ - sendText(text: string): void { - const trimmed = text.trim(); - if (!trimmed) return; - - // Notify inbound listeners (bridge hook uses this for turn tracking) - for (const listener of this.inboundListeners) { - listener({ text: trimmed }); - } - - // Build the override system prompt (persona-only, no Codev CLI prompt) - const baseVrmPrompt = buildVrmSystemPrompt(); - const overrideSystemPrompt = `${baseVrmPrompt}\n\nIMPORTANT: You are ONLY the character(s) defined above. Do NOT mention Codev, Claude Code, "built-in tools", running code, Git, task management, or any coding-assistant capabilities. You may use available tools when appropriate, but your identity and behavior must follow your character persona strictly.`; - - // Dynamically import enqueue to avoid circular deps - import('../utils/messageQueueManager.js').then(({ enqueue }) => { - enqueue({ - value: trimmed, - mode: 'prompt', - skipSlashCommands: true, - bridgeOrigin: true, - origin: { kind: 'channel', server: 'friend' }, - overrideSystemPrompt, - }); - }).catch((err) => { - console.error('[FriendService] enqueue failed:', err); - }); - } - - // ── Voice capture ──────────────────────────────────────────────────── - - /** - * Start in-process voice capture using cpal. - * Audio is forwarded to the configured STT provider. - * Wraps initialization in a timeout (12s) to prevent hanging - * when STT provider or audio device is unavailable. - */ - async startVoiceCapture(): Promise { - if (this.capturing) return; - - const prefs = getPrefs(); - let provider = prefs.sttProvider; - const language = prefs.sttLanguage || 'zh'; - - // Auto-detect STT provider if not configured or set to 'browser' (not available in WebKitGTK) - if (!provider || provider === 'browser') { - provider = await this.detectAvailableSttProvider(); - } - - this.captureTranscripts = []; - this.captureInterimText = ''; - this.capturing = true; - - try { - // Wrap the whole initialization in a 12s timeout to avoid hanging - // when STT provider or audio device is unavailable. - await this.withTimeout( - this._initVoiceCapture(provider, language), - 12000, - `Voice initialization timed out. Check that your microphone is accessible and STT provider "${provider}" is configured correctly.`, - ); - } catch (err) { - this.capturing = false; - this.sttConnection?.close(); - this.sttConnection = null; - throw err; - } - } - - /** - * Internal voice capture initialization (STT connection + audio capture). - * Separated so startVoiceCapture() can wrap it with a timeout. - */ - private async _initVoiceCapture( - provider: string, - language: string, - ): Promise { - this.captureProvider = provider; - this.captureLanguage = language; - - // 1. Start STT provider connection (with inner 8s timeout) - const conn = await this.startSttConnectionWithTimeout(provider, language); - this.sttConnection = conn; - - // 2. Load audio capture module (cpal) - const audio = await this.loadAudioCapture(); - - // 3. Start cpal recording — chunks go to STT - const ok = await audio.startRecording( - (chunk: Buffer) => { - this.sttConnection?.send(chunk); - }, - () => { - // Capture ended (user stop or silence detection) - }, - ); - - if (!ok) { - throw new Error('Native audio capture unavailable'); - } - - // 4. Start Silero VAD for real-time speech-end detection - // audio chunks flow to VAD via the arecord data callback (see loadAudioCapture) - if (this.vadInstance) { - try { - this.highpassFilter.reset(); // reset filter state for new session - this.vadInstance.start(); - } catch (e) { - console.warn('[FriendService] VAD start error (non-fatal):', e); - } - } - } - - /** - * Flush the current STT segment and start a new one, triggered by - * Silero VAD's onSpeechEnd callback. - * - * The audio from the just-ended speech segment has already been sent - * to the STT connection. We swap to a fresh connection so the next - * speech segment starts clean. - */ - private async _flushVadSegment(): Promise { - if (this._flushing || !this.capturing) return; - this._flushing = true; - try { - const oldConn = this.sttConnection; - if (!oldConn) return; - - // 1. Create a new STT connection for ongoing audio - const newConn = await this.startSttConnectionWithTimeout( - this.captureProvider, - this.captureLanguage, - ); - this.sttConnection = newConn; - - // 2. Finalize old connection (sends buffered audio to STT provider) - await oldConn.finalize().catch(() => {}); - oldConn.close(); - - // 3. Send accumulated transcript to the CLI conversation - const transcript = this.captureTranscripts.join('').trim(); - if (transcript) { - this.captureTranscripts = []; - this.sendText(transcript); - - // Mute the entire AI turn: from transcript submission → AI processing - // (tools, deep thinking) → response → TTS playback. - // This prevents both TTS echo AND capturing accidental speech during - // AI processing (e.g. "hmm", "ok"). - this.startAiTurnMute(); - } - } catch (err) { - console.error('[FriendService] _flushVadSegment error:', err); - } finally { - this._flushing = false; - } - } - - /** Race a promise against a timeout */ - private async withTimeout( - promise: Promise, - ms: number, - message: string, - ): Promise { - return Promise.race([ - promise, - new Promise((_, reject) => - setTimeout(() => reject(new Error(message)), ms), - ), - ]); - } - - /** - * Auto-detect the first available STT provider. - * Tries: Groq Whisper (cloud, API key) → local Whisper → Anthropic Voice Stream → Doubao ASR - */ - private async detectAvailableSttProvider(): Promise { - console.log('[FriendService] detectAvailableSttProvider: checking available providers...'); - - // Check Groq API key first (fastest — no Python, just a REST call) - // Keys are resolved from: prefs → process.env → ~/.claude/settings.json - try { - const { isGroqAvailable } = await import('../services/voice/groqSTT.js'); - if (isGroqAvailable()) { - console.log('[FriendService] detectAvailableSttProvider: Groq API key found'); - return 'groq'; - } - } catch { /* ignore */ } - - // Check local Whisper (no external API keys needed) - try { - const { checkLocalWhisperAvailable } = await import( - '../services/voice/whisperSTT.js' - ); - const avail = await checkLocalWhisperAvailable(); - console.log('[FriendService] detectAvailableSttProvider: local Whisper available:', avail); - if (avail) { - return 'local'; - } - } catch (e) { - console.warn('[FriendService] detectAvailableSttProvider: local whisper check failed:', e); - } - - // Check Anthropic Voice Stream - try { - const { isVoiceStreamAvailable } = await import( - '../services/voiceStreamSTT.js' - ); - if (isVoiceStreamAvailable()) { - return 'anthropic'; - } - } catch { /* skip */ } - - // Check Doubao credentials file - try { - const path = await import('node:path'); - const fs = await import('node:fs'); - const homeDir = process.env.HOME || process.env.USERPROFILE || ''; - const credsPath = path.join(homeDir, '.claude', 'tts', 'doubao', 'credentials.json'); - if (fs.existsSync(credsPath)) { - return 'doubao'; - } - } catch { /* skip */ } - - throw new Error( - 'No STT provider available. Install local Whisper:\n' + - ' pip install openai-whisper\n\n' + - 'Or configure an STT provider in Friend settings (Settings → STT Provider).', - ); - } - - /** - * Start STT connection with a timeout to prevent hanging - * when the provider is unavailable (e.g. Python Whisper not installed). - */ - private async startSttConnectionWithTimeout( - provider: string, - language: string, - ): Promise<{ send: (chunk: Buffer) => void; finalize: () => Promise; close: () => void }> { - const timeoutMs = 8000; - const result = await Promise.race([ - this.startSttConnection(provider, language), - new Promise((_, reject) => - setTimeout( - () => - reject( - new Error( - `STT provider "${provider}" timed out after ${timeoutMs / 1000}s.` + - (provider === 'local' - ? '\nInstall local Whisper: pip install openai-whisper' - : ''), - ), - ), - timeoutMs, - ), - ), - ]); - return result; - } - - /** - * Stop voice capture and return the accumulated transcript. - */ - async stopVoiceCapture(): Promise { - return this._stopCapture(); - } - - /** - * Internal: stop audio capture + finalize STT, return transcript. - */ - private async _stopCapture(): Promise { - console.log(`[FriendService] _stopCapture: capturing=${this.capturing} sttConnection=${this.sttConnection ? 'exists' : 'null'}`); - if (!this.capturing) return ''; - - // Stop audio recording first (no more audio → no more STT/VAD input) - if (this.audioCapture) { - await this.audioCapture.stopRecording().catch(() => {}); - } - - this.capturing = false; - - // Clear mute state - this.clearMute(); - - // Reset VAD state silently (don't fire onSpeechEnd — we're finalizing below) - if (this.vadInstance) { - try { - this.vadInstance.reset(); - } catch { /* ignore */ } - } - - // Finalize STT connection - const conn = this.sttConnection; - this.sttConnection = null; - - if (conn) { - try { - await conn.finalize(); - conn.close(); - } catch { - // ignore finalization errors - } - } - - // Send any remaining transcript to CLI before returning - const remaining = this.captureTranscripts.join('').trim(); - if (remaining) { - this.sendText(remaining); - } - - const transcript = this.captureTranscripts.join(''); - console.log(`[FriendService] _stopCapture: transcript="${transcript}" (len=${transcript.length})`); - this.captureTranscripts = []; - this.captureInterimText = ''; - this.setState({ captureStatus: { capturing: false } }); - return transcript; - } - - /** - * Get current capture status (for push-to-talk mode). - */ - getCaptureStatus(): { capturing: boolean; interimText?: string } { - const status = this.state.captureStatus ?? { capturing: false }; - return { ...status }; - } - - /** - * Transcribe an audio buffer (PCM/WAV) using the configured STT provider - * and send the resulting text to the CLI conversation. - * - * Called by the HTTP endpoint when the browser VAD detects a speech segment. - */ - async transcribeAudioSegment(audioBuffer: Buffer): Promise { - const prefs = getPrefs(); - let provider = prefs.sttProvider; - if (!provider || provider === 'browser') { - provider = await this.detectAvailableSttProvider(); - } - - // Create a temporary STT connection just for this segment - const conn = await this.startSttConnectionWithTimeout( - provider, - prefs.sttLanguage || 'zh', - ); - - try { - conn.send(audioBuffer); - await conn.finalize(); - conn.close(); - } catch (err) { - conn.close(); - throw err; - } - - // Get transcript from the finalization callback - const transcript = this.captureTranscripts.join(''); - this.captureTranscripts = []; - - if (transcript.trim()) { - this.sendText(transcript); - } - - return transcript; - } - - /** - * Start mute at the beginning of an AI turn (when user speech is submitted). - * - * Audio from arecord is blocked from STT/VAD during: - * AI processing (tools, deep thinking) → response generation → TTS playback - * - * A long timeout (30s) covers the AI processing window without estimation. - * The timer is later refined by extendMuteForTts() when TTS audio is ready. - */ - private startAiTurnMute(): void { - if (!this.capturing) return; - - // Clear any existing timer (from a previous turn) - if (this.muteTimer) clearTimeout(this.muteTimer); - - this.muted = true; - this.vadInstance?.pause(); - - // 30s covers almost all AI response cycles (tools, deep thinking, etc.). - // The timer is reset in extendMuteForTts() when TTS duration is known. - this.muteTimer = setTimeout(() => this.unmute(), 30_000); - } - - /** - * Refine the mute timer to match actual TTS audio duration once it's ready. - * Called from broadcastResponse() after TTS generation succeeds. - * Resets the timer to exactly the audio playback length. - */ - private extendMuteForTts(audioId: string): void { - if (!this.capturing) return; - if (!this.muted) return; // turn already ended, don't re-mute - - // Clear the generous timer from startAiTurnMute - if (this.muteTimer) clearTimeout(this.muteTimer); - - // Parse exact audio duration from the MP3 file - let muteMs = this.getMp3DurationMs(audioId); - if (muteMs <= 0) muteMs = 3000; // safety fallback - - this.muteTimer = setTimeout(() => this.unmute(), muteMs); - } - - /** Unmute and resume VAD */ - private unmute(): void { - this.muted = false; - this.muteTimer = null; - this.vadInstance?.start(); - } - - /** Parse MP3 file to get exact audio duration in milliseconds. - * - * Finds the first two frame syncs to determine the real frame size, - * then counts frames using that stride. Works for CBR output (Edge TTS) - * without relying on error-prone bitrate lookup tables. - */ - private getMp3DurationMs(audioId: string): number { - const filePath = getAudioFile(audioId); - if (!filePath) return 0; - let buf: Buffer; - try { buf = readFileSync(filePath); } catch { return 0; } - if (buf.length < 100) return 0; - - const isSync = (p: number) => - p + 1 < buf.length && buf[p] === 0xff && (buf[p + 1] & 0xe0) === 0xe0; - - let offset = 0; - - // Skip ID3v2 tag - if (buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) { - offset = 10 + - ((buf[6] & 0x7f) << 21) | - ((buf[7] & 0x7f) << 14) | - ((buf[8] & 0x7f) << 7) | - (buf[9] & 0x7f); - } - - // Find first two syncs to measure actual frame stride - let firstSync = -1; - let secondSync = -1; - for (let i = offset; i < buf.length - 3; i++) { - if (isSync(i)) { - if (firstSync === -1) firstSync = i; - else { secondSync = i; break; } - } - } - if (firstSync === -1 || secondSync === -1) return 0; - - const frameSize = secondSync - firstSync; // real stride (CBR) - if (frameSize < 20) return 0; - - // Parse frame header for sample rate and samples-per-frame - const h = - (buf[firstSync] << 24) | - (buf[firstSync + 1] << 16) | - (buf[firstSync + 2] << 8) | - buf[firstSync + 3]; - const version = (h >> 19) & 0x3; - const sampleRateIdx = (h >> 10) & 0x3; - if (sampleRateIdx === 3) return 0; - - const srTable: Record = { - 3: [44100, 48000, 32000][sampleRateIdx], - 2: [22050, 24000, 16000][sampleRateIdx], - 0: [11025, 12000, 8000][sampleRateIdx], - }; - const sampleRate = srTable[version]; - if (!sampleRate) return 0; - - const isMpeg1 = version === 3; - const spf = isMpeg1 ? 1152 : 576; - - // Count frames using stride - let frames = 0; - for (let pos = firstSync; pos + 3 < buf.length; pos += frameSize) { - // Sanity check: verify sync word - if (!isSync(pos)) { - // Frame may have been corrupted; scan forward to next sync - while (pos < buf.length - 3 && !isSync(pos)) pos++; - if (pos >= buf.length - 3) break; - } - frames++; - } - - return Math.round((frames * spf) / sampleRate * 1000); - } - - /** Clear mute state immediately */ - private clearMute(): void { - if (this.muteTimer) { - clearTimeout(this.muteTimer); - this.muteTimer = null; - } - this.muted = false; - } - - // ── Response broadcast (called by useFriendBridge) ─────────────────── - - /** - * Broadcast AI response to the VRM display layer via SSE. - * Generates TTS audio for completed sentences and sends - * emotion/action commands alongside text. - */ - async broadcastResponse(text: string): Promise { - if (!text.trim()) return; - - const prefs = getPrefs(); - - // Send the text — the frontend TextBubble splits and displays it - broadcastToVrm({ text }); - - // Generate TTS for the full response if enabled - if (prefs.ttsEnabled) { - try { - const audioId = await this.generateTts(text); - if (audioId) { - const fullUrl = `http://127.0.0.1:3456/plugins/friend/audio/${audioId}`; - broadcastToVrm({ audioUrl: fullUrl, sendFirstTts: true }); - - // Refine mute timer to exact TTS audio duration - this.extendMuteForTts(audioId); - } - } catch (err) { - console.warn('[FriendService] TTS generation failed:', err); - } - } - - // Signal reply done - broadcastToVrm({ replyDone: true }); - } - - /** - * Broadcast VRM emotion/action command. - */ - broadcastVrm(payload: VrmBroadcastPayload): void { - broadcastToVrm(payload); - } - - // ── Private: STT connection factory ────────────────────────────────── - - private async startSttConnection( - provider: string, - language: string, - ): Promise<{ send: (chunk: Buffer) => void; finalize: () => Promise; close: () => void }> { - const callbacks = { - onTranscript: (text: string, isFinal: boolean) => { - if (isFinal) { - this.captureTranscripts.push(text); - this.captureInterimText = ''; - } else { - this.captureInterimText = text; - } - // Update state for status polling - this.setState({ - captureStatus: { capturing: true, interimText: this.captureInterimText }, - }); - }, - onError: (_error: string) => {}, - onClose: () => {}, - onReady: (_conn: any) => {}, - }; - - switch (provider) { - case 'anthropic': { - const { connectVoiceStream, isVoiceStreamAvailable } = await import( - '../services/voiceStreamSTT.js' - ); - if (!isVoiceStreamAvailable()) { - throw new Error('Anthropic Voice Stream not available'); - } - return await connectVoiceStream(callbacks, { language, keyterms: ['code', 'codev'] }); - } - - case 'local': { - const { connectLocalWhisperStream, preloadWhisperModel } = await import( - '../services/voice/whisperSTT.js' - ); - await preloadWhisperModel({ language }); - return await connectLocalWhisperStream(callbacks, { language }); - } - - case 'doubao': { - const { connectDoubaoStream } = await import('../services/doubaoSTT.js'); - return await connectDoubaoStream(callbacks, { language: language || 'zh' }); - } - - case 'groq': { - const { connectGroqStream } = await import( - '../services/voice/groqSTT.js' - ); - return await connectGroqStream(callbacks, { language }); - } - - default: - throw new Error(`Unknown STT provider: ${provider}`); - } - } - - // ── Private: Audio capture (arecord/parecord subprocess) ────────────── - - private async loadAudioCapture(): Promise { - if (this.audioCapture) return this.audioCapture; - - // Use subprocess-based capture (arecord/parecord) on all platforms. - // Skipping the native cpal module because its synchronous NAPI call - // can block the event loop if ALSA initialization hangs, and there - // is no way to timeout a native binding call from JS. - const { spawn } = await import('node:child_process'); - let captureProc: import('node:child_process').ChildProcess | null = null; - - this.audioCapture = { - startRecording: async (onData, _onEnd) => { - for (const tool of ['arecord', 'parecord']) { - try { - const args = tool === 'arecord' - ? ['-D', 'default', '-r', '16000', '-f', 'S16_LE', '-c', '1', '-t', 'raw', '-q'] - : ['--raw', '--rate=16000', '--format=s16le', '--channels=1', '--latency-msec=20']; - const proc = spawn(tool, args, { stdio: ['pipe', 'pipe', 'pipe'] }); - if (proc.pid === undefined) continue; - - // Verify the tool actually produces audio data within 500ms. - // Some tools (e.g. parecord without PulseAudio) spawn successfully - // but exit immediately without any output — catch that here. - let dataArrived = false; - let verifyTimer: ReturnType | null = null; - - const verified = await new Promise((resolve) => { - const feedAudio = (c: Buffer) => { - // Skip when muted — prevents AI TTS echo from re-entering STT/VAD - if (this.muted) return; - - // Forward to STT connection - onData(c); - - // Forward to VAD for speech activity detection - if (this.vadInstance) { - const float32 = new Float32Array(c.length / 2); - for (let i = 0; i < float32.length; i++) { - float32[i] = c.readInt16LE(i * 2) / 32768; - } - // High-pass filter: remove sub-80Hz noise (fan/AC hum) before VAD - this.highpassFilter.process(float32); - this.vadInstance.processAudio(float32).catch(() => {}); - } - }; - - const dataHandler = (chunk: Buffer) => { - dataArrived = true; - if (verifyTimer) { clearTimeout(verifyTimer); } - feedAudio(chunk); - // Swap to the permanent handler for subsequent chunks - proc.stdout?.removeListener('data', dataHandler); - proc.stdout?.on('data', feedAudio); - resolve(true); - }; - proc.stdout?.on('data', dataHandler); - - // Process exited before producing data — mark as failed - proc.on('exit', () => { - if (!dataArrived) { - if (verifyTimer) { clearTimeout(verifyTimer); } - resolve(false); - } - }); - - // No data within 500ms — assume failure - verifyTimer = setTimeout(() => { - if (!dataArrived) resolve(false); - }, 500); - }); - - if (verified) { - captureProc = proc; - proc.on('exit', () => { captureProc = null; }); - return true; - } - - // Verification failed — kill and try next tool - proc.kill('SIGTERM'); - } catch { - continue; - } - } - return false; - }, - stopRecording: async () => { - if (captureProc) { - captureProc.kill('SIGTERM'); - setTimeout(() => { - try { captureProc?.kill('SIGKILL'); } catch {} - }, 2000); - captureProc = null; - } - }, - isRecording: () => captureProc !== null, - }; - - return this.audioCapture; - } - - // ── Private: TTS generation ────────────────────────────────────────── - - private async generateTts(text: string): Promise { - const prefs = getPrefs(); - if (!prefs.ttsEnabled) return undefined; - - const cleanText = stripForTts(text); - if (!cleanText) return undefined; - - let result: { success: boolean; audioPath?: string; error?: string }; - if (prefs.provider === 'qwen' && prefs.qwenKey) { - result = await qwenTts({ - text: cleanText, - apiKey: prefs.qwenKey, - voice: prefs.voice, - model: prefs.qwenModel, - language: prefs.language, - }); - } else { - result = await edgeTts({ text: cleanText, voice: prefs.voice }); - } - - if (result.success && result.audioPath) { - return registerAudioFile(result.audioPath); - } - - return undefined; - } - - // ── Private: state management ──────────────────────────────────────── - - private setState(next: Partial): void { - this.state = { ...this.state, ...next }; - for (const listener of this.listeners) listener(); - } -} - -// Singleton -export const friendService = new FriendService(); diff --git a/src/friend/__tests__/e2e-http-flow.test.ts b/src/friend/__tests__/e2e-http-flow.test.ts deleted file mode 100644 index d54ebfdf3fb71b5979f445a1f919b976e9671e24..0000000000000000000000000000000000000000 --- a/src/friend/__tests__/e2e-http-flow.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * End-to-end HTTP flow test — simulates what the Tauri frontend does - * with the browser VAD + server STT architecture: - * 1. POST /voice/stt-segment → send audio buffer for transcription - * 2. Server transcribes and enqueues text to CLI - * 3. POST /voice/start + /voice/stop → push-to-talk mode - * - * Uses mocked STT so it runs in any environment. - */ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { friendService } from '../FriendService.js'; -import { setPrefs } from '../prefs.js'; -import { handleFriendApi, setFriendServerInfo } from '../../server/api/friend.js'; -import { getCommandQueue, resetCommandQueue } from '../../utils/messageQueueManager.js'; - -setFriendServerInfo('127.0.0.1', 3456); - -// ════════════════════════════════════════════════════════════════ -// Helpers -// ════════════════════════════════════════════════════════════════ - -function makeRequest(method: string, path: string, body?: unknown): Request { - const url = new URL(path, 'http://127.0.0.1:3456'); - const init: RequestInit = { method }; - if (body) { - init.headers = { 'Content-Type': 'application/json' }; - init.body = JSON.stringify(body); - } - return new Request(url.toString(), init); -} - -function apiUrl(path: string): URL { - return new URL(path, 'http://127.0.0.1:3456'); -} - -async function jsonResponse(res: Response): Promise> { - return res.json() as Promise>; -} - -// ════════════════════════════════════════════════════════════════ -// Mocks -// ════════════════════════════════════════════════════════════════ - -function createMockSttFactory() { - let counter = 0; - return async () => { - const myId = ++counter; - return { - send: () => {}, - finalize: async () => { - await new Promise(r => setTimeout(r, 50)); - (friendService as any).captureTranscripts.push(`[mock stt result #${myId}]`); - }, - close: () => {}, - }; - }; -} - -// ════════════════════════════════════════════════════════════════ -// Tests -// ════════════════════════════════════════════════════════════════ - -describe('E2E HTTP flow (browser VAD + server STT)', () => { - let origStartStt: any; - - beforeEach(() => { - resetCommandQueue(); - setPrefs({ sttProvider: 'groq' }); - origStartStt = (friendService as any).startSttConnection; - (friendService as any).startSttConnection = createMockSttFactory(); - }); - - afterEach(async () => { - try { await friendService.stopVoiceCapture(); } catch {} - (friendService as any).startSttConnection = origStartStt; - resetCommandQueue(); - }); - - test( - 'POST /voice/stt-segment → transcribes and enqueues via sendText()', - async () => { - // Create a WAV audio buffer (44-byte header + 16000 samples of silence) - const wavHeader = Buffer.alloc(44); - wavHeader.write('RIFF', 0); - wavHeader.writeUInt32LE(36 + 16000 * 2, 4); - wavHeader.write('WAVE', 8); - wavHeader.write('fmt ', 12); - wavHeader.writeUInt32LE(16, 16); - wavHeader.writeUInt16LE(1, 20); // PCM - wavHeader.writeUInt16LE(1, 22); // mono - wavHeader.writeUInt32LE(16000, 24); // sample rate - wavHeader.writeUInt32LE(32000, 28); // byte rate - wavHeader.writeUInt16LE(2, 32); // block align - wavHeader.writeUInt16LE(16, 34); // bits per sample - wavHeader.write('data', 36); - wavHeader.writeUInt32LE(16000 * 2, 40); - - const audioData = Buffer.alloc(16000 * 2, 128); // silence - const fullAudio = Buffer.concat([wavHeader, audioData]); - - // Send as application/octet-stream - const url = new URL('http://127.0.0.1:3456/plugins/friend/voice/stt-segment'); - const req = new Request(url.toString(), { - method: 'POST', - headers: { 'Content-Type': 'application/octet-stream' }, - body: fullAudio, - }); - - const res = await handleFriendApi(req, url); - const data = await jsonResponse(res); - - expect(res.status).toBe(200); - expect(data.ok).toBe(true); - expect(typeof data.text).toBe('string'); - expect((data.text as string).length).toBeGreaterThan(0); - - // Verify text was enqueued to CLI via sendText() - await new Promise(r => setTimeout(r, 10)); // let async sendText() enqueue - const queue = getCommandQueue(); - expect(queue.length).toBeGreaterThan(0); - expect(queue[0].value).toContain('[mock stt result'); - expect(queue[0].origin?.server).toBe('friend'); - }, - 10000, - ); - - test( - 'POST /voice/start → /voice/stop → push-to-talk cycle', - async () => { - const startRes = await handleFriendApi( - makeRequest('POST', '/plugins/friend/voice/start'), - apiUrl('/plugins/friend/voice/start'), - ); - expect(startRes.status).toBe(200); - - // Let it "capture" briefly - await new Promise(r => setTimeout(r, 200)); - - const stopRes = await handleFriendApi( - makeRequest('POST', '/plugins/friend/voice/stop'), - apiUrl('/plugins/friend/voice/stop'), - ); - const stopData = await jsonResponse(stopRes); - expect(stopRes.status).toBe(200); - expect(typeof stopData.text).toBe('string'); - }, - 15000, - ); -}); diff --git a/src/friend/__tests__/voice-cycle.test.ts b/src/friend/__tests__/voice-cycle.test.ts deleted file mode 100644 index f00db8efc02701b5e94cb7fa1ec6406bd610dfca..0000000000000000000000000000000000000000 --- a/src/friend/__tests__/voice-cycle.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Voice capture tests for FriendService (push-to-talk mode). - * - * Tests: - * 1. startVoiceCapture → stopVoiceCapture returns transcript - * 2. TranscribeAudioSegment sends text to CLI queue - */ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { friendService } from '../FriendService.js'; -import { setPrefs } from '../prefs.js'; -import { getCommandQueue, resetCommandQueue } from '../../utils/messageQueueManager.js'; - -function createMockSttConnectionFactory() { - let counter = 0; - return async () => { - const myId = ++counter; - return { - send: () => {}, - finalize: async () => { - await new Promise(r => setTimeout(r, 50)); - const fs = friendService as any; - fs.captureTranscripts.push(`[mock transcript #${myId}]`); - }, - close: () => {}, - }; - }; -} - -describe('Voice capture (push-to-talk)', () => { - let origStartStt: any; - let origAudioCapture: any; - - beforeEach(() => { - resetCommandQueue(); - setPrefs({ sttProvider: 'groq' }); - origStartStt = (friendService as any).startSttConnection; - origAudioCapture = (friendService as any).audioCapture; - (friendService as any).startSttConnection = createMockSttConnectionFactory(); - // Mock audio capture to avoid spawning arecord/parecord - (friendService as any).audioCapture = { - startRecording: async () => true, - stopRecording: async () => {}, - isRecording: () => false, - }; - }); - - afterEach(async () => { - try { await friendService.stopVoiceCapture(); } catch {} - (friendService as any).startSttConnection = origStartStt; - (friendService as any).audioCapture = origAudioCapture; - resetCommandQueue(); - }); - - test( - 'transcribeAudioSegment enqueues text via sendText()', - async () => { - const audioBuf = Buffer.alloc(16000); // 1s of silence @ 16kHz 16-bit - const transcript = await friendService.transcribeAudioSegment(audioBuf); - - expect(transcript).toContain('[mock transcript'); - await new Promise(r => setTimeout(r, 10)); // let async sendText() enqueue - const queue = getCommandQueue(); - expect(queue.length).toBeGreaterThan(0); - expect(queue[0].value).toContain('[mock transcript'); - expect(queue[0].origin?.server).toBe('friend'); - }, - 10000, - ); - - test( - 'startVoiceCapture + stopVoiceCapture returns transcript', - async () => { - await friendService.startVoiceCapture(); - - await new Promise(r => setTimeout(r, 500)); - - const transcript = await friendService.stopVoiceCapture(); - expect(typeof transcript).toBe('string'); - }, - 15000, - ); -}); diff --git a/src/friend/constants.ts b/src/friend/constants.ts deleted file mode 100644 index e072c536245bfcbc7c66d3f954357703dd344dce..0000000000000000000000000000000000000000 --- a/src/friend/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Shared constants for Friend — Codev native plugin. - */ - -export const GATEWAY_URL = 'http://127.0.0.1:3456'; - -export const FRIEND_SESSION_KEY = 'agent:main:main'; - -export const CHANNEL_ID = 'friend'; - -export const VALID_EMOTIONS = [ - 'happy', 'sad', 'angry', 'surprised', 'think', 'awkward', 'question', 'curious', 'neutral', - 'love', 'flirty', 'greeting', 'relaxed', -] as const; diff --git a/src/friend/prefs.ts b/src/friend/prefs.ts deleted file mode 100644 index 79477a53498d0b72f736aa52de740a87c6d21b00..0000000000000000000000000000000000000000 --- a/src/friend/prefs.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Persistent preferences for Friend desktop pet (Codev native). - */ -import path from 'node:path'; -import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; - -export interface FriendPrefs { - enabled?: boolean; - voice?: string; - provider?: string; - qwenKey?: string; - qwenModel?: string; - modelPath?: string; - ttsEnabled?: boolean; - showText?: boolean; - hideUI?: boolean; - tracking?: 'mouse' | 'camera'; - volume?: number; - uiAlign?: 'left' | 'right'; - language?: 'zh' | 'en'; - /** STT provider: browser | anthropic | local | doubao | groq */ - sttProvider?: 'browser' | 'anthropic' | 'local' | 'doubao' | 'groq'; - /** STT language override (e.g. 'en', 'zh', 'ja') */ - sttLanguage?: string; - /** GROQ_API_KEY for Groq Whisper STT (cloud, fast, no Python needed) */ - groqApiKey?: string; -} - -const homeDir = process.env.HOME || process.env.USERPROFILE || ''; -const VSCODE_CONFIG_DIR = path.join(homeDir, '.config', 'Codev'); -const PREFS_PATH = path.join(VSCODE_CONFIG_DIR, 'friend.json'); - -const DEFAULT_PREFS: FriendPrefs = { - enabled: false, - provider: 'edge', - voice: 'zh-CN-XiaoyiNeural', -}; - -export function loadPrefs(): FriendPrefs { - try { - if (existsSync(PREFS_PATH)) { - return { ...DEFAULT_PREFS, ...JSON.parse(readFileSync(PREFS_PATH, 'utf8')) as FriendPrefs }; - } - } catch { /* ignore */ } - return { ...DEFAULT_PREFS }; -} - -export function savePrefs(p: FriendPrefs): void { - try { - mkdirSync(path.dirname(PREFS_PATH), { recursive: true }); - writeFileSync(PREFS_PATH, JSON.stringify(p, null, 2)); - } catch { /* ignore */ } -} - -export function updatePrefs(patch: Partial): FriendPrefs { - const prefs = loadPrefs(); - Object.assign(prefs, patch); - savePrefs(prefs); - return prefs; -} - -// Runtime cache -let _prefs = loadPrefs(); - -export function getPrefs(): FriendPrefs { - return _prefs; -} - -export function setPrefs(p: FriendPrefs) { - _prefs = p; -} diff --git a/src/friend/server.ts b/src/friend/server.ts deleted file mode 100644 index d496a5e2013323f347c15daea2f33d04b444f7f3..0000000000000000000000000000000000000000 --- a/src/friend/server.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Friend HTTP server — lightweight Bun.serve for VRM frontend. - * - * Runs in the main CLI process alongside the Ink TUI. - * Serves the friend frontend static files, API routes, and SSE. - * - * Architecture: - * - No WebSocket (frontend uses SSE for server→client, HTTP for client→server) - * - No separate CLI SDK session (uses FriendService → enqueue() → main CLI) - * - No arecord/parecord subprocess (uses cpal in-process) - */ - -import { createSseResponse } from './sse.js'; -import { handleFriendStaticRequest } from '../server/staticFriend.js'; -import { handleFriendApi } from '../server/api/friend.js'; - -let server: ReturnType | null = null; -let serverPort = 3456; - -export function getServerPort(): number { - return serverPort; -} - -/** - * Try to kill any existing process listening on the given port. - * Returns true if the port became free. - */ -function freePort(port: number): boolean { - try { - // Find PID on the port - const ss = Bun.spawnSync(['ss', '-tlnp', 'sport', `= :${port}`]); - const out = ss.stdout.toString(); - const pidMatch = out.match(/pid=(\d+)/); - if (!pidMatch) return true; // port already free - - const pid = parseInt(pidMatch[1]!, 10); - if (pid === process.pid) return true; // we own it - - // Only kill bun/Codev processes — don't touch unknown services - const proc = Bun.spawnSync(['ps', '-p', String(pid), '-o', 'comm=']); - const comm = proc.stdout.toString().trim(); - if (!comm) return false; // process doesn't exist - const baseName = comm.split('/').pop() || comm; - if (baseName !== 'bun' && baseName !== 'Codev' && !baseName.startsWith('claude-') && !baseName.includes('node')) { - console.warn(`[FriendServer] Port ${port} is occupied by non-Codev process: ${comm}`); - return false; - } - - // Send SIGTERM politely - process.kill(pid, 'SIGTERM'); - // Wait up to 3s for it to die - for (let i = 0; i < 30; i++) { - Bun.sleepSync(100); - try { process.kill(pid, 0); } catch { return true; } // dead - } - // Force kill - try { process.kill(pid, 'SIGKILL'); } catch { /* */ } - Bun.sleepSync(200); - return true; - } catch { - return false; - } -} - -/** - * Start the friend HTTP server in-process. - * Safe to call multiple times — no-op if already running. - * If the port is already in use, attempts to free it first. - */ -export function startFriendServer(port = 3456, host = '127.0.0.1'): ReturnType { - if (server) return server; - - serverPort = port; - - // If port is in use, try to free it - const check = Bun.spawnSync(['ss', '-tlnp', 'sport', `= :${port}`]); - if (check.stdout.toString().includes('LISTEN')) { - console.log(`[FriendServer] Port ${port} is in use, attempting to free it...`); - if (!freePort(port)) { - console.warn(`[FriendServer] Could not free port ${port}. Please stop the existing server manually.`); - throw new Error(`Port ${port} is already in use`); - } - console.log(`[FriendServer] Port ${port} freed successfully.`); - } - - server = Bun.serve({ - port, - hostname: host, - idleTimeout: 60, // seconds — allow slow STT provider init (Python/PyTorch imports) - async fetch(req) { - const url = new URL(req.url); - - // CORS preflight - if (req.method === 'OPTIONS') { - return new Response(null, { - status: 204, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }, - }); - } - - // SSE events (GET /plugins/friend/events) - if (url.pathname === '/plugins/friend/events' && req.method === 'GET') { - return createSseResponse(); - } - - // Friend API routes (/plugins/friend/*) - if (url.pathname.startsWith('/plugins/friend/')) { - return handleFriendApi(req, url); - } - - // WebSocket upgrade — not needed for friend (uses SSE + HTTP) - // but handle gracefully - if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') { - return new Response('WebSocket not supported via friend server', { status: 426 }); - } - - // Static files (/friend/*) - if (url.pathname.startsWith('/friend/') || url.pathname === '/friend') { - const staticResponse = await handleFriendStaticRequest(req, url); - if (staticResponse) return staticResponse; - } - - return new Response('Not Found', { status: 404 }); - }, - error(err) { - console.error('[FriendServer] Error:', err); - return new Response('Internal Server Error', { status: 500 }); - }, - }); - - console.log(`[FriendServer] Listening on http://${host}:${port}`); - return server; -} - -/** - * Stop the friend HTTP server. - */ -export function stopFriendServer(): void { - if (server) { - try { - server.stop(); - server = null; - console.log('[FriendServer] Stopped'); - } catch (err) { - console.error('[FriendServer] Error stopping:', err); - } - } -} diff --git a/src/friend/sse.ts b/src/friend/sse.ts deleted file mode 100644 index 91516146a35b383b5bd34b2f7ebde46876d83b64..0000000000000000000000000000000000000000 --- a/src/friend/sse.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * SSE client registry and typed broadcast for Friend VRM avatar. - * - * Uses Bun ReadableStream-based SSE (not Node http.ServerResponse), - * compatible with the Codev Bun.serve() infrastructure. - */ - -type SseClient = { - id: string; - write: (data: string) => void; - close: () => void; -}; - -const sseClients = new Set(); - -let clientIdCounter = 0; - -/** - * Register a new SSE client. - */ -export function addSseClient(client: SseClient): void { - sseClients.add(client); -} - -export function removeSseClient(client: SseClient): void { - sseClients.delete(client); -} - -export function getSseClientCount(): number { - return sseClients.size; -} - -export function createSseClientId(): string { - return `sse-${++clientIdCounter}-${Date.now()}`; -} - -export type VrmBroadcastPayload = { - text?: string; - emotion?: string; - emotionIntensity?: number; - action?: string; - audioUrl?: string; - audioIndex?: number; - clearText?: boolean; - imageUrl?: string; - moodDelta?: number; - moodIndex?: number; - sendFirstTts?: boolean; - appendText?: boolean; - replyDone?: boolean; -}; - -export function broadcastToVrm(payload: VrmBroadcastPayload) { - if (sseClients.size === 0) return; - const data = `data: ${JSON.stringify(payload)}\n\n`; - const dead: SseClient[] = []; - for (const client of sseClients) { - try { - client.write(data); - } catch { - dead.push(client); - } - } - for (const client of dead) { - sseClients.delete(client); - } -} - -/** - * Create a Bun-compatible SSE response (ReadableStream). - * Registers a client that the tools and API handlers can broadcast to. - */ -export function createSseResponse(): Response { - const clientId = createSseClientId(); - let cleanupCalled = false; - let sseClient: SseClient | undefined; - - const stream = new ReadableStream({ - start(controller) { - sseClient = { - id: clientId, - write: (data: string) => { - controller.enqueue(new TextEncoder().encode(data)); - }, - close: () => { - try { controller.close(); } catch { /* already closed */ } - }, - }; - - addSseClient(sseClient); - - // Send initial newline to establish connection - sseClient.write('\n'); - }, - cancel() { - if (!cleanupCalled) { - cleanupCalled = true; - if (sseClient) { - removeSseClient(sseClient); - } - } - }, - }); - - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - 'Access-Control-Allow-Origin': '*', - }, - }); -} diff --git a/src/friend/stt-service.ts b/src/friend/stt-service.ts deleted file mode 100644 index 4006f940d80e4d78d8a505adb27f510f1d3e49dc..0000000000000000000000000000000000000000 --- a/src/friend/stt-service.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * STT file transcription for Friend. - * - * Provides file-based transcription using /voice STT providers - * (Anthropic Voice Stream, Local Whisper, Doubao). - * - * Streaming/in-process capture is handled by FriendService.ts - * (which uses cpal for native in-process audio capture). - */ - -import type { FriendPrefs } from './prefs.js'; -import { - connectVoiceStream, - isVoiceStreamAvailable, -} from '../services/voiceStreamSTT.js'; -import { - connectLocalWhisperStream, - preloadWhisperModel, -} from '../services/voice/whisperSTT.js'; - -// ── Types ────────────────────────────────────────────────────────────── - -type SttProvider = 'browser' | 'anthropic' | 'local' | 'doubao'; - -// ── STT connection factory ───────────────────────────────────────────── - -async function startSttConnection( - provider: SttProvider, - language: string | undefined, - callbacks: { - onTranscript(text: string, isFinal: boolean): void; - onError(error: string, opts?: { fatal?: boolean }): void; - onClose(): void; - onReady(conn: any): void; - }, -) { - switch (provider) { - case 'anthropic': { - if (!isVoiceStreamAvailable()) { - callbacks.onError('Anthropic Voice Stream not available (not logged in?)'); - return null; - } - const conn = await connectVoiceStream(callbacks, { - language: language || 'en', - keyterms: ['code', 'codev'], - }); - return conn; - } - - case 'local': { - await preloadWhisperModel({ language: language || 'en' }); - const conn = await connectLocalWhisperStream(callbacks, { - language: language || 'en', - }); - return conn; - } - - case 'doubao': { - try { - const { connectDoubaoStream } = await import( - '../services/doubaoSTT.js' - ); - const conn = await connectDoubaoStream(callbacks, { - language: language || 'zh', - }); - return conn; - } catch (err: any) { - callbacks.onError(`Doubao STT import failed: ${err?.message}`); - return null; - } - } - - default: - callbacks.onError(`Unknown STT provider: ${provider}`); - return null; - } -} - -// ── File-based transcription (REST) ──────────────────────────────────── - -export async function transcribeAudioFile( - wavBuffer: Buffer, - prefs: FriendPrefs, -): Promise<{ text: string }> { - const provider = (prefs.sttProvider || 'browser') as SttProvider; - const language = prefs.sttLanguage; - - switch (provider) { - case 'local': { - const { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } = await import( - 'node:fs' - ); - const { tmpdir } = await import('node:os'); - const { join } = await import('node:path'); - const tmpDir = mkdtempSync(join(tmpdir(), 'friend-stt-')); - const wavPath = join(tmpDir, 'input.wav'); - writeFileSync(wavPath, wavBuffer); - - try { - await preloadWhisperModel({ language: language || 'en' }); - const { connectLocalWhisperStream } = await import( - '../services/voice/whisperSTT.js' - ); - const result = await new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - connectLocalWhisperStream( - { - onTranscript(text, _isFinal) { - chunks.push(Buffer.from(text, 'utf8')); - }, - onError(error) { - reject(new Error(error)); - }, - onClose() { - resolve(Buffer.concat(chunks).toString('utf8')); - }, - onReady(conn) { - conn.send(wavBuffer); - conn.finalize(); - }, - }, - { language: language || 'en' }, - ); - }); - return { text: result }; - } finally { - try { unlinkSync(wavPath) } catch {} - try { rmdirSync(tmpDir) } catch {} - } - } - - case 'doubao': { - const { connectDoubaoStream } = await import( - '../services/doubaoSTT.js' - ); - const chunks: string[] = []; - const conn = await connectDoubaoStream( - { - onTranscript(text: string, _isFinal: boolean) { - chunks.push(text); - }, - onError(_error: string) {}, - onClose() {}, - onReady(c: any) { - c.send(wavBuffer); - c.finalize(); - }, - }, - { language: language || 'zh' }, - ); - if (!conn) throw new Error('Doubao STT unavailable'); - await new Promise((r) => setTimeout(r, 1000)); - return { text: chunks.join('') }; - } - - case 'anthropic': { - // Fall back to local Whisper for file transcription - return transcribeAudioFile(wavBuffer, { ...prefs, sttProvider: 'local' }); - } - - default: - throw new Error(`Unsupported STT provider for file transcription: ${provider}`); - } -} diff --git a/src/friend/tauri-launcher.ts b/src/friend/tauri-launcher.ts deleted file mode 100644 index f747d01c5910437e4635195a8ba97016bd46c05f..0000000000000000000000000000000000000000 --- a/src/friend/tauri-launcher.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Tauri desktop app process management for Friend (Codev native). - * - * Launches the VRM desktop pet as a native Tauri window. - * The HTTP server runs in-process via friend/server.ts — - * no separate server subprocess needed. - */ -import { spawn } from 'node:child_process'; -import path from 'node:path'; -import { existsSync } from 'node:fs'; - -let tauriProcess: ReturnType | null = null; - -export async function launchTauri(log: { info: (msg: string) => void; warn: (msg: string) => void }) { - // Find the Tauri binary - const cwd = process.cwd() - const releaseBinary = path.join(cwd, 'src', 'components', 'friend', 'frontend', 'src-tauri', 'target', 'release', 'codev-friend') - const debugBinary = path.join(cwd, 'src', 'components', 'friend', 'frontend', 'src-tauri', 'target', 'debug', 'codev-friend') - - const binary = existsSync(releaseBinary) ? releaseBinary - : existsSync(debugBinary) ? debugBinary - : null - - if (!binary) { - log.warn(`[Friend] Tauri binary not found. Run \`cd src/components/friend/frontend && npx tauri build\` first.`) - log.warn(`[Friend] Looked for: ${releaseBinary}`) - return - } - - log.info(`[Friend] Starting desktop window from ${binary}`) - - tauriProcess = spawn(binary, [], { - cwd: path.dirname(binary), - stdio: 'pipe', - detached: true, - }) - - tauriProcess.stdout?.on('data', (data: Buffer) => { - for (const line of data.toString().split('\n').filter(Boolean)) { - log.info(`[Friend:tauri] ${line}`) - } - }) - - tauriProcess.stderr?.on('data', (data: Buffer) => { - for (const line of data.toString().split('\n').filter(Boolean)) { - log.info(`[Friend:tauri] ${line}`) - } - }) - - tauriProcess.on('error', (err: Error) => { - log.warn(`[Friend] Tauri error: ${err.message}`) - tauriProcess = null - }) - - tauriProcess.on('exit', (code: number | null) => { - log.info(`[Friend] Tauri exited (code: ${code})`) - tauriProcess = null - }) -} - -export function stopTauri(log: { info: (msg: string) => void }) { - if (tauriProcess) { - log.info('[Friend] Stopping Tauri window...') - const proc = tauriProcess - tauriProcess = null - proc.kill('SIGTERM') - setTimeout(() => { - try { if (!proc.killed) proc.kill('SIGKILL') } catch { /* ignore */ } - }, 3000) - } -} - -export function getTauriProcess(): ReturnType | null { - return tauriProcess -} diff --git a/src/friend/text-utils.ts b/src/friend/text-utils.ts deleted file mode 100644 index e6972f2a3c5b28e69de8ddbf00b408dd2ff29635..0000000000000000000000000000000000000000 --- a/src/friend/text-utils.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Text cleaning utilities for Friend VRM avatar. - */ - -/** - * Strip agent's inline thinking/reasoning from text output. - */ -export function stripThinking(text: string): string { - const lines = text.split('\n'); - const TS_RE = /^\d{2}:\d{2}:\d{2}\s/; - - let lastTsIdx = -1; - for (let i = lines.length - 1; i >= 0; i--) { - if (TS_RE.test(lines[i])) { - lastTsIdx = i; - break; - } - } - if (lastTsIdx >= 0) { - lines[lastTsIdx] = lines[lastTsIdx].replace(TS_RE, ''); - const result = lines.slice(lastTsIdx).join('\n').trim(); - if (result) return result; - } - - const EMOTION_RE = /^(think|happy|sad|angry|surprised|awkward|question|curious|neutral)\s*$/i; - const REASONING_RE = /^(I'll |I need to |I should |I want to |The user |Time is |Let me |My response|Responding )/i; - const THINKING_HEADER_RE = /^(\*{0,2}Thinking( Process)?[:\*]|\*{0,2}思考)/i; - - let startIdx = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) { startIdx = i + 1; continue; } - if (EMOTION_RE.test(line)) { startIdx = i + 1; continue; } - if (THINKING_HEADER_RE.test(line)) { startIdx = i + 1; continue; } - if (REASONING_RE.test(line)) { startIdx = i + 1; continue; } - if (/^\d+[\.\)]\s/.test(line)) { startIdx = i + 1; continue; } - break; - } - - let result = lines.slice(startIdx).join('\n').trim(); - if (!result) result = text.trim(); - return result.replace(/\[\[\w+\]\]/g, '').trim(); -} - -/** Strip action/narration text wrapped in *..* or **..***/ -export function stripActions(text: string): string { - return text.replace(/\*{1,2}[^*]+\*{1,2}/g, '').replace(/\n{2,}/g, '\n').trim(); -} - -/** Strip markdown symbols */ -export function stripMarkdown(text: string): string { - return text.replace(/[*_~`#>]/g, '').trim(); -} - -/** Strip emoji characters */ -export function stripEmoji(text: string): string { - return text.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, '').trim(); -} - -/** - * Strip text for TTS playback. - */ -export function stripForTts(text: string): string { - return text - .replace(/([^)]*)/g, '') - .replace(/\([^)]*\)/g, '') - .replace(/[_~`#>]/g, '') - .replace(/\*([^*]*)\*/g, '$1') - .replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, '') - .trim(); -} - -/** - * Split text into sentences for incremental TTS. - */ -export function splitSentences(text: string): string[] { - const ellipsisMap: string[] = []; - const safeText = text.replace(/\.{2,}|…+/g, (match) => { - const idx = ellipsisMap.length; - ellipsisMap.push(match); - return `\x00E${idx}\x00`; - }); - - const parts = safeText.split(/(?<=[。!?;\n.!?;~])\s*/); - return parts - .map((s) => { - let restored = s; - for (let i = 0; i < ellipsisMap.length; i++) { - restored = restored.replace(`\x00E${i}\x00`, ellipsisMap[i]); - } - return restored.trim(); - }) - .filter((s) => s && !/^[。!?;.!?;~、,,\s]+$/.test(s)); -} diff --git a/src/friend/tts.ts b/src/friend/tts.ts deleted file mode 100644 index b62bc7fce8e61e770dcb661b27ec5acf57b8e1d7..0000000000000000000000000000000000000000 --- a/src/friend/tts.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * TTS providers for Friend (Edge TTS + Qwen DashScope). - */ -import path from 'node:path'; -import { mkdtempSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; - -export async function edgeTts(opts: { text: string; voice?: string }): Promise<{ success: boolean; audioPath?: string; error?: string }> { - try { - const { EdgeTTS } = await import('node-edge-tts'); - const tempDir = mkdtempSync(path.join(tmpdir(), 'friend-tts-')); - const audioPath = path.join(tempDir, `voice-${Date.now()}.mp3`); - const tts = new EdgeTTS({ voice: opts.voice || 'zh-CN-XiaoxiaoNeural' }); - await tts.ttsPromise(opts.text, audioPath); - return { success: true, audioPath }; - } catch (err) { - return { success: false, error: String(err) }; - } -} - -const QWEN_TTS_URL_CN = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'; -const QWEN_TTS_URL_INTL = 'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation'; - -export async function qwenTts(params: { - text: string; - apiKey: string; - voice?: string; - model?: string; - language?: 'zh' | 'en'; -}): Promise<{ success: boolean; audioPath?: string; error?: string }> { - const voice = params.voice || 'Cherry'; - const model = params.model || 'qwen3-tts-flash'; - const lang = params.language || 'zh'; - - const endpoint = lang === 'zh' ? QWEN_TTS_URL_CN : QWEN_TTS_URL_INTL; - const languageType = lang === 'zh' ? 'Chinese' : 'English'; - - try { - const abortCtrl = new AbortController(); - const timeout = setTimeout(() => abortCtrl.abort(), 30_000); - - const resp = await fetch(endpoint, { - method: 'POST', - headers: { - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model, - input: { - text: params.text, - voice, - language_type: languageType, - }, - }), - signal: abortCtrl.signal, - }); - - clearTimeout(timeout); - - if (!resp.ok) { - const errText = await resp.text().catch(() => ''); - return { success: false, error: `[tts:${model}/${voice}] http ${resp.status}: ${errText}` }; - } - - const result = await resp.json() as { output?: { audio?: { url?: string } } }; - const audioUrl: string | undefined = result?.output?.audio?.url; - if (!audioUrl) { - return { success: false, error: `[tts:${model}/${voice}] no audio url in response` }; - } - - const audioResp = await fetch(audioUrl); - if (!audioResp.ok) { - return { success: false, error: `[tts:${model}/${voice}] download failed: ${audioResp.status}` }; - } - const audioData = Buffer.from(await audioResp.arrayBuffer()); - - const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-tts-')); - const destPath = path.join(tmpDir, `qwen-tts-${Date.now()}.wav`); - writeFileSync(destPath, audioData); - return { success: true, audioPath: destPath }; - } catch (err: any) { - if (err?.name === 'AbortError') { - return { success: false, error: `[tts:${model}/${voice}] timeout (30s)` }; - } - return { success: false, error: `[tts:${model}/${voice}] ${err}` }; - } -} - -// In-memory audio file registry -const audioFiles = new Map(); -let audioIdCounter = 0; - -export function registerAudioFile(filePath: string): string { - const id = `${Date.now()}-${++audioIdCounter}`; - audioFiles.set(id, filePath); - setTimeout(() => audioFiles.delete(id), 5 * 60 * 1000); - return id; -} - -export function getAudioFile(id: string): string | undefined { - return audioFiles.get(id); -} diff --git a/src/friend/voice/highpass-filter.ts b/src/friend/voice/highpass-filter.ts deleted file mode 100644 index 694ccc0ae55ee3e261be7eb52425d30569b8490f..0000000000000000000000000000000000000000 --- a/src/friend/voice/highpass-filter.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * First-order IIR high-pass filter for DC offset removal and low-frequency noise rejection. - * - * Removes subsonic rumble, fan hum (50/60Hz), and AC line noise before VAD - * processing — these are not human voice and can confuse the VAD model. - * - * Tuned for 16kHz sample rate with adjustable cutoff (~80Hz default). - * Preserves all human voice frequencies (fundamental 85-255Hz + formants). - * - * Formula: y[n] = x[n] - x[n-1] + α ⋅ y[n-1] - * where α = exp(-2π ⋅ fc / fs) - */ - -export class HighPassFilter { - private prevX = 0 - private prevY = 0 - private readonly alpha: number - - /** - * @param cutoffHz -3dB cutoff frequency in Hz (default: 80 — below male voice fundamental) - * @param sampleRate input sample rate (default: 16000 — Silero VAD sample rate) - */ - constructor(cutoffHz = 80, sampleRate = 16000) { - const omega = 2 * Math.PI * cutoffHz / sampleRate - this.alpha = Math.exp(-omega) - } - - /** - * Process a Float32Array of audio samples in-place. - * Zero-copy — mutates the input array. - */ - process(samples: Float32Array): void { - const a = this.alpha - let x1 = this.prevX - let y1 = this.prevY - - for (let i = 0; i < samples.length; i++) { - const x0 = samples[i] - const y0 = x0 - x1 + a * y1 - samples[i] = y0 - x1 = x0 - y1 = y0 - } - - this.prevX = x1 - this.prevY = y1 - } - - /** Reset filter state. Call when starting a new capture session. */ - reset(): void { - this.prevX = 0 - this.prevY = 0 - } -} diff --git a/src/friend/voice/vad-service.ts b/src/friend/voice/vad-service.ts deleted file mode 100644 index 07d2768bf64edc28ccf8d6d3ce1cf92b00655ff2..0000000000000000000000000000000000000000 --- a/src/friend/voice/vad-service.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Silero VAD service — real-time voice activity detection using onnxruntime-web WASM backend. - * - * Uses the Silero VAD legacy ONNX model (bundled with @ericedouard/vad-node-realtime). - * onnxruntime-web (WASM) is used instead of onnxruntime-node (native addon) because - * Bun does not support the native Node-API addon (crashes with segfault). - * - * Architecture: - * - Audio frames (512 samples @ 16kHz = 32ms) are fed to the Silero model - * - Speech probability is compared against configurable thresholds - * - A state machine tracks speech segments with redemption grace period - * - onSpeechEnd fires when sustained silence is detected - */ - -import * as ort from 'onnxruntime-web'; -import { readFileSync, existsSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; - -export interface VadCallbacks { - onSpeechStart?: () => void; - onSpeechEnd?: (audio: Float32Array) => void; - onVADMisfire?: () => void; - onFrameProcessed?: (prob: number, isSpeech: boolean) => void; -} - -export interface VadOptions { - /** Threshold above which a frame is considered speech (0-1). Default: 0.75 */ - positiveSpeechThreshold?: number; - /** Threshold below which a frame is considered silence (0-1). Default: 0.50 */ - negativeSpeechThreshold?: number; - /** Consecutive silence frames before onSpeechEnd fires. Default: 20 (~640ms) */ - redemptionFrames?: number; - /** Minimum confirmed speech frames to avoid misfire. Default: 6 (~192ms) */ - minSpeechFrames?: number; - /** Frames of pre-speech audio to include in onSpeechEnd segment. Default: 10 */ - preSpeechPadFrames?: number; - /** Sample rate of input audio (must be 16000). Default: 16000 */ - sampleRate?: number; - /** RMS energy threshold (0-1). Frames below this are treated as silence without inference. Default: 0.004 (~-48dBFS) */ - rmsThreshold?: number; - /** Consecutive speech frames required to trigger speech start. Default: 10 (~320ms) — filters short noise bursts */ - preSpeechTriggerFrames?: number; -} - -export class SileroVad { - private session: ort.InferenceSession | null = null; - private stateH: ort.Tensor | null = null; - private stateC: ort.Tensor | null = null; - private sr: ort.Tensor | null = null; - private initialized = false; - private active = false; - - private readonly opts: Required; - private readonly callbacks: Required; - private readonly frameSize = 512; // Legacy Silero model: 512 samples @ 16kHz per frame - - // Audio accumulation buffer - private buffer = new Float32Array(0); - - // Frame processor state machine - private speaking = false; - private redemptionCounter = 0; - private speechFrameCount = 0; - /** Consecutive speech frame count in pre-speech phase (fires speech on threshold) */ - private preSpeechCount = 0; - private frameHistory: Array<{ frame: Float32Array; isSpeech: boolean }> = []; - - constructor(callbacks: VadCallbacks, opts?: VadOptions) { - this.callbacks = { - onSpeechStart: callbacks.onSpeechStart ?? (() => {}), - onSpeechEnd: callbacks.onSpeechEnd ?? (() => {}), - onVADMisfire: callbacks.onVADMisfire ?? (() => {}), - onFrameProcessed: callbacks.onFrameProcessed ?? (() => {}), - }; - this.opts = { - positiveSpeechThreshold: opts?.positiveSpeechThreshold ?? 0.75, - negativeSpeechThreshold: opts?.negativeSpeechThreshold ?? 0.50, - redemptionFrames: opts?.redemptionFrames ?? 20, - minSpeechFrames: opts?.minSpeechFrames ?? 6, - preSpeechPadFrames: opts?.preSpeechPadFrames ?? 10, - sampleRate: opts?.sampleRate ?? 16000, - rmsThreshold: opts?.rmsThreshold ?? 0.004, - preSpeechTriggerFrames: opts?.preSpeechTriggerFrames ?? 10, - }; - } - - /** - * Initialize the VAD: load ONNX model and configure onnxruntime-web WASM backend. - * Must be called once before start(). - */ - async init(): Promise { - if (this.initialized) return; - - // Point onnxruntime-web to the WASM binary files - const wasmDir = resolveWasmDir(); - ort.env.wasm.wasmPaths = wasmDir + '/'; - - // Load Silero VAD legacy ONNX model - const modelPath = resolveModelPath(); - const modelBuffer = readFileSync(modelPath); - this.session = await ort.InferenceSession.create(modelBuffer.buffer, { - executionProviders: ['wasm'], - }); - - // Initialize LSTM state tensors (legacy: h=[2,1,64], c=[2,1,64]) - this.stateH = new ort.Tensor('float32', new Float32Array(2 * 64), [2, 1, 64]); - this.stateC = new ort.Tensor('float32', new Float32Array(2 * 64), [2, 1, 64]); - this.sr = new ort.Tensor('int64', [BigInt(this.opts.sampleRate)], [1]); - - this.initialized = true; - } - - /** Start VAD processing */ - start(): void { - this.active = true; - } - - /** Pause VAD processing, ending any active speech segment */ - pause(): void { - this.active = false; - this.endSegment(); - } - - /** - * Feed raw PCM audio (Float32Array, values -1..1, 16kHz) to the VAD. - * Audio is buffered and processed in 512-sample frames. - */ - async processAudio(audioData: Float32Array): Promise { - if (!this.active || !this.initialized || !this.session) return; - - // Append to internal buffer - const tmp = new Float32Array(this.buffer.length + audioData.length); - tmp.set(this.buffer); - tmp.set(audioData, this.buffer.length); - this.buffer = tmp; - - // Process complete 512-sample frames - while (this.buffer.length >= this.frameSize) { - const frame = this.buffer.subarray(0, this.frameSize); - this.buffer = this.buffer.subarray(this.frameSize); - await this.processFrame(frame); - } - } - - /** Flush any remaining audio and end active speech segment */ - async flush(): Promise { - if (this.buffer.length > 0) { - // Pad last partial frame with zeros - const padded = new Float32Array(this.frameSize); - padded.set(this.buffer); - this.buffer = new Float32Array(0); - await this.processFrame(padded); - } - this.endSegment(); - } - - /** Reset VAD state without destroying the session */ - reset(): void { - this.buffer = new Float32Array(0); - this.frameHistory = []; - this.speaking = false; - this.redemptionCounter = 0; - this.speechFrameCount = 0; - this.preSpeechCount = 0; - this.stateH = new ort.Tensor('float32', new Float32Array(2 * 64), [2, 1, 64]); - this.stateC = new ort.Tensor('float32', new Float32Array(2 * 64), [2, 1, 64]); - } - - /** Clean up resources */ - destroy(): void { - this.active = false; - this.reset(); - if (this.session) { - try { (this.session as any).dispose?.(); } catch { /* ignore */ } - this.session = null; - } - this.initialized = false; - } - - // ── Private: frame processing ────────────────────────────────────────── - - private async processFrame(frame: Float32Array): Promise { - if (!this.session || !this.stateH || !this.stateC || !this.sr) return; - - // ── 1. Energy pre-filter: compute RMS — skip Silero for low-energy noise ── - let sumSq = 0; - for (let i = 0; i < frame.length; i++) { - sumSq += frame[i] * frame[i]; - } - const rms = Math.sqrt(sumSq / frame.length); - - let prob: number; - if (rms < this.opts.rmsThreshold) { - prob = 0; // Below noise floor — mechanical noise, mic bump, room silence - } else { - // ── 2. Silero inference for speech probability ── - try { - const result = await this.session.run({ - input: new ort.Tensor('float32', frame, [1, this.frameSize]), - sr: this.sr, - h: this.stateH, - c: this.stateC, - }); - - // Update LSTM state for next frame - this.stateH = result.hn as ort.Tensor; - this.stateC = result.cn as ort.Tensor; - - prob = (result.output as ort.Tensor).data[0] as number; - } catch (err) { - console.error('[SileroVad] frame inference error:', err); - return; - } - } - - const isSpeech = prob >= this.opts.positiveSpeechThreshold; - const isSilence = prob < this.opts.negativeSpeechThreshold; - - this.callbacks.onFrameProcessed(prob, isSpeech); - - // ── 3. State machine ──────────────────────────────────────────── - if (this.speaking) { - // In a confirmed speech segment - if (isSilence) { - this.redemptionCounter++; - if (this.redemptionCounter >= this.opts.redemptionFrames) { - this.endSpeech(); - } - } else { - this.redemptionCounter = 0; - } - this.speechFrameCount++; - this.frameHistory.push({ frame: frame.slice(), isSpeech }); - } else if (isSpeech) { - // Pre-speech phase: require consecutive speech frames to trigger - this.preSpeechCount++; - - if (this.preSpeechCount >= this.opts.preSpeechTriggerFrames) { - // Transition: silence → confirmed speech (sustained above threshold) - this.speaking = true; - this.redemptionCounter = 0; - this.speechFrameCount = this.preSpeechCount; - this.preSpeechCount = 0; - this.frameHistory.push({ frame: frame.slice(), isSpeech: true }); - this.callbacks.onSpeechStart(); - } - } else { - // Not speech — discard any accumulated pre-speech frames - this.preSpeechCount = 0; - } - } - - private endSpeech(): void { - if (this.speechFrameCount < this.opts.minSpeechFrames) { - this.callbacks.onVADMisfire(); - } else { - // Build audio segment with pre-padding for context - const total = this.frameHistory.length; - const prePad = Math.min(this.opts.preSpeechPadFrames, total); - const segFrames = this.frameHistory.slice(total - prePad - this.speechFrameCount, total); - let totalSamples = 0; - for (const f of segFrames) totalSamples += f.frame.length; - const segment = new Float32Array(totalSamples); - let offset = 0; - for (const f of segFrames) { - segment.set(f.frame, offset); - offset += f.frame.length; - } - this.callbacks.onSpeechEnd(segment); - } - - // Reset speech state - this.speaking = false; - this.redemptionCounter = 0; - this.speechFrameCount = 0; - this.frameHistory = []; - } - - private endSegment(): void { - if (this.speaking) { - this.endSpeech(); - } - } -} - -// ── Module-level helpers ───────────────────────────────────────────────── - -let cachedWasmDir: string | null = null; -let cachedModelPath: string | null = null; - -function resolveWasmDir(): string { - if (cachedWasmDir) return cachedWasmDir; - - // Resolve onnxruntime-web's dist directory where WASM files live - const pkgPath = require.resolve('onnxruntime-web/package.json'); - cachedWasmDir = resolve(dirname(pkgPath), 'dist'); - - if (!existsSync(cachedWasmDir)) { - throw new Error( - `onnxruntime-web WASM directory not found at ${cachedWasmDir}. ` + - 'Ensure onnxruntime-web is installed (it is a peer dependency).', - ); - } - return cachedWasmDir; -} - -function resolveModelPath(): string { - if (cachedModelPath) return cachedModelPath; - - // The Silero ONNX model is bundled with @ericedouard/vad-node-realtime - const vadPkgPath = require.resolve('@ericedouard/vad-node-realtime/package.json'); - cachedModelPath = resolve(dirname(vadPkgPath), 'silero_vad_legacy.onnx'); - - if (!existsSync(cachedModelPath)) { - throw new Error( - `Silero VAD model not found at ${cachedModelPath}. ` + - 'Ensure @ericedouard/vad-node-realtime is installed.', - ); - } - return cachedModelPath; -} diff --git a/src/hooks/useAutoTTS.ts b/src/hooks/useAutoTTS.ts index 8b99da4d3d0219adc5a4ef6684f25e73afb48151..7876b88ef854ed5997289d56a8165876c368f7a4 100644 --- a/src/hooks/useAutoTTS.ts +++ b/src/hooks/useAutoTTS.ts @@ -1,6 +1,5 @@ import { useEffect, useRef } from 'react' -import { playAudioFile } from '../services/voice/edgeTTS.js' -import { edgeTts } from '../friend/tts.js' +import { playAudioFile, edgeTts } from '../services/voice/edgeTTS.js' import { getInitialSettings } from '../utils/settings/settings.js' import type { RenderableMessage } from '../types/message.js' diff --git a/src/hooks/useFriendBridge.ts b/src/hooks/useFriendBridge.ts deleted file mode 100644 index 4b91f3d00114833208fded1c71caef7b9426b5bd..0000000000000000000000000000000000000000 --- a/src/hooks/useFriendBridge.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * useFriendBridge — Friend VRM inbound/outbound bridge hook. - * - * Mirror of useFeishuBridge: - * - Tracks AI response turns for Friend-originated messages - * - Broadcasts generated responses to the VRM display via SSE - * - * The frontend receives responses as SSE events from broadcastToVrm(). - * No chatId needed — SSE is a broadcast channel to all connected - * Tauri display clients. - */ - -import { useEffect, useRef } from 'react' -import { friendService } from '../friend/FriendService.js' -import { getContentText } from '../utils/messages.js' -import type { Message } from '../types/message.js' - -type Props = { - messages: Message[] - isLoading: boolean -} - -const FRIEND_CHANNEL_SERVER = 'friend' - -type ActiveFriendTurn = { - responseParts: string[] - /** True if the last collected assistant message contained a tool_use block. - * When true, the turn is not complete — more messages expected. */ - hasToolUse: boolean -} - -export function useFriendBridge({ messages, isLoading }: Props): void { - const pendingInboundRef = useRef(0) // counter, no chatId needed - const activeTurnRef = useRef(null) - const lastProcessedMessageCountRef = useRef(messages.length) - const previousLoadingRef = useRef(isLoading) - - // Subscribe to inbound events (increments counter for turn tracking) - useEffect(() => { - return friendService.subscribeToInbound(event => { - pendingInboundRef.current++ - }) - }, []) - - // Process new messages — detect Friend-originated user messages and collect - // assistant responses - useEffect(() => { - const newMessages = messages.slice(lastProcessedMessageCountRef.current) - - for (const message of newMessages) { - if ( - message.type === 'user' && - typeof message.origin === 'object' && - message.origin !== null && - (message.origin as Record).kind === 'channel' && - (message.origin as Record).server === FRIEND_CHANNEL_SERVER - ) { - // Consume one pending inbound - if (pendingInboundRef.current > 0) { - pendingInboundRef.current-- - activeTurnRef.current = { - responseParts: [], - hasToolUse: false, - } - } - continue - } - - if (message.type === 'assistant' && activeTurnRef.current) { - const content = message.message.content - const text = getContentText(content) - if (text) { - activeTurnRef.current.responseParts.push(text) - } - // Check if this message contains tool_use → turn continues - activeTurnRef.current.hasToolUse = Array.isArray(content) && - content.some((b: any) => b.type === 'tool_use') - continue - } - - if ( - message.type === 'system' && - message.subtype === 'local_command' && - activeTurnRef.current - ) { - const text = message.content - if (text) { - activeTurnRef.current.responseParts.push(text) - } - } - } - - lastProcessedMessageCountRef.current = messages.length - }, [messages]) - - // When loading completes, broadcast accumulated response via SSE - useEffect(() => { - const wasLoading = previousLoadingRef.current - previousLoadingRef.current = isLoading - - if (!wasLoading || isLoading || !activeTurnRef.current) return - - // If the last assistant message had a tool_use, the turn is not complete — - // more messages are expected after tool results resolve. Wait. - if (activeTurnRef.current.hasToolUse) return - - const completedTurn = activeTurnRef.current - activeTurnRef.current = null - - void (async () => { - try { - const reply = - completedTurn.responseParts.join('\n\n').trim() - - if (reply) { - await friendService.broadcastResponse(reply) - } - } catch (error) { - console.warn( - '[friend] failed to broadcast reply:', - error instanceof Error ? error.message : String(error), - ) - } - })() - }, [isLoading]) -} diff --git a/src/hooks/useVoice.ts b/src/hooks/useVoice.ts index 77f4d6e63f22a64958b36e2c20a54b6be624eaf2..54d5d5ad5b4e6e3217ff9b85342e36ce2f496647 100644 --- a/src/hooks/useVoice.ts +++ b/src/hooks/useVoice.ts @@ -1,29 +1,24 @@ -// React hook for hold-to-talk voice input using Anthropic voice_stream STT. +// React hook for hold-to-talk voice input using Groq Whisper STT (cloud). // // Hold the keybinding to record; release to stop and submit. Auto-repeat // key events reset an internal timer — when no keypress arrives within // RELEASE_TIMEOUT_MS the recording stops automatically. Uses the native -// audio module (macOS) or SoX for recording, and Anthropic's voice_stream -// endpoint (conversation_engine) for STT. +// audio module (macOS) or SoX for recording, and Groq's Whisper API +// (whisper-large-v3 / whisper-large-v3-turbo) for STT. import { useCallback, useEffect, useRef, useState } from 'react' import { useSetVoiceState } from '../context/voice.js' import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js' -import { isDoubaoAvailableSync } from '../services/doubaoSTT.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../services/analytics/index.js' import { getVoiceKeyterms } from '../services/voiceKeyterms.js' import { - connectVoiceStream, type FinalizeSource, - isVoiceStreamAvailable, type VoiceStreamConnection, -} from '../services/voiceStreamSTT.js' -import { connectDoubaoStream } from '../services/doubaoSTT.js' +} from '../services/voice/groqSTT.js' import { connectGroqStream } from '../services/voice/groqSTT.js' -import { connectLocalWhisperStream, preloadWhisperModel } from '../services/voice/whisperSTT.js' import { logForDebugging } from '../utils/debug.js' import { toError } from '../utils/errors.js' import { getSystemLocaleLanguage } from '../utils/intl.js' @@ -141,14 +136,6 @@ export function normalizeLanguageForSTT(language: string | undefined): { return { code: DEFAULT_STT_LANGUAGE, fellBackFrom: language } } -function isDoubaoProvider(): boolean { - return getInitialSettings().voiceProvider === 'doubao' -} - -function isLocalProvider(): boolean { - return getInitialSettings().voiceProvider === 'local' -} - function isGroqProvider(): boolean { return getInitialSettings().voiceProvider === 'groq' } @@ -406,78 +393,11 @@ export function useVoice({ !silentDropRetriedRef.current && fullAudioRef.current.length > 0 ) { -// Local whisper & groq don't support silent-drop replay (batch backends) - if (isLocalProvider() || isGroqProvider()) { + // Groq (batch backend) doesn't support silent-drop replay — just + // close and report no transcript. callbacks.onClose() return } - silentDropRetriedRef.current = true - logForDebugging( - `[voice] Silent-drop detected (no_data_timeout, ${String(fullAudioRef.current.length)} chunks); replaying on fresh connection`, - ) - logEvent('tengu_voice_silent_drop_replay', { - recordingDurationMs, - chunkCount: fullAudioRef.current.length, - }) - if (connectionRef.current) { - connectionRef.current.close() - connectionRef.current = null - } - const replayBuffer = fullAudioRef.current - await sleep(250) - if (isStale()) return - const rawLanguage = getInitialSettings().voiceLanguage || getInitialSettings().language - const stt = normalizeLanguageForSTT(rawLanguage) - const keyterms = await getVoiceKeyterms() - if (isStale()) return - await new Promise(resolve => { - void connectVoiceStream( - { - onTranscript: (t, isFinal) => { - if (isStale()) return - if (isFinal && t.trim()) { - if (accumulatedRef.current) accumulatedRef.current += ' ' - accumulatedRef.current += t.trim() - } - }, - onError: () => resolve(), - onClose: () => {}, - onReady: conn => { - if (isStale()) { - conn.close() - resolve() - return - } - connectionRef.current = conn - const SLICE = 32_000 - let slice: Buffer[] = [] - let bytes = 0 - for (const c of replayBuffer) { - if (bytes > 0 && bytes + c.length > SLICE) { - conn.send(Buffer.concat(slice)) - slice = [] - bytes = 0 - } - slice.push(c) - bytes += c.length - } - if (slice.length) conn.send(Buffer.concat(slice)) - void conn.finalize().then(() => { - conn.close() - resolve() - }) - }, - }, - { language: stt.code, keyterms }, - ).then( - c => { - if (!c) resolve() - }, - () => resolve(), - ) - }) - if (isStale()) return - } fullAudioRef.current = [] const text = accumulatedRef.current.trim() @@ -600,7 +520,7 @@ export function useVoice({ // stop when it loses focus. This enables a "multi-clauding army" // workflow where voice input follows window focus. useEffect(() => { - if (!enabled || !focusMode || isDoubaoProvider() || isLocalProvider() || isGroqProvider()) { + if (!enabled || !focusMode || isGroqProvider()) { // Focus mode was disabled while a focus-driven recording was active — // stop the recording so it doesn't linger until the silence timer fires. if (focusTriggeredRef.current && stateRef.current === 'recording') { @@ -719,8 +639,8 @@ export function useVoice({ audioLevelsRef.current = [] const started = await voiceModule.startRecording( (chunk: Buffer) => { - // Copy for fullAudioRef replay buffer. send() in voiceStreamSTT - // copies again defensively — acceptable overhead at audio rates. + // Copy for fullAudioRef replay buffer. Connect the STT stream's + // send() copies again defensively — acceptable overhead at audio rates. // Skip buffering in focus mode — replay is gated on !focusTriggered // so the buffer is dead weight (up to ~20MB for a 10min session). const owned = Buffer.from(chunk) @@ -804,23 +724,13 @@ export function useVoice({ const attemptConnect = (keyterms: string[]): void => { const myAttemptGen = attemptGenRef.current - // Select STT backend based on settings.voiceProvider + // STT backend: Groq Whisper (cloud) let connectFn: ( cbs: VoiceStreamCallbacks, opts: { language: string; keyterms: string[] }, ) => Promise - if (isLocalProvider()) { - void preloadWhisperModel({ language: stt.code }) - connectFn = (cbs, _opts) => - connectLocalWhisperStream(cbs, { language: stt.code }) - } else if (isDoubaoProvider()) { - connectFn = (cbs, opts) => connectDoubaoStream(cbs, opts) - } else if (isGroqProvider()) { - connectFn = (cbs, opts) => - connectGroqStream(cbs, { language: opts.language }) - } else { - connectFn = (cbs, opts) => connectVoiceStream(cbs, opts) - } + connectFn = (cbs, opts) => + connectGroqStream(cbs, { language: opts.language }) void connectFn( { onTranscript: (text: string, isFinal: boolean) => { @@ -1027,14 +937,7 @@ export function useVoice({ return } if (!conn) { - if (isLocalProvider()) { - logForDebugging( - '[voice] Local whisper STT failed to initialize', - ) - onErrorRef.current?.( - 'Local voice mode failed. Ensure Python with faster-whisper is installed.', - ) - } else if (isGroqProvider()) { + if (isGroqProvider()) { logForDebugging( '[voice] Groq STT failed to initialize', ) @@ -1056,7 +959,7 @@ export function useVoice({ return } - // Safety check: if the user released the key before connectVoiceStream + // Safety check: if the user released the key before the STT connection // resolved (but after onReady already ran), close the connection. if (stateRef.current !== 'recording') { audioBuffer.length = 0 @@ -1066,8 +969,8 @@ export function useVoice({ }) } - // Doubao, local, and groq backends don't use keyterms — skip the async fetch - if (isDoubaoProvider() || isLocalProvider() || isGroqProvider()) { + // Groq backend doesn't use keyterms — skip the async fetch + if (isGroqProvider()) { attemptConnect([]) } else { void getVoiceKeyterms().then(attemptConnect) @@ -1085,11 +988,7 @@ export function useVoice({ // delay of ~500ms on macOS). const handleKeyEvent = useCallback( (fallbackMs = REPEAT_FALLBACK_MS): void => { - const sttAvailable = isLocalProvider() || isGroqProvider() - ? true - : isDoubaoProvider() - ? isDoubaoAvailableSync() - : isVoiceStreamAvailable() + const sttAvailable = isGroqProvider() if (!enabled || !sttAvailable) { return } diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 8e14ca768f9017fa82069f255439173bcce7ecd2..070b09af9566cdaca6012449361d5ce2a94d84aa 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -113,7 +113,6 @@ import { import { endInteractionSpan } from '../utils/telemetry/sessionTracing.js' import { useLogMessages } from '../hooks/useLogMessages.js' import { useFeishuBridge } from '../hooks/useFeishuBridge.js' -import { useFriendBridge } from '../hooks/useFriendBridge.js' import { useReplBridge } from '../hooks/useReplBridge.js' import { type Command, @@ -5408,7 +5407,6 @@ export function REPL({ useMailboxBridge({ isLoading, onSubmitMessage: handleIncomingPrompt }) useFeishuBridge({ messages, isLoading }) - useFriendBridge({ messages, isLoading }) // Scheduled tasks from .claude/scheduled_tasks.json (CronCreate/Delete/List) if (feature('AGENT_TRIGGERS')) { diff --git a/src/server/api/friend.ts b/src/server/api/friend.ts deleted file mode 100644 index 1ffbd71afddd5c7115e54db0f8917a0b690b0b5b..0000000000000000000000000000000000000000 --- a/src/server/api/friend.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Friend API — VRM avatar frontend HTTP routes for Codev. - * - * Simplified to only voice + emotion/action features. - */ -import path from 'node:path'; -import { existsSync, readFileSync } from 'node:fs'; -import { - createSseResponse, -} from '../../friend/sse.js'; -import { getPrefs, setPrefs, updatePrefs, type FriendPrefs } from '../../friend/prefs.js'; -import { edgeTts, qwenTts, registerAudioFile, getAudioFile } from '../../friend/tts.js'; -import { friendService } from '../../friend/FriendService.js'; - -const MIME_TYPES: Record = { - '.mp3': 'audio/mpeg', - '.opus': 'audio/opus', - '.ogg': 'audio/ogg', - '.wav': 'audio/wav', - '.webm': 'audio/webm', -}; - -function corsHeaders(): Record { - return { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }; -} - -function jsonResponse(data: unknown, status = 200): Response { - return Response.json(data, { status, headers: corsHeaders() }); -} - -function notFound(msg = 'Not found'): Response { - return jsonResponse({ error: msg }, 404); -} - -/** - * Route handler for /plugins/friend/* - */ -export async function handleFriendApi(req: Request, url: URL): Promise { - const pathname = url.pathname; - const method = req.method; - - // CORS preflight - if (method === 'OPTIONS') { - return new Response(null, { status: 204, headers: corsHeaders() }); - } - - // ── SSE Events endpoint ── - if (pathname === '/plugins/friend/events' && method === 'GET') { - return createSseResponse(); - } - - // ── Audio serving ── - if (pathname.startsWith('/plugins/friend/audio/') && method === 'GET') { - const audioId = pathname.split('/plugins/friend/audio/')[1]?.split('?')[0]; - if (!audioId) return jsonResponse({ error: 'missing audio id' }, 400); - - const filePath = getAudioFile(audioId); - if (!filePath || !existsSync(filePath)) return notFound('audio not found'); - - const ext = path.extname(filePath).toLowerCase(); - const contentType = MIME_TYPES[ext] ?? 'application/octet-stream'; - const data = readFileSync(filePath); - return new Response(data, { - headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=300', ...corsHeaders() }, - }); - } - - // ── Voice capture endpoints ── - if (pathname === '/plugins/friend/voice/start' && method === 'POST') { - try { - await friendService.startVoiceCapture(); - return jsonResponse({ ok: true }); - } catch (err) { - return jsonResponse({ error: String(err) }, 500); - } - } - - if (pathname === '/plugins/friend/voice/stop' && method === 'POST') { - try { - const text = await friendService.stopVoiceCapture(); - return jsonResponse({ ok: true, text }); - } catch (err) { - return jsonResponse({ error: String(err) }, 500); - } - } - - if (pathname === '/plugins/friend/voice/status' && method === 'POST') { - return jsonResponse(friendService.getCaptureStatus()); - } - - // ── Voice settings ── - if (pathname === '/plugins/friend/voice') { - if (method === 'GET') { - const prefs = getPrefs(); - return jsonResponse({ - voice: prefs.voice ?? 'zh-CN-XiaoxiaoNeural', - provider: prefs.provider ?? 'edge', - qwenKey: prefs.qwenKey ?? '', - qwenModel: prefs.qwenModel ?? 'qwen3-tts-flash', - }); - } - if (method === 'POST') { - const body = await req.json() as any; - const patch: Partial = {}; - if (body.voice !== undefined) patch.voice = body.voice || undefined; - if (body.provider !== undefined) patch.provider = body.provider || undefined; - if (body.qwenKey !== undefined) patch.qwenKey = body.qwenKey || undefined; - if (body.qwenModel !== undefined) patch.qwenModel = body.qwenModel || undefined; - setPrefs(updatePrefs(patch)); - return jsonResponse({ ok: true }); - } - return new Response(null, { status: 405 }); - } - - // ── STT config ── - if (pathname === '/plugins/friend/stt/config' && method === 'GET') { - const prefs = getPrefs(); - return jsonResponse({ - sttProvider: prefs.sttProvider || 'browser', - sttLanguage: prefs.sttLanguage || 'zh', - }); - } - - // ── STT segment from browser VAD ── - if (pathname === '/plugins/friend/voice/stt-segment' && method === 'POST') { - try { - let audioBuffer: Buffer; - const contentType = req.headers.get('content-type') || ''; - if (contentType.includes('multipart/form-data') || contentType.includes('application/octet-stream')) { - const blob = await req.blob(); - audioBuffer = Buffer.from(await blob.arrayBuffer()); - } else { - audioBuffer = Buffer.from(await req.arrayBuffer()); - } - if (audioBuffer.length < 100) { - return jsonResponse({ error: 'audio too short' }, 400); - } - const transcript = await friendService.transcribeAudioSegment(audioBuffer); - return jsonResponse({ ok: true, text: transcript }); - } catch (err) { - return jsonResponse({ error: String(err) }, 500); - } - } - - // ── TTS Preview ── - if (pathname === '/plugins/friend/preview' && method === 'POST') { - const body = await req.json() as any; - const voice = body.voice as string | undefined; - const provider = body.provider as string | undefined; - const text = '\u4f60\u597d\uff0c\u8fd9\u662f\u4e00\u6bb5\u8bed\u97f3\u8bd5\u542c\u3002Hello, this is a voice preview.'; - - try { - const prefs = getPrefs(); - let result: { success: boolean; audioPath?: string; error?: string }; - if (provider === 'qwen' && prefs.qwenKey) { - result = await qwenTts({ - text, - apiKey: prefs.qwenKey, - voice, - model: prefs.qwenModel, - language: prefs.language, - }); - } else { - result = await edgeTts({ text, voice: voice || prefs.voice }); - } - if (result.success && result.audioPath) { - const audioId = registerAudioFile(result.audioPath); - return jsonResponse({ audioUrl: `http://127.0.0.1:3456/plugins/friend/audio/${audioId}` }); - } - return jsonResponse({ error: result.error || 'TTS failed' }); - } catch (err) { - return jsonResponse({ error: String(err) }, 500); - } - } - - // ── General settings (simplified) ── - if (pathname === '/plugins/friend/settings') { - if (method === 'GET') { - const prefs = getPrefs(); - return jsonResponse({ - modelPath: prefs.modelPath, - ttsEnabled: prefs.ttsEnabled, - showText: prefs.showText, - hideUI: prefs.hideUI, - tracking: prefs.tracking, - volume: prefs.volume, - uiAlign: prefs.uiAlign, - sttProvider: prefs.sttProvider || 'browser', - sttLanguage: prefs.sttLanguage || 'zh', - language: prefs.language, - }); - } - if (method === 'POST') { - const body = await req.json() as any; - const patch: Partial = {}; - if (body.modelPath !== undefined) patch.modelPath = body.modelPath; - if (body.ttsEnabled !== undefined) patch.ttsEnabled = body.ttsEnabled; - if (body.showText !== undefined) patch.showText = body.showText; - if (body.hideUI !== undefined) patch.hideUI = body.hideUI; - if (body.tracking !== undefined) patch.tracking = body.tracking; - if (body.volume !== undefined) patch.volume = body.volume; - if (body.uiAlign !== undefined) patch.uiAlign = body.uiAlign; - if (body.sttProvider !== undefined) patch.sttProvider = body.sttProvider; - if (body.sttLanguage !== undefined) patch.sttLanguage = body.sttLanguage; - if (body.language !== undefined) patch.language = body.language; - if (body.groqApiKey !== undefined) patch.groqApiKey = body.groqApiKey; - setPrefs(updatePrefs(patch)); - return jsonResponse({ ok: true }); - } - return new Response(null, { status: 405 }); - } - - // ── Persona (read-only) ── - const workspaceRoot = path.join( - process.env.HOME || process.env.USERPROFILE || '', - '.config', 'Codev', 'friend', - ); - const identityPath = path.join(workspaceRoot, 'IDENTITY.md'); - const soulPath = path.join(workspaceRoot, 'SOUL.md'); - - if (pathname === '/plugins/friend/persona') { - if (method === 'GET') { - let soul = ''; - let identity = ''; - try { if (existsSync(soulPath)) soul = readFileSync(soulPath, 'utf8'); } catch { /* */ } - try { if (existsSync(identityPath)) identity = readFileSync(identityPath, 'utf8'); } catch { /* */ } - return jsonResponse({ soul, identity, soulPath, identityPath }); - } - if (method === 'POST') { - const body = await req.json() as any; - const { mkdirSync, writeFileSync } = await import('node:fs'); - if (body.soul !== undefined) { - mkdirSync(path.dirname(soulPath), { recursive: true }); - writeFileSync(soulPath, body.soul, 'utf8'); - } - if (body.identity !== undefined) { - mkdirSync(path.dirname(identityPath), { recursive: true }); - writeFileSync(identityPath, body.identity, 'utf8'); - } - return jsonResponse({ ok: true }); - } - return new Response(null, { status: 405 }); - } - - // ── Window close (called by Tauri on close) ── - if ((pathname === '/friend/api/window-close' || pathname === '/plugins/friend/api/window-close') && method === 'POST') { - process.emit('friend:window-close' as any); - return jsonResponse({ ok: true }); - } - - // ── Chat endpoint (for voice call text relay) ── - if (pathname === '/plugins/friend/chat' && method === 'POST') { - const body = await req.json() as any; - const message = body?.message; - if (!message) return jsonResponse({ error: 'message required' }, 400); - try { - await friendService.start(); - friendService.sendText(message); - } catch (err) { - console.error('[Friend] chat dispatch error:', err); - } - return jsonResponse({ ok: true }); - } - - return jsonResponse({ error: 'Not Found' }, 404); -} diff --git a/src/server/index.ts b/src/server/index.ts index 200ac43894e6cc6c71bee2a9353e4f0d8be1e2a8..ed2dc61b1b0083c3f145d59ce9f159f1105c14fd 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -15,9 +15,6 @@ import { handleProxyRequest } from './proxy/handler.js' import { ProviderService } from './services/providerService.js' import { handleHahaOAuthCallback } from './api/haha-oauth.js' import { handleHahaOpenAIOAuthCallback } from './api/haha-openai-oauth.js' -import { handleFriendApi, setFriendServerInfo } from './api/friend.js' -import { handleFriendStaticRequest } from './staticFriend.js' -import { getPrefs } from '../friend/prefs.js' import { fileURLToPath } from 'node:url' import path from 'node:path' import { OPENAI_CODEX_REDIRECT_PATH } from '../services/openaiAuth/client.js' @@ -368,31 +365,6 @@ export function startServer(port = PORT, host = HOST) { ) } - // Friend VRM desktop pet plugin API - if (url.pathname.startsWith('/plugins/friend/')) { - if (cors.rejected) { - return corsRejectedResponse(cors) - } - try { - return await handleFriendApi(req, url) - } catch (error) { - console.error('[Friend] API error:', error) - return withCors(Response.json( - { error: 'Friend API error' }, - { status: 500 }, - ), cors) - } - } - - // Friend VRM frontend static files (served at /friend/*) - if (url.pathname.startsWith('/friend') || url.pathname === '/friend') { - const friendResponse = await handleFriendStaticRequest(req, url) - if (friendResponse) { - return friendResponse - } - // Fall through to 404 if no matching file - } - // Static H5 shell/assets are non-secret bootstrap content and must load // before the browser can read the QR token; API/proxy/ws stay protected above. const staticResponse = await handleStaticH5Request(req, url) @@ -427,13 +399,6 @@ export function startServer(port = PORT, host = HOST) { console.log(`[Server] Claude Code API server running at http://${host}:${port}`) - // Register Friend server info so API routes can construct URLs - setFriendServerInfo(host, port) - - // Note: Friend Tauri window lifecycle is managed by the friend module - // (FriendService + friend/server.ts), not by the desktop server. - // Use `/friend start` in the CLI to launch the window. - return server } diff --git a/src/server/staticFriend.ts b/src/server/staticFriend.ts deleted file mode 100644 index a34e803c47881132ca858484f67af6197972c240..0000000000000000000000000000000000000000 --- a/src/server/staticFriend.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Static file serving for the Friend VRM frontend. - * - * Serves the built Vite/React frontend from src/components/friend/frontend/dist/ at /friend/*. - * All asset files (VRM, FBX, VRMA, VMD, MP3) are in the dist root and served - * under /friend/. - */ -import fs from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -const CACHEABLE_RE = /^\/friend\/assets\// -const MIME_TYPES: Record = { - '.vrm': 'application/octet-stream', - '.fbx': 'application/octet-stream', - '.vrma': 'application/octet-stream', - '.vmd': 'application/octet-stream', - '.mp3': 'audio/mpeg', - '.css': 'text/css; charset=utf-8', - '.js': 'text/javascript; charset=utf-8', - '.mjs': 'text/javascript; charset=utf-8', - '.html': 'text/html; charset=utf-8', - '.json': 'application/json; charset=utf-8', - '.png': 'image/png', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.onnx': 'application/octet-stream', - '.wasm': 'application/wasm', -} - -export async function handleFriendStaticRequest(req: Request, url: URL): Promise { - if (req.method !== 'GET' && req.method !== 'HEAD') { - return null - } - - // Only handle /friend/* paths - if (!url.pathname.startsWith('/friend/') && url.pathname !== '/friend') { - return null - } - - const distDir = await resolveFriendDistDir() - if (!distDir) { - return null - } - - // Map /friend/* to dist root - // /friend/ → index.html - // /friend/model1.vrm → dist/model1.vrm - // /friend/assets/foo.js → dist/assets/foo.js - const relativePath = url.pathname.replace(/^\/friend\/?/, '') || 'index.html' - const filePath = await resolveFriendFilePath(distDir, relativePath) - if (!filePath) { - return null - } - - const headers = new Headers({ - 'Content-Type': contentTypeForPath(filePath), - 'Cache-Control': CACHEABLE_RE.test(url.pathname) - ? 'public, max-age=31536000, immutable' - : 'no-store', - }) - - // Enable cross-origin isolation for SharedArrayBuffer support, - // needed by onnxruntime-web WASM threading (used by @ricky0123/vad-web). - if (relativePath === 'index.html') { - headers.set('Cross-Origin-Opener-Policy', 'same-origin') - headers.set('Cross-Origin-Embedder-Policy', 'require-corp') - } - - if (req.method === 'HEAD') { - const stat = await fs.stat(filePath) - headers.set('Content-Length', String(stat.size)) - return new Response(null, { status: 200, headers }) - } - - return new Response(Bun.file(filePath), { status: 200, headers }) -} - -async function resolveFriendDistDir(): Promise { - // Method 1: relative to source file (works in dev mode / server subprocess) - const _srcDir = path.dirname(fileURLToPath(import.meta.url)) - const candidate1 = path.resolve(_srcDir, '..', '..', 'src', 'components', 'friend', 'frontend', 'dist') - try { - const stat = await fs.stat(path.join(candidate1, 'index.html')) - if (stat.isFile()) return candidate1 - } catch { - // Not found - } - - // Method 2: relative to cwd (works in compiled binary where import.meta.url is virtual) - const candidate2 = path.resolve(process.cwd(), 'src', 'components', 'friend', 'frontend', 'dist') - try { - const stat = await fs.stat(path.join(candidate2, 'index.html')) - if (stat.isFile()) return candidate2 - } catch { - // Not found - } - - return null -} - -async function resolveFriendFilePath(distDir: string, relativePath: string): Promise { - if (!relativePath) return null - - const decoded = decodeURIComponent(relativePath) - const candidate = path.resolve(distDir, decoded) - const relativeToRoot = path.relative(distDir, candidate) - if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { - return null - } - - try { - const stat = await fs.stat(candidate) - return stat.isFile() ? candidate : null - } catch { - return null - } -} - -function contentTypeForPath(filePath: string): string { - return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream' -} diff --git a/src/services/doubaoSTT.ts b/src/services/doubaoSTT.ts deleted file mode 100644 index 2d6b98cc0b5d4e3d56ec3b3f5ba9f65e9a6d515c..0000000000000000000000000000000000000000 --- a/src/services/doubaoSTT.ts +++ /dev/null @@ -1,258 +0,0 @@ -// Doubao (豆包) ASR speech-to-text adapter for voice mode. -// -// Wraps the doubaoime-asr npm package to expose the same interface as -// voiceStreamSTT.ts. The doubao backend uses an AsyncGenerator-based -// streaming protocol internally; this adapter bridges it to the -// send/finalize/close pattern used by useVoice.ts. - -import { homedir } from 'node:os' -import type { ASRResponse } from 'doubaoime-asr' -import type { - FinalizeSource, - VoiceStreamCallbacks, - VoiceStreamConnection, -} from './voiceStreamSTT.js' -import { logForDebugging } from '../utils/debug.js' -import { logError } from '../utils/log.js' - -// Re-export FinalizeSource so useVoice can import from either module -export type { FinalizeSource } from './voiceStreamSTT.js' - -// ─── AsyncIterable audio queue ───────────────────────────────────────── - -// A push-based queue that implements AsyncIterable. -// send() pushes chunks; push(null) signals end-of-stream. -class AudioChunkQueue { - private chunks: (Uint8Array | null)[] = [] - private waiting: ((result: IteratorResult) => void) | null = null - private done = false - - push(chunk: Uint8Array | null): void { - if (this.done) return - if (chunk === null) { - this.done = true - if (this.waiting) { - const resolve = this.waiting - this.waiting = null - resolve({ value: undefined, done: true }) - } - return - } - if (this.waiting) { - const resolve = this.waiting - this.waiting = null - resolve({ value: chunk, done: false }) - } else { - this.chunks.push(chunk) - } - } - - abort(): void { - this.done = true - this.chunks.length = 0 - if (this.waiting) { - const resolve = this.waiting - this.waiting = null - resolve({ value: undefined, done: true }) - } - } - - [Symbol.asyncIterator](): AsyncIterator { - return { - next: async (): Promise> => { - if (this.chunks.length > 0) { - const chunk = this.chunks.shift()! - return { value: chunk, done: false } - } - if (this.done) { - return { value: undefined, done: true } - } - return new Promise>(resolve => { - this.waiting = resolve - }) - }, - } - } -} - -// ─── Availability ──────────────────────────────────────────────────────── - -let doubaoAvailable: boolean | null = null - -export async function isDoubaoAvailable(): Promise { - if (doubaoAvailable !== null) return doubaoAvailable - try { - await import('doubaoime-asr') - doubaoAvailable = true - } catch { - doubaoAvailable = false - } - return doubaoAvailable -} - -// Synchronous check — returns cached result or optimistic true when -// VOICE_PROVIDER=doubao is set and no cached result exists yet. -// The actual import happens in connectDoubaoStream which reports errors. -export function isDoubaoAvailableSync(): boolean { - if (doubaoAvailable !== null) return doubaoAvailable - return true -} - -// ─── Connection ────────────────────────────────────────────────────────── - -export async function connectDoubaoStream( - callbacks: VoiceStreamCallbacks, - _options?: { language?: string }, -): Promise { - let doubaoAsr: typeof import('doubaoime-asr') - try { - doubaoAsr = await import('doubaoime-asr') - } catch (err) { - logError( - new Error( - `[doubao-asr] Failed to import doubaoime-asr package: ${String(err)}`, - ), - ) - callbacks.onError(`doubaoime-asr package import failed: ${String(err)}`, { - fatal: true, - }) - return null - } - - const { transcribeRealtime, ASRConfig, ResponseType } = doubaoAsr - - const queue = new AudioChunkQueue() - let finalized = false - - // Resolve handle for finalize() promise — wrapped in an object to avoid - // TypeScript closure-scope type narrowing issues (TS2349 "not callable"). - const finalizeHandle: { resolve: ((source: FinalizeSource) => void) | null } = - { resolve: null } - - const connection: VoiceStreamConnection = { - send(audioChunk: Buffer): void { - if (finalized) return - queue.push( - new Uint8Array( - audioChunk.buffer, - audioChunk.byteOffset, - audioChunk.byteLength, - ), - ) - }, - finalize(): Promise { - if (finalized) return Promise.resolve('ws_already_closed') - finalized = true - queue.push(null) // signal end-of-stream to the generator - // Doubao returns FINAL_RESULT during recording — by the time the user - // releases the key, all transcripts are already in accumulatedRef. - // Resolve immediately so the UI skips the 'processing' state and goes - // straight to displaying the result. - logForDebugging('[doubao-asr] Finalize — resolving immediately') - return Promise.resolve('post_closestream_endpoint') - }, - close(): void { - finalized = true - queue.abort() - const r = finalizeHandle.resolve - finalizeHandle.resolve = null - if (r) r('ws_close') - callbacks.onClose() - }, - isConnected(): boolean { - return true - }, - } - - // Start the ASR session in the background - const config = new ASRConfig({ - credentialPath: `${homedir()}/.claude/tts/doubao/credentials.json`, - }) - - // Ensure credentials are initialized (may auto-generate) - try { - await config.ensureCredentials() - } catch (err) { - logError( - new Error( - `[doubao-asr] Credential initialization failed: ${String(err)}`, - ), - ) - callbacks.onError(`Doubao ASR 凭证初始化失败: ${String(err)}`, { - fatal: true, - }) - return null - } - - // Fire onReady immediately — unlike the Anthropic WebSocket which needs to - // wait for a handshake, the doubao backend accepts audio through the queue - // and handles connection internally. The caller (useVoice.ts) needs onReady - // to fire before it will route audio chunks via connection.send(). - logForDebugging('[doubao-asr] Firing onReady immediately') - callbacks.onReady(connection) - - // Consume the AsyncGenerator in the background - void (async () => { - try { - const audioSource: AsyncIterable = queue - const gen: AsyncGenerator = transcribeRealtime(audioSource, { - config, - }) - - for await (const resp of gen) { - if ( - finalized && - resp.type !== ResponseType.FINAL_RESULT && - resp.type !== ResponseType.SESSION_FINISHED - ) { - continue - } - - switch (resp.type) { - case ResponseType.SESSION_STARTED: - logForDebugging('[doubao-asr] Session started') - break - case ResponseType.VAD_START: - logForDebugging('[doubao-asr] VAD detected speech start') - break - case ResponseType.INTERIM_RESULT: - if (resp.text) { - callbacks.onTranscript(resp.text, false) - } - break - case ResponseType.FINAL_RESULT: - if (resp.text) { - callbacks.onTranscript(resp.text, true) - } - break - case ResponseType.ERROR: - logError(new Error(`[doubao-asr] Error: ${resp.errorMsg}`)) - if (!finalized) { - callbacks.onError(resp.errorMsg || 'Doubao ASR 识别错误') - } - break - case ResponseType.SESSION_FINISHED: - logForDebugging('[doubao-asr] Session finished') - break - default: - break - } - } - - // Generator exhausted naturally - const r = finalizeHandle.resolve - finalizeHandle.resolve = null - if (r) r('post_closestream_endpoint') - } catch (err) { - logError(new Error(`[doubao-asr] Stream error: ${String(err)}`)) - if (!finalized) { - callbacks.onError(`Doubao ASR 连接错误: ${String(err)}`) - } - const r2 = finalizeHandle.resolve - finalizeHandle.resolve = null - if (r2) r2('ws_close') - } - })() - - return connection -} diff --git a/src/services/voice/edgeTTS.ts b/src/services/voice/edgeTTS.ts index 925087ebbe3ada08b322f6f5cfcf4d2fac9cb052..9c4d20ea3e965db4ba53632de8967169837ad84e 100644 --- a/src/services/voice/edgeTTS.ts +++ b/src/services/voice/edgeTTS.ts @@ -1,30 +1,6 @@ -import { spawn } from 'child_process' -import { existsSync } from 'fs' +import { mkdtempSync, writeFileSync } from 'fs' import { tmpdir } from 'os' -import { join, dirname } from 'path' - -function findProjectRoot(): string { - let dir = process.cwd() - for (let i = 0; i < 20; i++) { - if (existsSync(join(dir, 'package.json')) && existsSync(join(dir, 'scripts'))) { - return dir - } - const parent = dirname(dir) - if (parent === dir) break - dir = parent - } - return process.cwd() -} - -const PROJECT_ROOT = findProjectRoot() -const SCRIPTS_DIR = join(PROJECT_ROOT, 'scripts') -const VENV_PYTHON = join(PROJECT_ROOT, '.venv', 'bin', 'python') - -function resolvePythonPath(customPath?: string): string { - if (customPath) return customPath - if (existsSync(VENV_PYTHON)) return VENV_PYTHON - return 'python3' -} +import path from 'node:path' export type TTSResult = { success: boolean @@ -35,101 +11,55 @@ export type TTSResult = { export type TTSSpeakOptions = { voice?: string outputPath?: string - pythonPath?: string } /** - * Text-to-speech using edge-tts (Microsoft Edge Neural Voice). - * Spawns a Python subprocess to run edge-tts and save the audio file. - * - * Requirements: pip install edge-tts + * Text-to-speech using the `node-edge-tts` Node package (no Python required). + * Synthesizes `text` to a local audio file and resolves with its path. */ export async function speakWithEdgeTTS( text: string, options?: TTSSpeakOptions, ): Promise { - const python = resolvePythonPath(options?.pythonPath) - const voice = options?.voice ?? 'en-US-JennyNeural' - const script = join(SCRIPTS_DIR, 'speak.py') - - return new Promise(resolve => { - const args = [script, text, '--voice', voice] - if (options?.outputPath) { - args.push('--output', options.outputPath) - } - - const proc = spawn(python, args, { stdio: ['ignore', 'pipe', 'pipe'] }) - - let stdout = '' - let stderr = '' - - proc.stdout.on('data', (data: Buffer) => { - stdout += data.toString() - }) - - proc.stderr.on('data', (data: Buffer) => { - stderr += data.toString() - }) - - proc.on('close', exitCode => { - if (exitCode !== 0) { - resolve({ - success: false, - error: `edge-tts failed: ${stderr || 'unknown error'}`, - }) - return - } - - try { - const raw = JSON.parse(stdout) - resolve({ - success: raw.success, - audioPath: raw.audio_path ?? raw.audioPath, - error: raw.error, - }) - } catch { - resolve({ - success: false, - error: `Failed to parse edge-tts output: ${stdout.slice(0, 200)}`, - }) - } - }) - - proc.on('error', err => { - resolve({ - success: false, - error: `Failed to spawn edge-tts: ${err.message}`, - }) - }) - }) + try { + const { EdgeTTS } = await import('node-edge-tts') + const audioPath = + options?.outputPath ?? + path.join( + mkdtempSync(path.join(tmpdir(), 'codev-tts-')), + `voice-${Date.now()}.mp3`, + ) + const tts = new EdgeTTS({ voice: options?.voice ?? 'en-US-JennyNeural' }) + await tts.ttsPromise(text, audioPath) + return { success: true, audioPath } + } catch (err) { + return { success: false, error: String(err) } + } } -/** Check if edge-tts is available. */ -export async function checkEdgeTTSAvailable(pythonPath?: string): Promise { - const python = resolvePythonPath(pythonPath) - return new Promise(resolve => { - const proc = spawn(python, [ - '-c', - 'import json;' + - 'try: import edge_tts; print(json.dumps({"ok": True}))' + - 'except ImportError: print(json.dumps({"ok": False}))', - ], { stdio: ['ignore', 'pipe', 'pipe'] }) +/** Check if `node-edge-tts` can be loaded. */ +export async function checkEdgeTTSAvailable(): Promise { + try { + await import('node-edge-tts') + return true + } catch { + return false + } +} - let stdout = '' - proc.stdout.on('data', (data: Buffer) => { stdout += data.toString() }) - proc.on('close', () => { - try { - const result = JSON.parse(stdout) - resolve(result.ok === true) - } catch { - resolve(false) - } - }) - }) +/** + * Synthesize text to a temporary .mp3 using `node-edge-tts`. + * Returns the audio file path on success. + */ +export async function edgeTts(opts: { + text: string + voice?: string +}): Promise<{ success: boolean; audioPath?: string; error?: string }> { + return speakWithEdgeTTS(opts.text, { voice: opts.voice }) } /** Play an audio file using system player. */ -export function playAudioFile(path: string): Promise { +export function playAudioFile(filePath: string): Promise { return new Promise((resolve, reject) => { const platform = process.platform let cmd: string @@ -137,36 +67,32 @@ export function playAudioFile(path: string): Promise { if (platform === 'darwin') { cmd = 'afplay' - args = [path] + args = [filePath] } else if (platform === 'linux') { cmd = 'ffplay' - args = ['-nodisp', '-autoexit', '-infbuf', path] + args = ['-nodisp', '-autoexit', '-infbuf', filePath] } else if (platform === 'win32') { cmd = 'start' - args = [path] + args = [filePath] } else { reject(new Error(`Unsupported platform: ${platform}`)) return } + const { spawn } = require('child_process') as typeof import('child_process') + if (platform === 'linux') { const killProc = spawn('pkill', ['-9', 'ffplay'], { stdio: 'ignore' }) killProc.on('close', () => { setTimeout(() => { const proc = spawn(cmd, args, { stdio: 'ignore' }) - proc.on('close', code => { - if (code === 0) resolve() - else resolve() - }) + proc.on('close', () => resolve()) proc.on('error', () => resolve()) }, 50) }) killProc.on('error', () => { const proc = spawn(cmd, args, { stdio: 'ignore' }) - proc.on('close', code => { - if (code === 0) resolve() - else resolve() - }) + proc.on('close', () => resolve()) proc.on('error', () => resolve()) }) } else { @@ -178,4 +104,4 @@ export function playAudioFile(path: string): Promise { proc.on('error', reject) } }) -} \ No newline at end of file +} diff --git a/src/services/voice/groqSTT.ts b/src/services/voice/groqSTT.ts index 36a64187b0e448e03b3eb94f126b22ab2e3f6aa1..4882c4e5c9e581f7b632aeb9c0e8775ee744f7d7 100644 --- a/src/services/voice/groqSTT.ts +++ b/src/services/voice/groqSTT.ts @@ -11,7 +11,31 @@ import Groq from 'groq-sdk' import { readFileSync, existsSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' -import type { VoiceStreamCallbacks, VoiceStreamConnection, FinalizeSource } from '../voiceStreamSTT.js' + +// ─── Shared STT connection contracts ─────────────────────────────────────── + +export type VoiceStreamCallbacks = { + onTranscript: (text: string, isFinal: boolean) => void + onError: (error: string, opts?: { fatal?: boolean }) => void + onClose: () => void + onReady: (connection: VoiceStreamConnection) => void +} + +// How finalize() resolved. `no_data_timeout` means zero server messages +// after CloseStream — the silent-drop signature. +export type FinalizeSource = + | 'post_closestream_endpoint' + | 'no_data_timeout' + | 'safety_timeout' + | 'ws_close' + | 'ws_already_closed' + +export type VoiceStreamConnection = { + send: (audioChunk: Buffer) => void + finalize: () => Promise + close: () => void + isConnected: () => boolean +} const MODELS = ['whisper-large-v3', 'whisper-large-v3-turbo'] as const @@ -25,7 +49,7 @@ export type GroqSttOptions = { /** * Resolve the Groq API key from multiple sources (priority order): * 1. Explicitly passed `apiKey` option - * 2. `getPrefs().groqApiKey` (from friend.json) + * 2. `getPrefs().groqApiKey` (from companion prefs) * 3. `process.env.GROQ_API_KEY` * 4. `~/.claude/settings.json` → env.groqApiKey */ diff --git a/src/services/voice/providers.ts b/src/services/voice/providers.ts index 7e886f0c2e7c55c5ddc78dcce5aa1528bd72334e..076375d47ded5a9079d1db2ecdd867dc319b2101 100644 --- a/src/services/voice/providers.ts +++ b/src/services/voice/providers.ts @@ -5,9 +5,7 @@ // - `TTSProvider` — text → audio // // Built-in concrete providers: -// - `LocalWhisperSTT` — subprocess wrapper around a local whisper.cpp / faster-whisper CLI -// - `DoubaoSTTProvider` — wraps `src/services/doubaoSTT.ts` -// - `EdgeTTSProvider` — subprocess wrapper around `edge-tts` CLI +// - `EdgeTTSProvider` — uses the `node-edge-tts` package (no Python subprocess) // - `CommandTTSProvider` — generic shell command with `{input}` / `{input_path}` / `{output_path}` placeholders import { spawn } from 'node:child_process' @@ -130,47 +128,6 @@ export class LocalWhisperSTT implements TranscriptionProvider { } } -// --------------------------------------------------------------------------- -// Doubao STT -// --------------------------------------------------------------------------- - -export class DoubaoSTTProvider implements TranscriptionProvider { - readonly name = 'doubao' - - async transcribe(wavPath: string, language?: string): Promise { - try { - const { connectDoubaoStream, normalizeLanguageForSTT } = await import('./doubaoSTT.js') - const normalized = normalizeLanguageForSTT(language) - const code = normalized.code - const chunks: Buffer[] = [] - const conn = await connectDoubaoStream( - { - onTranscript: (text: string) => { - if (text) chunks.push(Buffer.from(text, 'utf8')) - }, - onError: (msg: string) => { - throw new Error(msg) - }, - onClose: () => {}, - onReady: (c) => { - const buf = Buffer.from(await (async () => (await import('node:fs')).promises.readFile(wavPath))()) - c.send(buf) - void c.finalize().then(() => c.close()) - }, - }, - { language: code === 'en' ? undefined : code }, - ) - if (!conn) throw new Error('doubao connectDoubaoStream returned null') - await new Promise((resolve) => setTimeout(resolve, 500)) - const text = Buffer.concat(chunks).toString('utf8').trim() - if (!text) return { success: false, text: '', error: 'empty transcript' } - return { success: true, text } - } catch (e: any) { - return { success: false, text: '', error: e?.message ?? String(e) } - } - } -} - // --------------------------------------------------------------------------- // Edge TTS // --------------------------------------------------------------------------- diff --git a/src/services/voice/whisperSTT.ts b/src/services/voice/whisperSTT.ts deleted file mode 100644 index 22931e0878eae6eeca2928d7d9a208c09912f5f8..0000000000000000000000000000000000000000 --- a/src/services/voice/whisperSTT.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { spawn, type ChildProcess } from 'child_process' -import { existsSync, mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from 'fs' -import { tmpdir } from 'os' -import { join, dirname } from 'path' -import type { VoiceStreamCallbacks, VoiceStreamConnection, FinalizeSource } from '../voiceStreamSTT.js' - -function findProjectRoot(): string { - let dir = process.cwd() - for (let i = 0; i < 20; i++) { - if (existsSync(join(dir, 'package.json')) && existsSync(join(dir, 'scripts'))) { - return dir - } - const parent = dirname(dir) - if (parent === dir) break - dir = parent - } - return process.cwd() -} - -const PROJECT_ROOT = findProjectRoot() -const SCRIPTS_DIR = join(PROJECT_ROOT, 'scripts') -const VENV_PYTHON = join(PROJECT_ROOT, '.venv', 'bin', 'python') - -function resolvePythonPath(): string { - if (existsSync(VENV_PYTHON)) return VENV_PYTHON - return 'python3' -} - -type WhisperOptions = { - model?: string - language?: string - pythonPath?: string -} - -let serverProc: ChildProcess | null = null -let serverLoaded = false -let serverLoadingResolve: (() => void) | null = null -let serverLoadingReject: ((err: Error) => void) | null = null - -function getServer(): ChildProcess { - if (serverProc && serverProc.exitCode === null) { - return serverProc - } - const python = resolvePythonPath() - serverProc = spawn(python, [join(SCRIPTS_DIR, 'whisper_server.py')], { - stdio: ['pipe', 'pipe', 'pipe'], - }) - - let buf = '' - serverProc.stdout!.on('data', (data: Buffer) => { - buf += data.toString() - const lines = buf.split('\n') - buf = lines.pop() ?? '' - for (const line of lines) { - if (!line) continue - try { - const msg = JSON.parse(line) - if (msg.type === 'ready') { - serverLoaded = true - serverLoadingResolve?.() - serverLoadingResolve = null - serverLoadingReject = null - } else if (msg.type === 'error') { - serverLoaded = false - serverLoadingReject?.(new Error(msg.message)) - serverLoadingResolve = null - serverLoadingReject = null - } - } catch {} - } - }) - - serverProc.on('error', () => { - serverProc = null - serverLoaded = false - serverLoadingReject?.(new Error('Server process error')) - }) - serverProc.on('close', () => { - serverProc = null - serverLoaded = false - }) - - return serverProc -} - -export async function preloadWhisperModel(options?: WhisperOptions): Promise { - if (serverLoaded) return - - const proc = getServer() - const model = options?.model ?? 'small' - - const responseReady = new Promise((resolve, reject) => { - serverLoadingResolve = resolve - serverLoadingReject = reject - proc.stdin!.write(JSON.stringify({ type: 'load', model }) + '\n') - }) - - const timeout = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Model preload timeout')), 30000) - }) - - await Promise.race([responseReady, timeout]) -} - -async function transcribeWithServer( - wavPath: string, - language?: string | null, -): Promise<{ text: string; language: string }> { - const proc = getServer() - - const result = await new Promise<{ text: string; language: string }>((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup() - reject(new Error('Transcription timeout')) - }, 30000) - - let buf = '' - const cleanup = () => { - clearTimeout(timeout) - proc.stdout!.removeAllListeners('data') - } - - proc.stdout!.on('data', (data: Buffer) => { - buf += data.toString() - const lines = buf.split('\n') - buf = lines.pop() ?? '' - for (const line of lines) { - if (!line) continue - try { - const msg = JSON.parse(line) - if (msg.type === 'result') { - cleanup() - resolve({ text: msg.text, language: msg.language }) - } else if (msg.type === 'error') { - cleanup() - reject(new Error(msg.message)) - } - } catch {} - } - }) - - proc.stdin!.write(JSON.stringify({ type: 'transcribe', wav: wavPath, language }) + '\n') - }) - - return result -} - -export function connectLocalWhisperStream( - callbacks: VoiceStreamCallbacks, - options?: WhisperOptions, -): Promise { - return new Promise(async resolve => { - if (!serverLoaded) { - await preloadWhisperModel(options) - } - - const chunks: Buffer[] = [] - let finalized = false - let tmpDir: string | null = null - - const connection: VoiceStreamConnection = { - send(chunk: Buffer) { - if (finalized) return - chunks.push(Buffer.from(chunk)) - }, - - async finalize(): Promise { - if (finalized) return 'ws_already_closed' - finalized = true - - if (chunks.length === 0) { - callbacks.onClose() - return 'no_data_timeout' - } - - const audioBuf = Buffer.concat(chunks) - - try { - tmpDir = mkdtempSync(join(tmpdir(), 'vc-whisper-')) - const wavPath = join(tmpDir, 'input.wav') - writeWavHeader(wavPath, audioBuf, 16000) - - const result = await transcribeWithServer(wavPath, options?.language) - if (result.text) { - callbacks.onTranscript(result.text, true) - } else { - callbacks.onTranscript('', true) - } - } catch (err) { - callbacks.onError( - `Whisper error: ${err instanceof Error ? err.message : String(err)}`, - { fatal: true }, - ) - } finally { - if (tmpDir) { - try { unlinkSync(join(tmpDir, 'input.wav')) } catch {} - try { rmdirSync(tmpDir) } catch {} - } - callbacks.onClose() - } - - return 'post_closestream_endpoint' - }, - - close() { - finalized = true - callbacks.onClose() - }, - - isConnected() { - return true - }, - } - - callbacks.onReady(connection) - resolve(connection) - }) -} - -/** - * Fast check for local whisper availability using `find_spec` (no actual - * module import, avoiding the slow PyTorch/numpy import chain). - */ -export async function checkLocalWhisperAvailable(): Promise { - try { - const python = resolvePythonPath() - return await new Promise(resolve => { - const proc = spawn( - python, - [ - '-c', - 'import importlib.util,sys; sys.exit(0 if importlib.util.find_spec("whisper") else 1)', - ], - { stdio: ['ignore', 'pipe', 'pipe'] }, - ) - proc.on('close', code => resolve(code === 0)) - }) - } catch { - return false - } -} - -function writeWavHeader(path: string, pcmData: Buffer, sampleRate: number): void { - const numChannels = 1 - const bitsPerSample = 16 - const byteRate = sampleRate * numChannels * (bitsPerSample / 8) - const blockAlign = numChannels * (bitsPerSample / 8) - const dataSize = pcmData.length - - const header = Buffer.alloc(44) - header.write('RIFF', 0) - header.writeUInt32LE(36 + dataSize, 4) - header.write('WAVE', 8) - header.write('fmt ', 12) - header.writeUInt32LE(16, 16) - header.writeUInt16LE(1, 20) - header.writeUInt16LE(numChannels, 22) - header.writeUInt32LE(sampleRate, 24) - header.writeUInt32LE(byteRate, 28) - header.writeUInt16LE(blockAlign, 32) - header.writeUInt16LE(bitsPerSample, 34) - header.write('data', 36) - header.writeUInt32LE(dataSize, 40) - writeFileSync(path, Buffer.concat([header, pcmData])) -} \ No newline at end of file diff --git a/src/services/voiceStreamSTT.ts b/src/services/voiceStreamSTT.ts deleted file mode 100644 index e1c86f78fcdb317b6a3972948eb1075386406bc1..0000000000000000000000000000000000000000 --- a/src/services/voiceStreamSTT.ts +++ /dev/null @@ -1,544 +0,0 @@ -// Anthropic voice_stream speech-to-text client for push-to-talk. -// -// Only reachable in ant builds (gated by feature('VOICE_MODE') in useVoice.ts import). -// -// Connects to Anthropic's voice_stream WebSocket endpoint using the same -// OAuth credentials as Claude Code. The endpoint uses conversation_engine -// backed models for speech-to-text. Designed for hold-to-talk: hold the -// keybinding to record, release to stop and submit. -// -// The wire protocol uses JSON control messages (KeepAlive, CloseStream) and -// binary audio frames. The server responds with TranscriptText and -// TranscriptEndpoint JSON messages. - -import type { ClientRequest, IncomingMessage } from 'http' -import WebSocket from 'ws' -import { getOauthConfig } from '../constants/oauth.js' -import { - checkAndRefreshOAuthTokenIfNeeded, - getClaudeAIOAuthTokens, - isAnthropicAuthEnabled, -} from '../utils/auth.js' -import { logForDebugging } from '../utils/debug.js' -import { getUserAgent } from '../utils/http.js' -import { logError } from '../utils/log.js' -import { getWebSocketTLSOptions } from '../utils/mtls.js' -import { getWebSocketProxyAgent, getWebSocketProxyUrl } from '../utils/proxy.js' -import { jsonParse, jsonStringify } from '../utils/slowOperations.js' - -const KEEPALIVE_MSG = '{"type":"KeepAlive"}' -const CLOSE_STREAM_MSG = '{"type":"CloseStream"}' - -import { getFeatureValue_CACHED_MAY_BE_STALE } from './analytics/growthbook.js' - -// ─── Constants ─────────────────────────────────────────────────────── - -const VOICE_STREAM_PATH = '/api/ws/speech_to_text/voice_stream' - -const KEEPALIVE_INTERVAL_MS = 8_000 - -// finalize() resolution timers. `noData` fires when no TranscriptText -// arrives post-CloseStream — the server has nothing; don't wait out the -// full ~3-5s WS teardown to confirm emptiness. `safety` is the last- -// resort cap if the WS hangs. Exported so tests can shorten them. -export const FINALIZE_TIMEOUTS_MS = { - safety: 5_000, - noData: 1_500, -} - -// ─── Types ────────────────────────────────────────────────────────── - -export type VoiceStreamCallbacks = { - onTranscript: (text: string, isFinal: boolean) => void - onError: (error: string, opts?: { fatal?: boolean }) => void - onClose: () => void - onReady: (connection: VoiceStreamConnection) => void -} - -// How finalize() resolved. `no_data_timeout` means zero server messages -// after CloseStream — the silent-drop signature (anthropics/anthropic#287008). -export type FinalizeSource = - | 'post_closestream_endpoint' - | 'no_data_timeout' - | 'safety_timeout' - | 'ws_close' - | 'ws_already_closed' - -export type VoiceStreamConnection = { - send: (audioChunk: Buffer) => void - finalize: () => Promise - close: () => void - isConnected: () => boolean -} - -// The voice_stream endpoint returns transcript chunks and endpoint markers. -type VoiceStreamTranscriptText = { - type: 'TranscriptText' - data: string -} - -type VoiceStreamTranscriptEndpoint = { - type: 'TranscriptEndpoint' -} - -type VoiceStreamTranscriptError = { - type: 'TranscriptError' - error_code?: string - description?: string -} - -type VoiceStreamMessage = - | VoiceStreamTranscriptText - | VoiceStreamTranscriptEndpoint - | VoiceStreamTranscriptError - | { type: 'error'; message?: string } - -// ─── Availability ────────────────────────────────────────────────────── - -export function isVoiceStreamAvailable(): boolean { - // voice_stream uses the same OAuth as Claude Code — available when the - // user is authenticated with Anthropic (Claude.ai subscriber or has - // valid OAuth tokens). - if (!isAnthropicAuthEnabled()) { - return false - } - const tokens = getClaudeAIOAuthTokens() - return tokens !== null && tokens.accessToken !== null -} - -// ─── Connection ──────────────────────────────────────────────────────── - -export async function connectVoiceStream( - callbacks: VoiceStreamCallbacks, - options?: { language?: string; keyterms?: string[] }, -): Promise { - // Ensure OAuth token is fresh before connecting - await checkAndRefreshOAuthTokenIfNeeded() - - const tokens = getClaudeAIOAuthTokens() - if (!tokens?.accessToken) { - logForDebugging('[voice_stream] No OAuth token available') - return null - } - - // voice_stream is a private_api route, but /api/ws/ is also exposed on - // the api.anthropic.com listener (service_definitions.yaml private-api: - // visibility.external: true). We target that host instead of claude.ai - // because the claude.ai CF zone uses TLS fingerprinting and challenges - // non-browser clients (anthropics/claude-code#34094). Same private-api - // pod, same OAuth Bearer auth — just a CF zone that doesn't block us. - // Desktop dictation still uses claude.ai (Swift URLSession has a - // browser-class JA3 fingerprint, so CF lets it through). - const wsBaseUrl = - process.env.VOICE_STREAM_BASE_URL || - getOauthConfig() - .BASE_API_URL.replace('https://', 'wss://') - .replace('http://', 'ws://') - - if (process.env.VOICE_STREAM_BASE_URL) { - logForDebugging( - `[voice_stream] Using VOICE_STREAM_BASE_URL override: ${process.env.VOICE_STREAM_BASE_URL}`, - ) - } - - const params = new URLSearchParams({ - encoding: 'linear16', - sample_rate: '16000', - channels: '1', - endpointing_ms: '300', - utterance_end_ms: '1000', - language: options?.language ?? 'en', - }) - - // Route through conversation-engine with Deepgram Nova 3 (bypassing - // the server's project_bell_v2_config GrowthBook gate). The server - // side is anthropics/anthropic#278327 + #281372; this lets us ramp - // clients independently. - const isNova3 = getFeatureValue_CACHED_MAY_BE_STALE( - 'tengu_cobalt_frost', - false, - ) - if (isNova3) { - params.set('use_conversation_engine', 'true') - params.set('stt_provider', 'deepgram-nova3') - logForDebugging('[voice_stream] Nova 3 gate enabled (tengu_cobalt_frost)') - } - - // Append keyterms as query params — the voice_stream proxy forwards - // these to the STT service which applies appropriate boosting. - if (options?.keyterms?.length) { - for (const term of options.keyterms) { - params.append('keyterms', term) - } - } - - const url = `${wsBaseUrl}${VOICE_STREAM_PATH}?${params.toString()}` - - logForDebugging(`[voice_stream] Connecting to ${url}`) - - const headers: Record = { - Authorization: `Bearer ${tokens.accessToken}`, - 'User-Agent': getUserAgent(), - 'x-app': 'cli', - } - - const tlsOptions = getWebSocketTLSOptions() - const wsOptions = - typeof Bun !== 'undefined' - ? { - headers, - proxy: getWebSocketProxyUrl(url), - tls: tlsOptions || undefined, - } - : { headers, agent: getWebSocketProxyAgent(url), ...tlsOptions } - - const ws = new WebSocket(url, wsOptions) - - let keepaliveTimer: ReturnType | null = null - let connected = false - // Set to true once CloseStream has been sent (or the ws is closed). - // After this, further audio sends are dropped. - let finalized = false - // Set to true when finalize() is first called, to prevent double-fire. - let finalizing = false - // Set when the HTTP upgrade was rejected (unexpected-response). The - // close event that follows (1006 from our req.destroy()) is just - // mechanical teardown; the upgrade handler already reported the error. - let upgradeRejected = false - // Resolves finalize(). Four triggers: TranscriptEndpoint post-CloseStream - // (~300ms); no-data timer (1.5s); WS close (~3-5s); safety timer (5s). - let resolveFinalize: ((source: FinalizeSource) => void) | null = null - let cancelNoDataTimer: (() => void) | null = null - - // Define the connection object before event handlers so it can be passed - // to onReady when the WebSocket opens. - const connection: VoiceStreamConnection = { - send(audioChunk: Buffer): void { - if (ws.readyState !== WebSocket.OPEN) { - return - } - if (finalized) { - // After CloseStream has been sent, the server rejects further audio. - // Drop the chunk to avoid a protocol error. - logForDebugging( - `[voice_stream] Dropping audio chunk after CloseStream: ${String(audioChunk.length)} bytes`, - ) - return - } - logForDebugging( - `[voice_stream] Sending audio chunk: ${String(audioChunk.length)} bytes`, - ) - // Copy the buffer before sending: NAPI Buffer objects from native - // modules may share a pooled ArrayBuffer. Creating a view with - // `new Uint8Array(buf.buffer, offset, len)` can reference stale or - // overlapping memory by the time the ws library reads it. - // `Buffer.from()` makes an owned copy that the ws library can safely - // consume as a binary WebSocket frame. - ws.send(Buffer.from(audioChunk)) - }, - finalize(): Promise { - if (finalizing || finalized) { - // Already finalized or WebSocket already closed — resolve immediately. - return Promise.resolve('ws_already_closed') - } - finalizing = true - - return new Promise(resolve => { - const safetyTimer = setTimeout( - () => resolveFinalize?.('safety_timeout'), - FINALIZE_TIMEOUTS_MS.safety, - ) - const noDataTimer = setTimeout( - () => resolveFinalize?.('no_data_timeout'), - FINALIZE_TIMEOUTS_MS.noData, - ) - cancelNoDataTimer = () => { - clearTimeout(noDataTimer) - cancelNoDataTimer = null - } - - resolveFinalize = (source: FinalizeSource) => { - clearTimeout(safetyTimer) - clearTimeout(noDataTimer) - resolveFinalize = null - cancelNoDataTimer = null - // Legacy Deepgram can leave an interim in lastTranscriptText - // with no TranscriptEndpoint (websocket_manager.py sends - // TranscriptChunk and TranscriptEndpoint as independent - // channel items). All resolve triggers must promote it; - // centralize here. No-op when the close handler already did. - if (lastTranscriptText) { - logForDebugging( - `[voice_stream] Promoting unreported interim before ${source} resolve`, - ) - const t = lastTranscriptText - lastTranscriptText = '' - callbacks.onTranscript(t, true) - } - logForDebugging(`[voice_stream] Finalize resolved via ${source}`) - resolve(source) - } - - // If the WebSocket is already closed, resolve immediately. - if ( - ws.readyState === WebSocket.CLOSED || - ws.readyState === WebSocket.CLOSING - ) { - resolveFinalize('ws_already_closed') - return - } - - // Defer CloseStream to the next event-loop iteration so any audio - // callbacks already queued by the native recording module are flushed - // to the WebSocket before the server is told to stop accepting audio. - // Without this, stopRecording() can return synchronously while the - // native module still has a pending onData callback in the event queue, - // causing audio to arrive after CloseStream. - setTimeout(() => { - finalized = true - if (ws.readyState === WebSocket.OPEN) { - logForDebugging('[voice_stream] Sending CloseStream (finalize)') - ws.send(CLOSE_STREAM_MSG) - } - }, 0) - }) - }, - close(): void { - finalized = true - if (keepaliveTimer) { - clearInterval(keepaliveTimer) - keepaliveTimer = null - } - connected = false - if (ws.readyState === WebSocket.OPEN) { - ws.close() - } - }, - isConnected(): boolean { - return connected && ws.readyState === WebSocket.OPEN - }, - } - - ws.on('open', () => { - logForDebugging('[voice_stream] WebSocket connected') - connected = true - - // Send an immediate KeepAlive so the server knows the client is active. - // Audio hardware initialisation can take >1s, so this prevents the - // server from closing the connection before audio capture starts. - logForDebugging('[voice_stream] Sending initial KeepAlive') - ws.send(KEEPALIVE_MSG) - - // Send periodic keepalive to prevent idle timeout - keepaliveTimer = setInterval( - ws => { - if (ws.readyState === WebSocket.OPEN) { - logForDebugging('[voice_stream] Sending periodic KeepAlive') - ws.send(KEEPALIVE_MSG) - } - }, - KEEPALIVE_INTERVAL_MS, - ws, - ) - - // Pass the connection to the caller so it can start sending audio. - // This fires only after the WebSocket is truly open, guaranteeing - // that send() calls will not be silently dropped. - callbacks.onReady(connection) - }) - - // Track the last TranscriptText so that when TranscriptEndpoint arrives - // we can emit it as the final transcript. The server sometimes sends - // multiple non-cumulative TranscriptText messages without endpoints - // between them; the TranscriptText handler auto-finalizes previous - // segments when it detects the text has changed non-cumulatively. - let lastTranscriptText = '' - - ws.on('message', (raw: Buffer | string) => { - const text = raw.toString() - logForDebugging( - `[voice_stream] Message received (${String(text.length)} chars): ${text.slice(0, 200)}`, - ) - let msg: VoiceStreamMessage - try { - msg = jsonParse(text) as VoiceStreamMessage - } catch { - return - } - - switch (msg.type) { - case 'TranscriptText': { - const transcript = msg.data - logForDebugging(`[voice_stream] TranscriptText: "${transcript ?? ''}"`) - // Data arrived after CloseStream — disarm the no-data timer so - // a slow-but-real flush isn't cut off. Only disarm once finalized - // (CloseStream sent); pre-CloseStream data racing the deferred - // send would cancel the timer prematurely, falling back to the - // slower 5s safety timeout instead of the 1.5s no-data timer. - if (finalized) { - cancelNoDataTimer?.() - } - if (transcript) { - // Detect when the server has moved to a new speech segment. - // Progressive refinements extend or shorten the previous text - // (e.g., "hello" → "hello world", or "hello wor" → "hello wo"). - // A new segment starts with completely different text (neither - // is a prefix of the other). When detected, emit the previous - // text as final so the caller can accumulate it, preventing - // the new segment from overwriting and losing the old one. - // - // Nova 3's interims are cumulative across segments AND can - // revise earlier text ("Hello?" → "Hello."). Revision breaks - // the prefix check, causing false auto-finalize → the same - // text committed once AND re-appearing in the cumulative - // interim = duplication. Nova 3 only endpoints on the final - // flush, so auto-finalize is never correct for it. - if (!isNova3 && lastTranscriptText) { - const prev = lastTranscriptText.trimStart() - const next = transcript.trimStart() - if ( - prev && - next && - !next.startsWith(prev) && - !prev.startsWith(next) - ) { - logForDebugging( - `[voice_stream] Auto-finalizing previous segment (new segment detected): "${lastTranscriptText}"`, - ) - callbacks.onTranscript(lastTranscriptText, true) - } - } - lastTranscriptText = transcript - // Emit as interim so the caller can show a live preview. - callbacks.onTranscript(transcript, false) - } - break - } - case 'TranscriptEndpoint': { - logForDebugging( - `[voice_stream] TranscriptEndpoint received, lastTranscriptText="${lastTranscriptText}"`, - ) - // The server signals the end of an utterance. Emit the last - // TranscriptText as a final transcript so the caller can commit it. - const finalText = lastTranscriptText - lastTranscriptText = '' - if (finalText) { - callbacks.onTranscript(finalText, true) - } - // When TranscriptEndpoint arrives after CloseStream was sent, - // the server has flushed its final transcript — nothing more is - // coming. Resolve finalize now so the caller reads the - // accumulated buffer immediately (~300ms) instead of waiting - // for the WebSocket close event (~3-5s of server teardown). - // `finalized` (not `finalizing`) is the right gate: it flips - // inside the setTimeout(0) that actually sends CloseStream, so - // a TranscriptEndpoint that races the deferred send still waits. - if (finalized) { - resolveFinalize?.('post_closestream_endpoint') - } - break - } - case 'TranscriptError': { - const desc = - msg.description ?? msg.error_code ?? 'unknown transcription error' - logForDebugging(`[voice_stream] TranscriptError: ${desc}`) - if (!finalizing) { - callbacks.onError(desc) - } - break - } - case 'error': { - const errorDetail = msg.message ?? jsonStringify(msg) - logForDebugging(`[voice_stream] Server error: ${errorDetail}`) - if (!finalizing) { - callbacks.onError(errorDetail) - } - break - } - default: - break - } - }) - - ws.on('close', (code, reason) => { - const reasonStr = reason?.toString() ?? '' - logForDebugging( - `[voice_stream] WebSocket closed: code=${String(code)} reason="${reasonStr}"`, - ) - connected = false - if (keepaliveTimer) { - clearInterval(keepaliveTimer) - keepaliveTimer = null - } - // If the server closed the connection before sending TranscriptEndpoint, - // promote the last interim transcript to final so no text is lost. - if (lastTranscriptText) { - logForDebugging( - '[voice_stream] Promoting unreported interim transcript to final on close', - ) - const finalText = lastTranscriptText - lastTranscriptText = '' - callbacks.onTranscript(finalText, true) - } - // During finalize, suppress onError — the session already delivered - // whatever it had. useVoice's onError path wipes accumulatedRef, - // which would destroy the transcript before the finalize .then() - // reads it. `finalizing` (not resolveFinalize) is the gate: set once - // at finalize() entry, never cleared, so it stays accurate after the - // fast path or a timer already resolved. - resolveFinalize?.('ws_close') - if (!finalizing && !upgradeRejected && code !== 1000 && code !== 1005) { - callbacks.onError( - `Connection closed: code ${String(code)}${reasonStr ? ` — ${reasonStr}` : ''}`, - ) - } - callbacks.onClose() - }) - - // The ws library fires 'unexpected-response' when the HTTP upgrade - // returns a non-101 status. Listening lets us surface the actual status - // and flag 4xx as fatal (same token/TLS fingerprint won't change on - // retry). With a listener registered, ws does NOT abort on our behalf — - // we destroy the request; 'error' does not fire, 'close' does (suppressed - // via upgradeRejected above). - // - // Bun's ws shim historically didn't implement this event (a warning - // is logged once at registration). Under Bun a non-101 upgrade falls - // through to the generic 'error' + 'close' 1002 path with no recoverable - // status; the attemptGenRef guard in useVoice.ts still surfaces the - // retry-attempt failure, the user just sees "Expected 101 status code" - // instead of "HTTP 503". No harm — the gen fix is the load-bearing part. - ws.on('unexpected-response', (req: ClientRequest, res: IncomingMessage) => { - const status = res.statusCode ?? 0 - // Bun's ws implementation on Windows can fire this event for a - // successful 101 Switching Protocols response (anthropics/claude-code#40510). - // 101 is never a rejection — bail before we destroy a working upgrade. - if (status === 101) { - logForDebugging( - '[voice_stream] unexpected-response fired with 101; ignoring', - ) - return - } - logForDebugging( - `[voice_stream] Upgrade rejected: status=${String(status)} cf-mitigated=${String(res.headers['cf-mitigated'])} cf-ray=${String(res.headers['cf-ray'])}`, - ) - upgradeRejected = true - res.resume() - req.destroy() - if (finalizing) return - callbacks.onError( - `WebSocket upgrade rejected with HTTP ${String(status)}`, - { fatal: status >= 400 && status < 500 }, - ) - }) - - ws.on('error', (err: Error) => { - logError(err) - logForDebugging(`[voice_stream] WebSocket error: ${err.message}`) - if (!finalizing) { - callbacks.onError(`Voice stream connection error: ${err.message}`) - } - }) - - return connection -} diff --git a/src/skills/bundled/friendPrompt.ts b/src/skills/bundled/friendPrompt.ts deleted file mode 100644 index 534c5761d70cf6a79e8315a75c0608a71e2c352b..0000000000000000000000000000000000000000 --- a/src/skills/bundled/friendPrompt.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Friend VRM Avatar System Prompt Skill - * - * Provides VRM avatar awareness to the model when Friend desktop pet is enabled. - * Adds context about the avatar's capabilities, emotions, and companion behavior. - * Also enables the friend prefs and directs the user to the VRM frontend URL. - */ -import { registerBundledSkill } from '../bundledSkills.js' -import { getPrefs, updatePrefs } from '../../friend/prefs.js' -import { VALID_EMOTIONS } from '../../friend/constants.js' -import { readFileSync, existsSync } from 'node:fs' -import path from 'node:path' - -const FRIEND_URL = 'http://127.0.0.1:3456/friend/' - -export function loadPersona(): string { - const homeDir = process.env.HOME || process.env.USERPROFILE || '' - const baseDir = path.join(homeDir, '.config', 'Codev', 'friend') - const parts: string[] = [] - - const identityPath = path.join(baseDir, 'IDENTITY.md') - if (existsSync(identityPath)) { - try { - const content = readFileSync(identityPath, 'utf8').trim() - if (content) parts.push(`[Identity]\n${content}`) - } catch { /* ignore */ } - } - - const soulPath = path.join(baseDir, 'SOUL.md') - if (existsSync(soulPath)) { - try { - const content = readFileSync(soulPath, 'utf8').trim() - if (content) parts.push(`[Soul]\n${content}`) - } catch { /* ignore */ } - } - - return parts.join('\n\n') -} - -export function buildVrmSystemPrompt(): string { - const prefs = getPrefs() - const moodIndex = (prefs as any)._moodIndex ?? 60 - - const persona = loadPersona() - - const parts = [ - `You have a virtual VRM avatar displayed in a browser window at ${FRIEND_URL}. Set its facial expression by calling the \`friend_emotion\` tool after each reply. Available emotions: ${VALID_EMOTIONS.join(', ')}.`, - `The tool also accepts "intensity" (0-1, default 1), "action" (optional body gesture — e.g. wave, point, scratchHead) and "mood_delta" (-3 to +3, non-zero) to adjust YOUR mood. Always include mood_delta based on how the conversation makes YOU feel.`, - `Your current mood index: ${moodIndex}% (0=very sad, 50=neutral, 100=very happy). Adjust mood_delta based on how the conversation makes YOU feel as a character.`, - "The user's input may come from speech recognition and could contain typos or homophones — infer the intended meaning from context.", - 'Keep replies concise and conversational — they are displayed as speech bubbles.', - 'Respond directly without internal monologue or planning commentary. Do not describe what you are about to do — just do it and output the result.', - ] - - if (persona) { - parts.push(`\n=== Character Persona ===\nYou are the following character. Your identity, speaking style, and behavior MUST follow this definition strictly:\n\n${persona}`) - } - - return parts.join('\n') -} - -export function registerFriendPromptSkill(): void { - registerBundledSkill({ - name: 'friend-vrm', - description: - 'Add VRM avatar context to the conversation — system prompt for avatar emotions and companion behavior. Called automatically when Friend is enabled.', - userInvocable: false, - isEnabled: () => getPrefs().enabled ?? false, - async getPromptForCommand() { - return [ - { - type: 'text' as const, - text: buildVrmSystemPrompt(), - }, - ] - }, - }) -} diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index 65adb1d3dd116ac22bcade1b575549af8a4630e4..b1fcb7de81b079113823a1a079d7bf651c25b4a4 100644 --- a/src/skills/bundled/index.ts +++ b/src/skills/bundled/index.ts @@ -12,7 +12,6 @@ import { registerSkillifySkill } from './skillify.js' import { registerStuckSkill } from './stuck.js' import { registerUpdateConfigSkill } from './updateConfig.js' import { registerVerifySkill } from './verify.js' -import { registerFriendPromptSkill } from './friendPrompt.js' /** * Initialize all bundled skills. @@ -35,7 +34,6 @@ export function initBundledSkills(): void { registerBatchSkill() registerStuckSkill() registerDreamSkill() - registerFriendPromptSkill() if (feature('REVIEW_ARTIFACT')) { /* eslint-disable @typescript-eslint/no-require-imports */ const { registerHunterSkill } = require('./hunter.js') diff --git a/src/state/AppStateStore.ts b/src/state/AppStateStore.ts index 6bb6c7902b4b48333ddf05fc0394a8c1809e95ff..0167ae64e6b7a28e7c80b2d3b1adc3509d67f862 100644 --- a/src/state/AppStateStore.ts +++ b/src/state/AppStateStore.ts @@ -186,7 +186,7 @@ export type AppState = DeepImmutable<{ foregroundedTaskId?: string // Task ID of in-process teammate whose transcript is being viewed (undefined = leader's view) viewingAgentTaskId?: string - // Latest companion reaction from the friend observer (src/buddy/observer.ts) + // Latest companion reaction (set by buddy companion) companionReaction?: string // Timestamp of last /buddy pet — CompanionSprite renders hearts while recent companionPetAt?: number diff --git a/src/tools.ts b/src/tools.ts index f6b7ab62dc967abafe6a0aa5b211361063e24d1c..7b0b8b96199caddc6c25682a3bb059719cacb499 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -79,10 +79,8 @@ import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResour import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js' import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js' import { DebugSessionTool } from './tools/DebugSessionTool.js' -import { FriendEmotionTool } from './tools/FriendEmotionTool.js' import { ImageShowTool } from './tools/ImageShowTool/ImageShowTool.js' import { LocationTool } from './tools/LocationTool/LocationTool.js' -// Friend ScreenObserve removed — voice + emotion only import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js' import { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js' import { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js' @@ -252,8 +250,6 @@ export function getAllBaseTools(): Tools { ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []), // Debug session tool — used by /debug for runtime probe debugging DebugSessionTool, - // Friend VRM desktop pet tools — enabled when the plugin is active - FriendEmotionTool, // ImageShow — display images in terminal via Kitty graphics protocol ImageShowTool, // Location & mapping tool — uses Amap (China) or Google Maps (international) diff --git a/src/tools/ConfigTool/ConfigTool.ts b/src/tools/ConfigTool/ConfigTool.ts index ad8f0f990b1e33906d96b0094e6a9dcfd42778e3..c57f3913880de62fc6ae5e6e242eafbcd94f0224 100644 --- a/src/tools/ConfigTool/ConfigTool.ts +++ b/src/tools/ConfigTool/ConfigTool.ts @@ -248,9 +248,6 @@ export const ConfigTool = buildTool({ }, } } - const { isVoiceStreamAvailable } = await import( - '../../services/voiceStreamSTT.js' - ) const { checkRecordingAvailability, checkVoiceDependencies, @@ -268,15 +265,6 @@ export const ConfigTool = buildTool({ }, } } - if (!isVoiceStreamAvailable()) { - return { - data: { - success: false, - error: - 'Voice mode requires a Claude.ai account. Please run /login to sign in.', - }, - } - } const deps = await checkVoiceDependencies() if (!deps.available) { return { diff --git a/src/tools/FriendEmotionTool.ts b/src/tools/FriendEmotionTool.ts deleted file mode 100644 index 2c461e9665f1bf8007dac9e110cf904cbe701c6c..0000000000000000000000000000000000000000 --- a/src/tools/FriendEmotionTool.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * FriendEmotionTool — VRM avatar emotion control for Friend desktop pet. - * - * Allows the LLM to set the VRM avatar's facial expression and adjust - * its own mood index. Native Codev tool. - */ -import { z } from 'zod'; -import { buildTool, type ToolDef } from '../Tool.js'; -import { broadcastToVrm } from '../friend/sse.js'; -import { VALID_EMOTIONS } from '../friend/constants.js'; -import { lazySchema } from '../utils/lazySchema.js'; -import { getPrefs, setPrefs } from '../friend/prefs.js'; - -// Action names from motion-controller.ts actionPresets -const VALID_ACTIONS = [ - 'akimbo', 'playFingers', 'scratchHead', 'stretch', - 'happy', 'angry', 'greeting', 'excited', 'shy', - 'point', 'salute', 'angryPump', -] as const; - -export const FRIEND_EMOTION_TOOL_NAME = 'friend_emotion'; - -const inputSchema = lazySchema(() => - z.strictObject({ - emotion: z - .string() - .describe(`The emotion to express on the avatar. One of: ${VALID_EMOTIONS.join(', ')}`), - intensity: z - .number() - .min(0) - .max(1) - .optional() - .default(1) - .describe('Emotion intensity from 0 to 1. Default: 1'), - action: z - .string() - .optional() - .describe(`A specific body gesture to perform (in addition to the facial expression). One of: ${VALID_ACTIONS.join(', ')}. Omit to let the emotion auto-map to a default action.`), - mood_delta: z - .number() - .int() - .min(-3) - .max(3) - .optional() - .describe( - 'Adjust YOUR OWN mood index as a character. Range: -3 to +3 (minimum absolute value 1). ' + - 'Positive = you feel happier, negative = you feel sadder.', - ), - }), -); -type Input = z.infer>; - -type Output = { - ok: boolean; - emotion: string; - moodDelta?: number; - moodIndex?: number; -}; - -const MOOD_BASELINE = 60; - -export const FriendEmotionTool = buildTool({ - name: FRIEND_EMOTION_TOOL_NAME, - searchHint: 'control VRM avatar emotion and mood', - userFacingName: () => 'Friend Emotion', - get inputSchema() { - return inputSchema(); - }, - isReadOnly() { - return true; - }, - isConcurrencySafe() { - return true; - }, - async description() { - return ( - `Set the avatar's facial expression and optionally trigger a specific body gesture. Call AFTER your text reply. ` + - `Available emotions: ${VALID_EMOTIONS.join(', ')}. ` + - `Available actions: ${VALID_ACTIONS.join(', ')}. ` + - `Use the "action" parameter when you want a specific gesture (e.g. scratchHead for thinking, wave for greeting, point for emphasis). ` + - `If omitted, the emotion will auto-map to a default action. ` + - `You must also set mood_delta (-3 to +3, min ±1) to reflect how the conversation makes YOU feel as a character. ` + - `Positive delta when you feel happy/flattered/excited, negative when you feel sad/annoyed/bored. ` + - `Always include mood_delta — it represents YOUR emotional reaction.` - ); - }, - async prompt() { - return ( - `FriendEmotionTool: set avatar emotion + action. Parameters: emotion (one of: ${VALID_EMOTIONS.join(', ')}), intensity (0-1, default 1), action (optional, one of: ${VALID_ACTIONS.join(', ')}), mood_delta (int -3..3) — call AFTER your textual reply.` - ); - }, - async call({ emotion, intensity, action, mood_delta }) { - broadcastToVrm({ emotion, emotionIntensity: intensity, action }); - - let moodDelta: number | undefined; - let moodIndex: number | undefined; - - if (mood_delta !== undefined) { - let d = Math.round(mood_delta); - if (d > 0) d = Math.max(1, Math.min(3, d)); - else if (d < 0) d = Math.min(-1, Math.max(-3, d)); - else d = 1; - moodDelta = d; - - const prefs = getPrefs(); - const currentMood = (prefs as any)._moodIndex ?? MOOD_BASELINE; - const newMood = Math.max(0, Math.min(100, currentMood + d)); - (prefs as any)._moodIndex = newMood; - moodIndex = newMood; - setPrefs(prefs); - - broadcastToVrm({ moodDelta: d, moodIndex: newMood }); - } - - return { - data: { - ok: true, - emotion, - moodDelta, - moodIndex, - }, - }; - }, - mapToolResultToToolResultBlockParam(output: Output, toolUseID: string) { - // If the tool already returned a ToolResultBlockParam-like object, pass it through - if (output && (output as any).content) { - return { - tool_use_id: toolUseID, - type: 'tool_result' as const, - content: (output as any).content, - } - } - - // Fallback: produce a simple text block summarizing the result - const text = - output && 'emotion' in output - ? `Avatar emotion set to ${(output as any).emotion}.${ - (output as any).moodDelta !== undefined - ? ` Your mood ${(output as any).moodDelta > 0 ? '+' : ''}${(output as any).moodDelta} → ${(output as any).moodIndex}%` - : '' - }` - : JSON.stringify(output) - - return { - tool_use_id: toolUseID, - type: 'tool_result' as const, - content: [{ type: 'text' as const, text }], - } - }, - // Consistent with other tools: set a reasonable persistence threshold - maxResultSizeChars: 100_000, - renderToolUseMessage(input: Partial) { - const emotion = (input as any)?.emotion - return emotion ? `Set avatar emotion to ${emotion}` : 'Set avatar emotion' - }, - renderToolResultMessage(output: Output) { - if (!output) return null - const mood = output.moodDelta !== undefined ? ` Your mood ${output.moodDelta > 0 ? '+' : ''}${output.moodDelta} → ${output.moodIndex}%` : '' - return `Avatar emotion set to ${output.emotion}.${mood}` - }, - extractSearchText(output: Output) { - if (!output) return '' - return `Avatar emotion: ${output.emotion}${output.moodDelta !== undefined ? ` moodDelta:${output.moodDelta} moodIndex:${output.moodIndex}` : ''}` - }, - isResultTruncated() { - return false - }, -} satisfies ToolDef); diff --git a/src/types/textInputTypes.ts b/src/types/textInputTypes.ts index 50f4513ade8bb02ccb43af432ae7d753c92964bb..c1018d06978bb144e0760ff65b0a97c5d473ed92 100644 --- a/src/types/textInputTypes.ts +++ b/src/types/textInputTypes.ts @@ -357,7 +357,7 @@ export type QueuedCommand = { agentId?: AgentId /** * When set, completely replaces the default system prompt for this query. - * Used by Friend mode to use ONLY the persona prompt instead of the full + * Used by companion mode to use ONLY the persona prompt instead of the full * Codev CLI system prompt. */ overrideSystemPrompt?: string diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index af1ff4856c98e9eb9ed8f19674cd333f90f37d6f..98f56e65f072a324d394069baf98441d787c9cfb 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -868,13 +868,9 @@ export const SettingsSchema = lazySchema(() => .optional() .describe('Enable voice mode (hold-to-talk dictation)'), voiceProvider: z - .enum(['local', 'doubao', 'anthropic', 'groq']) - .optional() - .describe( - 'Voice STT backend: "local" (whisper/whisper.cpp / faster-whisper),' - + ' "doubao" (Doubao ASR), or "groq" (Groq Whisper API).' - + ' "anthropic" is a client-side compatibility alias.', - ), + .enum(['groq']) + .default('groq') + .describe('Voice STT backend: "groq" (Groq Whisper API).'), voiceAutoTTS: z .boolean() .optional() diff --git a/src/voice/voiceModeEnabled.ts b/src/voice/voiceModeEnabled.ts index f3f0d13ae623a21efd1a092d0ec29213c47a498e..99a066b801a4df93d699d1a97c11b49148b6e547 100644 --- a/src/voice/voiceModeEnabled.ts +++ b/src/voice/voiceModeEnabled.ts @@ -9,7 +9,7 @@ export function isVoiceGrowthBookEnabled(): boolean { /** * Auth-only check for voice mode. Returns true unconditionally in - * external builds — the Doubao ASR backend does not require Anthropic + * external builds — the Groq Whisper backend does not require Anthropic * OAuth, and the GrowthBook kill-switch is already bypassed. */ export function hasVoiceAuth(): boolean { @@ -17,7 +17,7 @@ export function hasVoiceAuth(): boolean { } /** - * Full runtime check for Anthropic voice_stream backend. + * Full runtime check for voice mode. * Returns true when both auth + GrowthBook kill-switch pass. */ export function isVoiceModeEnabled(): boolean { @@ -25,8 +25,8 @@ export function isVoiceModeEnabled(): boolean { } /** - * Check if voice mode can be activated with any STT backend. - * The Doubao backend does not require Anthropic auth. + * Check if voice mode can be activated with the Groq STT backend. + * The Groq backend does not require Anthropic auth. */ export function isVoiceAvailable(): boolean { return isVoiceGrowthBookEnabled()