summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-20 22:18:57 +0300
committerPaul Buetow <paul@buetow.org>2025-07-20 22:18:57 +0300
commite580fb57a29ec3c3f3e180b20cfa6ec28687689b (patch)
treede74f04450b830268e4c1644a91acb9fd45c3802 /internal/cli
parent9e3328a6aaefe4bd1aa0ec3e8bf6e93d6033180b (diff)
Refactor main.go into focused packages
- Reduced main.go from 961 lines to 89 lines (91% reduction) - Created new packages for better separation of concerns: - cli: Command-line interface setup and configuration - processor: Core word processing logic and orchestration - batch: Batch file processing functionality - translation: Bulgarian to English translation services - models: OpenAI model listing functionality - phonetic: Phonetic information fetching - Each package has clear documentation in doc.go files - Improved testability and maintainability - All existing functionality preserved - All tests passing and build successful
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/command.go136
-rw-r--r--internal/cli/doc.go4
-rw-r--r--internal/cli/flags.go46
3 files changed, 186 insertions, 0 deletions
diff --git a/internal/cli/command.go b/internal/cli/command.go
new file mode 100644
index 0000000..6f9f1c0
--- /dev/null
+++ b/internal/cli/command.go
@@ -0,0 +1,136 @@
+package cli
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+
+ "codeberg.org/snonux/totalrecall/internal"
+)
+
+// CreateRootCommand creates and configures the root cobra command
+func CreateRootCommand(flags *Flags) *cobra.Command {
+ rootCmd := &cobra.Command{
+ Use: "totalrecall [word]",
+ Short: "Bulgarian Anki Flashcard Generator",
+ Long: `totalrecall generates Anki flashcard materials from Bulgarian words.
+
+It creates audio pronunciation files using OpenAI TTS and downloads
+representative images from web search APIs.
+
+Examples:
+ totalrecall # Launch interactive GUI (default)
+ totalrecall ябълка # Generate materials for "apple" via CLI
+ totalrecall --batch words.txt # Process multiple words from file
+ totalrecall --gui # Explicitly launch GUI mode`,
+ Args: cobra.MaximumNArgs(1),
+ Version: internal.Version,
+ }
+
+ // Set up flags
+ setupFlags(rootCmd, flags)
+
+ return rootCmd
+}
+
+func setupFlags(cmd *cobra.Command, flags *Flags) {
+ // Set default output directory
+ home, _ := os.UserHomeDir()
+ defaultOutputDir := filepath.Join(home, "Downloads")
+
+ // Global flags
+ cmd.PersistentFlags().StringVar(&flags.CfgFile, "config", "", "config file (default is $HOME/.totalrecall.yaml)")
+
+ // Local flags
+ cmd.Flags().StringVarP(&flags.OutputDir, "output", "o", defaultOutputDir, "Output directory")
+ cmd.Flags().StringVarP(&flags.AudioFormat, "format", "f", flags.AudioFormat, "Audio format (wav or mp3)")
+ cmd.Flags().StringVar(&flags.ImageAPI, "image-api", flags.ImageAPI, "Image source (only openai supported)")
+ cmd.Flags().StringVar(&flags.BatchFile, "batch", "", "Process words from file (one per line)")
+ cmd.Flags().BoolVar(&flags.SkipAudio, "skip-audio", false, "Skip audio generation")
+ cmd.Flags().BoolVar(&flags.SkipImages, "skip-images", false, "Skip image download")
+ cmd.Flags().BoolVar(&flags.GenerateAnki, "anki", false, "Generate Anki import file (APKG format by default, use --anki-csv for legacy CSV)")
+ cmd.Flags().BoolVar(&flags.AnkiCSV, "anki-csv", false, "Generate legacy CSV format instead of APKG when using --anki")
+ cmd.Flags().StringVar(&flags.DeckName, "deck-name", flags.DeckName, "Deck name for APKG export")
+ cmd.Flags().BoolVar(&flags.ListModels, "list-models", false, "List available OpenAI models for the current API key")
+ cmd.Flags().BoolVar(&flags.AllVoices, "all-voices", false, "Generate audio in all available voices (creates multiple files)")
+ cmd.Flags().BoolVar(&flags.GUIMode, "gui", false, "Launch interactive GUI mode")
+
+ // OpenAI flags
+ cmd.Flags().StringVar(&flags.OpenAIModel, "openai-model", flags.OpenAIModel, "OpenAI TTS model: tts-1, tts-1-hd, gpt-4o-mini-tts")
+ cmd.Flags().StringVar(&flags.OpenAIVoice, "openai-voice", "", "OpenAI voice: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse (default: random)")
+ cmd.Flags().Float64Var(&flags.OpenAISpeed, "openai-speed", flags.OpenAISpeed, "OpenAI speech speed (0.25 to 4.0, may be ignored by gpt-4o-mini-tts)")
+ cmd.Flags().StringVar(&flags.OpenAIInstruction, "openai-instruction", "", "Voice instructions for gpt-4o-mini-tts model (e.g., 'speak slowly with a Bulgarian accent')")
+
+ // OpenAI Image Generation flags
+ cmd.Flags().StringVar(&flags.OpenAIImageModel, "openai-image-model", flags.OpenAIImageModel, "OpenAI image model: dall-e-2 or dall-e-3")
+ cmd.Flags().StringVar(&flags.OpenAIImageSize, "openai-image-size", flags.OpenAIImageSize, "Image size: 256x256, 512x512, 1024x1024 (dall-e-3: also 1024x1792, 1792x1024)")
+ cmd.Flags().StringVar(&flags.OpenAIImageQuality, "openai-image-quality", flags.OpenAIImageQuality, "Image quality: standard or hd (dall-e-3 only)")
+ cmd.Flags().StringVar(&flags.OpenAIImageStyle, "openai-image-style", flags.OpenAIImageStyle, "Image style: natural or vivid (dall-e-3 only)")
+
+ // Bind flags to viper
+ bindFlagsToViper(cmd)
+}
+
+func bindFlagsToViper(cmd *cobra.Command) {
+ viper.BindPFlag("audio.provider", cmd.Flags().Lookup("audio-provider"))
+ viper.BindPFlag("audio.voice", cmd.Flags().Lookup("voice"))
+ viper.BindPFlag("audio.format", cmd.Flags().Lookup("format"))
+ viper.BindPFlag("audio.pitch", cmd.Flags().Lookup("pitch"))
+ viper.BindPFlag("audio.amplitude", cmd.Flags().Lookup("amplitude"))
+ viper.BindPFlag("audio.word_gap", cmd.Flags().Lookup("word-gap"))
+ viper.BindPFlag("audio.openai_model", cmd.Flags().Lookup("openai-model"))
+ viper.BindPFlag("audio.openai_voice", cmd.Flags().Lookup("openai-voice"))
+ viper.BindPFlag("audio.openai_speed", cmd.Flags().Lookup("openai-speed"))
+ viper.BindPFlag("audio.openai_instruction", cmd.Flags().Lookup("openai-instruction"))
+ viper.BindPFlag("output.directory", cmd.Flags().Lookup("output"))
+ viper.BindPFlag("image.provider", cmd.Flags().Lookup("image-api"))
+ // Bind OpenAI image flags
+ viper.BindPFlag("image.openai_model", cmd.Flags().Lookup("openai-image-model"))
+ viper.BindPFlag("image.openai_size", cmd.Flags().Lookup("openai-image-size"))
+ viper.BindPFlag("image.openai_quality", cmd.Flags().Lookup("openai-image-quality"))
+ viper.BindPFlag("image.openai_style", cmd.Flags().Lookup("openai-image-style"))
+}
+
+// InitConfig initializes viper configuration
+func InitConfig(cfgFile string) {
+ if cfgFile != "" {
+ // Use config file from the flag
+ viper.SetConfigFile(cfgFile)
+ } else {
+ // Find home directory
+ home, err := os.UserHomeDir()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err)
+ return
+ }
+
+ // Search config in home directory with name ".totalrecall" (without extension)
+ viper.AddConfigPath(home)
+ viper.AddConfigPath(".")
+ viper.SetConfigType("yaml")
+ viper.SetConfigName(".totalrecall")
+ }
+
+ // Environment variables
+ viper.SetEnvPrefix("TOTALRECALL")
+ viper.AutomaticEnv()
+
+ // Read config file
+ if err := viper.ReadInConfig(); err == nil {
+ fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
+ }
+}
+
+// GetOpenAIKey retrieves the OpenAI API key from environment or config
+func GetOpenAIKey() string {
+ // First check environment variable
+ if key := os.Getenv("OPENAI_API_KEY"); key != "" {
+ return key
+ }
+
+ // Then check config file
+ return viper.GetString("audio.openai_key")
+}
diff --git a/internal/cli/doc.go b/internal/cli/doc.go
new file mode 100644
index 0000000..6ed189b
--- /dev/null
+++ b/internal/cli/doc.go
@@ -0,0 +1,4 @@
+// Package cli provides command-line interface setup and configuration
+// for the totalrecall application. It handles flag parsing, command
+// creation, and configuration management using cobra and viper.
+package cli
diff --git a/internal/cli/flags.go b/internal/cli/flags.go
new file mode 100644
index 0000000..64575a0
--- /dev/null
+++ b/internal/cli/flags.go
@@ -0,0 +1,46 @@
+package cli
+
+// Flags holds all command-line flag values
+type Flags struct {
+ // General flags
+ CfgFile string
+ OutputDir string
+ AudioFormat string
+ ImageAPI string
+ BatchFile string
+ SkipAudio bool
+ SkipImages bool
+ GenerateAnki bool
+ AnkiCSV bool
+ DeckName string
+ ListModels bool
+ AllVoices bool
+ GUIMode bool
+
+ // OpenAI flags
+ OpenAIModel string
+ OpenAIVoice string
+ OpenAISpeed float64
+ OpenAIInstruction string
+
+ // OpenAI Image flags
+ OpenAIImageModel string
+ OpenAIImageSize string
+ OpenAIImageQuality string
+ OpenAIImageStyle string
+}
+
+// NewFlags creates a new Flags instance with default values
+func NewFlags() *Flags {
+ return &Flags{
+ AudioFormat: "mp3",
+ ImageAPI: "openai",
+ DeckName: "Bulgarian Vocabulary",
+ OpenAIModel: "gpt-4o-mini-tts",
+ OpenAISpeed: 0.9,
+ OpenAIImageModel: "dall-e-3",
+ OpenAIImageSize: "1024x1024",
+ OpenAIImageQuality: "standard",
+ OpenAIImageStyle: "natural",
+ }
+}