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
|
package processor
// CLIConfigResolver applies CLI-flag vs config-file precedence for run-mode
// settings used by the CLI, GUI wiring, and downstream helpers (audio format,
// provider names, Nano Banana models). It does not own API clients or I/O;
// those stay on Processor.
import (
"strings"
"codeberg.org/snonux/totalrecall/internal/audio"
"codeberg.org/snonux/totalrecall/internal/cli"
"codeberg.org/snonux/totalrecall/internal/gui"
"codeberg.org/snonux/totalrecall/internal/image"
"codeberg.org/snonux/totalrecall/internal/phonetic"
"codeberg.org/snonux/totalrecall/internal/translation"
)
// CLIConfigResolver holds the resolved CLI flags and config snapshot from the
// composition root. Methods implement precedence rules between explicit flags
// and YAML config without importing Viper.
type CLIConfigResolver struct {
Flags *cli.Flags
Config *Config
}
// AudioProviderName returns the configured audio provider name, preferring the
// config-file value over the CLI flag so config-file settings win.
func (r *CLIConfigResolver) AudioProviderName() string {
if r.Config.AudioProvider != "" {
return r.Config.AudioProvider
}
if r != nil && r.Flags != nil {
return strings.ToLower(strings.TrimSpace(r.Flags.AudioProvider))
}
return ""
}
// EffectiveAudioFormat resolves the audio format from flags and config, with
// CLI flag taking precedence, then the config-file value, then provider-specific defaults.
func (r *CLIConfigResolver) EffectiveAudioFormat() string {
if r != nil && r.Flags != nil && r.Flags.AudioFormatSpecified {
if format := strings.ToLower(strings.TrimSpace(r.Flags.AudioFormat)); format != "" {
return format
}
}
if r.Config.AudioFormatSet && r.Config.AudioFormat != "" {
return r.Config.AudioFormat
}
if r != nil && r.Flags != nil {
if format := strings.ToLower(strings.TrimSpace(r.Flags.AudioFormat)); format != "" {
return format
}
}
if r.AudioProviderName() == "gemini" {
return audio.DefaultProviderConfig().OutputFormat
}
return "mp3"
}
// GeminiTTSModel returns the Gemini TTS model, preferring the config-file value over the CLI flag.
func (r *CLIConfigResolver) GeminiTTSModel() string {
if r.Config.GeminiTTSModel != "" {
return r.Config.GeminiTTSModel
}
if r != nil && r.Flags != nil {
return strings.TrimSpace(r.Flags.GeminiTTSModel)
}
return ""
}
// GeminiVoice returns the Gemini voice, preferring the config-file value over the CLI flag.
func (r *CLIConfigResolver) GeminiVoice() string {
if r.Config.GeminiVoice != "" {
return r.Config.GeminiVoice
}
if r != nil && r.Flags != nil {
return strings.TrimSpace(r.Flags.GeminiVoice)
}
return ""
}
// OpenAIVoice returns the OpenAI voice, preferring the config-file value over the CLI flag.
func (r *CLIConfigResolver) OpenAIVoice() string {
if r.Config.OpenAIVoice != "" {
return r.Config.OpenAIVoice
}
if r != nil && r.Flags != nil {
return strings.TrimSpace(r.Flags.OpenAIVoice)
}
return ""
}
// GUIConfig returns a gui.Config populated from flags and config.
// Callers (typically cmd/main.go) use this to construct the GUI application
// so that gui.New() lives outside the processor package and the processor→gui
// dependency is limited to the Config type only.
func (r *CLIConfigResolver) GUIConfig() *gui.Config {
imageProvider := r.Flags.ImageAPI
if !r.Flags.ImageAPISpecified {
imageProvider = gui.DefaultConfig().ImageProvider
}
openAIKey := cli.GetOpenAIKey()
googleAPIKey := cli.GetGoogleAPIKey()
translationProvider := translation.Provider(r.Config.TranslationProvider)
phoneticProvider := phonetic.Provider(r.Config.PhoneticProvider)
phoneticFetcher := phonetic.NewFetcher(&phonetic.Config{
Provider: phoneticProvider,
OpenAIKey: openAIKey,
GoogleAPIKey: googleAPIKey,
})
translator := translation.NewTranslator(&translation.Config{
Provider: translationProvider,
OpenAIKey: openAIKey,
GeminiModel: r.Config.TranslationGeminiModel,
})
return &gui.Config{
AudioFormat: r.EffectiveAudioFormat(),
AudioProvider: r.AudioProviderName(),
ImageProvider: imageProvider,
OpenAIKey: openAIKey,
GoogleAPIKey: googleAPIKey,
NanoBananaModel: r.NanoBananaModelForRunMode(),
NanoBananaTextModel: r.NanoBananaTextModelForRunMode(),
GeminiTTSModel: r.GeminiTTSModel(),
GeminiVoice: r.GeminiVoice(),
TranslationProvider: translationProvider,
PhoneticProvider: phoneticProvider,
AutoPlay: !r.Flags.NoAutoPlay, // Invert the flag (--no-auto-play disables auto-play)
PhoneticFetcher: phoneticFetcher,
Translator: translator,
}
}
// NanoBananaModelForRunMode resolves the NanoBanana image model, preferring
// the explicit CLI flag value when set, then the config-file value, then the
// package default.
func (r *CLIConfigResolver) NanoBananaModelForRunMode() string {
if r != nil && r.Flags != nil && r.Flags.NanoBananaModelSpecified {
if model := strings.TrimSpace(r.Flags.NanoBananaModel); model != "" {
return model
}
}
if r.Config.ImageNanoBananaModel != "" {
return r.Config.ImageNanoBananaModel
}
if r != nil && r.Flags != nil {
if model := strings.TrimSpace(r.Flags.NanoBananaModel); model != "" {
return model
}
}
return image.DefaultNanoBananaModel
}
// NanoBananaTextModelForRunMode resolves the NanoBanana text (prompt) model
// using the same CLI-flag-over-config precedence as NanoBananaModelForRunMode.
func (r *CLIConfigResolver) NanoBananaTextModelForRunMode() string {
if r != nil && r.Flags != nil && r.Flags.NanoBananaTextModelSpecified {
if model := strings.TrimSpace(r.Flags.NanoBananaTextModel); model != "" {
return model
}
}
if r.Config.ImageNanoBananaTextModel != "" {
return r.Config.ImageNanoBananaTextModel
}
if r != nil && r.Flags != nil {
if model := strings.TrimSpace(r.Flags.NanoBananaTextModel); model != "" {
return model
}
}
return image.DefaultNanoBananaTextModel
}
|