fix(voice): 主/子系统会话的发音人跟「生效人格」走(r2 P2-1)+ 错误码可区分
P2-1(实质):主会话(web)的 persona_id 恒为空且无重绑路径,而"空"的真实语义是 「继承全局默认人格」(loop/persona.go 回退 Options.PersonaID;前端 lib/persona.ts 的 bound ?? defaultPersona(personas) 是同一口径)。此前只有 TTS 把空读成"没有生效人格" ⇒ 落到 voice-tts.json 的 default:主会话聊天是 furina、声音却是 default 那个人, 换全局人格后也不跟声 —— 正是主人抱怨的那件事,且发生在唯一能给普通聊天出声的路径。 修法: - turnEnvResolver 新增 resolvePersona/effectivePersona(绑定 → 全局默认人格 → ""), 默认人格扫描抽成 defaultPersonaIDFrom 一处(*Server.defaultPersonaID 转调它); - ownerWithPersona 复用**同一次**会话读取同时给出 loop 用的 binding 与呈现用的 effective —— WS 语音回合为选音色不新增任何 I/O; - REST:现有闸内改用 effectivePersona;WS:voice.go 那次 env 解析处取一次传给 sink。 因此 `default` 只剩"连默认人格都没有"这一种用途。 - service.go 那句"换人格后用老角色声音不可能发生"收回为限定说法:本包无缓存、无兜底 角色、未映射即拒绝,但**调用方必须传生效人格**(网关两处已修)。 回归锁:voice_persona_inherit_test.go 复现复审用例(名字不同、两条口各一条)—— 未绑定会话 + 全局默认 furina + default 指向 cyrene ⇒ 必须用 furina 的声音; 变异(effectivePersona 退回「空 = 无」)实测两条都红,报文与复审一致。 建议 3(顺手做):persona 未映射时状态码仍 500(服务端配置缺失),但错误码换成 tts_persona_unmapped / tts_no_character,不再与一般合成失败混成一个码; 用例 TestSpeechEndpointPersonaUnmappedErrorCode 钉住。 P2-2(nit):voicetts.go 开头删掉「格式、」两字(该键已删除)。 go build ./... + go vet ./... + go test(含 -race)全绿。
This commit is contained in:
1 parent
8820b0b316
commit
e684061e56
6 files changed
+259
-20
No files matched your search
@@ -174,30 +174,32 @@ func (h *hub) runVoiceTurn(c *wsConn, req voiceInputRequest) {
|
||||
}
|
||||
// Web 主会话是主人的私密对话,因此它绑定全访问 env:记忆读+写,行为不变。
|
||||
// 角色来自该会话的绑定(design/22 §4.5)。
|
||||
env := h.envResolver.owner(ctx, req.SessionID)
|
||||
// 语音收尾要用**生效人格**选音色(会话绑定 → 全局默认人格),而 env 只带绑定
|
||||
// (loop 自己回退)。这里**复用同一次会话读取**,不为选音色多查一次库。
|
||||
env, turnPersona := h.envResolver.ownerWithPersona(ctx, req.SessionID)
|
||||
msgs := []model.Message{model.TextMessage(model.RoleUser, transcript)}
|
||||
// 两条「怎么回到这条连接」的通路都由本连接给出:收尾回调(音频回复,C1)与
|
||||
// 定向拒绝回调(队列满,G5)。编排器只把它们当成本回合的传输线索。
|
||||
ctx = withVoiceReplySink(ctx, h.voiceReplySink(c, req.SessionID, req.DeviceID, env.PersonaID, transcript))
|
||||
ctx = withVoiceReplySink(ctx, h.voiceReplySink(c, req.SessionID, req.DeviceID, turnPersona.Effective, transcript))
|
||||
ctx = withPendingFullNotifier(ctx, c.notifyPendingFull)
|
||||
if h.orchestrator != nil {
|
||||
h.orchestrator.Submit(ctx, req.SessionID, msgs, env)
|
||||
return
|
||||
}
|
||||
h.runVoiceTurnDirect(ctx, c, req.SessionID, req.DeviceID, transcript, env)
|
||||
h.runVoiceTurnDirect(ctx, c, req.SessionID, req.DeviceID, turnPersona.Effective, transcript, env)
|
||||
}
|
||||
|
||||
// runVoiceTurnDirect 是未接编排器时的旧直跑路径(未接线的构建 / 裸 hub 单元
|
||||
// 测试):转写之后的回合直接跑 loop,然后立即合成并回音频。它与编排器路径共用
|
||||
// 同一个帧构造(emitVoiceOutput),因此两条路径的 voice_output 契约一致。
|
||||
func (h *hub) runVoiceTurnDirect(ctx context.Context, c *wsConn, sessionID, deviceID, transcript string, env loop.SessionEnv) {
|
||||
func (h *hub) runVoiceTurnDirect(ctx context.Context, c *wsConn, sessionID, deviceID, personaID, transcript string, env loop.SessionEnv) {
|
||||
res, err := h.runner.Run(loop.WithSessionEnv(ctx, env), sessionID, []model.Message{model.TextMessage(model.RoleUser, transcript)})
|
||||
if err != nil {
|
||||
h.voiceLog().Warn("voice chat run", "err", err, "session", sessionID, "device_id", deviceID)
|
||||
h.voiceError(c, "voice chat failed")
|
||||
return
|
||||
}
|
||||
h.synthesizeVoiceReply(ctx, c, sessionID, deviceID, env.PersonaID, transcript, voiceReplyText(res))
|
||||
h.synthesizeVoiceReply(ctx, c, sessionID, deviceID, personaID, transcript, voiceReplyText(res))
|
||||
}
|
||||
|
||||
// voiceReplySink 返回本次语音请求的收尾回调:回合结算后取助手正文 → TTS →
|
||||
|
||||
Reference in new issue
Block a user