feat(channelbridge): 出站语音结论落 voice 列并挂出站 Media(M58)
- voice.Ref 增 SourceTTS 与 FallbackReason,Validate/IsZero/validSource 同步 - 出站成功:INSERT -> 补写 voice 列 -> notify(首帧即带 voice) - 出站降级:source=tts + status=failed + fallbackReason,正文原样 - 首条气泡挂出站 Media(新开条目,不复制 target.Media) - gateway historyVoice 投影 fallbackReason - 新增 outbound_voice.go 与整链 e2e 测试(含变异打红验证时序)
This commit is contained in:
1 parent
9f6243a729
commit
28afa11aeb
13 files changed
+755
-44
No files matched your search
@@ -9,8 +9,12 @@ package gateway
|
||||
//
|
||||
// 四条硬口径:
|
||||
//
|
||||
// - **🎤 判据 = `voice` 键存在**(契约明写):非语音消息整键缺席(`omitempty` 的
|
||||
// 指针 nil),**不是** `{}` 也不是 `null`。客户端绝不从正文里认 `[语音]` 字样。
|
||||
// - **`voice` 键存在 = 这条消息有语音事实**:非语音消息整键缺席(`omitempty` 的指针
|
||||
// nil),**不是** `{}` 也不是 `null`。客户端绝不从正文里认 `[语音]` 字样。
|
||||
// ⚠️ 出站合成降级(source=tts、status=failed + fallbackReason)同样**有键**,但
|
||||
// 没有可播副本(name/mime/size/url 四键一起缺席)⇒ 客户端按 `status` 分支:只挂一个
|
||||
// 「本来想用语音」的小标、**不**画播放器,也不打「来自语音」的 🎤(正文是文本,
|
||||
// 不是转写文本)。契约文档对 🎤 判据的措辞由塔统一收口。
|
||||
// - **副本四键同生共死**:`name`/`mime`/`size`/`url` 只在**真的有副本**时一起出现
|
||||
// (`Ref.Validate` 在解码时已保证,本文件再用 `HasCopy` 复核一次),绝不出现
|
||||
// 「`url` 指着一个不存在的 `name`」。
|
||||
@@ -29,7 +33,8 @@ package gateway
|
||||
// ⚠️ 实时帧那条路对**时序**有依赖(已由 M44 的跨层修复保证):`voice` 是**读列**得来的,
|
||||
// 所以帧必须在 `voice` 列写好之后才发出。渠道桥原来「先 notify、后写列」,导致入站语音的
|
||||
// **第一帧**读不到 voice;M44 把它改成「INSERT → 写 voice 列 → notify」(见 channelbridge
|
||||
// 的 persistBatchUsers / persistVoiceFirstNotify),本文件因此无需再区别对待实时帧与读面。
|
||||
// 的 persistBatchUsers / persistVoiceFirstNotify),出站语音(outbound_voice.go)走的是
|
||||
// 同一条顺序纪律。本文件因此无需再区别对待实时帧与读面。
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -45,10 +50,12 @@ import (
|
||||
// `source` / `status` 恒在(属于闭集,见 `voice.Source` / `voice.Status`);其余
|
||||
// 全部 `omitempty` —— 给不出那一项时它**缺席**,绝不用 `0` 或空串编一个值。
|
||||
type historyVoice struct {
|
||||
// Source ∈ asr(服务端识别,含云端)| platform(渠道平台自带转写)。
|
||||
// Source ∈ asr(服务端识别,含云端)| platform(渠道平台自带转写)|
|
||||
// tts(我们自己合成的出站语音)。
|
||||
Source string `json:"source"`
|
||||
// Status ∈ transcribed(正文即转写文本)| empty(转写成功但没识别出文字)|
|
||||
// failed(没拿到转写文本,正文是一行中文诚实提示)。闭集三态。
|
||||
// Status ∈ transcribed(正文即转写文本 / 出站合成成功)| empty(转写成功但没识别出
|
||||
// 文字)| failed(没拿到转写文本,正文是一行中文诚实提示;出站方向 = 合成降级)。
|
||||
// 闭集三态。
|
||||
Status string `json:"status"`
|
||||
// Name 是音频副本的存储名;无副本时**整键缺席**(与 mime/size/url 同生共死)。
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -58,6 +65,10 @@ type historyVoice struct {
|
||||
Size int64 `json:"size,omitempty"`
|
||||
// DurationMs 只有渠道如实给出、或转码产物可确证时才有;读不出来就缺席,绝不估算。
|
||||
DurationMs int64 `json:"durationMs,omitempty"`
|
||||
// FallbackReason 只在**出站合成降级**时出现(source=tts、status=failed):说明这条
|
||||
// 消息为什么没能变成语音(能力未接线 / 预算超时 / 合成为空)。正文此时照发文本,
|
||||
// 客户端据 status=failed 只挂一个小标,**绝不**把它当成可播气泡。
|
||||
FallbackReason string `json:"fallbackReason,omitempty"`
|
||||
// URL 是服务端相对路径 `/api/v1/media/<name>`(唯一派生字段,由 MediaPath 一处拼出)。
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
@@ -148,6 +159,11 @@ func historyVoiceOf(ref voiceref.Ref) *historyVoice {
|
||||
return nil
|
||||
}
|
||||
out := &historyVoice{Source: string(ref.Source), Status: string(ref.Status)}
|
||||
// 降级原因只在失败态下发(`Ref.Validate` 已保证它不会挂在其它状态上):客户端据此
|
||||
// 显示「这条本来想用语音」的小标,正文仍是原文本。
|
||||
if ref.Status == voiceref.StatusFailed {
|
||||
out.FallbackReason = ref.FallbackReason
|
||||
}
|
||||
if ref.HasCopy() {
|
||||
// 四键同生共死:有副本时 name/mime/size/url 一起在,没有时一起缺席。
|
||||
out.Name, out.MIME, out.Size = ref.Name, ref.MIME, ref.Size
|
||||
|
||||
Reference in new issue
Block a user