summaryrefslogtreecommitdiff
path: root/internal/audio
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-16 20:38:22 +0300
committerPaul Buetow <paul@buetow.org>2025-07-16 20:38:22 +0300
commitd46669426aa6b0ece71d0d05d0b6f2966686b17a (patch)
tree9f927c3a8bc763943764ad63e3badafe8a9a7f62 /internal/audio
parente49ecfe601c924fa68671477331a860acf8a62f7 (diff)
feat: add custom image prompt support and keyboard shortcuts
- Add text area next to image display for custom image generation prompts - Users can specify their own prompts or leave empty for auto-generation - Display the used prompt in the text area after generation - Load prompts from attribution files when navigating to existing cards - Add keyboard shortcuts for all GUI buttons: - G: Generate, N: New Word, I: Regenerate Image, A: Regenerate Audio - R: Regenerate All, D: Delete, P: Play audio - Left/Right arrows: Navigate between words - Y/N: Confirm/cancel delete dialog - Update UI layout with equal 50/50 split between image and prompt - Enable text wrapping in prompt text area - Add 25% chance to ask OpenAI for creative photo style suggestions - Fix concurrent processing to properly use custom prompts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal/audio')
-rw-r--r--internal/audio/openai_provider.go64
1 files changed, 32 insertions, 32 deletions
diff --git a/internal/audio/openai_provider.go b/internal/audio/openai_provider.go
index b72d793..0f3a0ad 100644
--- a/internal/audio/openai_provider.go
+++ b/internal/audio/openai_provider.go
@@ -9,7 +9,7 @@ import (
"os"
"path/filepath"
"strings"
-
+
"github.com/sashabaranov/go-openai"
)
@@ -26,23 +26,23 @@ func NewOpenAIProvider(config *Config) (Provider, error) {
if config.OpenAIKey == "" {
return nil, fmt.Errorf("OpenAI API key is required")
}
-
+
client := openai.NewClient(config.OpenAIKey)
-
+
provider := &OpenAIProvider{
client: client,
config: config,
cacheDir: config.CacheDir,
enableCache: config.EnableCache,
}
-
+
// Create cache directory if caching is enabled
if provider.enableCache && provider.cacheDir != "" {
if err := os.MkdirAll(provider.cacheDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create cache directory: %w", err)
}
}
-
+
return provider, nil
}
@@ -52,7 +52,7 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
if err := ValidateBulgarianText(text); err != nil {
return err
}
-
+
// Check cache first
if p.enableCache {
cacheFile := p.getCacheFilePath(text)
@@ -61,10 +61,10 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
return p.copyFile(cacheFile, outputFile)
}
}
-
+
// Preprocess text for clearer Bulgarian pronunciation
processedText := p.preprocessBulgarianText(text)
-
+
// Prepare the TTS request
// OpenAI TTS will automatically detect and pronounce Bulgarian text
fmt.Printf("OpenAI TTS: Using model '%s' with voice '%s' at speed %.2f\n", p.config.OpenAIModel, p.config.OpenAIVoice, p.config.OpenAISpeed)
@@ -72,19 +72,19 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
fmt.Printf("OpenAI TTS Instruction: '%s'\n", p.config.OpenAIInstruction)
}
fmt.Printf("OpenAI TTS Input: '%s'\n", processedText)
-
+
req := openai.CreateSpeechRequest{
Model: openai.SpeechModel(p.config.OpenAIModel),
Input: processedText,
Voice: openai.SpeechVoice(p.config.OpenAIVoice),
Speed: p.config.OpenAISpeed,
}
-
+
// Add instructions for gpt-4o-mini-tts model
if p.config.OpenAIInstruction != "" && (p.config.OpenAIModel == "gpt-4o-mini-tts" || p.config.OpenAIModel == "gpt-4o-mini-audio-preview") {
req.Instructions = p.config.OpenAIInstruction
}
-
+
// Determine response format based on output file extension
ext := strings.ToLower(filepath.Ext(outputFile))
switch ext {
@@ -104,7 +104,7 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
outputFile += ".mp3"
}
}
-
+
// Make the API call
response, err := p.client.CreateSpeech(ctx, req)
if err != nil {
@@ -116,7 +116,7 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
return fmt.Errorf("OpenAI TTS API error: %w", err)
}
defer response.Close()
-
+
// Ensure output directory exists
dir := filepath.Dir(outputFile)
if dir != "" && dir != "." {
@@ -124,30 +124,30 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
return fmt.Errorf("failed to create output directory: %w", err)
}
}
-
+
// Create output file
out, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer out.Close()
-
+
// Copy the audio data
written, err := io.Copy(out, response)
if err != nil {
return fmt.Errorf("failed to write audio file: %w", err)
}
-
+
if written == 0 {
return fmt.Errorf("no audio data received from OpenAI")
}
-
+
// Cache the result if caching is enabled
if p.enableCache {
cacheFile := p.getCacheFilePath(text)
_ = p.copyFile(outputFile, cacheFile) // Ignore cache errors
}
-
+
return nil
}
@@ -161,7 +161,7 @@ func (p *OpenAIProvider) IsAvailable() error {
if p.config.OpenAIKey == "" {
return fmt.Errorf("OpenAI API key not configured")
}
-
+
// We could make a test API call here, but that would use credits
// For now, just check that we have a key
return nil
@@ -171,20 +171,20 @@ func (p *OpenAIProvider) IsAvailable() error {
func (p *OpenAIProvider) preprocessBulgarianText(text string) string {
// First, clean the text and remove punctuation that shouldn't be spoken
cleanedText := strings.TrimSpace(text)
-
+
// Remove common punctuation marks that shouldn't be pronounced
punctuationToRemove := []string{"!", "?", ".", ",", ";", ":", "\"", "'", "(", ")", "[", "]", "{", "}", "-", "—", "–"}
for _, punct := range punctuationToRemove {
cleanedText = strings.ReplaceAll(cleanedText, punct, "")
}
-
+
// Trim any remaining whitespace
cleanedText = strings.TrimSpace(cleanedText)
-
+
// For single words, we add subtle punctuation to create natural pauses
// This helps the TTS engine pronounce it more carefully
- processedText := fmt.Sprintf("%s...", cleanedText)
-
+ processedText := cleanedText // fmt.Sprintf("%s...", cleanedText)
+
return processedText
}
@@ -201,11 +201,11 @@ func (p *OpenAIProvider) getCacheFilePath(text string) string {
h.Write([]byte(p.config.OpenAIInstruction))
}
hash := hex.EncodeToString(h.Sum(nil))
-
+
// Use first 2 chars as subdirectory for better file system performance
subdir := hash[:2]
filename := hash[2:] + ".mp3"
-
+
return filepath.Join(p.cacheDir, subdir, filename)
}
@@ -218,19 +218,19 @@ func (p *OpenAIProvider) copyFile(src, dst string) error {
return err
}
}
-
+
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()
-
+
destination, err := os.Create(dst)
if err != nil {
return err
}
defer destination.Close()
-
+
_, err = io.Copy(destination, source)
return err
}
@@ -248,7 +248,7 @@ func (p *OpenAIProvider) GetCacheStats() (fileCount int, totalSize int64, err er
if !p.enableCache || p.cacheDir == "" {
return 0, 0, nil
}
-
+
err = filepath.Walk(p.cacheDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -259,6 +259,6 @@ func (p *OpenAIProvider) GetCacheStats() (fileCount int, totalSize int64, err er
}
return nil
})
-
+
return fileCount, totalSize, err
-} \ No newline at end of file
+}