feat(voice): 语音消息落库 + 转码接入(切片 0020)
- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息
⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
This commit is contained in:
1 parent
120d926cf8
commit
ede00d9928
17 files changed
+2125
-31
No files matched your search
@@ -137,7 +137,10 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
// 它必须在「丢空判据」之前发生,否则 telegram 的纯语音(无 caption ⇒ Content
|
||||
// 为空)会在下面被整条丢掉。归一写回 msg.Content 之后,最小打扰判定、落库与
|
||||
// 模型可见消息看到的都是**同一份**文本(转写结果,或一行诚实的失败提示)。
|
||||
b.normalizeInboundVoice(ctx, &msg)
|
||||
//
|
||||
// 它同时给出这条语音的**来源与三态结论**(切片 CODE-VOICE;design/24 §10.2 第 2 步),
|
||||
// 由本函数的调用方一路带到闸门之后的落副本步与落库步 —— 结论与正文同源,绝不重算。
|
||||
inboundVoice := b.normalizeInboundVoice(ctx, &msg)
|
||||
|
||||
// design/12 §6 策略一补充:系统通知在进入任何下游之前先过「传入前审查」——
|
||||
// 归一化 → 剥离异常字段 → 分类 → fail-safe。被判为可忽略(或畸形/未识别)的
|
||||
@@ -224,7 +227,7 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
}
|
||||
}
|
||||
|
||||
b.runInboundTurn(ctx, msg, a, text, actor, sess)
|
||||
b.runInboundTurn(ctx, msg, a, text, actor, sess, inboundVoice)
|
||||
}
|
||||
|
||||
// blacklistDrop 对非主人消息应用黑白名单,返回
|
||||
@@ -270,19 +273,28 @@ func inboundTargetFor(sess session.Session, platform string) inboundTarget {
|
||||
// runInboundTurn 为被放行的消息驱动 agent 轮次:解析
|
||||
// 路由目标、把消息携带的文件类附件**落库并挂成文档部件**
|
||||
// (design/24 §3;切片 M1)、把图片媒体内联成 data: URI 部件挂上既有视觉链路
|
||||
// (切片 IM-IMG;design/24 §4.2 末)、构建模型可见的消息列表(再前置**该会话自己**最近的
|
||||
// 历史转录:主人私聊取主会话、群聊/陌生人取该隔离会话,见 transcript.go),在轮次之前持久化入站
|
||||
// 用户消息**连同它们的 `document` 引用列与 `image` 列**(因此即使渠道回复失败它也已落地,
|
||||
// 客户端也有附件芯片与图片可看),并在轮次之后把抽取结论补回同一列,运行
|
||||
// 循环并把结果交给 deliverReply。
|
||||
// (切片 IM-IMG;design/24 §4.2 末)、把那条被归一过的语音的**可播副本**落盘并
|
||||
// 产出 `voice` 引用(design/24 §10.2 第 4 步;切片 CODE-VOICE)、构建模型可见的
|
||||
// 消息列表(再前置**该会话自己**最近的历史转录:主人私聊取主会话、群聊/陌生人取
|
||||
// 该隔离会话,见 transcript.go),在轮次之前持久化入站
|
||||
// 用户消息**连同它们的 `document` 引用列、`image` 列与 `voice` 列**(因此即使渠道
|
||||
// 回复失败它也已落地,客户端也有附件芯片、图片与 🎤 可看),并在轮次之后把抽取结论
|
||||
// 补回同一 document 列,运行循环并把结果交给 deliverReply。
|
||||
//
|
||||
// voiceTurn(切片 CODE-VOICE)是那条语音的归一结论 —— **只在放行之后**才可能落副本:
|
||||
// 被黑白名单或最小打扰拦下的语音,盘上不留任何文件(与附件同一纪律)。
|
||||
func (b *Bridge) runInboundTurn(ctx context.Context, msg adapter.UnifiedMessage, a adapter.PlatformAdapter,
|
||||
text string, actor adapter.Actor, sess session.Session) {
|
||||
text string, actor adapter.Actor, sess session.Session, voiceIn voiceTurn) {
|
||||
target := inboundTargetFor(sess, msg.Platform)
|
||||
|
||||
// 入站附件摄取留在**闸门之后**(本函数只在消息被放行后调用):被黑白名单或
|
||||
// 最小打扰拦下的消息绝不在盘上留下文件。成功者挂成文档部件,装配期
|
||||
// resolveDocuments 照既有链路抽取/注入;读不到的走诚实提示。
|
||||
attachments := b.ingestInboundDocuments(ctx, msg)
|
||||
// 语音副本落盘与附件摄取**并列**、同处闸门之后:不可播的格式先转码(复用
|
||||
// internal/capability/transcode 的既有能力),转码不可用就**不落副本**并如实记分类。
|
||||
// 它拿到的字节是**原始字节**(转写早已在流水线最前端用同一份原始字节做完)。
|
||||
voiceRef := b.ingestInboundVoiceCopy(ctx, msg, voiceIn)
|
||||
// 图片内联与文档摄取**并列**、互不影响:文档走存储与抽取,图片走 data: URI 部件
|
||||
// (同一条用户消息上两条都能挂)。它没有 ctx —— 这里没有任何 I/O(字节由适配器取回)。
|
||||
images := b.ingestInboundImages(msg)
|
||||
@@ -312,10 +324,9 @@ func (b *Bridge) runInboundTurn(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
attachInboundContent(loopMsgs, attachments, images)
|
||||
loopMsgs = b.seedTurnTranscript(ctx, target, loopMsgs)
|
||||
|
||||
// 附件引用与图片必须同批落进用户消息各自的列(design/24 §2.4 / design/16 §4.4):
|
||||
// 只把部件挂在内存里的模型可见消息上,客户端读到的那条消息就是一片空白 —— 附件芯片
|
||||
// 只认 `document` 列,图片只认 `image` 列(**图片绝不进 `document` 列**)。这里写的是
|
||||
// 引用与身份,文档结论要等回合跑完(下一段)。
|
||||
// 只认 `document` 列,图片只认 `image` 列(**图片绝不进 `document` 列**),🎤 只认
|
||||
// `voice` 列。这里写的是引用与身份,文档结论要等回合跑完(下一段)。
|
||||
document := b.inboundDocumentColumn(msg, attachments)
|
||||
image := inboundImageColumn(images)
|
||||
|
||||
@@ -330,6 +341,9 @@ func (b *Bridge) runInboundTurn(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
} else {
|
||||
userMsg = b.persistIsolated(ctx, history.RoleUser, text, document, image, deviceIDFor(msg), sess, msg.Platform, senderName, adapterName(a), turnID)
|
||||
}
|
||||
// 语音事实与用户消息**同源落库**(design/24 §10.2 第 5 步):写进刚落的那一行,
|
||||
// 客户端因此能按结构化字段显示 🎤 与播放控件(绝不从正文里认媒体类型)。
|
||||
b.persistMessageVoice(userMsg, voiceRef)
|
||||
|
||||
res, err := b.run.Run(loop.WithTurnID(loop.WithSessionEnv(ctx, runEnv), turnID), target.sessionID, loopMsgs)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user