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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
|
package audio
import (
"fmt"
"path/filepath"
"strings"
)
// SidecarMetadataParams describes the metadata written alongside generated audio.
type SidecarMetadataParams struct {
Provider string
OutputFormat string
CardType string
AudioFile string
AudioFileBack string
OpenAIModel string
OpenAIVoice string
OpenAISpeed float64
OpenAIInstruction string
GeminiTTSModel string
GeminiVoice string
GeminiSpeed float64
}
// ProcessedTextForWord returns the sanitized text sent to TTS providers.
func ProcessedTextForWord(text string) string {
cleanedText := strings.TrimSpace(text)
punctuationToRemove := []string{"!", "?", ".", ",", ";", ":", "\"", "'", "(", ")", "[", "]", "{", "}", "-", "—", "–"}
for _, punct := range punctuationToRemove {
cleanedText = strings.ReplaceAll(cleanedText, punct, "")
}
return fmt.Sprintf("%s...", strings.TrimSpace(cleanedText))
}
// ProcessedTextForProvider returns the exact text shape the provider path sends to TTS.
func ProcessedTextForProvider(provider, text string) string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "openai":
return openAIProcessedText(text)
case "gemini":
return strings.TrimSpace(text)
default:
return strings.TrimSpace(text)
}
}
// InstructionForProvider returns the provider-specific instruction semantics written to attribution files.
// It accepts the flat Config and extracts the relevant sub-config internally.
func InstructionForProvider(provider string, config *Config) string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "openai":
return openAIInstructionForAttribution(openAIAudioConfigFrom(config))
case "gemini":
return geminiPromptInstruction(geminiAudioConfigFrom(config))
default:
return ""
}
}
func openAIProcessedText(text string) string {
cleanedText := strings.TrimSpace(text)
punctuationToRemove := []string{"!", "?", ".", ",", ";", ":", "\"", "'", "(", ")", "[", "]", "{", "}", "-", "—", "–"}
for _, punct := range punctuationToRemove {
cleanedText = strings.ReplaceAll(cleanedText, punct, "")
}
return strings.TrimSpace(cleanedText)
}
func openAIInstructionForAttribution(config OpenAIAudioConfig) string {
if !openAIModelUsesInstructions(config.Model) {
return ""
}
return strings.TrimSpace(config.Instruction)
}
func openAIModelUsesInstructions(model string) bool {
switch strings.TrimSpace(model) {
case "gpt-4o-mini-tts", "gpt-4o-mini-audio-preview":
return true
default:
return false
}
}
func geminiPromptInstruction(config GeminiAudioConfig) string {
var prompt strings.Builder
prompt.WriteString("You are speaking Bulgarian language (български език). ")
prompt.WriteString("Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian.")
if speedHint := geminiSpeedHint(config.Speed); speedHint != "" {
prompt.WriteString(" ")
prompt.WriteString(speedHint)
}
prompt.WriteString("\n\nSpeak the following Bulgarian text:")
if voice := strings.TrimSpace(config.Voice); voice != "" {
prompt.WriteString("\n\nUse a clear, natural delivery that matches the voice named ")
prompt.WriteString(voice)
prompt.WriteString(".")
}
return prompt.String()
}
func geminiSpeedHint(speed float64) string {
switch {
case speed < 0.95:
return "Speak slowly and clearly for language learners."
case speed > 1.05:
return "Speak slightly faster than normal while staying clear."
default:
return "Speak at a natural pace."
}
}
// BuildSidecarMetadata formats the audio metadata written for GUI reloads and batch exports.
func BuildSidecarMetadata(params SidecarMetadataParams) string {
var b strings.Builder
provider := strings.ToLower(strings.TrimSpace(params.Provider))
if provider == "" {
provider = "openai"
}
if params.CardType == "" {
params.CardType = inferCardType(params.AudioFile, params.AudioFileBack)
}
fmt.Fprintf(&b, "provider=%s\n", provider)
switch provider {
case "gemini":
model := strings.TrimSpace(params.GeminiTTSModel)
if model == "" {
model = DefaultProviderConfig().GeminiTTSModel
}
fmt.Fprintf(&b, "model=%s\n", model)
voice := strings.TrimSpace(params.GeminiVoice)
if voice == "" {
voice = "model-default"
}
fmt.Fprintf(&b, "voice=%s\n", voice)
fmt.Fprintf(&b, "speed=%.2f\n", params.GeminiSpeed)
default:
model := strings.TrimSpace(params.OpenAIModel)
if model == "" {
model = DefaultProviderConfig().OpenAIModel
}
fmt.Fprintf(&b, "model=%s\n", model)
voice := strings.TrimSpace(params.OpenAIVoice)
if voice != "" {
fmt.Fprintf(&b, "voice=%s\n", voice)
}
speed := params.OpenAISpeed
if speed <= 0 {
speed = DefaultProviderConfig().OpenAISpeed
}
fmt.Fprintf(&b, "speed=%.2f\n", speed)
if openAIModelUsesInstructions(model) {
if instruction := strings.TrimSpace(params.OpenAIInstruction); instruction != "" {
fmt.Fprintf(&b, "instruction=%s\n", instruction)
}
}
}
format := strings.TrimSpace(params.OutputFormat)
if format == "" {
format = DefaultProviderConfig().OutputFormat
}
fmt.Fprintf(&b, "format=%s\n", format)
if params.CardType != "" {
fmt.Fprintf(&b, "cardtype=%s\n", params.CardType)
}
if audioFile := strings.TrimSpace(params.AudioFile); audioFile != "" {
fmt.Fprintf(&b, "audio_file=%s\n", filepath.Base(audioFile))
}
if audioFileBack := strings.TrimSpace(params.AudioFileBack); audioFileBack != "" {
fmt.Fprintf(&b, "audio_file_back=%s\n", filepath.Base(audioFileBack))
}
return b.String()
}
func inferCardType(audioFile, audioFileBack string) string {
if strings.TrimSpace(audioFileBack) != "" {
return "bg-bg"
}
base := filepath.Base(strings.TrimSpace(audioFile))
switch {
case strings.HasPrefix(base, "audio_front."):
return "bg-bg"
case strings.HasPrefix(base, "audio_back."):
return "bg-bg"
default:
return "en-bg"
}
}
|