Rodger-Wang
|
f9b9c8c72d
|
client: 翻译默认归档进语音纪要;列表改本地优先;登录按包收敛;EMAI 走 A2DP
## 通话/同声/面对面翻译 → 语音纪要
三种翻译模式默认边翻边录,结束时把音频**连同已有转写**一起归档
(rtype=VOICETRANSLAT),服务端跳过识别、直接用智能模板(General-Autopilot)总结,
在会议纪要里就能看到。转写段的构造见 translation_export.dart(纯函数,带金测试):
- 时间是**相对录音起点的毫秒**并扣掉暂停区间——面对面是逐句按住说话、松开就暂停的,
不扣的话越往后偏得越多;写成绝对时间戳会让点一句跳到音频末尾之外;
- 说话人用 Speaker_0/Speaker_1,与服务端转写同一口径(转写页直接把这个字段当文案显示,
裸 0/1 会和别的纪要长得不一样,喂给 LLM 抽待办时也会混出 Speaker2 这种负责人);
- AST socket 断开后 _terminateCallOnError 会取消事件订阅,重开时收不到任何转写
(真机上表现为「有日志但转写 0 段」),改成重新订阅。
## 语音纪要列表:服务端是真源、本地 sqlite 是缓存
列表接口改回骨架字段后(服务端同批),客户端改成本地优先渲染 + 后台对齐:
- Synchrodata 重写:拉骨架 → upsert → 删掉「本地有服务端没有」的记录(连本地音频、
上传队列、轮询任务一起清)。**业务错误时 data 是 null 不是空列表**,判空错了
会把本地整个清空;
- 详情进页面时才拉整条;离线操作队列那套上行是死代码(服务端接口整个注释掉了),删除。
## 登录按包收敛
苹果包只留苹果+游客、安卓包只留微信+游客、谷歌包只留邮箱+游客。
判定抽成 resolveLoginMethods 纯函数(login_methods.dart + 8 条测试),
邮箱包还要锁死国家区号选择。
## EMAI 与其它
- 手机侧触发的对话走 A2DP 而不是语音处理通道:iOS 上 A2DP 输出与 voiceChat 模式
互斥,开了后者蓝牙耳机会当场切到手机扬声器。按当前实际输出路由决定要不要开
voiceProcessing/回声消除;
- EMAI「开始对话」闪退:探测输入格式时 new 了一个一次性 AVAudioEngine 去读 inputNode,
配置变更时崩在 AVAudioIONodeImpl(EXC_BAD_ACCESS)。改成直接问 AVAudioSession;
- 语音纪要文件名可改:每行右侧加铅笔图标,复用已有的重命名弹窗(原来只有长按菜单,
实际入口已不可达);
- 拾忆上报 IANA 时区名:DateTime.now().timeZoneName 给的是「CST」这种缩写,
服务端 LoadLocation 解析不了、静默退回容器时区,海外用户的提醒时刻会全按北京时间算。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
3 weeks ago |
Rodger-Wang
|
63634532ce
|
语音纪要等待态换成自绘动效;修 tasktype 存两张表导致重进后等待态消失
## 等待态
原来是 assets/lottie/meeting_wait.json —— 一张「JOIN」对话气泡插画,和本页在做的事
(把音频变成文稿)没有任何关系,配色也与全站的 222° 冷蓝灰不搭,深色主题下更是一块
突兀的白。改成自绘:上面一排声波表示音频正在被读,下面几条带流光的骨架行预告即将
生成的文稿形状,两者共用同一个 AnimationController。
自绘而不是再换一个 Lottie:① 不用为一个等待态再背一份 json 资源;② 颜色全部走
AppPalette,明暗主题自动跟随,插画做不到;③ 四个 tab(概览/发言/全文/思维导图)
共用同一个等待态,自绘好调。meeting_wait.json 一并删除(全仓零引用;assets/lottie/
在 pubspec 里是按目录声明的,不用改配置)。
## 重进后等待态消失 —— tasktype 在两张表里各存一份
真机上那条记录的实际状态是 meeting.tasktype=1 / meetingdetails.tasktype=0:
列表页读前者,显示「转写中」;详情页读后者,拿到 0,于是什么等待态都没有。
两张表分头写,而且中间态漏了:
- submitTask / refreshTask / readTask 只调 editMeetingTitle,**只写 meeting**
- _queryTask 里写 meetingdetails 的两次都藏在里程碑分支里(>=3、==5/6),
1→2(等待转写→转写中)这一跳、以及失败态 10002,meetingdetails 一次都不写
首次进入之所以有动画,靠的是详情控制器里那句内存赋值 meetingDetails.value.tasktype=1;
控制器一销毁,重进读库就是 0 —— 表现为「退出重进特效就不见了」。
改法:
1. SqfliteApi.setMeetingTaskType(id, type):一个事务同时写两张表,成为写 tasktype 的
**唯一入口**。⚠️ 别再直接 editMeetingTitle({'tasktype': x}),那只写一张。
2. 轮询里的状态推进从两个里程碑分支里提到分支之后无条件执行 —— 数据先落、状态最后推,
详情页的 ever(tasktype) 监听读到新状态时数据已经就位。tasktype.value 同样不再只在
分支里更新,1→2 这一跳现在也会通知 UI。
3. 详情页以 meeting 表那份为准(它是历史上唯一每次状态变化都会写的),给**存量**错位
数据兜底:不用等下一次状态跳变就能立刻正常。
真机验证(华为 ALN-AL80):用那条 details=0/meeting=1 的错位记录进详情,等待态正常
(改前是空白页);退出重进,等待态仍在;退出后查库两张表都是 1;轮询把状态推到失败态后
两张表都是 10002(改前 details 恒为 0);终态下等待态正确消失、显示「分析失败」。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
4 weeks ago |
Rodger-Wang
|
714a05b6d4
|
修 iOS EMAI 没有语音播报、连续对话上行断供;接入 MCP 令牌
iOS 上助手一直不出声,和这次改连续对话无关,是播报本来就坏的。三轮真机对照
实测:just_audio 喂的是「长度未知的直播 WAV 流」,AVPlayer 只发一次
`Range: bytes=0-1` 的嗅探请求就判定资源不可播,回 -11850 且 processingState
直接落 idle,一次都没真正起播。声明长度、支持 range、把收到的字节全缓存下来
允许重放,三种都试过,结论一样。ExoPlayer 不发这个嗅探请求,所以安卓一直是
好的——这就是「安卓有声、苹果没声」的全部原因。
iOS 改走原生 AVAudioEngine 排队播 PCM(PcmSpeakerPlayer),安卓不动。
验证方式:一次性灌 3 秒音频,7ms 喂完但 3708ms 才播完,证明是实时渲染不是丢弃。
连续对话的上行也是断的,服务端一直回 ClientAudioTimeout。两个原因:
- _ensureContinuousMic 被 Started 事件和 _enterContinuous 并发调用,两边都在
`_micRunning` 置位前通过守卫,同一个 recorder 被 startStream 两遍,第二次把
第一次那条流顶掉。控制器侧改成同步置位,MicPcmStreamer 内部再加一层 in-flight 去重。
- 开麦前没配好并激活音频会话。会话停在 soloAmbient 且未激活时,startStream
会**成功返回**、引擎也起得来,但输入 tap 一个 buffer 都不回调,客户端毫无报错。
麦克风和播放器统一到 EmaiAudioSession 一份会话配置:各配各的会在连续对话下
互相覆盖。iOS 固定 voiceChat——record 的 echoCancel 会打开 voice-processing 并
把 mode 改成 voiceChat 持有着,播放这边写回 default 就是两边互相掀桌子。
androidAudioAttributes 仍是 media,别改成 voiceCommunication(华为会静音)。
连续对话交互按要求改:进页面只建会话不开麦,短按开始、再按结束、可反复,
退出即停,10 秒无对话往来自动结束。计时依据必须是对话事件不是音频包——
麦克风没人说话也在稳定出包,拿它续命这个超时永远不会触发。
另修 _enterContinuous 在 reconnect() 一返回就判 `!= ready`:ready 要等服务端回
Started(一次网络往返),几乎每次都命中,于是刚打开的连续对话被当场关掉,
而 Started 稍后到达又会把麦开起来,留下一个界面上看不见的常开麦克风。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
1 month ago |
Rodger-Wang
|
2d1ee0b783
|
apps/client 首次入库(Flutter 移动端)
从 /Users/yunyanzhineng/work/eaimar_client-main 搬入,未带原 .git,历史留在原仓库。
3215 个文件 / 约 118MB,其中约 40MB 是构建必需的预编译 SDK(azure_speech 的
MicrosoftCognitiveServicesSpeech.xcframework、device_jieli 的 JL_BLEKit 等)。
build/ (5.2G)、ios/Pods/ (311M)、android/.gradle/ (120M) 已由 .gitignore 挡掉。
本次入库的代码包含两项刚做完的改动:
1) 游客登录入口接上服务端开关。此前 _checkGuestLoginConfig() 恒定写死 true,
入口根本不读配置。现改为取服务端算好的结论,并把入口所在的 Builder 改成 Obx
——配置是异步拿到的,Builder 只渲染 onInit 时的初值 false,入口再也不会出现。
拿不到配置时按隐藏处理:默认显示会在送审期把不该露的入口露给审核员。
⚠️ 已发布的老客户端关不掉(写死 true),只对本版及以后生效。
2) iOS 上架合规修复:
- 移除 ATT/IDFA:删掉 Info.plist 的 NSUserTrackingUsageDescription、
SKAdNetworkItems、NSAdvertisingAttributionReportEndpoint,删掉 splash 里的
授权请求与 app_tracking_transparency 依赖。本 App 从不读取 IDFA,也没有任何
广告/归因 SDK,弹框纯粹劝退用户,还要在 App Store Connect 申报追踪数据类型。
- ios/Podfile 里那段 config.build_settings.delete('NSUserTrackingUsageDescription')
是无效代码(该 key 在 Info.plist 里,不是 build setting),换成注释说明。
- 通讯录符号(ITMS-90683 被拒原因)随之一并解决:permission_handler 走 SPM 后
按 Info.plist 有无对应 key 决定编不编,判定结果被 SwiftPM 按内容哈希缓存,
改完 plist 必须清 DerivedData 与 manifests 缓存才会重新求值。
另附 GOOGLE_PLAY_RELEASE_CHECKLIST.md:Google Play 上架阻塞项的实测梳理
(targetSdk 36 与 Billing 8 的 2026-08-31 期限、16KB 页对齐不合规的两个 .so、
支付回退逻辑的下架风险)。
⚠️ android/app/sign/ 下的 eaimar.keystore 与 Eaimar.properties(含明文签名密码)
随本次提交入库,与本仓库"私有仓库下接受真实凭据"的既有取舍一致。
仓库若要公开,这两个必须先摘出去。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
1 month ago |