- 引入 cloudwego/eino v0.9.9 和 eino-ext/components/model/openai v0.1.13 - 新增 internal/eino/ 包: - types.go: PipelineInput/Output、STTOutput、TokenUsage 类型定义 - state.go: PipelineState 跨节点状态收集(线程安全) - callback.go: ChatModel OnEndWithStreamOutput 回调,逐 token 推送 llm_chunk - nodes_stt.go: STT Lambda,支持文本/语音输入模式 - nodes_history.go: 历史组装 Lambda,含多模态图片支持 - nodes_splitter.go: 句子分割 Transform Lambda - nodes_tts.go: TTS Lambda,逐句合成推送音频 - nodes_done.go: Done Lambda,发送 llm_done 并追加历史 Co-Authored-By: Claude <noreply@anthropic.com>
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
package eino
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/cloudwego/eino/compose"
|
||
|
||
"github.com/hhs/camtalk/internal/ai/stt"
|
||
"github.com/hhs/camtalk/internal/logger"
|
||
"github.com/hhs/camtalk/internal/models"
|
||
)
|
||
|
||
// NewSTTLambda 创建 STT Lambda 节点。
|
||
// 输入: PipelineInput → 输出: STTOutput
|
||
//
|
||
// 文本输入模式:跳过 STT,直接返回用户输入文本。
|
||
// 语音模式:调用 sttService.Recognize() 进行语音识别。
|
||
// 识别结果通过 Sender 发送 stt_result 到客户端。
|
||
func NewSTTLambda(sttService stt.Service) *compose.Lambda {
|
||
return compose.InvokableLambda(func(ctx context.Context, input *PipelineInput) (*STTOutput, error) {
|
||
log := logger.Log
|
||
sender := senderFromCtx(ctx)
|
||
requestID := requestIDFromCtx(ctx)
|
||
|
||
// 文本输入模式:跳过 STT
|
||
if input.Text != "" {
|
||
log.Infow("使用文本输入,跳过 STT",
|
||
"request_id", requestID, "text", input.Text)
|
||
|
||
// 发送 stt_result 保持前端消息流一致性
|
||
if sender != nil {
|
||
if err := sender.SendSTTResult(models.WsSTTResult{
|
||
Type: "stt_result",
|
||
RequestID: requestID,
|
||
Text: input.Text,
|
||
IsFinal: true,
|
||
}); err != nil {
|
||
log.Errorw("发送 stt_result 失败", "error", err)
|
||
}
|
||
}
|
||
|
||
// 写入 State
|
||
if state := stateFromCtx(ctx); state != nil {
|
||
state.mu.Lock()
|
||
state.TranscribedText = input.Text
|
||
state.mu.Unlock()
|
||
}
|
||
|
||
return &STTOutput{
|
||
Text: input.Text,
|
||
Language: input.Language,
|
||
IsSkipped: true,
|
||
}, nil
|
||
}
|
||
|
||
// 语音模式:解码音频
|
||
if len(input.AudioData) == 0 {
|
||
return nil, fmt.Errorf("stt: no audio data provided")
|
||
}
|
||
|
||
log.Infow("开始语音识别",
|
||
"request_id", requestID, "audio_bytes", len(input.AudioData))
|
||
|
||
// 调用 STT 服务
|
||
text, err := sttService.Recognize(ctx, input.AudioData, stt.Options{
|
||
Encoding: "pcm_s16le",
|
||
SampleRate: 16000,
|
||
Language: input.Language,
|
||
})
|
||
if err != nil {
|
||
log.Errorw("语音识别失败", "error", err, "request_id", requestID)
|
||
if sender != nil {
|
||
sender.SendError(models.WsError{
|
||
Type: "error",
|
||
RequestID: requestID,
|
||
Code: "STT_ERROR",
|
||
Message: "语音识别失败: " + err.Error(),
|
||
})
|
||
}
|
||
return nil, fmt.Errorf("stt: recognize: %w", err)
|
||
}
|
||
|
||
// STT 返回空文本
|
||
if strings.TrimSpace(text) == "" {
|
||
log.Infow("语音识别结果为空", "request_id", requestID)
|
||
text = "(未识别到语音)"
|
||
}
|
||
|
||
log.Infow("语音识别完成", "request_id", requestID, "text", text)
|
||
|
||
// 发送 stt_result
|
||
if sender != nil {
|
||
if err := sender.SendSTTResult(models.WsSTTResult{
|
||
Type: "stt_result",
|
||
RequestID: requestID,
|
||
Text: text,
|
||
IsFinal: true,
|
||
}); err != nil {
|
||
log.Errorw("发送 stt_result 失败", "error", err)
|
||
}
|
||
}
|
||
|
||
// 写入 State
|
||
if state := stateFromCtx(ctx); state != nil {
|
||
state.mu.Lock()
|
||
state.TranscribedText = text
|
||
state.mu.Unlock()
|
||
}
|
||
|
||
return &STTOutput{
|
||
Text: text,
|
||
Language: input.Language,
|
||
IsSkipped: false,
|
||
}, nil
|
||
})
|
||
}
|
||
|