diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-16 20:38:22 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-16 20:38:22 +0300 |
| commit | d46669426aa6b0ece71d0d05d0b6f2966686b17a (patch) | |
| tree | 9f927c3a8bc763943764ad63e3badafe8a9a7f62 /internal/audio | |
| parent | e49ecfe601c924fa68671477331a860acf8a62f7 (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.go | 64 |
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 +} |
