1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
package audio
import (
"context"
"fmt"
"strings"
"codeberg.org/snonux/totalrecall/internal/config"
"codeberg.org/snonux/totalrecall/internal/registry"
)
// Provider defines the interface for text-to-speech providers.
// All provider-specific behavior (voices, attribution) is encapsulated here
// so callers never need to switch on the provider name (OCP).
type Provider interface {
// GenerateAudio generates audio from text and saves it to the specified file.
GenerateAudio(ctx context.Context, text string, outputFile string) error
// Name returns the provider name.
Name() string
// IsAvailable checks if the provider is properly configured and available.
IsAvailable() error
// Voices returns the list of voice names supported by this provider.
Voices() []string
// BuildAttribution returns the attribution text for a generated audio file.
BuildAttribution(params AttributionParams) string
}
// OpenAIAudioConfig holds settings specific to the OpenAI TTS backend.
// Callers that only use Gemini never need to populate these fields.
type OpenAIAudioConfig struct {
Key string
Model string // "tts-1", "tts-1-hd", or "gpt-4o-mini-tts"
Voice string // One of OpenAIVoices.
Speed float64 // 0.25 to 4.0
Instruction string // Voice instructions for gpt-4o-mini-tts model
}
// GeminiAudioConfig holds settings specific to the Gemini TTS backend.
// Callers that only use OpenAI never need to populate these fields.
type GeminiAudioConfig struct {
APIKey string
TTSModel string // "gemini-2.5-flash-preview-tts"
Voice string // One of GeminiVoices; empty lets the caller choose a random voice.
Speed float64 // Prompt hint for desired speech speed
}
// Config holds common configuration for audio providers. Provider-specific
// settings are grouped into OpenAI and Gemini sub-configs so callers and
// implementations only see the fields relevant to their backend.
type Config struct {
Provider string // Provider name: "openai" or "gemini"
OutputDir string // Directory for output files
OutputFormat string // Output format: "mp3" or "wav"
// OpenAI-specific settings — ignored when Provider == "gemini".
OpenAIKey string
OpenAIModel string // "tts-1", "tts-1-hd", or "gpt-4o-mini-tts"
OpenAIVoice string // One of OpenAIVoices.
OpenAISpeed float64 // 0.25 to 4.0
OpenAIInstruction string // Voice instructions for gpt-4o-mini-tts model
// Gemini-specific settings — ignored when Provider == "openai".
GoogleAPIKey string
GeminiTTSModel string // "gemini-2.5-flash-preview-tts"
GeminiVoice string // One of GeminiVoices; empty lets the caller choose a random voice.
GeminiSpeed float64 // Prompt hint for desired speech speed
}
// VoicesFor returns the voice list for the named provider. This is a
// convenience for callers that need voices before constructing a Provider.
func VoicesFor(providerName string) []string {
if strings.ToLower(strings.TrimSpace(providerName)) == "gemini" {
return GeminiVoices
}
return OpenAIVoices
}
// BuildAttributionFor builds the attribution text for the named provider
// without requiring a Provider instance. Use Provider.BuildAttribution when
// you already have an instance.
func BuildAttributionFor(providerName string, params AttributionParams) string {
if strings.ToLower(strings.TrimSpace(providerName)) == "gemini" {
return BuildGeminiAttribution(params)
}
return BuildOpenAIAttribution(params)
}
// openAIAudioConfigFrom extracts the OpenAI-specific sub-config from the flat Config.
// A nil Config produces a zero-value OpenAIAudioConfig.
func openAIAudioConfigFrom(c *Config) OpenAIAudioConfig {
if c == nil {
return OpenAIAudioConfig{}
}
return OpenAIAudioConfig{
Key: c.OpenAIKey,
Model: c.OpenAIModel,
Voice: c.OpenAIVoice,
Speed: c.OpenAISpeed,
Instruction: c.OpenAIInstruction,
}
}
// geminiAudioConfigFrom extracts the Gemini-specific sub-config from the flat Config.
// A nil Config produces a zero-value GeminiAudioConfig.
func geminiAudioConfigFrom(c *Config) GeminiAudioConfig {
if c == nil {
return GeminiAudioConfig{}
}
return GeminiAudioConfig{
APIKey: c.GoogleAPIKey,
TTSModel: c.GeminiTTSModel,
Voice: c.GeminiVoice,
Speed: c.GeminiSpeed,
}
}
// DefaultProviderConfig returns default configuration (shared literals live in
// internal/config/defaults.go).
func DefaultProviderConfig() *Config {
return &Config{
Provider: config.ProviderGemini,
OutputDir: "./",
OutputFormat: config.DefaultAudioOutputFormat,
OpenAIModel: config.DefaultOpenAIAudioModel,
OpenAIVoice: config.DefaultOpenAIVoice,
OpenAISpeed: config.DefaultOpenAIAudioSpeed,
OpenAIInstruction: config.DefaultOpenAIAudioInstruction,
GeminiTTSModel: config.DefaultGeminiTTSModel,
GeminiSpeed: config.DefaultGeminiAudioSpeed,
}
}
// ProviderFactory is the canonical type for functions that construct an audio
// Provider from a Config. Using a named type avoids duplicating the raw function
// signature in every package that needs to inject or replace the factory
// (processor, gui). The production default is audio.NewProvider itself.
type ProviderFactory func(*Config) (Provider, error)
// defaultAudioProviders maps provider name to constructor. New providers are
// registered here so NewProvider does not grow a new switch branch each time.
var defaultAudioProviders = func() *registry.Registry[string, func(*Config) (Provider, error)] {
r := registry.New[string, func(*Config) (Provider, error)]()
r.Register("openai", newOpenAIProviderFromConfig)
r.Register("gemini", newGeminiProviderFromConfig)
return r
}()
func newOpenAIProviderFromConfig(config *Config) (Provider, error) {
if config.OpenAIKey == "" {
return nil, fmt.Errorf("OpenAI API key is required")
}
return NewOpenAIProvider(openAIAudioConfigFrom(config), config.OutputFormat)
}
func newGeminiProviderFromConfig(config *Config) (Provider, error) {
if config.GoogleAPIKey == "" {
return nil, fmt.Errorf("google API key is required")
}
return NewGeminiProvider(geminiAudioConfigFrom(config), config.OutputFormat)
}
// NewProvider creates the appropriate audio provider based on configuration.
// It extracts provider-specific sub-configs so each implementation only
// receives the fields it needs (ISP).
func NewProvider(config *Config) (Provider, error) {
if config == nil {
config = DefaultProviderConfig()
}
name := strings.ToLower(strings.TrimSpace(config.Provider))
fn, ok := defaultAudioProviders.Get(name)
if !ok {
return nil, fmt.Errorf("unknown audio provider: %s", config.Provider)
}
return fn(config)
}
|