diff --git a/adapter/volcano/response.go b/adapter/volcano/response.go index 90e53c8..b5085ed 100644 --- a/adapter/volcano/response.go +++ b/adapter/volcano/response.go @@ -72,7 +72,7 @@ func ParseStream(body io.Reader, started time.Time) (*ParsedStream, error) { switch resp.Event { case "TTSSentenceStart": if common.DebugLog { - log.Printf("Sentence start: sequence=%d, sentence=%s", resp.Sequence, resp.Sentence) + log.Printf("Sentence start: sequence=%d, sentence=%s", resp.Sequence, resp.SentenceText()) } case "TTSSentenceEnd": if common.DebugLog { @@ -81,7 +81,7 @@ func ParseStream(body io.Reader, started time.Time) (*ParsedStream, error) { case "TTSSubtitle": if resp.Data != "" { out.Subtitles = append(out.Subtitles, dto.SubtitleEntry{ - Text: resp.Sentence, + Text: resp.SentenceText(), Sequence: resp.Sequence, }) } @@ -105,10 +105,15 @@ func ParseStream(body io.Reader, started time.Time) (*ParsedStream, error) { gotFirstChunk = true } case "": - // 传输帧,跳过 + // 无 event 字段的元数据行(上游 TTSSentenceStart 偶发省略 event), + // debug 模式打印便于排查;正常情况静默跳过。 + if common.DebugLog && resp.Code == 0 { + log.Printf("volcano: 无 event 字段 code=%d sequence=%d sentence=%s data_len=%d", resp.Code, resp.Sequence, resp.SentenceText(), len(resp.Data)) + } + } default: if common.DebugLog { - log.Printf("volcano: 忽略未识别事件 event=%q sequence=%d", resp.Event, resp.Sequence) + log.Printf("volcano: 忽略未识别事件 event=%q sequence=%d sentence=%s data_len=%d", resp.Event, resp.Sequence, resp.SentenceText(), len(resp.Data)) } } } diff --git a/dto/tts.go b/dto/tts.go index 70e595a..b0420cf 100644 --- a/dto/tts.go +++ b/dto/tts.go @@ -1,6 +1,9 @@ package dto -import "time" +import ( + "encoding/json" + "time" +) // OpenAITTSRequest 是 /v1/audio/speech 接收的请求体。 // 仅 input / speed / response_format 实际影响火山侧; @@ -14,16 +17,40 @@ type OpenAITTSRequest struct { } // V3TTSResponse 是火山 v3 HTTP Chunked 流式响应中每一行的 JSON 结构。 +// Sentence 字段上游有时返回字符串(TTSSentenceStart 里的句文本),有时返回对象 +// ({"phonemes":[...],"text":"...","words":[...]}),用 json.RawMessage 兼容两种形态, +// 避免任意一种上游变更都导致整行解析失败。 type V3TTSResponse struct { - ReqID string `json:"reqid"` - Code int `json:"code"` - Message string `json:"message"` - Event string `json:"event"` - Sequence int `json:"sequence"` - Data string `json:"data"` - Sentence string `json:"sentence,omitempty"` - IsFinal bool `json:"is_final"` - Usage *V3Usage `json:"usage,omitempty"` + ReqID string `json:"reqid"` + Code int `json:"code"` + Message string `json:"message"` + Event string `json:"event"` + Sequence int `json:"sequence"` + Data string `json:"data"` + Sentence json.RawMessage `json:"sentence,omitempty"` + IsFinal bool `json:"is_final"` + Usage *V3Usage `json:"usage,omitempty"` +} + +// SentenceText 从 Sentence 提取可读文本: +// - 字符串直接返回 +// - 对象尝试取 .text 字段 +// - 其它情况返回原始 JSON +func (r *V3TTSResponse) SentenceText() string { + if len(r.Sentence) == 0 { + return "" + } + var s string + if err := json.Unmarshal(r.Sentence, &s); err == nil { + return s + } + var obj struct { + Text string `json:"text"` + } + if err := json.Unmarshal(r.Sentence, &obj); err == nil && obj.Text != "" { + return obj.Text + } + return string(r.Sentence) } // V3Usage 由 X-Control-Require-Usage-Tokens-Return 触发,包含计费字符数。