本文目录9 节

页面上只有一个按钮,背后却不是一个状态

聊天消息旁边的语音按钮看起来很简单:点击后生成音频,再播放或暂停。实际联调时,问题分散在多个层次:有些会话没有开放语音,有些消息只有旁白没有对白,流式文本尚未结束,签名服务可能要求购买,缓存消息可能没有完整业务标识,App 进入后台后页面也不一定卸载。

如果把这些条件都压进按钮的 v-if,界面可能看起来正确,请求和播放器仍然会留下绕过路径。

我最后把 TTS 拆成三层:

  1. 会话权限:当前对话是否开放 TTS;
  2. 消息资格:这条消息是否存在真正需要朗读的内容;
  3. 请求与播放安全:点击后是否具备有效标识、文本和可控的播放器生命周期。

第一层:会话字段决定功能是否开放

后端在创建会话时返回一个布尔开关。前端需要把它保存到会话记录,再从当前会话传递到消息组件。

这里使用严格布尔判断:只有明确为 true 才开放;字段缺失、false 或其他类型都按关闭处理。这样可以避免旧会话或异常响应意外显示功能。

按钮和隐藏音频元素受这个开关控制,但请求处理函数仍会再次检查。UI 条件只负责展示,不能作为唯一的权限保护;旧状态、手动事件或异步更新都可能绕过它。

第二层:消息是否适合朗读,是另一条规则

产品规则确认后,纯旁白不生成语音,包含有效对白的消息才显示入口。会话开放不代表每一条消息都适合 TTS。

项目内容使用半角双引号标记对白。我把消息拆成:

text    -> 真正朗读的对白
context -> 去掉对白后的动作、旁白和场景

context 只给语音服务提供语气参考,不应该被读出来;没有对白时,前端隐藏按钮并在请求层阻止空文本。

流式消息还增加了一个时机问题。文本没有完成时,估算函数可能只能返回一个很短的兜底秒数;最终内容到达后重新估算,用户会看到数字跳变。处理方式是流式阶段不持久化兜底值,等消息稳定后再计算和保存。

角色、消息和播放器需要不同身份

联调过程中,我逐渐确认了三个容易混用的标识:

  • 角色稳定标识:决定同一角色长期复用哪一种声音;
  • 业务消息标识:用于签名、缓存和消息级请求;
  • 音频元素:决定浏览器里当前真正播放的是哪一个实例。

早期播放器用业务消息标识派发互斥事件。部分缓存音频没有完整消息标识,结果是两条音频都能播放,却无法互相通知暂停。

我没有为缓存消息伪造业务 ID,而是让播放器使用真实音频元素作为运行时身份:当前元素继续播放,其他元素立即暂停;不携带播放源的全局事件仍表示“暂停全部”。

业务身份保持业务含义,运行时身份由真实对象承担,修改范围更小,也不会影响消息持久化和接口参数。

购买动作应该复用已有业务入口

生成音频前的签名响应可能带回购买动作。前端不需要再做一套新的弹窗,只需把不同动作映射到项目已有的额度或订阅入口。

这类响应也再次说明,TTS 不是一个纯媒体接口。会话权限、余额、订阅、签名有效期和生成服务共同决定最终结果。组件只处理按钮展示,动作分发和请求安全应该留在已有业务层。

页面离开和 App 进入后台不是同一件事

原页面已经在正常离开时停止音频,但 iOS 锁屏或 App 切到后台时,Ionic 缓存页仍然存在,页面离开生命周期不会稳定触发,音频会继续播放。

最终在 Capacitor 全局 App 生命周期中监听活跃状态:当 App 进入非活跃状态时,暂停缓存聊天页中的音频,并同步播放器状态。返回前台后不自动继续播放,用户可以主动重新开始。

这个问题的根因不在 <audio> 的暂停方法,而在于最初选择了错误的生命周期信号。

分层验证服务链路,也要保持发布边界

联调早期,正式应用链路尚未完全可用。我把验证拆成服务可达、请求认证、语音生成和 iOS WebView 播放四个层次,根据每一层的响应继续缩小问题范围。

技术验证不能绕过正式应用服务的权限与额度控制,也不能把服务间配置带进客户端源码。验证完成后,前端只保留已经确认的文本拆分、语言映射、声音复用和播放器能力,网络入口仍由正式发布架构负责。

验证结果与剩余边界

目前已经验证:

  • 会话关闭时不展示也不能触发请求;
  • 纯旁白不显示入口,旁白加对白仍可朗读;
  • 流式完成后重新估算时长;
  • 开场消息具备有效标识时可以生成;
  • 同一角色复用稳定声音;
  • 多条缓存音频保持互斥;
  • iOS 切后台或锁屏时停止播放;
  • 定向测试与生产构建通过。

仍需继续覆盖:

  • 不同 Android 与 iOS 版本的完整真机矩阵;
  • 真实付费限制账号的购买流程;
  • 签名过期、连续点击和快速切换角色;
  • 后端调整对白格式后的兼容范围。

这次问题留下的判断

会话是否开放、消息是否适合朗读、请求是否安全,是三个不同层次。把它们分开后,按钮展示、内容规则和播放器生命周期各自拥有清楚的职责。

以后看到一个“点击后没反应”的媒体按钮,我不会只检查组件状态。更完整的排查应该同时包含功能权限、内容资格、服务链路、运行时身份和 App 生命周期。

Recommended

继续阅读

  1. Ionic 缓存页面里,URL 变化、页面显示和数据刷新为什么是三件事前端与移动端 · 9 分钟
  2. 把 AI 生图与生视频迁到 Web 时,真正需要迁移的是什么前端与移动端 · 9 分钟