2026-06-19 21:49:28 +08:00
|
|
|
|
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 {
|
2026-06-19 21:58:17 +08:00
|
|
|
|
return compose.InvokableLambda(func(ctx context.Context, input PipelineInput) (STTOutput, error) {
|
2026-06-19 21:49:28 +08:00
|
|
|
|
log := logger.Log
|
|
|
|
|
|
sender := senderFromCtx(ctx)
|
|
|
|
|
|
requestID := requestIDFromCtx(ctx)
|
|
|
|
|
|
|
2026-06-19 21:58:17 +08:00
|
|
|
|
// 将输入元数据写入 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 21:49:28 +08:00
|
|
|
|
// 文本输入模式:跳过 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 21:58:17 +08:00
|
|
|
|
return STTOutput{
|
2026-06-19 21:49:28 +08:00
|
|
|
|
Text: input.Text,
|
|
|
|
|
|
Language: input.Language,
|
|
|
|
|
|
IsSkipped: true,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 语音模式:解码音频
|
|
|
|
|
|
if len(input.AudioData) == 0 {
|
2026-06-19 21:58:17 +08:00
|
|
|
|
return STTOutput{}, fmt.Errorf("stt: no audio data provided")
|
2026-06-19 21:49:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-06-19 21:58:17 +08:00
|
|
|
|
return STTOutput{}, fmt.Errorf("stt: recognize: %w", err)
|
2026-06-19 21:49:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 21:58:17 +08:00
|
|
|
|
return STTOutput{
|
2026-06-19 21:49:28 +08:00
|
|
|
|
Text: text,
|
|
|
|
|
|
Language: input.Language,
|
|
|
|
|
|
IsSkipped: false,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|