summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-15 23:28:13 +0300
committerPaul Buetow <paul@buetow.org>2025-07-15 23:28:13 +0300
commit61529facc2c5321de9f0ab9123cb1de25bcab62c (patch)
tree0768d5d5e68c71ea52fc31ca2d33950c93977314
parent9c77f2a7bef485fa137f123cbf55b42cacb2b285 (diff)
feat: remove espeak, add random voice/style selection, fix punctuation in TTS
- Removed espeak audio provider completely, now only uses OpenAI TTS - Audio now uses random voice selection by default (can override with --openai-voice) - Added --all-voices flag to generate audio in all 11 OpenAI voices - Images now use random art styles (13 different styles including superhero, yoga, cat-themed) - Fixed TTS to remove punctuation marks before speaking - Updated Bulgarian pronunciation instructions to explicitly avoid Russian accent 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--.totalrecall.yaml.example23
-rw-r--r--CLAUDE.md33
-rw-r--r--GPT4O_AUDIO_NOTE.md35
-rw-r--r--README.md98
-rw-r--r--TODO.md16
-rw-r--r--cmd/totalrecall/main.go109
-rw-r--r--internal/audio/doc.go2
-rw-r--r--internal/audio/espeak.go217
-rw-r--r--internal/audio/espeak_provider.go65
-rw-r--r--internal/audio/espeak_test.go198
-rw-r--r--internal/audio/openai_provider.go16
-rw-r--r--internal/audio/provider.go27
-rw-r--r--internal/audio/validate.go28
-rw-r--r--internal/image/openai.go26
-rw-r--r--internal/image/openai_test.go23
15 files changed, 158 insertions, 758 deletions
diff --git a/.totalrecall.yaml.example b/.totalrecall.yaml.example
index e41b97f..e649bde 100644
--- a/.totalrecall.yaml.example
+++ b/.totalrecall.yaml.example
@@ -1,21 +1,14 @@
-# TotalRecall Configuration Example
+# TotalRecalooConfiguration Example
# Copy this to ~/.totalrecall.yaml or ./.totalrecall.yaml
# Audio configuration
audio:
- # Provider: espeak or openai
- provider: openai
+ # Audio is generated using OpenAI TTS
+ # Provider field removed - OpenAI is now the only option
# 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
@@ -24,12 +17,12 @@ audio:
# 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"
+ openai_instruction: "You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners."
# 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"
+ # openai_instruction: "Speak in Bulgarian (not Russian!). Use native Bulgarian pronunciation with clear articulation for each syllable."
+ # openai_instruction: "You are a Bulgarian language teacher. Pronounce the Bulgarian words slowly with authentic Bulgarian accent and phonetics."
+ # openai_instruction: "Speak Bulgarian text with proper Bulgarian pronunciation. Avoid Russian accent. Speak clearly at a pace suitable for beginners."
# Caching
enable_cache: true
@@ -56,4 +49,4 @@ image:
# Output configuration
output:
- directory: ./anki_cards \ No newline at end of file
+ directory: ./anki_cards
diff --git a/CLAUDE.md b/CLAUDE.md
index 4b972ef..4ab7679 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
**totalrecall** - Bulgarian Anki Flashcard Generator
A Go CLI tool that generates Anki flashcard materials from Bulgarian words:
-- Generates audio pronunciation using espeak-ng
+- Generates audio pronunciation using OpenAI TTS
- Downloads representative images via web search
- Creates Anki-compatible output files
@@ -60,7 +60,7 @@ golangci-lint run
totalrecall/
├── cmd/totalrecall/ # CLI entry point
├── internal/ # Private packages
-│ ├── audio/ # Audio generation (espeak-ng wrapper)
+│ ├── audio/ # Audio generation (OpenAI TTS)
│ ├── image/ # Image search functionality
│ ├── anki/ # Anki format generation
│ ├── config/ # Configuration management
@@ -68,20 +68,13 @@ totalrecall/
```
### Key Design Decisions
-1. **espeak-ng for TTS**: Open source, supports Bulgarian, no API keys needed
-2. **Modular image search**: Support multiple providers (Pixabay, Unsplash)
+1. **OpenAI TTS**: High-quality, natural-sounding Bulgarian pronunciation
+2. **Modular image search**: Support multiple providers (Pixabay, Unsplash, OpenAI DALL-E)
3. **Configuration via YAML**: User-friendly configuration with viper
4. **Cobra for CLI**: Industry-standard CLI framework
### External Dependencies
-- **espeak-ng**: Must be installed on the system
- ```bash
- # Ubuntu/Debian
- sudo apt-get install espeak-ng
-
- # macOS
- brew install espeak-ng
- ```
+- **OpenAI API Key**: Required for audio generation
### API Configuration
Image search APIs require configuration in `.bulg.yaml`:
@@ -89,24 +82,12 @@ Image search APIs require configuration in `.bulg.yaml`:
- **Unsplash**: Required API key
## Testing Approach
-1. Unit tests mock external commands (espeak-ng) and API calls
+1. Unit tests mock API calls
2. Integration tests use real services when available
3. Test with common Bulgarian words: ябълка, котка, куче, хляб
## Common Issues and Solutions
-### espeak-ng Bulgarian pronunciation
-There have been reported issues with Bulgarian pronunciation in espeak-ng v1.49.3. If pronunciation sounds wrong, try:
-```bash
-# Check version
-espeak-ng --version
-
-# Test Bulgarian voice
-espeak-ng -v bg "Здравей"
-
-# Try different voice variants
-espeak-ng -v bg+f1 "Здравей"
-```
### Package Declaration Error
If you see an error about `package main`, ensure `cmd/totalrecall/main.go` has:
@@ -125,4 +106,4 @@ package main // NOT package bulg
## Bulgarian Language Notes
- Input should be in Cyrillic script
- Common test words: ябълка (apple), котка (cat), куче (dog)
-- Voice variants: bg+m1 (male), bg+f1 (female) \ No newline at end of file
+- OpenAI voices: nova, alloy, echo, shimmer (work well for Bulgarian) \ No newline at end of file
diff --git a/GPT4O_AUDIO_NOTE.md b/GPT4O_AUDIO_NOTE.md
deleted file mode 100644
index a227c60..0000000
--- a/GPT4O_AUDIO_NOTE.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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 c866e23..7177b66 100644
--- a/README.md
+++ b/README.md
@@ -4,46 +4,28 @@
It has mainly been vibe coded using Claude Code CLI.
-⚠️ **Important:** This tool uses OpenAI services by default, which requires an API key. See [Quick Start](#quick-start) for setup instructions or use the free alternatives with `--audio-provider espeak --image-api pixabay`.
+⚠️ **Important:** This tool uses OpenAI services for audio generation, which requires an API key. See [Quick Start](#quick-start) for setup instructions.
## Features
-- Audio generation with multiple providers:
- - **espeak-ng**: Free, offline Bulgarian voices (robotic quality)
- - **OpenAI TTS**: High-quality, natural-sounding voices (requires API key)
+- Audio generation using **OpenAI TTS**: High-quality, natural-sounding voices (requires API key)
+ - Random voice selection by default for variety
+ - Option to generate in all 11 available voices
- Image search and generation:
- **Pixabay**: Free stock photo search (optional API key)
- **Unsplash**: High-quality photo search (requires API key)
- - **OpenAI DALL-E**: AI-generated educational images (requires API key)
+ - **OpenAI DALL-E**: AI-generated educational images with random art styles (requires API key)
- Batch processing of multiple words
- Anki-compatible CSV export
- Configurable voice variants and speech speed
- Support for WAV and MP3 audio formats
-- Audio caching to save API costs (OpenAI)
+- Audio and image caching to save API costs
## Installation
### Prerequisites
-1. **For espeak-ng audio** (free, offline):
- ```bash
- # Ubuntu/Debian
- sudo apt-get install espeak-ng
-
- # macOS
- brew install espeak-ng
- ```
-
-2. **ffmpeg** (optional, for MP3 conversion with espeak):
- ```bash
- # Ubuntu/Debian
- sudo apt-get install ffmpeg
-
- # macOS
- brew install ffmpeg
- ```
-
-3. **For OpenAI TTS** (paid, high quality):
+1. **For OpenAI TTS** (required for audio generation):
- Create an account at https://platform.openai.com
- Generate an API key at https://platform.openai.com/api-keys
- Set the key using one of these methods:
@@ -76,9 +58,9 @@ export OPENAI_API_KEY="sk-..."
totalrecall ябълка
```
-2. Use free alternatives (espeak + pixabay):
+2. Use free Pixabay for images:
```bash
- totalrecall ябълка --audio-provider espeak --image-api pixabay
+ totalrecall ябълка --image-api pixabay
```
3. Process multiple words from a file:
@@ -97,20 +79,14 @@ Create a `.totalrecall.yaml` file in your home directory or project folder:
```yaml
audio:
- provider: openai # Audio provider (espeak or openai) - default: openai
format: mp3 # Audio format (wav or mp3)
- # ESpeak settings
- voice: bg+f1 # Voice variant (bg, bg+m1, bg+f1, etc.)
- speed: 150 # Speech speed (80-450 words/minute)
- pitch: 50 # Pitch adjustment (0-99)
-
# OpenAI settings
openai_key: "sk-..." # Your OpenAI API key
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
+ openai_instruction: "You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian." # For gpt-4o-mini models only
# Caching
enable_cache: true
@@ -153,18 +129,13 @@ totalrecall [word] [flags]
- `--skip-images`: Skip image download
- `--images-per-word int`: Number of images per word (default 1)
- `--image-api string`: Image source - pixabay, unsplash, or openai (default "openai")
+- `--all-voices`: Generate audio in all available OpenAI voices (creates 11 files per word)
-#### Audio Provider Options
-- `--audio-provider string`: Audio provider - espeak or openai (default "openai")
-
-#### ESpeak Tuning Options
-- `--pitch int`: Pitch adjustment 0-99 (default 50, lower=deeper, espeak only)
-- `--amplitude int`: Volume 0-200 (default 100, espeak only)
-- `--word-gap int`: Gap between words in 10ms units (default 0, espeak only)
+#### Audio Options
#### OpenAI Audio Options
- `--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-voice string`: Voice - alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse (default: random)
- `--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")
@@ -197,11 +168,8 @@ totalrecall [word] [flags]
# Single word (uses OpenAI by default)
totalrecall котка
-# Using espeak-ng (free alternative)
-totalrecall котка --audio-provider espeak
-
# High-quality OpenAI with specific voice
-totalrecall ябълка --audio-provider openai --openai-model tts-1-hd --openai-voice alloy
+totalrecall ябълка --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"
@@ -209,9 +177,6 @@ totalrecall ябълка --openai-instruction "Speak like a patient Bulgarian te
# Multiple words with custom output
totalrecall --batch animals.txt -o ./animal_cards
-# ESpeak with tuning
-totalrecall ябълка --pitch 40 --word-gap 3
-
# Skip images, audio only
totalrecall куче --skip-images
@@ -225,7 +190,10 @@ totalrecall ябълка --image-api openai
totalrecall котка --image-api openai --openai-image-model dall-e-3 --openai-image-quality hd
# Combine OpenAI audio and images
-totalrecall куче --audio-provider openai --image-api openai
+totalrecall куче --image-api openai
+
+# Generate audio in all 11 OpenAI voices
+totalrecall котка --all-voices --skip-images
```
### Batch File Format
@@ -255,8 +223,6 @@ Available Bulgarian voices:
## Troubleshooting
-### espeak-ng not found
-Make sure espeak-ng is installed and in your PATH.
### No images found
- Check your internet connection
@@ -275,48 +241,28 @@ Make sure espeak-ng is installed and in your PATH.
- Both services cache results to avoid regenerating identical content
### Free Alternatives
-- **Audio**: Use espeak-ng (free but robotic quality)
- **Images**: Use Pixabay without API key (limited rate)
### OpenAI Troubleshooting
- Check the API key has proper permissions enabled
- If you get rate limit errors, wait a moment and try again
-- The tool will automatically fall back to espeak-ng if OpenAI audio fails
-
-### Audio sounds robotic
-The Bulgarian voice in espeak-ng can sound robotic. To improve quality:
-
-```bash
-# Test with different settings
-espeak-ng -v bg -p 40 -s 140 "Здравей" # Deeper, slower
-espeak-ng -v bg+f1 -p 60 -g 2 "Здравей" # Higher pitch, word gaps
-
-# Using totalrecall with tuning
-totalrecall ябълка --pitch 40 --word-gap 2 --amplitude 120
-```
-Recommended settings for clearer pronunciation:
-- `--pitch 40`: Slightly deeper voice (less robotic)
-- `--word-gap 2-5`: Small gaps between words
-- `--amplitude 120`: Slightly louder
-- `-v bg+f1`: Female variant often sounds clearer
-### Using OpenAI for Better Quality
+### OpenAI TTS Configuration
-OpenAI TTS provides much more natural Bulgarian pronunciation:
+OpenAI TTS provides natural Bulgarian pronunciation:
```bash
# Option 1: Use environment variable
export OPENAI_API_KEY="sk-your-key-here"
-totalrecall ябълка --audio-provider openai
+totalrecall ябълка
# Option 2: Set in .totalrecall.yaml
audio:
- provider: openai
openai_key: "sk-your-key-here"
# Use with custom voice
-totalrecall ябълка --audio-provider openai --openai-voice alloy
+totalrecall ябълка --openai-voice alloy
```
**OpenAI TTS Models**:
diff --git a/TODO.md b/TODO.md
index 4ceec03..9b41e0a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,18 +1,2 @@
# TODO's
-## Completed
-1. [x] Implement OpenAI DALL-E image generation for flashcards
- - [x] Create OpenAI image provider implementing ImageSearcher interface
- - [x] Add configuration flags for DALL-E model, size, quality, and style
- - [x] Implement caching mechanism to avoid regenerating identical images
- - [x] Create educational prompt generation for language learning
- - [x] Add OpenAI provider to image download workflow
- - [x] Update documentation with examples and configuration
-
-## In Progress / Remaining
-1. [ ] Write unit tests for OpenAI image provider
-2. [ ] Add cost estimation warnings in output (show estimated API costs)
-3. [ ] Test with common Bulgarian words (ябълка, котка, куче, хляб)
-4. [ ] Consider adding batch image generation for cost optimization
-5. [ ] Add image style presets for different learning contexts (e.g., children, adults)
-6. [ ] Implement fallback from OpenAI to other providers on failure
diff --git a/cmd/totalrecall/main.go b/cmd/totalrecall/main.go
index df9e666..2a43d51 100644
--- a/cmd/totalrecall/main.go
+++ b/cmd/totalrecall/main.go
@@ -3,10 +3,12 @@ package main
import (
"context"
"fmt"
+ "math/rand"
"os"
"path/filepath"
"sort"
"strings"
+ "time"
"github.com/sashabaranov/go-openai"
"github.com/spf13/cobra"
@@ -21,7 +23,7 @@ import (
var (
// Flags
cfgFile string
- voice string
+ // voice removed - was only for espeak
outputDir string
audioFormat string
imageAPI string
@@ -31,12 +33,8 @@ var (
imagesPerWord int
generateAnki bool
listModels bool
- // Audio provider flags
- audioProvider string
- // Audio tuning flags (espeak)
- audioPitch int
- audioAmplitude int
- audioWordGap int
+ allVoices bool
+ // Audio provider flags removed - now only OpenAI
// OpenAI flags
openAIModel string
openAIVoice string
@@ -55,7 +53,7 @@ var rootCmd = &cobra.Command{
Short: "Bulgarian Anki Flashcard Generator",
Long: `totalrecall generates Anki flashcard materials from Bulgarian words.
-It creates audio pronunciation files using espeak-ng and downloads
+It creates audio pronunciation files using OpenAI TTS and downloads
representative images from web search APIs.
Example:
@@ -69,11 +67,13 @@ Example:
func init() {
cobra.OnInitialize(initConfig)
+ // Initialize random number generator
+ rand.Seed(time.Now().UnixNano())
+
// Global flags
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.totalrecall.yaml)")
// Local flags
- rootCmd.Flags().StringVarP(&voice, "voice", "v", "bg+f1", "Voice variant (bg, bg+m1, bg+f1, etc.)")
rootCmd.Flags().StringVarP(&outputDir, "output", "o", "./anki_cards", "Output directory")
rootCmd.Flags().StringVarP(&audioFormat, "format", "f", "mp3", "Audio format (wav or mp3)")
rootCmd.Flags().StringVar(&imageAPI, "image-api", "openai", "Image source (pixabay, unsplash, or openai)")
@@ -83,18 +83,13 @@ func init() {
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")
+ rootCmd.Flags().BoolVar(&allVoices, "all-voices", false, "Generate audio in all available voices (creates multiple files)")
- // Audio provider selection
- rootCmd.Flags().StringVar(&audioProvider, "audio-provider", "openai", "Audio provider: espeak or openai")
-
- // Audio tuning flags (espeak)
- rootCmd.Flags().IntVar(&audioPitch, "pitch", 50, "Audio pitch adjustment (0-99, default 50, espeak only)")
- rootCmd.Flags().IntVar(&audioAmplitude, "amplitude", 100, "Audio volume (0-200, default 100, espeak only)")
- rootCmd.Flags().IntVar(&audioWordGap, "word-gap", 0, "Gap between words in 10ms units (default 0, espeak only)")
+ // Audio provider removed - now only OpenAI
// OpenAI flags
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().StringVar(&openAIVoice, "openai-voice", "", "OpenAI voice: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse (default: random)")
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')")
@@ -237,23 +232,47 @@ func processWord(word string) error {
}
func generateAudio(word string) error {
+ allVoicesList := []string{"alloy", "ash", "ballad", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer", "verse"}
+
+ // Get list of voices to use
+ var voices []string
+ if allVoices {
+ voices = allVoicesList
+ } else if openAIVoice != "" {
+ // Use explicitly specified voice
+ voices = []string{openAIVoice}
+ fmt.Printf(" Using specified voice: %s\n", openAIVoice)
+ } else {
+ // Select a random voice
+ randomVoice := allVoicesList[rand.Intn(len(allVoicesList))]
+ voices = []string{randomVoice}
+ fmt.Printf(" Using random voice: %s\n", randomVoice)
+ }
+
+ // Generate audio for each voice
+ for i, voice := range voices {
+ if allVoices {
+ fmt.Printf(" Generating audio %d/%d (voice: %s)...\n", i+1, len(voices), voice)
+ }
+ if err := generateAudioWithVoice(word, voice); err != nil {
+ return fmt.Errorf("failed to generate audio with voice %s: %w", voice, err)
+ }
+ }
+
+ return nil
+}
+
+func generateAudioWithVoice(word, voice string) error {
// Create audio provider configuration
providerConfig := &audio.Config{
- Provider: audioProvider,
+ Provider: "openai",
OutputDir: outputDir,
OutputFormat: audioFormat,
- // ESpeak settings
- ESpeakVoice: voice,
- ESpeakSpeed: viper.GetInt("audio.speed"),
- ESpeakPitch: audioPitch,
- ESpeakAmplitude: audioAmplitude,
- ESpeakWordGap: audioWordGap,
-
// OpenAI settings
OpenAIKey: getOpenAIKey(),
OpenAIModel: openAIModel,
- OpenAIVoice: openAIVoice,
+ OpenAIVoice: voice,
OpenAISpeed: openAISpeed,
OpenAIInstruction: openAIInstruction,
@@ -263,26 +282,11 @@ func generateAudio(word string) error {
}
// Set defaults
- if providerConfig.ESpeakSpeed == 0 {
- providerConfig.ESpeakSpeed = 150
- }
if providerConfig.CacheDir == "" {
providerConfig.CacheDir = "./.audio_cache"
}
// Use config file values if not overridden by flags
- if audioProvider == "openai" && viper.IsSet("audio.provider") {
- providerConfig.Provider = viper.GetString("audio.provider")
- }
- if audioPitch == 50 && viper.IsSet("audio.pitch") {
- providerConfig.ESpeakPitch = viper.GetInt("audio.pitch")
- }
- if audioAmplitude == 100 && viper.IsSet("audio.amplitude") {
- providerConfig.ESpeakAmplitude = viper.GetInt("audio.amplitude")
- }
- if audioWordGap == 0 && viper.IsSet("audio.word_gap") {
- providerConfig.ESpeakWordGap = viper.GetInt("audio.word_gap")
- }
if openAIModel == "gpt-4o-mini-tts" && viper.IsSet("audio.openai_model") {
providerConfig.OpenAIModel = viper.GetString("audio.openai_model")
}
@@ -299,25 +303,20 @@ func generateAudio(word string) error {
// Create the audio provider
provider, err := audio.NewProvider(providerConfig)
if err != nil {
- // If OpenAI fails, try to create a fallback to espeak
- if providerConfig.Provider == "openai" {
- fmt.Printf("Warning: OpenAI audio provider failed (%v), falling back to espeak-ng\n", err)
- providerConfig.Provider = "espeak"
- fallbackProvider, fallbackErr := audio.NewProvider(providerConfig)
- if fallbackErr != nil {
- return fmt.Errorf("both OpenAI and espeak-ng failed: %v", fallbackErr)
- }
- provider = fallbackProvider
- } else {
- return err
- }
+ return err
}
// Generate audio file
+ ctx := context.Background()
filename := sanitizeFilename(word)
- outputFile := filepath.Join(outputDir, fmt.Sprintf("%s.%s", filename, audioFormat))
- ctx := context.Background()
+ // Add voice name to filename if generating multiple voices
+ if allVoices {
+ outputFile := filepath.Join(outputDir, fmt.Sprintf("%s_%s.%s", filename, voice, audioFormat))
+ return provider.GenerateAudio(ctx, word, outputFile)
+ }
+
+ outputFile := filepath.Join(outputDir, fmt.Sprintf("%s.%s", filename, audioFormat))
return provider.GenerateAudio(ctx, word, outputFile)
}
diff --git a/internal/audio/doc.go b/internal/audio/doc.go
index 1fd216b..c8a5ce4 100644
--- a/internal/audio/doc.go
+++ b/internal/audio/doc.go
@@ -1,3 +1,3 @@
-// Package audio provides audio generation functionality using espeak-ng
+// Package audio provides audio generation functionality using OpenAI TTS
// for Bulgarian text-to-speech conversion.
package audio
diff --git a/internal/audio/espeak.go b/internal/audio/espeak.go
deleted file mode 100644
index cd42360..0000000
--- a/internal/audio/espeak.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package audio
-
-import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
-)
-
-// ESpeakConfig holds configuration for espeak-ng audio generation
-type ESpeakConfig struct {
- Voice string // Voice variant (e.g., "bg", "bg+m1", "bg+f1")
- Speed int // Speech speed in words per minute (default: 150)
- Pitch int // Pitch adjustment, 0 to 99 (default: 50)
- Amplitude int // Volume/amplitude, 0 to 200 (default: 100)
- WordGap int // Gap between words in 10ms units (default: 0)
- OutputDir string // Directory for output files
-}
-
-// DefaultConfig returns the default configuration for Bulgarian voice
-func DefaultConfig() *ESpeakConfig {
- return &ESpeakConfig{
- Voice: "bg",
- Speed: 150,
- Pitch: 50,
- Amplitude: 100,
- WordGap: 0,
- OutputDir: "./",
- }
-}
-
-// ESpeak provides an interface to the espeak-ng text-to-speech engine
-type ESpeak struct {
- config *ESpeakConfig
-}
-
-// New creates a new ESpeak instance with the given configuration
-func New(config *ESpeakConfig) (*ESpeak, error) {
- // Check if espeak-ng is installed
- if err := checkESpeakInstalled(); err != nil {
- return nil, err
- }
-
- if config == nil {
- config = DefaultConfig()
- }
-
- return &ESpeak{config: config}, nil
-}
-
-// GenerateAudio generates an audio file for the given Bulgarian text
-func (e *ESpeak) GenerateAudio(text string, outputFile string) error {
- // Validate input
- if text == "" {
- return fmt.Errorf("text cannot be empty")
- }
-
- // Ensure output directory exists
- dir := filepath.Dir(outputFile)
- if dir != "" && dir != "." {
- if err := os.MkdirAll(dir, 0755); err != nil {
- return fmt.Errorf("failed to create output directory: %w", err)
- }
- }
-
- // Build espeak-ng command
- args := []string{
- "-v", e.config.Voice, // Voice selection
- "-s", fmt.Sprintf("%d", e.config.Speed), // Speed
- "-p", fmt.Sprintf("%d", e.config.Pitch), // Pitch
- "-a", fmt.Sprintf("%d", e.config.Amplitude), // Amplitude/volume
- }
-
- // Add word gap if specified
- if e.config.WordGap > 0 {
- args = append(args, "-g", fmt.Sprintf("%d", e.config.WordGap))
- }
-
- // Add output file and text
- args = append(args, "-w", outputFile, text)
-
- cmd := exec.Command("espeak-ng", args...)
-
- // Run the command
- output, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("espeak-ng failed: %w\nOutput: %s", err, string(output))
- }
-
- return nil
-}
-
-// SetVoice updates the voice variant
-func (e *ESpeak) SetVoice(voice string) {
- e.config.Voice = voice
-}
-
-// SetSpeed updates the speech speed
-func (e *ESpeak) SetSpeed(speed int) {
- if speed < 80 {
- speed = 80
- } else if speed > 450 {
- speed = 450
- }
- e.config.Speed = speed
-}
-
-// SetPitch updates the pitch (0-99, 50 is default)
-func (e *ESpeak) SetPitch(pitch int) {
- if pitch < 0 {
- pitch = 0
- } else if pitch > 99 {
- pitch = 99
- }
- e.config.Pitch = pitch
-}
-
-// SetAmplitude updates the volume/amplitude (0-200, 100 is default)
-func (e *ESpeak) SetAmplitude(amplitude int) {
- if amplitude < 0 {
- amplitude = 0
- } else if amplitude > 200 {
- amplitude = 200
- }
- e.config.Amplitude = amplitude
-}
-
-// SetWordGap updates the gap between words in 10ms units
-func (e *ESpeak) SetWordGap(gap int) {
- if gap < 0 {
- gap = 0
- }
- e.config.WordGap = gap
-}
-
-// checkESpeakInstalled verifies that espeak-ng is available on the system
-func checkESpeakInstalled() error {
- cmd := exec.Command("espeak-ng", "--version")
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("espeak-ng is not installed or not in PATH: %w", err)
- }
- return nil
-}
-
-// ValidateBulgarianText performs basic validation of Bulgarian text
-func ValidateBulgarianText(text string) error {
- if text == "" {
- return fmt.Errorf("text cannot be empty")
- }
-
- // Check if text contains at least one Cyrillic character
- hasCyrillic := false
- for _, r := range text {
- // Bulgarian Cyrillic range
- if (r >= 'А' && r <= 'я') || r == 'Ё' || r == 'ё' {
- hasCyrillic = true
- break
- }
- }
-
- if !hasCyrillic {
- return fmt.Errorf("text must contain Bulgarian Cyrillic characters")
- }
-
- return nil
-}
-
-// ListVoices returns available Bulgarian voice variants
-func ListVoices() []string {
- return []string{
- "bg", // Default Bulgarian voice
- "bg+m1", // Bulgarian male voice 1
- "bg+m2", // Bulgarian male voice 2
- "bg+m3", // Bulgarian male voice 3
- "bg+f1", // Bulgarian female voice 1
- "bg+f2", // Bulgarian female voice 2
- "bg+f3", // Bulgarian female voice 3
- }
-}
-
-// ConvertWAVToMP3 converts a WAV file to MP3 using ffmpeg
-func ConvertWAVToMP3(wavFile, mp3File string) error {
- // Check if ffmpeg is installed
- if err := exec.Command("ffmpeg", "-version").Run(); err != nil {
- return fmt.Errorf("ffmpeg is not installed or not in PATH: %w", err)
- }
-
- cmd := exec.Command("ffmpeg", "-i", wavFile, "-acodec", "mp3", "-y", mp3File)
- output, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("ffmpeg conversion failed: %w\nOutput: %s", err, string(output))
- }
-
- return nil
-}
-
-// GenerateMP3 generates an MP3 file for the given Bulgarian text
-func (e *ESpeak) GenerateMP3(text string, outputFile string) error {
- // Generate temporary WAV file
- tempWAV := strings.TrimSuffix(outputFile, filepath.Ext(outputFile)) + "_temp.wav"
-
- // Generate WAV
- if err := e.GenerateAudio(text, tempWAV); err != nil {
- return err
- }
-
- // Convert to MP3
- if err := ConvertWAVToMP3(tempWAV, outputFile); err != nil {
- // Clean up temporary file
- os.Remove(tempWAV)
- return err
- }
-
- // Clean up temporary file
- return os.Remove(tempWAV)
-} \ No newline at end of file
diff --git a/internal/audio/espeak_provider.go b/internal/audio/espeak_provider.go
deleted file mode 100644
index 177e2a6..0000000
--- a/internal/audio/espeak_provider.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package audio
-
-import (
- "context"
- "path/filepath"
- "strings"
-)
-
-// ESpeakProvider implements Provider interface for espeak-ng
-type ESpeakProvider struct {
- espeak *ESpeak
- format string
-}
-
-// NewESpeakProvider creates a new espeak-ng provider
-func NewESpeakProvider(config *ESpeakConfig) (Provider, error) {
- espeak, err := New(config)
- if err != nil {
- return nil, err
- }
-
- return &ESpeakProvider{
- espeak: espeak,
- format: "mp3", // default format
- }, nil
-}
-
-// GenerateAudio generates audio using espeak-ng
-func (p *ESpeakProvider) GenerateAudio(ctx context.Context, text string, outputFile string) error {
- // Validate Bulgarian text
- if err := ValidateBulgarianText(text); err != nil {
- return err
- }
-
- // Determine format from output file extension
- ext := strings.ToLower(filepath.Ext(outputFile))
-
- switch ext {
- case ".mp3":
- return p.espeak.GenerateMP3(text, outputFile)
- case ".wav":
- return p.espeak.GenerateAudio(text, outputFile)
- default:
- // Default to MP3 if extension is unclear
- if !strings.HasSuffix(outputFile, ".mp3") {
- outputFile += ".mp3"
- }
- return p.espeak.GenerateMP3(text, outputFile)
- }
-}
-
-// Name returns the provider name
-func (p *ESpeakProvider) Name() string {
- return "espeak-ng"
-}