package eino import ( "context" "encoding/base64" "io" "github.com/cloudwego/eino/compose" "github.com/cloudwego/eino/schema" "github.com/hhs/camtalk/internal/ai/tts" "github.com/hhs/camtalk/internal/models" "github.com/hhs/camtalk/internal/trace" ) // NewTTSLambda 创建 TTS Transform Lambda 节点。 // 输入: StreamReader[string](句子流)→ 输出: StreamReader[struct{}](结果流) // // 流式消费每个句子,调用 ttsService.SynthesizeStream() 合成, // 逐 chunk 推送 tts_audio 到客户端。TTS 失败静默跳过。 func NewTTSLambda(ttsService tts.Service, ttsVoice string, ttsSpeed float64, ttsOutputFmt string, ttsSampleRate int) *compose.Lambda { return compose.TransformableLambda(func(ctx context.Context, input *schema.StreamReader[string]) (*schema.StreamReader[struct{}], error) { sr, sw := schema.Pipe[struct{}](8) go func() { defer sw.Close() defer input.Close() log := trace.FromContext(ctx) sender := senderFromCtx(ctx) requestID := requestIDFromCtx(ctx) if sender == nil || requestID == "" { // 消费并丢弃流 for { _, err := input.Recv() if err != nil { return } } } // 收集句子,按批次合成 TTS var sentences []string for { sentence, err := input.Recv() if err != nil { if err == io.EOF { break } log.Errorw("TTS: stream recv error", "error", err) break } if sentence != "" { sentences = append(sentences, sentence) } } if len(sentences) == 0 { sw.Send(struct{}{}, nil) return } log.Infow("开始 TTS 合成", "sentence_count", len(sentences)) // 将句子数组转为 channel sentenceCh := make(chan string, len(sentences)) for _, s := range sentences { sentenceCh <- s } close(sentenceCh) // 调用 TTS 服务 ttsStream, err := ttsService.SynthesizeStream(ctx, sentenceCh, tts.Options{ Voice: ttsVoice, Speed: ttsSpeed, OutputFmt: ttsOutputFmt, SampleRate: ttsSampleRate, }) if err != nil { log.Errorw("TTS 合成启动失败(已跳过)", "error", err) sw.Send(struct{}{}, nil) return } // 消费 TTS 音频流,推送到客户端 for chunk := range ttsStream { select { case <-ctx.Done(): log.Debugw("tts stream interrupted") sw.Send(struct{}{}, ctx.Err()) return default: } audioBase64 := base64.StdEncoding.EncodeToString(chunk.Audio) if err := sender.SendTTSAudio(models.WsTTSAudio{ Type: "tts_audio", RequestID: requestID, Audio: audioBase64, MimeType: "audio/mp3", IsLast: chunk.IsLast, Final: chunk.Final, }); err != nil { log.Errorw("发送 tts_audio 失败", "error", err) } } log.Infow("TTS 合成完成") sw.Send(struct{}{}, nil) }() return sr, nil }) }