Files
CamTalk/backend/internal/eino/nodes_tts.go

93 lines
2.6 KiB
Go
Raw Normal View History

package eino
import (
"context"
"encoding/base64"
"github.com/cloudwego/eino/compose"
"github.com/hhs/camtalk/internal/ai/tts"
"github.com/hhs/camtalk/internal/logger"
"github.com/hhs/camtalk/internal/models"
)
// NewTTSLambda 创建 TTS Lambda 节点。
// 输入: []string句子数组框架自动从 StreamReader concat→ 输出: struct{}
//
// 将句子数组转为 channel调用 ttsService.SynthesizeStream() 流式合成,
// 逐 chunk 推送 tts_audio 到客户端。TTS 失败静默跳过。
func NewTTSLambda(ttsService tts.Service, ttsVoice string, ttsSpeed float64, ttsOutputFmt string, ttsSampleRate int) *compose.Lambda {
return compose.InvokableLambda(func(ctx context.Context, sentences []string) (struct{}, error) {
log := logger.Log
sender := senderFromCtx(ctx)
requestID := requestIDFromCtx(ctx)
state := stateFromCtx(ctx)
// 检查 TTS 是否启用(从 State 或 context 获取)
// TTSEnabled 信息在 PipelineInput 中,通过 State 传递
if state != nil {
state.mu.Lock()
ttsEnabled := true // 默认启用,由适配器通过 State 设置
state.mu.Unlock()
if !ttsEnabled {
return struct{}{}, nil
}
}
if len(sentences) == 0 {
return struct{}{}, nil
}
if sender == nil || requestID == "" {
return struct{}{}, nil
}
log.Infow("开始 TTS 合成", "request_id", requestID, "sentence_count", len(sentences))
// 将句子数组转为 channelttsService.SynthesizeStream 需要 <-chan string
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, "request_id", requestID)
return struct{}{}, nil // TTS 失败不中断流程
}
// 消费 TTS 音频流,推送到客户端
for chunk := range ttsStream {
select {
case <-ctx.Done():
log.Infow("TTS 流被中断", "request_id", requestID)
return struct{}{}, ctx.Err()
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 合成完成", "request_id", requestID)
return struct{}{}, nil
})
}