feat: 更新配置文档

This commit is contained in:
hhs
2026-06-13 19:57:35 +08:00
parent f764b3a7d8
commit 0d9d3db73b
12 changed files with 83 additions and 34 deletions

View File

@@ -16,17 +16,22 @@ import (
// DeepgramService 基于 Deepgram WebSocket API 的语音识别实现。
type DeepgramService struct {
apiKey string
model string
endpoint string
logger *zap.SugaredLogger
}
// NewDeepgramService 创建 Deepgram STT 服务。
func NewDeepgramService(apiKey, endpoint string, logger *zap.SugaredLogger) *DeepgramService {
func NewDeepgramService(apiKey, model, endpoint string, logger *zap.SugaredLogger) *DeepgramService {
if model == "" {
model = "nova-2"
}
if endpoint == "" {
endpoint = "wss://api.deepgram.com/v1/listen"
}
return &DeepgramService{
apiKey: apiKey,
model: model,
endpoint: endpoint,
logger: logger,
}
@@ -128,7 +133,7 @@ func (d *DeepgramService) buildURL(opts Options) string {
q.Set("encoding", encoding)
q.Set("sample_rate", fmt.Sprintf("%d", sampleRate))
q.Set("language", language)
q.Set("model", "nova-2")
q.Set("model", d.model)
q.Set("punctuate", "true")
u.RawQuery = q.Encode()

View File

@@ -74,7 +74,7 @@ func TestDeepgramService_Recognize_Success(t *testing.T) {
})
defer srv.Close()
svc := NewDeepgramService("test-key", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
svc := NewDeepgramService("test-key", "", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
text, err := svc.Recognize(context.Background(), []byte("fake-pcm-audio"), Options{
Encoding: "pcm_s16le",
@@ -90,7 +90,7 @@ func TestDeepgramService_Recognize_Success(t *testing.T) {
}
func TestDeepgramService_Recognize_EmptyAudio(t *testing.T) {
svc := NewDeepgramService("test-key", "ws://localhost", zap.NewNop().Sugar())
svc := NewDeepgramService("test-key", "", "ws://localhost", zap.NewNop().Sugar())
_, err := svc.Recognize(context.Background(), nil, Options{})
if err == nil {
t.Fatal("Recognize() with empty audio should return error")
@@ -98,7 +98,7 @@ func TestDeepgramService_Recognize_EmptyAudio(t *testing.T) {
}
func TestDeepgramService_Recognize_ConnectError(t *testing.T) {
svc := NewDeepgramService("test-key", "ws://localhost:1", zap.NewNop().Sugar())
svc := NewDeepgramService("test-key", "", "ws://localhost:1", zap.NewNop().Sugar())
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
@@ -117,7 +117,7 @@ func TestDeepgramService_Recognize_Timeout(t *testing.T) {
})
defer srv.Close()
svc := NewDeepgramService("test-key", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
svc := NewDeepgramService("test-key", "", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
@@ -147,7 +147,7 @@ func TestDeepgramService_Recognize_MultipleFinals(t *testing.T) {
})
defer srv.Close()
svc := NewDeepgramService("test-key", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
svc := NewDeepgramService("test-key", "", wsToWss(srv.URL)+"/v1/listen", zap.NewNop().Sugar())
text, err := svc.Recognize(context.Background(), []byte("audio"), Options{})
if err != nil {
@@ -159,7 +159,7 @@ func TestDeepgramService_Recognize_MultipleFinals(t *testing.T) {
}
func TestDeepgramService_buildURL(t *testing.T) {
svc := NewDeepgramService("key", "wss://api.deepgram.com/v1/listen", zap.NewNop().Sugar())
svc := NewDeepgramService("key", "", "wss://api.deepgram.com/v1/listen", zap.NewNop().Sugar())
tests := []struct {
name string

View File

@@ -15,6 +15,7 @@ import (
// OpenAIService 基于 OpenAI TTS API 的语音合成实现。
type OpenAIService struct {
apiKey string
model string
voice string
speed float64
endpoint string
@@ -24,7 +25,10 @@ type OpenAIService struct {
}
// NewOpenAIService 创建 OpenAI TTS 服务。
func NewOpenAIService(apiKey, voice, endpoint string, speed float64, timeoutSec int, logger *zap.SugaredLogger) *OpenAIService {
func NewOpenAIService(apiKey, model, voice, endpoint string, speed float64, timeoutSec int, logger *zap.SugaredLogger) *OpenAIService {
if model == "" {
model = "tts-1"
}
if voice == "" {
voice = "alloy"
}
@@ -40,6 +44,7 @@ func NewOpenAIService(apiKey, voice, endpoint string, speed float64, timeoutSec
}
return &OpenAIService{
apiKey: apiKey,
model: model,
voice: voice,
speed: speed,
endpoint: endpoint,
@@ -109,7 +114,7 @@ func (o *OpenAIService) synthesize(ctx context.Context, text, voice string, spee
defer cancel()
body := ttsRequest{
Model: "tts-1",
Model: o.model,
Input: text,
Voice: voice,
ResponseFormat: "mp3",

View File

@@ -58,7 +58,7 @@ func TestOpenAIService_SynthesizeStream_Success(t *testing.T) {
})
defer srv.Close()
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
textStream := sendSentences("你好", "世界", "")
@@ -110,7 +110,7 @@ func TestOpenAIService_SynthesizeStream_APIError(t *testing.T) {
})
defer srv.Close()
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
textStream := sendSentences("你好")
@@ -142,7 +142,7 @@ func TestOpenAIService_SynthesizeStream_Timeout(t *testing.T) {
defer srv.Close()
// 1 秒超时
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 1, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 1, zap.NewNop().Sugar())
textStream := sendSentences("很长的句子")
@@ -177,7 +177,7 @@ func TestOpenAIService_SynthesizeStream_EmptyText(t *testing.T) {
})
defer srv.Close()
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
// 空句子应该被跳过
textStream := sendSentences("", "你好", "")
@@ -210,7 +210,7 @@ func TestOpenAIService_SynthesizeStream_ContextCancelled(t *testing.T) {
})
defer srv.Close()
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
// 发送多个句子,但在第一个后取消
textStream := make(chan string, 3)
@@ -252,7 +252,7 @@ func TestOpenAIService_SynthesizeStream_PartialFailure(t *testing.T) {
})
defer srv.Close()
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
textStream := sendSentences("第一句", "第二句", "第三句")
@@ -286,7 +286,7 @@ func TestOpenAIService_SynthesizeStream_CustomVoice(t *testing.T) {
})
defer srv.Close()
svc := NewOpenAIService("test-key", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
svc := NewOpenAIService("test-key", "", "alloy", srv.URL, 1.0, 5, zap.NewNop().Sugar())
textStream := sendSentences("你好")

View File

@@ -5,6 +5,7 @@ import (
"os"
"strings"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)
@@ -50,6 +51,7 @@ type AIConfig struct {
type STTConfig struct {
Provider string `mapstructure:"provider"`
APIKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"`
Endpoint string `mapstructure:"endpoint"`
}
@@ -64,6 +66,7 @@ type LLMConfig struct {
type TTSConfig struct {
Provider string `mapstructure:"provider"`
APIKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"`
Voice string `mapstructure:"voice"`
Speed float64 `mapstructure:"speed"`
Endpoint string `mapstructure:"endpoint"`
@@ -98,12 +101,14 @@ func Load() (*Config, error) {
v.SetDefault("redis.addr", "localhost:6379")
v.SetDefault("redis.db", 0)
v.SetDefault("ai.stt.provider", "deepgram")
v.SetDefault("ai.stt.model", "nova-2")
v.SetDefault("ai.stt.endpoint", "wss://api.deepgram.com/v1/listen")
v.SetDefault("ai.llm.provider", "openai")
v.SetDefault("ai.llm.model", "gpt-4o")
v.SetDefault("ai.llm.endpoint", "https://api.openai.com/v1")
v.SetDefault("ai.llm.timeout", 10)
v.SetDefault("ai.tts.provider", "openai")
v.SetDefault("ai.tts.model", "tts-1")
v.SetDefault("ai.tts.voice", "alloy")
v.SetDefault("ai.tts.speed", 1.0)
v.SetDefault("ai.tts.endpoint", "https://api.openai.com/v1")
@@ -125,6 +130,11 @@ func Load() (*Config, error) {
_ = v.MergeInConfig()
}
// 加载 .env 文件(不覆盖已有环境变量)
// 按优先级尝试:当前目录、上级目录(兼容从 backend/ 或项目根目录启动)
_ = godotenv.Load()
_ = godotenv.Load("../.env")
// 环境变量覆盖
v.SetEnvPrefix("CAMTALK")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

View File

@@ -22,6 +22,7 @@ type Pipeline struct {
llmService llm.Service
ttsService tts.Service
sessionMgr session.Manager
model string // LLM 模型名,用于 llm_done 上报
}
// New 创建 Pipeline 实例。
@@ -30,12 +31,14 @@ func New(
llmService llm.Service,
ttsService tts.Service,
sessionMgr session.Manager,
model string,
) *Pipeline {
return &Pipeline{
sttService: sttService,
llmService: llmService,
ttsService: ttsService,
sessionMgr: sessionMgr,
model: model,
}
}
@@ -206,7 +209,7 @@ func (p *Pipeline) ProcessQuery(
Type: "llm_done",
RequestID: req.RequestID,
FullText: fullText,
Model: "gpt-4o",
Model: p.model,
LatencyMs: latency,
}); err != nil {
log.Errorw("发送 llm_done 失败", "error", err)

View File

@@ -254,7 +254,7 @@ func TestProcessQuery_Success(t *testing.T) {
mockSender.On("SendTTSAudio", mock.Anything).Return(nil)
// 创建 Pipeline
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
// 执行
ctx := context.Background()
@@ -305,7 +305,7 @@ func TestProcessQuery_STTError(t *testing.T) {
mockSender.On("SendError", mock.Anything).Return(nil)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
ctx := context.Background()
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
@@ -357,7 +357,7 @@ func TestProcessQuery_LLMError(t *testing.T) {
mockSender.On("SendError", mock.Anything).Return(nil)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
ctx := context.Background()
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
@@ -415,7 +415,7 @@ func TestProcessQuery_TTSError(t *testing.T) {
mockTTS.On("SynthesizeStream", mock.Anything, mock.Anything, mock.Anything).
Return(nil, errors.New("TTS service unavailable"))
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
ctx := context.Background()
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
@@ -485,7 +485,7 @@ func TestProcessQuery_ContextCancelled(t *testing.T) {
}()
mockTTS.On("SynthesizeStream", mock.Anything, mock.Anything, mock.Anything).Return((<-chan tts.Chunk)(ttsCh), nil)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
// 创建可取消的上下文
ctx, cancel := context.WithCancel(context.Background())
@@ -545,7 +545,7 @@ func TestProcessQuery_DisabledTTS(t *testing.T) {
mockSender.On("SendLLMChunk", mock.Anything).Return(nil)
mockSender.On("SendLLMDone", mock.Anything).Return(nil)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
ctx := context.Background()
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
@@ -617,7 +617,7 @@ func TestProcessQuery_InvalidAudio(t *testing.T) {
mockSender.On("SendError", mock.Anything).Return(nil)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
ctx := context.Background()
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
@@ -650,7 +650,7 @@ func TestProcessQuery_SessionNotFound(t *testing.T) {
mockSender.On("SendError", mock.Anything).Return(nil)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
ctx := context.Background()
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)