feat: 规范化配置文档 #44
21
.env.example
Normal file
21
.env.example
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# CamTalk 环境变量模板
|
||||||
|
# 复制为 .env 并填入实际值:cp .env.example .env
|
||||||
|
# .env 已在 .gitignore 中,不会提交到版本控制
|
||||||
|
|
||||||
|
# ---- AI 服务 API Key ----
|
||||||
|
CAMTALK_AI_LLM_API_KEY=sk-xxx
|
||||||
|
CAMTALK_AI_STT_API_KEY=
|
||||||
|
CAMTALK_AI_TTS_API_KEY=
|
||||||
|
|
||||||
|
# ---- 可选覆盖(默认值见 config.yaml)----
|
||||||
|
# CAMTALK_AI_LLM_MODEL=gpt-4o
|
||||||
|
# CAMTALK_AI_LLM_ENDPOINT=https://api.openai.com/v1
|
||||||
|
# CAMTALK_AI_LLM_TIMEOUT=10
|
||||||
|
# CAMTALK_AI_STT_ENDPOINT=wss://api.deepgram.com/v1/listen
|
||||||
|
# CAMTALK_AI_TTS_ENDPOINT=https://api.openai.com/v1
|
||||||
|
# CAMTALK_AI_TTS_VOICE=alloy
|
||||||
|
# CAMTALK_AI_TTS_SPEED=1.0
|
||||||
|
# CAMTALK_AI_TTS_TIMEOUT=5
|
||||||
|
|
||||||
|
# ---- 应用 ----
|
||||||
|
# APP_ENV=dev
|
||||||
@@ -46,12 +46,12 @@ func main() {
|
|||||||
defer sessionMgr.(*session.MemoryManager).Stop()
|
defer sessionMgr.(*session.MemoryManager).Stop()
|
||||||
|
|
||||||
// 初始化 AI 服务
|
// 初始化 AI 服务
|
||||||
sttService := stt.NewDeepgramService(cfg.AI.STT.APIKey, cfg.AI.STT.Endpoint, logger.Log)
|
sttService := stt.NewDeepgramService(cfg.AI.STT.APIKey, cfg.AI.STT.Model, cfg.AI.STT.Endpoint, logger.Log)
|
||||||
llmService := llm.NewOpenAIService(cfg.AI.LLM.APIKey, cfg.AI.LLM.Model, cfg.AI.LLM.Endpoint, cfg.AI.LLM.Timeout, logger.Log)
|
llmService := llm.NewOpenAIService(cfg.AI.LLM.APIKey, cfg.AI.LLM.Model, cfg.AI.LLM.Endpoint, cfg.AI.LLM.Timeout, logger.Log)
|
||||||
ttsService := tts.NewOpenAIService(cfg.AI.TTS.APIKey, cfg.AI.TTS.Voice, cfg.AI.TTS.Endpoint, cfg.AI.TTS.Speed, cfg.AI.TTS.Timeout, logger.Log)
|
ttsService := tts.NewOpenAIService(cfg.AI.TTS.APIKey, cfg.AI.TTS.Model, cfg.AI.TTS.Voice, cfg.AI.TTS.Endpoint, cfg.AI.TTS.Speed, cfg.AI.TTS.Timeout, logger.Log)
|
||||||
|
|
||||||
// 初始化 Orchestrator
|
// 初始化 Orchestrator
|
||||||
orch := orchestrator.New(sttService, llmService, ttsService, sessionMgr)
|
orch := orchestrator.New(sttService, llmService, ttsService, sessionMgr, cfg.AI.LLM.Model)
|
||||||
|
|
||||||
// Gin 模式
|
// Gin 模式
|
||||||
if cfg.App.Env == "prod" {
|
if cfg.App.Env == "prod" {
|
||||||
|
|||||||
@@ -15,18 +15,20 @@ redis:
|
|||||||
|
|
||||||
ai:
|
ai:
|
||||||
stt:
|
stt:
|
||||||
provider: deepgram
|
provider: Xiaomi MiMo
|
||||||
endpoint: "wss://api.deepgram.com/v1/listen"
|
model: mimo-v2.5
|
||||||
|
endpoint: "https://token-plan-cn.xiaomimimo.com/v1"
|
||||||
llm:
|
llm:
|
||||||
provider: openai
|
provider: dashscope
|
||||||
model: gpt-4o
|
model: qwen3-vl-plus
|
||||||
endpoint: "https://api.openai.com/v1"
|
endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
timeout: 10
|
timeout: 30
|
||||||
tts:
|
tts:
|
||||||
provider: openai
|
provider: Xiaomi MiMo
|
||||||
|
model: mimo-v2.5
|
||||||
voice: alloy
|
voice: alloy
|
||||||
speed: 1.0
|
speed: 1.0
|
||||||
endpoint: "https://api.openai.com/v1"
|
endpoint: "https://token-plan-cn.xiaomimimo.com/v1"
|
||||||
timeout: 5
|
timeout: 5
|
||||||
|
|
||||||
storage:
|
storage:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
|||||||
@@ -16,17 +16,22 @@ import (
|
|||||||
// DeepgramService 基于 Deepgram WebSocket API 的语音识别实现。
|
// DeepgramService 基于 Deepgram WebSocket API 的语音识别实现。
|
||||||
type DeepgramService struct {
|
type DeepgramService struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
|
model string
|
||||||
endpoint string
|
endpoint string
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeepgramService 创建 Deepgram STT 服务。
|
// 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 == "" {
|
if endpoint == "" {
|
||||||
endpoint = "wss://api.deepgram.com/v1/listen"
|
endpoint = "wss://api.deepgram.com/v1/listen"
|
||||||
}
|
}
|
||||||
return &DeepgramService{
|
return &DeepgramService{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
model: model,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
@@ -128,7 +133,7 @@ func (d *DeepgramService) buildURL(opts Options) string {
|
|||||||
q.Set("encoding", encoding)
|
q.Set("encoding", encoding)
|
||||||
q.Set("sample_rate", fmt.Sprintf("%d", sampleRate))
|
q.Set("sample_rate", fmt.Sprintf("%d", sampleRate))
|
||||||
q.Set("language", language)
|
q.Set("language", language)
|
||||||
q.Set("model", "nova-2")
|
q.Set("model", d.model)
|
||||||
q.Set("punctuate", "true")
|
q.Set("punctuate", "true")
|
||||||
u.RawQuery = q.Encode()
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func TestDeepgramService_Recognize_Success(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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{
|
text, err := svc.Recognize(context.Background(), []byte("fake-pcm-audio"), Options{
|
||||||
Encoding: "pcm_s16le",
|
Encoding: "pcm_s16le",
|
||||||
@@ -90,7 +90,7 @@ func TestDeepgramService_Recognize_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDeepgramService_Recognize_EmptyAudio(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{})
|
_, err := svc.Recognize(context.Background(), nil, Options{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Recognize() with empty audio should return error")
|
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) {
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ func TestDeepgramService_Recognize_Timeout(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -147,7 +147,7 @@ func TestDeepgramService_Recognize_MultipleFinals(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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{})
|
text, err := svc.Recognize(context.Background(), []byte("audio"), Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,7 +159,7 @@ func TestDeepgramService_Recognize_MultipleFinals(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDeepgramService_buildURL(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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
// OpenAIService 基于 OpenAI TTS API 的语音合成实现。
|
// OpenAIService 基于 OpenAI TTS API 的语音合成实现。
|
||||||
type OpenAIService struct {
|
type OpenAIService struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
|
model string
|
||||||
voice string
|
voice string
|
||||||
speed float64
|
speed float64
|
||||||
endpoint string
|
endpoint string
|
||||||
@@ -24,7 +25,10 @@ type OpenAIService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewOpenAIService 创建 OpenAI TTS 服务。
|
// 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 == "" {
|
if voice == "" {
|
||||||
voice = "alloy"
|
voice = "alloy"
|
||||||
}
|
}
|
||||||
@@ -40,6 +44,7 @@ func NewOpenAIService(apiKey, voice, endpoint string, speed float64, timeoutSec
|
|||||||
}
|
}
|
||||||
return &OpenAIService{
|
return &OpenAIService{
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
model: model,
|
||||||
voice: voice,
|
voice: voice,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
@@ -109,7 +114,7 @@ func (o *OpenAIService) synthesize(ctx context.Context, text, voice string, spee
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
body := ttsRequest{
|
body := ttsRequest{
|
||||||
Model: "tts-1",
|
Model: o.model,
|
||||||
Input: text,
|
Input: text,
|
||||||
Voice: voice,
|
Voice: voice,
|
||||||
ResponseFormat: "mp3",
|
ResponseFormat: "mp3",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func TestOpenAIService_SynthesizeStream_Success(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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("你好", "世界", "!")
|
textStream := sendSentences("你好", "世界", "!")
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ func TestOpenAIService_SynthesizeStream_APIError(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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("你好")
|
textStream := sendSentences("你好")
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ func TestOpenAIService_SynthesizeStream_Timeout(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
// 1 秒超时
|
// 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("很长的句子")
|
textStream := sendSentences("很长的句子")
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ func TestOpenAIService_SynthesizeStream_EmptyText(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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("", "你好", "")
|
textStream := sendSentences("", "你好", "")
|
||||||
@@ -210,7 +210,7 @@ func TestOpenAIService_SynthesizeStream_ContextCancelled(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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)
|
textStream := make(chan string, 3)
|
||||||
@@ -252,7 +252,7 @@ func TestOpenAIService_SynthesizeStream_PartialFailure(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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("第一句", "第二句", "第三句")
|
textStream := sendSentences("第一句", "第二句", "第三句")
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ func TestOpenAIService_SynthesizeStream_CustomVoice(t *testing.T) {
|
|||||||
})
|
})
|
||||||
defer srv.Close()
|
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("你好")
|
textStream := sendSentences("你好")
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ type AIConfig struct {
|
|||||||
type STTConfig struct {
|
type STTConfig struct {
|
||||||
Provider string `mapstructure:"provider"`
|
Provider string `mapstructure:"provider"`
|
||||||
APIKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
Model string `mapstructure:"model"`
|
||||||
Endpoint string `mapstructure:"endpoint"`
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ type LLMConfig struct {
|
|||||||
type TTSConfig struct {
|
type TTSConfig struct {
|
||||||
Provider string `mapstructure:"provider"`
|
Provider string `mapstructure:"provider"`
|
||||||
APIKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
Model string `mapstructure:"model"`
|
||||||
Voice string `mapstructure:"voice"`
|
Voice string `mapstructure:"voice"`
|
||||||
Speed float64 `mapstructure:"speed"`
|
Speed float64 `mapstructure:"speed"`
|
||||||
Endpoint string `mapstructure:"endpoint"`
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
@@ -98,12 +101,14 @@ func Load() (*Config, error) {
|
|||||||
v.SetDefault("redis.addr", "localhost:6379")
|
v.SetDefault("redis.addr", "localhost:6379")
|
||||||
v.SetDefault("redis.db", 0)
|
v.SetDefault("redis.db", 0)
|
||||||
v.SetDefault("ai.stt.provider", "deepgram")
|
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.stt.endpoint", "wss://api.deepgram.com/v1/listen")
|
||||||
v.SetDefault("ai.llm.provider", "openai")
|
v.SetDefault("ai.llm.provider", "openai")
|
||||||
v.SetDefault("ai.llm.model", "gpt-4o")
|
v.SetDefault("ai.llm.model", "gpt-4o")
|
||||||
v.SetDefault("ai.llm.endpoint", "https://api.openai.com/v1")
|
v.SetDefault("ai.llm.endpoint", "https://api.openai.com/v1")
|
||||||
v.SetDefault("ai.llm.timeout", 10)
|
v.SetDefault("ai.llm.timeout", 10)
|
||||||
v.SetDefault("ai.tts.provider", "openai")
|
v.SetDefault("ai.tts.provider", "openai")
|
||||||
|
v.SetDefault("ai.tts.model", "tts-1")
|
||||||
v.SetDefault("ai.tts.voice", "alloy")
|
v.SetDefault("ai.tts.voice", "alloy")
|
||||||
v.SetDefault("ai.tts.speed", 1.0)
|
v.SetDefault("ai.tts.speed", 1.0)
|
||||||
v.SetDefault("ai.tts.endpoint", "https://api.openai.com/v1")
|
v.SetDefault("ai.tts.endpoint", "https://api.openai.com/v1")
|
||||||
@@ -125,6 +130,11 @@ func Load() (*Config, error) {
|
|||||||
_ = v.MergeInConfig()
|
_ = v.MergeInConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载 .env 文件(不覆盖已有环境变量)
|
||||||
|
// 按优先级尝试:当前目录、上级目录(兼容从 backend/ 或项目根目录启动)
|
||||||
|
_ = godotenv.Load()
|
||||||
|
_ = godotenv.Load("../.env")
|
||||||
|
|
||||||
// 环境变量覆盖
|
// 环境变量覆盖
|
||||||
v.SetEnvPrefix("CAMTALK")
|
v.SetEnvPrefix("CAMTALK")
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Pipeline struct {
|
|||||||
llmService llm.Service
|
llmService llm.Service
|
||||||
ttsService tts.Service
|
ttsService tts.Service
|
||||||
sessionMgr session.Manager
|
sessionMgr session.Manager
|
||||||
|
model string // LLM 模型名,用于 llm_done 上报
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建 Pipeline 实例。
|
// New 创建 Pipeline 实例。
|
||||||
@@ -30,12 +31,14 @@ func New(
|
|||||||
llmService llm.Service,
|
llmService llm.Service,
|
||||||
ttsService tts.Service,
|
ttsService tts.Service,
|
||||||
sessionMgr session.Manager,
|
sessionMgr session.Manager,
|
||||||
|
model string,
|
||||||
) *Pipeline {
|
) *Pipeline {
|
||||||
return &Pipeline{
|
return &Pipeline{
|
||||||
sttService: sttService,
|
sttService: sttService,
|
||||||
llmService: llmService,
|
llmService: llmService,
|
||||||
ttsService: ttsService,
|
ttsService: ttsService,
|
||||||
sessionMgr: sessionMgr,
|
sessionMgr: sessionMgr,
|
||||||
|
model: model,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@ func (p *Pipeline) ProcessQuery(
|
|||||||
Type: "llm_done",
|
Type: "llm_done",
|
||||||
RequestID: req.RequestID,
|
RequestID: req.RequestID,
|
||||||
FullText: fullText,
|
FullText: fullText,
|
||||||
Model: "gpt-4o",
|
Model: p.model,
|
||||||
LatencyMs: latency,
|
LatencyMs: latency,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Errorw("发送 llm_done 失败", "error", err)
|
log.Errorw("发送 llm_done 失败", "error", err)
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ func TestProcessQuery_Success(t *testing.T) {
|
|||||||
mockSender.On("SendTTSAudio", mock.Anything).Return(nil)
|
mockSender.On("SendTTSAudio", mock.Anything).Return(nil)
|
||||||
|
|
||||||
// 创建 Pipeline
|
// 创建 Pipeline
|
||||||
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
|
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
|
||||||
|
|
||||||
// 执行
|
// 执行
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -305,7 +305,7 @@ func TestProcessQuery_STTError(t *testing.T) {
|
|||||||
|
|
||||||
mockSender.On("SendError", mock.Anything).Return(nil)
|
mockSender.On("SendError", mock.Anything).Return(nil)
|
||||||
|
|
||||||
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
|
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
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)
|
mockSender.On("SendError", mock.Anything).Return(nil)
|
||||||
|
|
||||||
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
|
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
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).
|
mockTTS.On("SynthesizeStream", mock.Anything, mock.Anything, mock.Anything).
|
||||||
Return(nil, errors.New("TTS service unavailable"))
|
Return(nil, errors.New("TTS service unavailable"))
|
||||||
|
|
||||||
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
|
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
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)
|
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())
|
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("SendLLMChunk", mock.Anything).Return(nil)
|
||||||
mockSender.On("SendLLMDone", 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()
|
ctx := context.Background()
|
||||||
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
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)
|
mockSender.On("SendError", mock.Anything).Return(nil)
|
||||||
|
|
||||||
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
|
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
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)
|
mockSender.On("SendError", mock.Anything).Return(nil)
|
||||||
|
|
||||||
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession)
|
pipeline := New(mockSTT, mockLLM, mockTTS, mockSession, "gpt-4o")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
err := pipeline.ProcessQuery(ctx, "session-123", req, nil, mockSender)
|
||||||
|
|||||||
Reference in New Issue
Block a user