is_interrupted=false,从名字看,很容易理解成“这句话不要打断别人”。Q1 的PlayTts规则却留了一个前提:只有两条语音的最终权重相等,这个开关才决定排队还是抢占。
新请求权重更高,照样打断。更低,则直接丢弃。
顺着这个规则往下读,还会碰到第二件事:请求返回成功,并没有告诉应用那句话已经说完。把两处连起来看,就能解释一种很别扭的交互——问候被警告打断,欢迎卡片却仍在几秒后准时弹出来。
下面安排四条请求走一遍,既看谁能说话,也看被打断之后,程序还能根据什么继续。依据为 Q1 SDK v1.0.0.0,场景按文档规则推演。
1. 先算权重,600是怎么来的
假定问候A已经在播放:“你好,欢迎来认识我。”它使用默认建议的交互级别INTERACTION_L6,子权重设为0,最终权重就是600。
PlayTtsRequest用两个字段表达优先级。priority_level决定业务层级,priority_weight在同级内作0—99的微调。文档给出的合成方法是:
最终权重 = priority_level × 100 + priority_weight子权重只在主级内部调整次序。L6+99是699,仍低于L8+0的800;同为L6时,50又能压过20。所以业务分类要先选对,同级微调再放进priority_weight。
| 主级 | 数值 | 文档给出的典型用途 |
|---|---|---|
BACKGROUND_L1 | 1 | 后台提醒 |
SERVICE_L2 | 2 | 主动服务与常规通知 |
MISSION_L4 | 4 | 任务关键节点播报 |
INTERACTION_L6 | 6 | 实时交互首句,推荐默认值 |
SYSTEM_L7 | 7 | 系统重要提示,见下方版本说明 |
WARNING_L8 | 8 | 避障受阻、传感器遮挡等预警 |
SAFETY_L10 | 10 | 紧急告知 |
版本说明:SYSTEM_L7仅见于文档表格,未见于同包TtsPriorityLevel.msg,数值 7 的设备行为尚未核实。本文场景使用两处资料均有定义的级别。
2. 同一个false,两种结果
现在,技能B想补一句“你也可以摸摸我的头”,同样使用权重600,并把is_interrupted设为false。它与A同权重,按文档规则进入缓冲区,等待当前播报完成后处理。
如果B把开关设为true,结局就不同:正在说的A会被掐断,由B接替。这个开关控制的是同等最终权重下,是等待还是抢占。字段名里的“不打断”,不能脱离权重比较单独理解。
接下来,请求C需要播报“前方通行受阻,请协助清理”。它使用WARNING_L8,子权0,权重800。即使C也设置is_interrupted=false,800仍高于正在播放的600,文档规定它强制打断当前播报。
按这套规则,C的高权重已经决定了抢占,无需再用同权重的开关作判断。把所有技能都提到高优先级,再靠这个开关维持秩序,逻辑上就行不通。
图 1:依据 Q1 v1.0.0.0 文档整理。比较对象是新请求与当前播报;该图不描述多个等待任务之间的完整排序。
C播放期间,再有一个权重400的任务通知D到达,会怎样?它比当前任务低,按文档被直接丢弃,内层响应的is_success为false,不会因为设置了“非抢占”就自动等到警告结束。
到此,A 被打断,B 已等待,C 占用通道,D 被拒。公开规则没有完整说明 C 结束后 B 如何重新调度,应用暂时只能把它记为已被接受、后续状态未知。这个缺口将直接影响下一步交互。
3. 把规则缩成几行代码
文档中的两两比较可以写成一个很短的Python函数。输入当前权重、新权重和开关,输出打断、等待或丢弃:
defarbitrate(current,incoming,is_interrupted):ifincoming>current:return"interrupt"ifincoming<current:return"discard"return"interrupt"ifis_interruptedelse"wait"它只表达新请求与当前播报之间的关系,等待队列仍由设备服务管理。把欢迎流程里的数字代入,有两种结果值得比较:
current=600 incoming=600 flag=False -> wait current=600 incoming=800 flag=False -> interrupt同样是false,结局不同。再把输入改成当前620、新请求650,即L6+20与L6+50,结果仍是打断。影响判断的是最终权重,不仅是两条请求的主级名称。
欢迎语和它的补充说明,适合保持同权重、非抢占。警告确实需要插入时,再使用更高主级。升权重的代价就是另一句话说不完整;如果产品并不希望付这个代价,就不该只为了“尽快播出来”而升权重。
4. 回执里的成功,停在哪一步
假如 B 入队成功,调用方很快拿到回执,接着开始挥手或显示欢迎卡片。这些动作可能抢在 B 开口之前;C 随后插入,又会让原定节奏发生变化。
PlayTts返回的是请求处理结果。外层header是CommonResponse,用于检查公共状态和诊断信息;内层tts_resp才包含is_success、estimated_duration等语音字段。两层结果都需要检查。内层成功意味着请求被接受,还不足以把本地业务标为“播报完成”。
响应虽然提供时长字段,但当前版本的estimated_duration固定返回 0,文档也说明暂未开放播报结束的话题通知。可用的节奏参考,是约 200 毫秒/字的经验估算:20 个字,大致安排 4 秒。
假设B有20个字,应用一拿到回执就开始计4秒。可B此刻还在等A,随后又插进一个C。4秒里到底有多少用于播放B,应用并不知道。计时器照样到点,卡片照样弹出,于是声音和界面各走各的。
这就决定了估时能用在哪儿。欢迎卡片不必卡准最后一个字,按估时展示可以接受;下一步必须等用户听清并确认,就应等待显式确认,或使用有完成信号的链路。别让一个方便的计时器,承担它无法回答的“已经说完了吗”。
图 2:本文提出的应用侧组织方式。实线表示请求与回执,橙色虚线表示估时后的本地继续;当前接口没有提供播报完成事件。
5. 互动17结束了,卡片别跑到18里
即使暂时拿不到播报完成事件,过期卡片仍然能拦住。应用把自己发出的语音请求和后续步骤放在一起管理,给每段互动一个编号即可。
设问候流程编号为 17,已经安排了一张稍后显示的欢迎卡片。计时尚未结束,用户又发起新任务,应用把当前互动切换为 18。如果卡片回调只检查“时间到了没有”,旧欢迎卡片就会出现在新任务中。
让卡片回调带着17这个编号,执行前再与当前编号比较,就能发现它已经过期。切换任务时撤销计时器,回调里再查编号,两处都有用:前者减少无用调度,后者拦住已经排进执行队列的旧回调。
撤销的是本地后续步骤。设备已受理的TTS请求,要由设备提供取消能力才能撤回;其他进程发出的语音,也仍交给设备仲裁。
这样一来,A到底在什么时刻结束可以暂时未知,但互动17的卡片不会再闯进互动18。应用至少管住了自己能管的部分。
6. 留下哪些日志,才查得出错位
为每次业务互动保留任务编号,为每次语音请求保留trace_id,就能把一段互动中的多次尝试串起来。domain标识调用来源,文档将其用于资源隔离与日志审计;它和trace_id都不参与公开的权重公式。
请求权重、开关、提交与响应时刻、两层响应结果,是这段流程最有用的记录。卡片继续或取消的原因也一并记下:D被拒,可以沿权重比较查;卡片没出现,可以分清是请求失败,还是互动17已经失效。播放完成时间观察不到,留空即可。
超时则单独记为结果未知。没收到响应,服务可能没接受,也可能已经接受;直接重发有重复播报的风险,文档没有承诺按trace_id幂等去重。欢迎语通常不值得无条件补发,先看用户是否仍在这段互动里,再决定要不要重试。
到这里,最初那个false就不难理解了:它只管同权重的请求,排在权重比较之后。而“请求成功”只说明设备接受了请求,后面的欢迎流程还得由应用接着管理。
如果只改一个地方,先把“收到成功回执就开始下一步”拆开。卡片可以估时,关键步骤等待确认,旧互动随编号失效。这几处改动,比把欢迎语的优先级再抬高一级更能解决交互错位。
参考:启元 Q1 SDK v1.0.0.0 的PlayTts.srv、请求/响应消息、TtsPriorityLevel.msg与《接口说明》PlayTts 章节。相关开发讨论可在启元开发者社区继续交流。