summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.totalrecall.yaml.example59
-rw-r--r--GPT4O_AUDIO_NOTE.md35
-rw-r--r--README.md26
-rw-r--r--cmd/totalrecall/main.go129
-rw-r--r--internal/audio/openai_provider.go41
-rw-r--r--internal/audio/provider.go16
-rw-r--r--internal/image/openai.go63
-rw-r--r--internal/image/translate.go9
8 files changed, 330 insertions, 48 deletions
diff --git a/.totalrecall.yaml.example b/.totalrecall.yaml.example
new file mode 100644
index 0000000..e41b97f
--- /dev/null
+++ b/.totalrecall.yaml.example
@@ -0,0 +1,59 @@
+# TotalRecall Configuration Example
+# Copy this to ~/.totalrecall.yaml or ./.totalrecall.yaml
+
+# Audio configuration
+audio:
+ # Provider: espeak or openai
+ provider: openai
+
+ # Audio output format
+ format: mp3
+
+ # ESpeak settings
+ voice: bg+f1
+ speed: 150
+ pitch: 50
+ amplitude: 100
+ word_gap: 0
+
+ # OpenAI TTS settings
+ openai_key: ${OPENAI_API_KEY} # Can also use environment variable
+ openai_model: gpt-4o-mini-tts # Options: tts-1, tts-1-hd, gpt-4o-mini-tts
+ openai_voice: nova # Options: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse
+ openai_speed: 0.8 # Range: 0.25 to 4.0 (may be ignored by gpt-4o-mini models)
+
+ # Voice instructions for gpt-4o-mini-tts model
+ # This allows you to customize how the AI speaks
+ openai_instruction: "Speak slowly and clearly with natural Bulgarian pronunciation, emphasizing each syllable distinctly"
+
+ # Alternative instruction examples:
+ # openai_instruction: "Pronounce with a native Bulgarian accent, speaking at a moderate pace suitable for language learning"
+ # openai_instruction: "Speak as a patient Bulgarian language teacher, clearly articulating each sound"
+ # openai_instruction: "Use clear Bulgarian pronunciation with slight pauses between syllables"
+
+ # Caching
+ enable_cache: true
+ cache_dir: ./.audio_cache
+
+# Image configuration
+image:
+ # Provider: pixabay, unsplash, or openai
+ provider: openai
+
+ # API keys for image providers
+ pixabay_key: "" # Optional for Pixabay (higher rate limits with key)
+ unsplash_key: "" # Required for Unsplash
+
+ # OpenAI DALL-E settings
+ openai_model: dall-e-3 # Options: dall-e-2, dall-e-3
+ openai_size: 1024x1024 # Options vary by model
+ openai_quality: standard # Options: standard, hd (dall-e-3 only)
+ openai_style: natural # Options: natural, vivid (dall-e-3 only)
+
+ # Caching
+ enable_cache: true
+ cache_dir: ./.image_cache
+
+# Output configuration
+output:
+ directory: ./anki_cards \ No newline at end of file
diff --git a/GPT4O_AUDIO_NOTE.md b/GPT4O_AUDIO_NOTE.md
new file mode 100644
index 0000000..a227c60
--- /dev/null
+++ b/GPT4O_AUDIO_NOTE.md
@@ -0,0 +1,35 @@
+# GPT-4o Audio Support Note
+
+## Current Status
+
+The standard OpenAI Text-to-Speech API (`/v1/audio/speech`) currently supports only:
+- `tts-1` - Standard quality
+- `tts-1-hd` - High definition quality
+
+## GPT-4o Audio Capabilities
+
+According to OpenAI documentation, GPT-4o models have audio capabilities, but these work differently:
+
+1. **Realtime API**: GPT-4o audio generation might be part of the new Realtime API, which uses WebSockets for bidirectional audio streaming.
+
+2. **Chat Completions with Audio**: GPT-4o might support audio output through the chat completions API with special modality parameters, but this requires different request/response handling than the standard TTS API.
+
+3. **Model Names**: Models like `gpt-4o-audio-preview` or `gpt-4o-mini` with audio capabilities might not be compatible with the standard TTS endpoint.
+
+## Experimental Usage
+
+You can try experimental model names with the `--openai-model` flag:
+```bash
+./totalrecall "word" --openai-model gpt-4o-audio-preview
+```
+
+However, this will likely result in a 404 error as these models require different API endpoints.
+
+## Future Implementation
+
+To properly support GPT-4o audio generation, we would need to:
+1. Implement support for the Realtime API (WebSocket-based)
+2. Or implement the chat completions API with audio modalities
+3. Handle different request/response formats for audio data
+
+For now, stick with `tts-1` or `tts-1-hd` for reliable audio generation. \ No newline at end of file
diff --git a/README.md b/README.md
index c5575fa..c866e23 100644
--- a/README.md
+++ b/README.md
@@ -107,9 +107,10 @@ audio:
# OpenAI settings
openai_key: "sk-..." # Your OpenAI API key
- openai_model: "tts-1" # Model: tts-1 or tts-1-hd
- openai_voice: "nova" # Voice: alloy, echo, fable, onyx, nova, shimmer
- openai_speed: 1.0 # Speed: 0.25 to 4.0
+ openai_model: "gpt-4o-mini-tts" # Model: tts-1, tts-1-hd, or gpt-4o-mini-tts
+ openai_voice: "nova" # Voice: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse
+ openai_speed: 0.8 # Speed: 0.25 to 4.0 (may be ignored by gpt-4o-mini models)
+ openai_instruction: "Speak slowly and clearly with natural Bulgarian pronunciation" # For gpt-4o-mini models only
# Caching
enable_cache: true
@@ -162,9 +163,10 @@ totalrecall [word] [flags]
- `--word-gap int`: Gap between words in 10ms units (default 0, espeak only)
#### OpenAI Audio Options
-- `--openai-model string`: Model - tts-1 or tts-1-hd (default "tts-1")
-- `--openai-voice string`: Voice - alloy, echo, fable, onyx, nova, shimmer (default "nova")
-- `--openai-speed float`: Speech speed 0.25-4.0 (default 1.0)
+- `--openai-model string`: Model - tts-1, tts-1-hd, or gpt-4o-mini-tts (default "gpt-4o-mini-tts", requires special access)
+- `--openai-voice string`: Voice - alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse (default "nova")
+- `--openai-speed float`: Speech speed 0.25-4.0 (default 0.8, may be ignored by gpt-4o-mini-tts)
+- `--openai-instruction string`: Voice instructions for gpt-4o-mini-tts model (e.g., "speak with a Bulgarian accent")
#### OpenAI Image Options
- `--openai-image-model string`: Model - dall-e-2 or dall-e-3 (default "dall-e-2")
@@ -201,6 +203,9 @@ totalrecall котка --audio-provider espeak
# High-quality OpenAI with specific voice
totalrecall ябълка --audio-provider openai --openai-model tts-1-hd --openai-voice alloy
+# Use gpt-4o-mini-tts with custom voice instructions
+totalrecall ябълка --openai-instruction "Speak like a patient Bulgarian teacher, very slowly and clearly"
+
# Multiple words with custom output
totalrecall --batch animals.txt -o ./animal_cards
@@ -314,11 +319,12 @@ audio:
totalrecall ябълка --audio-provider openai --openai-voice alloy
```
-**OpenAI Pricing**:
-- tts-1: $0.015 per 1K characters (~$0.0001 per word)
-- tts-1-hd: $0.030 per 1K characters (~$0.0002 per word)
+**OpenAI TTS Models**:
+- **gpt-4o-mini-tts** (default): New model with voice instruction support for customizable speech styles. Requires special API access.
+- **tts-1**: Standard quality at $0.015 per 1K characters (~$0.0001 per word)
+- **tts-1-hd**: Higher quality at $0.030 per 1K characters (~$0.0002 per word)
-The tool caches audio to avoid repeated API calls for the same words.
+The gpt-4o-mini-tts model allows you to control how the voice speaks using natural language instructions, making it ideal for language learning applications. The tool caches audio to avoid repeated API calls for the same words.
## License
diff --git a/cmd/totalrecall/main.go b/cmd/totalrecall/main.go
index f6f666a..df9e666 100644
--- a/cmd/totalrecall/main.go
+++ b/cmd/totalrecall/main.go
@@ -5,7 +5,10 @@ import (
"fmt"
"os"
"path/filepath"
+ "sort"
+ "strings"
+ "github.com/sashabaranov/go-openai"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -27,6 +30,7 @@ var (
skipImages bool
imagesPerWord int
generateAnki bool
+ listModels bool
// Audio provider flags
audioProvider string
// Audio tuning flags (espeak)
@@ -34,9 +38,10 @@ var (
audioAmplitude int
audioWordGap int
// OpenAI flags
- openAIModel string
- openAIVoice string
- openAISpeed float64
+ openAIModel string
+ openAIVoice string
+ openAISpeed float64
+ openAIInstruction string
// OpenAI Image flags
openAIImageModel string
openAIImageSize string
@@ -77,6 +82,7 @@ func init() {
rootCmd.Flags().BoolVar(&skipImages, "skip-images", false, "Skip image download")
rootCmd.Flags().IntVar(&imagesPerWord, "images-per-word", 1, "Number of images to download per word")
rootCmd.Flags().BoolVar(&generateAnki, "anki", false, "Generate Anki import CSV file")
+ rootCmd.Flags().BoolVar(&listModels, "list-models", false, "List available OpenAI models for the current API key")
// Audio provider selection
rootCmd.Flags().StringVar(&audioProvider, "audio-provider", "openai", "Audio provider: espeak or openai")
@@ -87,13 +93,14 @@ func init() {
rootCmd.Flags().IntVar(&audioWordGap, "word-gap", 0, "Gap between words in 10ms units (default 0, espeak only)")
// OpenAI flags
- rootCmd.Flags().StringVar(&openAIModel, "openai-model", "tts-1", "OpenAI model: tts-1 or tts-1-hd")
- rootCmd.Flags().StringVar(&openAIVoice, "openai-voice", "nova", "OpenAI voice: alloy, echo, fable, onyx, nova, shimmer")
- rootCmd.Flags().Float64Var(&openAISpeed, "openai-speed", 1.0, "OpenAI speech speed (0.25 to 4.0)")
+ rootCmd.Flags().StringVar(&openAIModel, "openai-model", "gpt-4o-mini-tts", "OpenAI TTS model: tts-1, tts-1-hd, gpt-4o-mini-tts")
+ rootCmd.Flags().StringVar(&openAIVoice, "openai-voice", "nova", "OpenAI voice: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse")
+ rootCmd.Flags().Float64Var(&openAISpeed, "openai-speed", 0.8, "OpenAI speech speed (0.25 to 4.0, may be ignored by gpt-4o-mini-tts)")
+ rootCmd.Flags().StringVar(&openAIInstruction, "openai-instruction", "", "Voice instructions for gpt-4o-mini-tts model (e.g., 'speak slowly with a Bulgarian accent')")
// OpenAI Image Generation flags
- rootCmd.Flags().StringVar(&openAIImageModel, "openai-image-model", "dall-e-2", "OpenAI image model: dall-e-2 or dall-e-3")
- rootCmd.Flags().StringVar(&openAIImageSize, "openai-image-size", "512x512", "Image size: 256x256, 512x512, 1024x1024 (dall-e-3: also 1024x1792, 1792x1024)")
+ rootCmd.Flags().StringVar(&openAIImageModel, "openai-image-model", "dall-e-3", "OpenAI image model: dall-e-2 or dall-e-3")
+ rootCmd.Flags().StringVar(&openAIImageSize, "openai-image-size", "1024x1024", "Image size: 256x256, 512x512, 1024x1024 (dall-e-3: also 1024x1792, 1792x1024)")
rootCmd.Flags().StringVar(&openAIImageQuality, "openai-image-quality", "standard", "Image quality: standard or hd (dall-e-3 only)")
rootCmd.Flags().StringVar(&openAIImageStyle, "openai-image-style", "natural", "Image style: natural or vivid (dall-e-3 only)")
@@ -107,6 +114,7 @@ func init() {
viper.BindPFlag("audio.openai_model", rootCmd.Flags().Lookup("openai-model"))
viper.BindPFlag("audio.openai_voice", rootCmd.Flags().Lookup("openai-voice"))
viper.BindPFlag("audio.openai_speed", rootCmd.Flags().Lookup("openai-speed"))
+ viper.BindPFlag("audio.openai_instruction", rootCmd.Flags().Lookup("openai-instruction"))
viper.BindPFlag("output.directory", rootCmd.Flags().Lookup("output"))
viper.BindPFlag("image.provider", rootCmd.Flags().Lookup("image-api"))
// Bind OpenAI image flags
@@ -143,6 +151,11 @@ func initConfig() {
}
func runCommand(cmd *cobra.Command, args []string) error {
+ // Handle --list-models flag
+ if listModels {
+ return listAvailableModels()
+ }
+
// Determine words to process
var words []string
@@ -238,10 +251,11 @@ func generateAudio(word string) error {
ESpeakWordGap: audioWordGap,
// OpenAI settings
- OpenAIKey: getOpenAIKey(),
- OpenAIModel: openAIModel,
- OpenAIVoice: openAIVoice,
- OpenAISpeed: openAISpeed,
+ OpenAIKey: getOpenAIKey(),
+ OpenAIModel: openAIModel,
+ OpenAIVoice: openAIVoice,
+ OpenAISpeed: openAISpeed,
+ OpenAIInstruction: openAIInstruction,
// Caching
EnableCache: viper.GetBool("audio.enable_cache"),
@@ -269,15 +283,18 @@ func generateAudio(word string) error {
if audioWordGap == 0 && viper.IsSet("audio.word_gap") {
providerConfig.ESpeakWordGap = viper.GetInt("audio.word_gap")
}
- if openAIModel == "tts-1" && viper.IsSet("audio.openai_model") {
+ if openAIModel == "gpt-4o-mini-tts" && viper.IsSet("audio.openai_model") {
providerConfig.OpenAIModel = viper.GetString("audio.openai_model")
}
if openAIVoice == "nova" && viper.IsSet("audio.openai_voice") {
providerConfig.OpenAIVoice = viper.GetString("audio.openai_voice")
}
- if openAISpeed == 1.0 && viper.IsSet("audio.openai_speed") {
+ if openAISpeed == 0.8 && viper.IsSet("audio.openai_speed") {
providerConfig.OpenAISpeed = viper.GetFloat64("audio.openai_speed")
}
+ if openAIInstruction == "" && viper.IsSet("audio.openai_instruction") {
+ providerConfig.OpenAIInstruction = viper.GetString("audio.openai_instruction")
+ }
// Create the audio provider
provider, err := audio.NewProvider(providerConfig)
@@ -337,10 +354,10 @@ func downloadImages(word string) error {
}
// Use config file values if not overridden by flags
- if openAIImageModel == "dall-e-2" && viper.IsSet("image.openai_model") {
+ if openAIImageModel == "dall-e-3" && viper.IsSet("image.openai_model") {
openaiConfig.Model = viper.GetString("image.openai_model")
}
- if openAIImageSize == "512x512" && viper.IsSet("image.openai_size") {
+ if openAIImageSize == "1024x1024" && viper.IsSet("image.openai_size") {
openaiConfig.Size = viper.GetString("image.openai_size")
}
if openAIImageQuality == "standard" && viper.IsSet("image.openai_quality") {
@@ -497,6 +514,86 @@ func getOpenAIKey() string {
return viper.GetString("audio.openai_key")
}
+func listAvailableModels() error {
+ // Get OpenAI API key
+ apiKey := getOpenAIKey()
+ if apiKey == "" {
+ return fmt.Errorf("OpenAI API key not found. Set OPENAI_API_KEY environment variable or configure in .totalrecall.yaml")
+ }
+
+ // Create OpenAI client
+ client := openai.NewClient(apiKey)
+
+ // List models
+ ctx := context.Background()
+ models, err := client.ListModels(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to list models: %w", err)
+ }
+
+ // Categorize models
+ ttsModels := []string{}
+ imageModels := []string{}
+ chatModels := []string{}
+
+ for _, model := range models.Models {
+ modelID := model.ID
+ if strings.Contains(modelID, "tts") || strings.Contains(modelID, "audio") {
+ ttsModels = append(ttsModels, modelID)
+ } else if strings.Contains(modelID, "dall-e") {
+ imageModels = append(imageModels, modelID)
+ } else if strings.Contains(modelID, "gpt") || strings.Contains(modelID, "chat") {
+ chatModels = append(chatModels, modelID)
+ }
+ }
+
+ // Sort models
+ sort.Strings(ttsModels)
+ sort.Strings(imageModels)
+ sort.Strings(chatModels)
+
+ // Print models
+ fmt.Println("Available OpenAI Models:")
+ fmt.Println("\nText-to-Speech (TTS) Models:")
+ if len(ttsModels) == 0 {
+ fmt.Println(" No TTS models found")
+ } else {
+ for _, model := range ttsModels {
+ fmt.Printf(" %s\n", model)
+ }
+ }
+
+ fmt.Println("\nImage Generation Models:")
+ if len(imageModels) == 0 {
+ fmt.Println(" No image models found")
+ } else {
+ for _, model := range imageModels {
+ fmt.Printf(" %s\n", model)
+ }
+ }
+
+ fmt.Println("\nChat/Translation Models (for Bulgarian translation):")
+ if len(chatModels) > 10 {
+ // Show only relevant models
+ relevantModels := []string{}
+ for _, model := range chatModels {
+ if strings.Contains(model, "gpt-4") || strings.Contains(model, "gpt-3.5") {
+ relevantModels = append(relevantModels, model)
+ }
+ }
+ for _, model := range relevantModels {
+ fmt.Printf(" %s\n", model)
+ }
+ fmt.Printf(" ... and %d more models\n", len(chatModels)-len(relevantModels))
+ } else {
+ for _, model := range chatModels {
+ fmt.Printf(" %s\n", model)
+ }
+ }
+
+ return nil
+}
+
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
diff --git a/internal/audio/openai_provider.go b/internal/audio/openai_provider.go
index 9efbcd2..a61957a 100644
--- a/internal/audio/openai_provider.go
+++ b/internal/audio/openai_provider.go
@@ -62,14 +62,29 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
}
}
+ // 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)
+ if p.config.OpenAIInstruction != "" && (p.config.OpenAIModel == "gpt-4o-mini-tts" || p.config.OpenAIModel == "gpt-4o-mini-audio-preview") {
+ 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: text,
+ 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 {
@@ -93,6 +108,11 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF
// Make the API call
response, err := p.client.CreateSpeech(ctx, req)
if err != nil {
+ // Check if it's a model access error
+ errStr := err.Error()
+ if strings.Contains(errStr, "does not have access to model") && (p.config.OpenAIModel == "gpt-4o-mini-tts" || p.config.OpenAIModel == "gpt-4o-mini-audio-preview") {
+ return fmt.Errorf("OpenAI TTS API error: %w\nNote: The %s model requires access. Try using --openai-model tts-1-hd instead", err, p.config.OpenAIModel)
+ }
return fmt.Errorf("OpenAI TTS API error: %w", err)
}
defer response.Close()
@@ -147,6 +167,21 @@ func (p *OpenAIProvider) IsAvailable() error {
return nil
}
+// preprocessBulgarianText prepares Bulgarian text for clearer TTS pronunciation
+func (p *OpenAIProvider) preprocessBulgarianText(text string) string {
+ // For single words, we add subtle punctuation to create natural pauses
+ // without repeating the word
+
+ // First, clean the text
+ cleanedText := strings.TrimSpace(text)
+
+ // Add ellipsis after the word to create a natural pause and slow down
+ // This helps the TTS engine pronounce it more carefully
+ processedText := fmt.Sprintf("%s...", cleanedText)
+
+ return processedText
+}
+
// getCacheFilePath generates a cache file path for the given text
func (p *OpenAIProvider) getCacheFilePath(text string) string {
// Create a hash of the text and settings
@@ -155,6 +190,10 @@ func (p *OpenAIProvider) getCacheFilePath(text string) string {
h.Write([]byte(p.config.OpenAIModel))
h.Write([]byte(p.config.OpenAIVoice))
h.Write([]byte(fmt.Sprintf("%.2f", p.config.OpenAISpeed)))
+ // Include instruction in cache key for gpt-4o-mini-tts
+ if p.config.OpenAIModel == "gpt-4o-mini-tts" && p.config.OpenAIInstruction != "" {
+ h.Write([]byte(p.config.OpenAIInstruction))
+ }
hash := hex.EncodeToString(h.Sum(nil))
// Use first 2 chars as subdirectory for better file system performance
diff --git a/internal/audio/provider.go b/internal/audio/provider.go
index c803b61..94605b7 100644
--- a/internal/audio/provider.go
+++ b/internal/audio/provider.go
@@ -31,10 +31,11 @@ type Config struct {
ESpeakWordGap int
// OpenAI-specific settings
- OpenAIKey string
- OpenAIModel string // "tts-1" or "tts-1-hd"
- OpenAIVoice string // "alloy", "echo", "fable", "onyx", "nova", "shimmer"
- OpenAISpeed float64 // 0.25 to 4.0
+ OpenAIKey string
+ OpenAIModel string // "tts-1", "tts-1-hd", or "gpt-4o-mini-tts"
+ OpenAIVoice string // "alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"
+ OpenAISpeed float64 // 0.25 to 4.0
+ OpenAIInstruction string // Voice instructions for gpt-4o-mini-tts model
// Caching settings
EnableCache bool
@@ -52,9 +53,10 @@ func DefaultProviderConfig() *Config {
ESpeakPitch: 50,
ESpeakAmplitude: 100,
ESpeakWordGap: 0,
- OpenAIModel: "tts-1",
- OpenAIVoice: "nova",
- OpenAISpeed: 1.0,
+ OpenAIModel: "gpt-4o-mini-tts", // New model with voice instructions support
+ OpenAIVoice: "nova",
+ OpenAISpeed: 0.8, // Slightly slower for clarity (note: may be ignored by gpt-4o-mini-tts)
+ OpenAIInstruction: "Speak slowly and clearly with natural Bulgarian pronunciation, emphasizing each syllable distinctly",
EnableCache: true,
CacheDir: "./.audio_cache",
}
diff --git a/internal/image/openai.go b/internal/image/openai.go
index a5a3e31..839c735 100644
--- a/internal/image/openai.go
+++ b/internal/image/openai.go
@@ -48,10 +48,10 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient {
// Set defaults
if config.Model == "" {
- config.Model = "dall-e-2"
+ config.Model = "dall-e-3"
}
if config.Size == "" {
- config.Size = "512x512"
+ config.Size = "1024x1024"
}
if config.Quality == "" {
config.Quality = "standard"
@@ -97,6 +97,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc
cacheFile := c.getCacheFilePath(opts.Query)
if info, err := os.Stat(cacheFile); err == nil && info.Size() > 0 {
// Return cached result
+ fmt.Printf("Using cached image for '%s'\n", opts.Query)
result := SearchResult{
ID: c.generateImageID(opts.Query),
URL: cacheFile,
@@ -112,11 +113,20 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc
}
// Translate Bulgarian word to English for better results
- translatedWord := translateBulgarianToEnglish(opts.Query)
+ translatedWord, err := c.translateBulgarianToEnglish(ctx, opts.Query)
+ if err != nil {
+ // If translation fails, fall back to using the original word
+ fmt.Printf("Translation failed: %v, using original word\n", err)
+ translatedWord = opts.Query
+ }
// Create educational prompt
prompt := c.createEducationalPrompt(opts.Query, translatedWord)
+ // Log the prompt to stdout for debugging
+ fmt.Printf("OpenAI Image Generation Prompt: %s\n", prompt)
+ fmt.Printf("OpenAI Image Generation: Using model '%s' with size '%s'\n", c.model, c.size)
+
// Create the image generation request
req := openai.ImageRequest{
Prompt: prompt,
@@ -220,22 +230,47 @@ func (c *OpenAIClient) Name() string {
// createEducationalPrompt generates a prompt optimized for language learning
func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation string) string {
- // Create a prompt that generates clear, educational images
- // suitable for language learning flashcards
+ // Create a simple, clear prompt for educational images
return fmt.Sprintf(
- "A simple, clear, photorealistic educational image showing %s, "+
- "suitable for language learning flashcards. "+
- "The image should be easily recognizable, with good lighting, "+
- "plain background, and focused on a single clear subject. "+
+ "Generate a simple, clear image of: %s. "+
+ "This is for the Bulgarian word '%s' which means %s. "+
+ "The image should be educational and suitable for language learning flashcards. "+
+ "Requirements: single main subject, plain background, clear and recognizable. "+
"No text, labels, or writing in the image.",
- englishTranslation,
+ englishTranslation, bulgarianWord, englishTranslation,
)
}
-// translateBulgarianToEnglish translates a Bulgarian word to English
-func translateBulgarianToEnglish(word string) string {
- // Use the existing translation function from translate.go
- return translateBulgarianQuery(word)
+// translateBulgarianToEnglish translates a Bulgarian word to English using OpenAI
+func (c *OpenAIClient) translateBulgarianToEnglish(ctx context.Context, word string) (string, error) {
+ // Use OpenAI chat completion to translate
+ fmt.Printf("OpenAI Translation: Using model 'gpt-4o-mini' to translate '%s'\n", word)
+
+ req := openai.ChatCompletionRequest{
+ Model: openai.GPT4oMini,
+ Messages: []openai.ChatCompletionMessage{
+ {
+ Role: openai.ChatMessageRoleUser,
+ Content: fmt.Sprintf("Translate the Bulgarian word '%s' to English. Respond with only the English translation, nothing else.", word),
+ },
+ },
+ Temperature: 0.3, // Lower temperature for more consistent translations
+ MaxTokens: 50,
+ }
+
+ resp, err := c.client.CreateChatCompletion(ctx, req)
+ if err != nil {
+ return "", fmt.Errorf("translation failed: %w", err)
+ }
+
+ if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" {
+ return "", fmt.Errorf("no translation received")
+ }
+
+ translation := strings.TrimSpace(resp.Choices[0].Message.Content)
+ fmt.Printf("Translated '%s' to '%s'\n", word, translation)
+
+ return translation, nil
}
// getCacheFilePath generates a cache file path for the given word
diff --git a/internal/image/translate.go b/internal/image/translate.go
index 03d5875..38d16f9 100644
--- a/internal/image/translate.go
+++ b/internal/image/translate.go
@@ -8,6 +8,15 @@ func translateBulgarianQuery(query string) string {
// Common Bulgarian words for flashcard creation
translations := map[string]string{
"ябълка": "apple",
+ "малинка": "raspberry",
+ "ягода": "strawberry",
+ "череша": "cherry",
+ "круша": "pear",
+ "праскова": "peach",
+ "грозде": "grapes",
+ "банан": "banana",
+ "портокал": "orange",
+ "лимон": "lemon",
"котка": "cat",
"куче": "dog",
"хляб": "bread",