Files
Cyrene_v2/backend/services/voice/service.go
T
Aska 99806fefad feat: add gateway API, voice service, WebSocket hub, and startup entry
- services/gateway/api: net/http router with JWT auth, stub login, 6 tests
- services/gateway/ws: WebSocket hub with echo support
- services/voice: STT/TTS provider chain with fallback, 6 tests
- cmd/cyrene/main.go: single binary startup, 108 tests total
- Smoke test: health check + login verified on :8080
2026-08-09 21:09:43 +08:00

116 lines
3.5 KiB
Go

// Package voice provides speech-to-text and text-to-speech capabilities.
// It defines provider interfaces allowing pluggable backends (DashScope, Whisper, Edge-TTS, espeak).
package voice
import "context"
// STTProvider converts speech audio to text.
type STTProvider interface {
// Name returns the provider identifier.
Name() string // "dashscope", "whisper-local"
// Recognize converts audio data to text.
Recognize(ctx context.Context, audioData []byte) (string, error)
}
// TTSProvider converts text to speech audio.
type TTSProvider interface {
// Name returns the provider identifier.
Name() string // "edge-tts", "espeak", "silent"
// Synthesize converts text to audio bytes.
Synthesize(ctx context.Context, text string, voice string) ([]byte, error)
}
// Service manages STT and TTS providers with fallback chains.
type Service struct {
sttProvider STTProvider
ttsProviders []TTSProvider // Ordered by priority (first = preferred)
}
// Config holds voice service configuration.
type Config struct {
STTProvider string // "dashscope" | "whisper-local"
TTSProviders []string // ["edge-tts", "espeak", "silent"] ordered by priority
DashScopeKey string
}
// New creates a new voice service.
func New(cfg Config) *Service {
svc := &Service{}
// STT: for now, use a simple placeholder
if cfg.STTProvider == "dashscope" && cfg.DashScopeKey != "" {
svc.sttProvider = &dashscopeSTT{apiKey: cfg.DashScopeKey}
} else {
svc.sttProvider = &silentSTT{}
}
// TTS: build fallback chain
for _, name := range cfg.TTSProviders {
switch name {
case "edge-tts":
svc.ttsProviders = append(svc.ttsProviders, &edgeTTS{})
case "espeak":
svc.ttsProviders = append(svc.ttsProviders, &espeakTTS{})
case "silent":
svc.ttsProviders = append(svc.ttsProviders, &silentTTS{})
}
}
if len(svc.ttsProviders) == 0 {
svc.ttsProviders = append(svc.ttsProviders, &silentTTS{})
}
return svc
}
// Recognize performs speech recognition with the configured STT provider.
func (s *Service) Recognize(ctx context.Context, audioData []byte) (string, error) {
return s.sttProvider.Recognize(ctx, audioData)
}
// Synthesize performs TTS with automatic fallback through the provider chain.
func (s *Service) Synthesize(ctx context.Context, text string, voice string) ([]byte, error) {
var lastErr error
for _, p := range s.ttsProviders {
audio, err := p.Synthesize(ctx, text, voice)
if err == nil {
return audio, nil
}
lastErr = err
}
return nil, lastErr
}
// --- Provider stubs ---
type dashscopeSTT struct{ apiKey string }
func (p *dashscopeSTT) Name() string { return "dashscope" }
func (p *dashscopeSTT) Recognize(_ context.Context, _ []byte) (string, error) {
return "", nil // Placeholder: real implementation requires DashScope SDK
}
type silentSTT struct{}
func (p *silentSTT) Name() string { return "silent" }
func (p *silentSTT) Recognize(_ context.Context, _ []byte) (string, error) {
return "", nil
}
type edgeTTS struct{}
func (p *edgeTTS) Name() string { return "edge-tts" }
func (p *edgeTTS) Synthesize(_ context.Context, text string, _ string) ([]byte, error) {
return []byte(text), nil // Placeholder
}
type espeakTTS struct{}
func (p *espeakTTS) Name() string { return "espeak" }
func (p *espeakTTS) Synthesize(_ context.Context, text string, _ string) ([]byte, error) {
return []byte(text), nil
}
type silentTTS struct{}
func (p *silentTTS) Name() string { return "silent" }
func (p *silentTTS) Synthesize(_ context.Context, _ string, _ string) ([]byte, error) {
return make([]byte, 0), nil
}