diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-02 22:26:43 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-02 22:26:43 +0300 |
| commit | 5a1a6b863c3adaa8ec9d087ba130f7676fc9575b (patch) | |
| tree | 31494a962d15f46bed22f8ba090afe7511dc54c7 | |
| parent | 842ce63eda42a5dfa1599bf69b5ea22b332a954d (diff) | |
task zz: add Voices()/BuildAttribution() to Provider interface [OCP]
Extend audio.Provider with Voices() []string and BuildAttribution() string
so all provider-specific behaviour is encapsulated in the implementation
rather than scattered as switch-cases across callers.
Add package-level VoicesFor(name) and BuildAttributionFor(name, params)
for callers (processor, GUI) that need these before constructing a
Provider instance. Add AttributionParamsFrom(config, word, ...) so
callers can build AttributionParams from the flat Config without a manual
provider switch. Implement both new interface methods in OpenAIProvider
and GeminiProvider. Update all Provider mock/fake types in tests.
Migrate audioVoicesForProvider() and saveAudioAttribution() in both
processor.go and gui/generator.go to use the new package-level helpers,
replacing the 10+ duplicated switch blocks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/audio/attribution.go | 28 | ||||
| -rw-r--r-- | internal/audio/gemini_provider.go | 10 | ||||
| -rw-r--r-- | internal/audio/openai_provider.go | 10 | ||||
| -rw-r--r-- | internal/audio/provider.go | 36 | ||||
| -rw-r--r-- | internal/audio/provider_test.go | 8 | ||||
| -rw-r--r-- | internal/gui/generator.go | 70 | ||||
| -rw-r--r-- | internal/gui/generator_test.go | 8 | ||||
| -rw-r--r-- | internal/processor/processor.go | 35 | ||||
| -rw-r--r-- | internal/processor/processor_test.go | 8 |
9 files changed, 136 insertions, 77 deletions
diff --git a/internal/audio/attribution.go b/internal/audio/attribution.go index 7c3cda2..57f8c1d 100644 --- a/internal/audio/attribution.go +++ b/internal/audio/attribution.go @@ -18,6 +18,34 @@ type AttributionParams struct { GeneratedAt time.Time } +// AttributionParamsFrom builds an AttributionParams from a flat Config and the +// word being transcribed, reading provider-appropriate sub-config fields to +// eliminate the need for callers to switch on config.Provider themselves. +func AttributionParamsFrom(config *Config, word, instruction, processedText string, generatedAt time.Time) AttributionParams { + base := AttributionParams{ + Word: word, + Instruction: instruction, + ProcessedText: processedText, + GeneratedAt: generatedAt, + } + if config == nil { + return base + } + switch strings.ToLower(strings.TrimSpace(config.Provider)) { + case "gemini": + g := geminiAudioConfigFrom(config) + base.Model = g.TTSModel + base.Voice = g.Voice + base.Speed = g.Speed + default: + o := openAIAudioConfigFrom(config) + base.Model = o.Model + base.Voice = o.Voice + base.Speed = o.Speed + } + return base +} + // AttributionPath returns the sidecar attribution file path for a generated audio file. func AttributionPath(audioFile string) string { return strings.TrimSuffix(audioFile, filepath.Ext(audioFile)) + "_attribution.txt" diff --git a/internal/audio/gemini_provider.go b/internal/audio/gemini_provider.go index bda01f5..5100c5e 100644 --- a/internal/audio/gemini_provider.go +++ b/internal/audio/gemini_provider.go @@ -97,6 +97,16 @@ func (p *GeminiProvider) Name() string { return "gemini" } +// Voices returns the list of Gemini voices supported by the app. +func (p *GeminiProvider) Voices() []string { + return GeminiVoices +} + +// BuildAttribution returns the Gemini attribution text for a generated audio file. +func (p *GeminiProvider) BuildAttribution(params AttributionParams) string { + return BuildGeminiAttribution(params) +} + // IsAvailable checks if the Google API key is configured. func (p *GeminiProvider) IsAvailable() error { if p == nil || strings.TrimSpace(p.config.APIKey) == "" { diff --git a/internal/audio/openai_provider.go b/internal/audio/openai_provider.go index 9c6d3a4..e6212f8 100644 --- a/internal/audio/openai_provider.go +++ b/internal/audio/openai_provider.go @@ -147,6 +147,16 @@ func (p *OpenAIProvider) IsAvailable() error { return nil } +// Voices returns the list of OpenAI voices supported by the app. +func (p *OpenAIProvider) Voices() []string { + return OpenAIVoices +} + +// BuildAttribution returns the OpenAI attribution text for a generated audio file. +func (p *OpenAIProvider) BuildAttribution(params AttributionParams) string { + return BuildOpenAIAttribution(params) +} + // preprocessBulgarianText prepares Bulgarian text for clearer TTS pronunciation func (p *OpenAIProvider) preprocessBulgarianText(text string) string { return openAIProcessedText(text) diff --git a/internal/audio/provider.go b/internal/audio/provider.go index fac0cd9..60be848 100644 --- a/internal/audio/provider.go +++ b/internal/audio/provider.go @@ -3,18 +3,27 @@ package audio import ( "context" "fmt" + "strings" ) -// Provider defines the interface for text-to-speech providers +// 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 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 returns the provider name. Name() string - // IsAvailable checks if the provider is properly configured and available + // 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. @@ -58,6 +67,25 @@ type Config struct { 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 { diff --git a/internal/audio/provider_test.go b/internal/audio/provider_test.go index 5702016..9b8037c 100644 --- a/internal/audio/provider_test.go +++ b/internal/audio/provider_test.go @@ -28,6 +28,14 @@ func (m *mockProvider) IsAvailable() error { return m.availableErr } +func (m *mockProvider) Voices() []string { + return nil +} + +func (m *mockProvider) BuildAttribution(params AttributionParams) string { + return "" +} + func TestDefaultProviderConfig(t *testing.T) { config := DefaultProviderConfig() diff --git a/internal/gui/generator.go b/internal/gui/generator.go index 72c7c51..4763149 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -15,8 +15,10 @@ import ( "codeberg.org/snonux/totalrecall/internal/image" ) +// promptAwareImageClient extends ImageClient with prompt-callback support +// used by the GUI to capture and display the last generated image prompt. type promptAwareImageClient interface { - image.ImageSearcher + image.ImageClient SetPromptCallback(func(prompt string)) } @@ -49,13 +51,9 @@ func (a *Application) audioProviderName() string { return audio.DefaultProviderConfig().Provider } +// audioVoices returns the voice list for the configured provider. func (a *Application) audioVoices() []string { - switch a.audioProviderName() { - case "gemini": - return audio.GeminiVoices - default: - return audio.OpenAIVoices - } + return audio.VoicesFor(a.audioProviderName()) } func (a *Application) audioVoiceAndSpeed() (string, float64) { @@ -455,45 +453,29 @@ func (a *Application) imagePromptCallback(cardDir, word string) func(prompt stri } } -// saveAudioAttribution saves attribution info for generated audio +// saveAudioAttribution saves attribution info for generated audio. +// Uses BuildAttributionFor so no switch on provider name is needed here. func (a *Application) saveAudioAttribution(word, audioFile, voice string, speed float64) error { processedText := audio.ProcessedTextForWord(word) - var attribution string - switch a.audioProviderName() { - case "gemini": - model := audio.DefaultProviderConfig().GeminiTTSModel - if a.audioConfig != nil && strings.TrimSpace(a.audioConfig.GeminiTTSModel) != "" { - model = a.audioConfig.GeminiTTSModel - } - attribution = audio.BuildGeminiAttribution(audio.AttributionParams{ - Word: word, - Model: model, - Voice: voice, - Speed: speed, - ProcessedText: processedText, - GeneratedAt: time.Now(), - }) - default: - model := audio.DefaultProviderConfig().OpenAIModel - instruction := audio.DefaultProviderConfig().OpenAIInstruction - if a.audioConfig != nil { - if strings.TrimSpace(a.audioConfig.OpenAIModel) != "" { - model = a.audioConfig.OpenAIModel - } - if strings.TrimSpace(a.audioConfig.OpenAIInstruction) != "" { - instruction = a.audioConfig.OpenAIInstruction - } - } - attribution = audio.BuildOpenAIAttribution(audio.AttributionParams{ - Word: word, - Model: model, - Voice: voice, - Speed: speed, - Instruction: instruction, - ProcessedText: processedText, - GeneratedAt: time.Now(), - }) - } + providerName := a.audioProviderName() + + // Build an ephemeral Config from the current audioConfig so we can use + // AttributionParamsFrom to read provider-specific fields without a switch. + cfg := a.audioConfig + if cfg == nil { + cfg = audio.DefaultProviderConfig() + } + // Override voice and speed with the values used for this specific generation. + cfgCopy := *cfg + cfgCopy.Provider = providerName + cfgCopy.GeminiVoice = voice + cfgCopy.GeminiSpeed = speed + cfgCopy.OpenAIVoice = voice + cfgCopy.OpenAISpeed = speed + + instruction := audio.InstructionForProvider(providerName, &cfgCopy) + params := audio.AttributionParamsFrom(&cfgCopy, word, instruction, processedText, time.Now()) + attribution := audio.BuildAttributionFor(providerName, params) // Save to file attrPath := audio.AttributionPath(audioFile) diff --git a/internal/gui/generator_test.go b/internal/gui/generator_test.go index 6892fb1..4e4e3cc 100644 --- a/internal/gui/generator_test.go +++ b/internal/gui/generator_test.go @@ -84,6 +84,14 @@ func (f *fakeAudioProvider) IsAvailable() error { return nil } +func (f *fakeAudioProvider) Voices() []string { + return nil +} + +func (f *fakeAudioProvider) BuildAttribution(params audio.AttributionParams) string { + return "" +} + func TestGenerateImagesWithPromptUsesNanoBananaProvider(t *testing.T) { originalNanoBananaClient := newNanoBananaImageClient originalOpenAIClient := newOpenAIImageClient diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 40487b9..41d6858 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -311,13 +311,10 @@ func (p *Processor) openAIVoice() string { return "" } +// audioVoicesForProvider returns the voice list for the configured provider +// without needing a Provider instance (uses the package-level VoicesFor). func (p *Processor) audioVoicesForProvider() []string { - switch p.audioProviderName() { - case "gemini": - return audio.GeminiVoices - default: - return audio.OpenAIVoices - } + return audio.VoicesFor(p.audioProviderName()) } func (p *Processor) audioVoiceForProvider() string { @@ -1008,29 +1005,9 @@ func (p *Processor) saveAudioAttribution(word, audioFile string, config *audio.C processedText := audio.ProcessedTextForProvider(config.Provider, word) instruction := audio.InstructionForProvider(config.Provider, config) - var attribution string - switch strings.ToLower(strings.TrimSpace(config.Provider)) { - case "gemini": - attribution = audio.BuildGeminiAttribution(audio.AttributionParams{ - Word: word, - Model: config.GeminiTTSModel, - Voice: config.GeminiVoice, - Speed: config.GeminiSpeed, - Instruction: instruction, - ProcessedText: processedText, - GeneratedAt: time.Now(), - }) - default: - attribution = audio.BuildOpenAIAttribution(audio.AttributionParams{ - Word: word, - Model: config.OpenAIModel, - Voice: config.OpenAIVoice, - Speed: config.OpenAISpeed, - Instruction: instruction, - ProcessedText: processedText, - GeneratedAt: time.Now(), - }) - } + // Build params from the provider-specific sub-config to avoid a manual switch. + params := audio.AttributionParamsFrom(config, word, instruction, processedText, time.Now()) + attribution := audio.BuildAttributionFor(config.Provider, params) // Save to file attrPath := audio.AttributionPath(audioFile) diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index cfa4545..34cc844 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -101,6 +101,14 @@ func (f *fakeAudioProvider) IsAvailable() error { return nil } +func (f *fakeAudioProvider) Voices() []string { + return nil +} + +func (f *fakeAudioProvider) BuildAttribution(params audio.AttributionParams) string { + return "" +} + func captureStdout(t *testing.T, fn func()) (output string) { t.Helper() |
