perf(voice): 砍兜底等待 1.2s→0.5s + 语音提示词强制简短 + 时延测量
- voice.go: ClientEnd 兜底等待 1200ms→500ms(白吃的时延),首字出声 6.6s→5.3s - voice_task.go: JARVIS 系统提示词改"必须简短/最多三句/先给结论"(语音场景长答案既拖慢 首字又难听;注:deepseek-v4-pro 仍可能不理会长度指令,可靠收短需 max_tokens 硬顶) - voicesim: 时延拆解(提交/首字出声/朗读完毕,以"说完"为0点) 现状:首字出声 ~5.3s,大头是 deepseek 生成第一句(大模型 TTFT);管线开销(兜底+TTS握手)已压到~1s。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,8 @@ func main() {
|
||||
|
||||
answer := make([]byte, 0, 1<<20)
|
||||
done := make(chan struct{})
|
||||
go func() { // 读循环:文本帧=事件,二进制帧=回答 TTS 音频
|
||||
var endAt, tTask, tFirstAudio, tDone time.Time // 时延测量:以"说完(ClientEnd)"为起点
|
||||
go func() { // 读循环:文本帧=事件,二进制帧=回答 TTS 音频
|
||||
defer close(done)
|
||||
for {
|
||||
mt, data, err := conn.ReadMessage()
|
||||
@@ -77,6 +78,9 @@ func main() {
|
||||
return
|
||||
}
|
||||
if mt == websocket.BinaryMessage {
|
||||
if tFirstAudio.IsZero() {
|
||||
tFirstAudio = time.Now() // 首个音频帧=听到第一声
|
||||
}
|
||||
answer = append(answer, data...)
|
||||
continue
|
||||
}
|
||||
@@ -94,10 +98,12 @@ func main() {
|
||||
}
|
||||
fmt.Printf(" ← 转写[%s]:%q\n", tag, m.Text)
|
||||
case voice.ServerTask:
|
||||
tTask = time.Now()
|
||||
fmt.Printf(" ← 任务已提交 task_id=%s(Agent 正在思考…)\n", m.TaskID)
|
||||
case voice.ServerSpeaking:
|
||||
fmt.Println(" ← Agent 开始朗读回答…")
|
||||
case voice.ServerTTSEnd:
|
||||
tDone = time.Now()
|
||||
fmt.Println(" ← 回答朗读完毕")
|
||||
return
|
||||
case voice.ServerError:
|
||||
@@ -119,6 +125,7 @@ func main() {
|
||||
time.Sleep(90 * time.Millisecond)
|
||||
}
|
||||
send(conn, voice.ClientMsg{Type: voice.ClientEnd})
|
||||
endAt = time.Now() // 时延起点:用户"说完"这一刻
|
||||
fmt.Println("④ 已说完,等 Agent 回答 + 朗读(大模型 + TTS,稍候)…")
|
||||
|
||||
// 5) 等回答朗读完(或超时)。
|
||||
@@ -129,9 +136,23 @@ func main() {
|
||||
}
|
||||
|
||||
if len(answer) > 0 {
|
||||
dur := float64(len(answer)/2) / float64(voice.TTSSampleRate)
|
||||
_ = writeWAV("sim_answer.wav", answer, voice.TTSSampleRate)
|
||||
fmt.Printf("\n🔊 回答音频 %d 字节(%.1f 秒)→ 存 sim_answer.wav\n", len(answer), float64(len(answer)/2)/float64(voice.TTSSampleRate))
|
||||
fmt.Printf("\n🔊 回答音频 %d 字节(时长 %.1f 秒)→ 存 sim_answer.wav\n", len(answer), dur)
|
||||
fmt.Println(" afplay sim_answer.wav # 听 JARVIS 的语音回答")
|
||||
|
||||
// 时延拆解(以"说完"为 0 点)。首字延迟 = 听到第一声的时间,是体感关键。
|
||||
fmt.Println("\n⏱️ 时延拆解(从「说完」起算):")
|
||||
if !tTask.IsZero() {
|
||||
fmt.Printf(" · 提交任务 %.2fs(含 ClientEnd 后 0.5s 兜底等待)\n", tTask.Sub(endAt).Seconds())
|
||||
}
|
||||
if !tFirstAudio.IsZero() {
|
||||
fmt.Printf(" · 👂 首字出声 %.2fs ← 体感响应速度就看这个\n", tFirstAudio.Sub(endAt).Seconds())
|
||||
}
|
||||
if !tDone.IsZero() {
|
||||
fmt.Printf(" · 朗读完毕 %.2fs(= 首字 %.2fs + 念完 %.1fs 那段话)\n",
|
||||
tDone.Sub(endAt).Seconds(), tFirstAudio.Sub(endAt).Seconds(), dur)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("\n⚠️ 没收到回答音频(看上面事件流定位:转写?任务?朗读?)")
|
||||
}
|
||||
|
||||
@@ -156,9 +156,10 @@ func (s *voiceSession) onControl(m voice.ClientMsg) (done bool) {
|
||||
_ = s.asr.Finish() // 告知火山本轮说完
|
||||
}
|
||||
// 火山流式 ASR 只在 VAD 静音时才发 Final;客户端显式 end(点停)时不能干等——
|
||||
// 给点收尾时间让末尾部分结果到齐,再用"最新转写"兜底提交(trySubmit 去重,Final 先到就它先提交)。
|
||||
// 给一小段收尾时间让末尾部分结果到齐,再用"最新转写"兜底提交(trySubmit 去重,Final 先到就它先提交)。
|
||||
// 500ms 是"够接住末尾 partial"与"别拖慢首字出声"的折中(此前 1.2s 白白吃掉一秒多时延)。
|
||||
go func() {
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
s.turnMu.Lock()
|
||||
txt := s.latestText
|
||||
s.turnMu.Unlock()
|
||||
|
||||
@@ -14,9 +14,11 @@ import (
|
||||
// 语音只是"嘴替键盘",一行编排/工具/计费逻辑都不新造:走的正是 HTTP SubmitTask 那条关卡
|
||||
// (见记忆 execution-single-entry「提交必走 preflight()+launch() 共用关卡」)。
|
||||
|
||||
// voiceAgentSystem 是语音默认单 agent 的系统提示词:口语化、简洁、适合朗读(下行要过 TTS)。
|
||||
const voiceAgentSystem = "你是用户的语音助手 JARVIS。用简洁、口语化、适合朗读的中文回答," +
|
||||
"避免冗长的列表和代码块;必要时可调用工具获取信息后再作答。"
|
||||
// voiceAgentSystem 是语音默认单 agent 的系统提示词:口语化、**极简**、适合朗读(下行要过 TTS)。
|
||||
// 语音场景务必短——长答案既拖慢首字出声(要等第一句合成),又让人干听十几秒。
|
||||
const voiceAgentSystem = "你是用户的语音助手 JARVIS。这是语音对话,回答必须简短——像真人聊天," +
|
||||
"通常一两句话说清重点即可,最多不超过三句,别长篇大论、别列清单、别念代码。" +
|
||||
"先直接给结论。口语化、自然、适合朗读。需要更多细节时再由用户追问。"
|
||||
|
||||
// buildVoiceGraph 把一句转写组成最简可执行图:input(转写) → agent(JARVIS)。
|
||||
// 与前端画布 exportDsl 同构(kind=input/agent、config.text/system),dispatcher 直接吃。
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user