139 lines
3.6 KiB
Go
139 lines
3.6 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"
|
||
"github.com/hhs/camtalk/internal/util"
|
||
)
|
||
|
||
// 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)
|
||
|
||
// 将输入元数据写入 State,供下游节点(History、Done)读取
|
||
if state := stateFromCtx(ctx); state != nil {
|
||
state.mu.Lock()
|
||
state.SessionID = input.SessionID
|
||
state.RequestID = input.RequestID
|
||
state.ImageData = input.ImageData
|
||
state.Scenario = input.Scenario
|
||
state.DetailLevel = "low"
|
||
state.Language = input.Language
|
||
state.TTSEnabled = input.TTSEnabled
|
||
state.mu.Unlock()
|
||
}
|
||
|
||
// 文本输入模式:跳过 STT
|
||
if input.Text != "" {
|
||
log.Debugw("text input mode, skipping stt",
|
||
"request_id", requestID,
|
||
"text_len", len(input.Text),
|
||
"text_preview", util.Truncate(input.Text, 50))
|
||
|
||
// 发送 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 STTOutput{}, 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 STTOutput{}, fmt.Errorf("stt: recognize: %w", err)
|
||
}
|
||
|
||
// STT 返回空文本
|
||
if strings.TrimSpace(text) == "" {
|
||
log.Infow("语音识别结果为空", "request_id", requestID)
|
||
text = "(未识别到语音)"
|
||
}
|
||
|
||
log.Debugw("stt recognition completed",
|
||
"request_id", requestID,
|
||
"text_len", len(text),
|
||
"text_preview", util.Truncate(text, 50))
|
||
|
||
// 发送 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
|
||
})
|
||
}
|
||
|