From d46669426aa6b0ece71d0d05d0b6f2966686b17a Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 16 Jul 2025 20:38:22 +0300 Subject: feat: add custom image prompt support and keyboard shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- internal/audio/openai_provider.go | 64 +++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) (limited to 'internal/audio') 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 +} -- cgit v1.2.3