4e822df5edf7b854971f082b0c1c14093c7687be
上行(用户按住麦克风说话): - `AudioRecord` 16 kHz / 单声道 / 16-bit PCM → 自封 44 字节 RIFF/WAVE 头。 不用 `MediaRecorder`:它默认出 m4a/aac,**不在**契约白名单 wav|mp3|ogg 里; WAV 纯手写、零新依赖(`domain/chat/VoiceClip.kt` 是纯函数,可穷举单测)。 - 体积闸门 5 MiB(解码后):契约没给 voice_input 的上限,按已知最严的那一档 (chat.image 的 base64 硬顶)设计;录满 60 秒只用掉 36%,它是第二道闸。 空 / 太短(<1s)/ 超限三档**明确拒绝并说一句**,绝不静默。 - `voice_input` 补上 `deviceId` / `clientMsgId`(与原骨架的四个键纯增量, 省略时逐字节不变)。幂等键由纯函数 `voiceClientMsgIdFor` 决定:**重发复用同一个**。 - 乐观气泡与文本同源:同一个 `pending` 列表(`PendingMessage.voiceDurationMs`)、 同一个 `rollbackPending`;失败时那段 WAV 留在 VM 私有字段里,界面上给「重发 / 丢弃」。 语音**不进**自动补发队列(几 MB 字节不该钉在队列里等连接)。 下行(她的语音回复): - `voice_output` 解析补齐 `sessionId`(帧壳没有,从 data 提到帧上,会话闸才真的生效)、 `deviceId`、`provider`。 - `provider == "silent"`(或 audio 缺席)⇒ **不画播放器**,只显示 reply 文字 + 一句说明。 - `text` = 转写(那段音频被识别成什么),`reply` = **她说的话**(音频念的就是它): 显示 `reply` 优先、缺席才退到 `text`(纯函数 `voiceReplyText`)。 - 音频 base64 解码落 `cacheDir/media/`,复用 W62 那条 `VoicePlayer` 通道播放 (新增 `toggleClip`,同一套 MediaPlayer 生命周期与身份比对);自动播放只认第一次 (切走再回来不会突然响一段旧语音)。 其它:Manifest 加 RECORD_AUDIO(运行时申请,被拒不再重复申请、文案指路系统设置); UI 全部 Miuix(extended 包的 Mic),录音中整行换成「录音中 0:07 / 取消 / 发送」。 验收:assembleDebug 通过;testDebugUnitTest 全量 133 suites / 1670 tests / 0 failures (基线 129 / 1636,新增 4 个 suite / 34 个用例)。
Releases
5
Alpha_v0.1.0_4
Latest