summaryrefslogtreecommitdiff
path: root/internal/audio/provider.go
blob: 60be84813293873a077b4c505aaf4e320c31d9a8 (plain)
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
package audio

import (
	"context"
	"fmt"
	"strings"
)

// 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,
	}
}

// DefaultConfig returns default configuration
func DefaultProviderConfig() *Config {
	return &Config{
		Provider:     "gemini",
		OutputDir:    "./",
		OutputFormat: "mp3",
		OpenAIModel:  "gpt-4o-mini-tts", // New model with voice instructions support
		OpenAIVoice:  "alloy",
		OpenAISpeed:  1.0,
		// OpenAISpeed:       0.98, // Default speed for clarity
		OpenAIInstruction: "You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.",
		GeminiTTSModel:    "gemini-2.5-flash-preview-tts",
		GeminiSpeed:       1.0,
	}
}

// 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()
	}

	switch config.Provider {
	case "openai":
		if config.OpenAIKey == "" {
			return nil, fmt.Errorf("OpenAI API key is required")
		}
		return NewOpenAIProvider(openAIAudioConfigFrom(config), config.OutputFormat)
	case "gemini":
		if config.GoogleAPIKey == "" {
			return nil, fmt.Errorf("google API key is required")
		}
		return NewGeminiProvider(geminiAudioConfigFrom(config), config.OutputFormat)
	default:
		return nil, fmt.Errorf("unknown audio provider: %s", config.Provider)
	}
}