summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-14 22:37:40 +0300
committerPaul Buetow <paul@buetow.org>2025-07-14 22:37:40 +0300
commit77f53d2260acd747c8227d60449251dd9606eb70 (patch)
tree32a347c9ccb03711d56e6b6059d42b873c4db40a
parentcbb1581356ed59e81cf5fedb30145c7521165e3d (diff)
rename from bulg to totalrecall
-rw-r--r--CLAUDE.md12
-rw-r--r--LICENSE2
-rw-r--r--README.md48
-rw-r--r--TODO.md2
-rw-r--r--Taskfile.yaml6
-rw-r--r--cmd/bulg/main.go447
-rw-r--r--go.mod11
-rw-r--r--go.sum16
-rw-r--r--internal/config/doc.go2
9 files changed, 59 insertions, 487 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index ade1542..4b972ef 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3,7 +3,7 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
-**bulg** - Bulgarian Anki Flashcard Generator
+**totalrecall** - Bulgarian Anki Flashcard Generator
A Go CLI tool that generates Anki flashcard materials from Bulgarian words:
- Generates audio pronunciation using espeak-ng
@@ -35,10 +35,10 @@ task install
### Common Development Commands
```bash
# Build for current platform
-go build -o bulg ./cmd/bulg
+go build -o totalrecall ./cmd/totalrecall
# Run without building
-go run ./cmd/bulg "ябълка"
+go run ./cmd/totalrecall "ябълка"
# Run tests with coverage
go test -v -cover ./...
@@ -57,8 +57,8 @@ golangci-lint run
### Package Structure
```
-bulg/
-├── cmd/bulg/ # CLI entry point
+totalrecall/
+├── cmd/totalrecall/ # CLI entry point
├── internal/ # Private packages
│ ├── audio/ # Audio generation (espeak-ng wrapper)
│ ├── image/ # Image search functionality
@@ -109,7 +109,7 @@ espeak-ng -v bg+f1 "Здравей"
```
### Package Declaration Error
-If you see an error about `package main`, ensure `cmd/bulg/main.go` has:
+If you see an error about `package main`, ensure `cmd/totalrecall/main.go` has:
```go
package main // NOT package bulg
```
diff --git a/LICENSE b/LICENSE
index 275734a..c46b4ff 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 bulg contributors
+Copyright (c) 2025 totalrecall contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index e342fd7..f4d6b96 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# bulg - Bulgarian Anki Flashcard Generator
+# totalrecall - Bulgarian Anki Flashcard Generator
-`bulg` is a command-line tool that generates Anki flashcard materials from Bulgarian words. It creates audio pronunciation files using espeak-ng or OpenAI TTS and downloads representative images from web search APIs.
+`totalrecall` is a command-line tool that generates Anki flashcard materials from Bulgarian words. It creates audio pronunciation files using espeak-ng or OpenAI TTS and downloads representative images from web search APIs.
## Features
@@ -41,42 +41,42 @@
- Generate an API key at https://platform.openai.com/api-keys
- Set the key using one of these methods:
- Environment variable: `export OPENAI_API_KEY="sk-..."`
- - Configuration file: Add to `.bulg.yaml`
+ - Configuration file: Add to `.totalrecall.yaml`
### Building from Source
```bash
-git clone https://github.com/yourusername/bulg.git
-cd bulg
-go build -o bulg ./cmd/bulg
+git clone https://github.com/yourusername/totalrecall.git
+cd totalrecall
+go build -o totalrecall ./cmd/totalrecall
```
Or install directly:
```bash
-go install codeberg.org/snonux/bulg/cmd/bulg@latest
+go install codeberg.org/snonux/totalrecall/cmd/totalrecall@latest
```
## Quick Start
1. Generate materials for a single word:
```bash
- bulg ябълка
+ totalrecall ябълка
```
2. Process multiple words from a file:
```bash
- bulg --batch words.txt
+ totalrecall --batch words.txt
```
3. Generate with Anki CSV:
```bash
- bulg ябълка --anki
+ totalrecall ябълка --anki
```
## Configuration
-Create a `.bulg.yaml` file in your home directory or project folder:
+Create a `.totalrecall.yaml` file in your home directory or project folder:
```yaml
audio:
@@ -112,7 +112,7 @@ output:
## Usage
```bash
-bulg [word] [flags]
+totalrecall [word] [flags]
```
### Flags
@@ -155,25 +155,25 @@ bulg [word] [flags]
### Basic Usage
```bash
# Single word with espeak-ng
-bulg котка
+totalrecall котка
# Using OpenAI TTS (requires API key in config)
-bulg котка --audio-provider openai
+totalrecall котка --audio-provider openai
# High-quality OpenAI with specific voice
-bulg ябълка --audio-provider openai --openai-model tts-1-hd --openai-voice alloy
+totalrecall ябълка --audio-provider openai --openai-model tts-1-hd --openai-voice alloy
# Multiple words with custom output
-bulg --batch animals.txt -o ./animal_cards
+totalrecall --batch animals.txt -o ./animal_cards
# ESpeak with tuning
-bulg ябълка --pitch 40 --word-gap 3
+totalrecall ябълка --pitch 40 --word-gap 3
# Skip images, audio only
-bulg куче --skip-images
+totalrecall куче --skip-images
# Generate Anki import file
-bulg --batch words.txt --anki
+totalrecall --batch words.txt --anki
```
### Batch File Format
@@ -225,8 +225,8 @@ The Bulgarian voice in espeak-ng can sound robotic. To improve quality:
espeak-ng -v bg -p 40 -s 140 "Здравей" # Deeper, slower
espeak-ng -v bg+f1 -p 60 -g 2 "Здравей" # Higher pitch, word gaps
-# Using bulg with tuning
-bulg ябълка --pitch 40 --word-gap 2 --amplitude 120
+# Using totalrecall with tuning
+totalrecall ябълка --pitch 40 --word-gap 2 --amplitude 120
```
Recommended settings for clearer pronunciation:
@@ -242,15 +242,15 @@ OpenAI TTS provides much more natural Bulgarian pronunciation:
```bash
# Option 1: Use environment variable
export OPENAI_API_KEY="sk-your-key-here"
-bulg ябълка --audio-provider openai
+totalrecall ябълка --audio-provider openai
-# Option 2: Set in .bulg.yaml
+# Option 2: Set in .totalrecall.yaml
audio:
provider: openai
openai_key: "sk-your-key-here"
# Use with custom voice
-bulg ябълка --audio-provider openai --openai-voice alloy
+totalrecall ябълка --audio-provider openai --openai-voice alloy
```
**OpenAI Pricing**:
diff --git a/TODO.md b/TODO.md
index 9679bab..b0a1c78 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,4 +1,4 @@
# TODO's
-[ ] Rename the project from bulg to totalrecall. Look at all bulg references, and rename them. Also change the Go module name.
+[x] Rename the project from bulg to totalrecall. Look at all bulg references, and rename them. Also change the Go module name.
[ ] Ultra think about an Implementation of using OpenAPI key to use an OpenAI LLM to generate an image for the flash card. And add all to-do's into this file.
diff --git a/Taskfile.yaml b/Taskfile.yaml
index dc10b97..0c8c31f 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -3,13 +3,13 @@ version: '3'
tasks:
default:
cmds:
- - go build -o bulg ./cmd/bulg
+ - go build -o totalrecall ./cmd/totalrecall
run:
cmds:
- - go run ./cmd/bulg
+ - go run ./cmd/totalrecall
test:
cmds:
- go test ./...
install:
cmds:
- - go install ./cmd/bulg
+ - go install ./cmd/totalrecall
diff --git a/cmd/bulg/main.go b/cmd/bulg/main.go
deleted file mode 100644
index b3c2af1..0000000
--- a/cmd/bulg/main.go
+++ /dev/null
@@ -1,447 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
-
- "codeberg.org/snonux/bulg/internal"
- "codeberg.org/snonux/bulg/internal/anki"
- "codeberg.org/snonux/bulg/internal/audio"
- "codeberg.org/snonux/bulg/internal/image"
-)
-
-var (
- // Flags
- cfgFile string
- voice string
- outputDir string
- audioFormat string
- imageAPI string
- batchFile string
- skipAudio bool
- skipImages bool
- imagesPerWord int
- generateAnki bool
- // Audio provider flags
- audioProvider string
- // Audio tuning flags (espeak)
- audioPitch int
- audioAmplitude int
- audioWordGap int
- // OpenAI flags
- openAIModel string
- openAIVoice string
- openAISpeed float64
-)
-
-// rootCmd represents the base command when called without any subcommands
-var rootCmd = &cobra.Command{
- Use: "bulg [word]",
- Short: "Bulgarian Anki Flashcard Generator",
- Long: `bulg generates Anki flashcard materials from Bulgarian words.
-
-It creates audio pronunciation files using espeak-ng and downloads
-representative images from web search APIs.
-
-Example:
- bulg ябълка # Generate materials for "apple"
- bulg --batch words.txt # Process multiple words from file`,
- Args: cobra.MaximumNArgs(1),
- RunE: runCommand,
- Version: internal.Version,
-}
-
-func init() {
- cobra.OnInitialize(initConfig)
-
- // Global flags
- rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bulg.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", "pixabay", "Image source (pixabay or unsplash)")
- rootCmd.Flags().StringVar(&batchFile, "batch", "", "Process words from file (one per line)")
- rootCmd.Flags().BoolVar(&skipAudio, "skip-audio", false, "Skip audio generation")
- 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")
-
- // Audio provider selection
- rootCmd.Flags().StringVar(&audioProvider, "audio-provider", "espeak", "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)")
-
- // 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)")
-
- // Bind flags to viper
- viper.BindPFlag("audio.provider", rootCmd.Flags().Lookup("audio-provider"))
- viper.BindPFlag("audio.voice", rootCmd.Flags().Lookup("voice"))
- viper.BindPFlag("audio.format", rootCmd.Flags().Lookup("format"))
- viper.BindPFlag("audio.pitch", rootCmd.Flags().Lookup("pitch"))
- viper.BindPFlag("audio.amplitude", rootCmd.Flags().Lookup("amplitude"))
- viper.BindPFlag("audio.word_gap", rootCmd.Flags().Lookup("word-gap"))
- 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("output.directory", rootCmd.Flags().Lookup("output"))
- viper.BindPFlag("image.provider", rootCmd.Flags().Lookup("image-api"))
-}
-
-func initConfig() {
- if cfgFile != "" {
- // Use config file from the flag
- viper.SetConfigFile(cfgFile)
- } else {
- // Find home directory
- home, err := os.UserHomeDir()
- cobra.CheckErr(err)
-
- // Search config in home directory with name ".bulg" (without extension)
- viper.AddConfigPath(home)
- viper.AddConfigPath(".")
- viper.SetConfigType("yaml")
- viper.SetConfigName(".bulg")
- }
-
- // Environment variables
- viper.SetEnvPrefix("BULG")
- viper.AutomaticEnv()
-
- // Read config file
- if err := viper.ReadInConfig(); err == nil {
- fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
- }
-}
-
-func runCommand(cmd *cobra.Command, args []string) error {
- // Determine words to process
- var words []string
-
- if batchFile != "" {
- // Read words from file
- content, err := os.ReadFile(batchFile)
- if err != nil {
- return fmt.Errorf("failed to read batch file: %w", err)
- }
- // Split by newlines and filter empty lines
- lines := string(content)
- for _, line := range splitLines(lines) {
- if line = trimSpace(line); line != "" {
- words = append(words, line)
- }
- }
- } else if len(args) > 0 {
- // Single word from command line
- words = []string{args[0]}
- } else {
- // No input provided
- return fmt.Errorf("please provide a Bulgarian word or use --batch flag")
- }
-
- // Validate words
- for _, word := range words {
- if err := audio.ValidateBulgarianText(word); err != nil {
- return fmt.Errorf("invalid word '%s': %w", word, err)
- }
- }
-
- // Create output directory
- if err := os.MkdirAll(outputDir, 0755); err != nil {
- return fmt.Errorf("failed to create output directory: %w", err)
- }
-
- // Process each word
- for i, word := range words {
- fmt.Printf("\nProcessing %d/%d: %s\n", i+1, len(words), word)
-
- if err := processWord(word); err != nil {
- fmt.Fprintf(os.Stderr, "Error processing '%s': %v\n", word, err)
- // Continue with next word
- }
- }
-
- // Generate Anki CSV if requested
- if generateAnki {
- fmt.Printf("\nGenerating Anki import file...\n")
- if err := generateAnkiCSV(); err != nil {
- fmt.Fprintf(os.Stderr, "Warning: Failed to generate Anki CSV: %v\n", err)
- } else {
- fmt.Println("Anki import file created: anki_import.csv")
- }
- }
-
- fmt.Println("\nDone! Materials saved to:", outputDir)
- return nil
-}
-
-func processWord(word string) error {
- // Generate audio
- if !skipAudio {
- fmt.Printf(" Generating audio...\n")
- if err := generateAudio(word); err != nil {
- return fmt.Errorf("audio generation failed: %w", err)
- }
- }
-
- // Download images
- if !skipImages {
- fmt.Printf(" Downloading images...\n")
- if err := downloadImages(word); err != nil {
- return fmt.Errorf("image download failed: %w", err)
- }
- }
-
- return nil
-}
-
-func generateAudio(word string) error {
- // Create audio provider configuration
- providerConfig := &audio.Config{
- Provider: audioProvider,
- 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,
- OpenAISpeed: openAISpeed,
-
- // Caching
- EnableCache: viper.GetBool("audio.enable_cache"),
- CacheDir: viper.GetString("audio.cache_dir"),
- }
-
- // 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 == "espeak" && 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 == "tts-1" && 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") {
- providerConfig.OpenAISpeed = viper.GetFloat64("audio.openai_speed")
- }
-
- // 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 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
- }
- }
-
- // Generate audio file
- filename := sanitizeFilename(word)
- outputFile := filepath.Join(outputDir, fmt.Sprintf("%s.%s", filename, audioFormat))
-
- ctx := context.Background()
- return provider.GenerateAudio(ctx, word, outputFile)
-}
-
-func downloadImages(word string) error {
- // Create image searcher based on provider
- var searcher image.ImageSearcher
- var err error
-
- switch imageAPI {
- case "pixabay":
- apiKey := viper.GetString("image.pixabay_key")
- searcher = image.NewPixabayClient(apiKey)
-
- case "unsplash":
- apiKey := viper.GetString("image.unsplash_key")
- if apiKey == "" {
- return fmt.Errorf("Unsplash API key is required in config")
- }
- searcher, err = image.NewUnsplashClient(apiKey)
- if err != nil {
- return err
- }
-
- default:
- return fmt.Errorf("unknown image provider: %s", imageAPI)
- }
-
- // Create downloader
- downloadOpts := &image.DownloadOptions{
- OutputDir: outputDir,
- OverwriteExisting: false,
- CreateDir: true,
- FileNamePattern: "{word}_{index}",
- MaxSizeBytes: 5 * 1024 * 1024, // 5MB
- }
-
- downloader := image.NewDownloader(searcher, downloadOpts)
-
- // Download images
- ctx := context.Background()
- if imagesPerWord == 1 {
- _, path, err := downloader.DownloadBestMatch(ctx, word)
- if err != nil {
- return err
- }
- fmt.Printf(" Downloaded: %s\n", path)
- } else {
- paths, err := downloader.DownloadMultiple(ctx, word, imagesPerWord)
- if err != nil {
- return err
- }
- for _, path := range paths {
- fmt.Printf(" Downloaded: %s\n", path)
- }
- }
-
- return nil
-}
-
-func sanitizeFilename(s string) string {
- // Simple filename sanitization
- result := ""
- for _, r := range s {
- if isAlphaNumeric(r) || r == '-' || r == '_' {
- result += string(r)
- } else {
- result += "_"
- }
- }
- return result
-}
-
-func isAlphaNumeric(r rune) bool {
- return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
- (r >= '0' && r <= '9') || (r >= 'а' && r <= 'я') ||
- (r >= 'А' && r <= 'Я')
-}
-
-func splitLines(s string) []string {
- // Simple line splitter
- var lines []string
- current := ""
- for _, r := range s {
- if r == '\n' {
- lines = append(lines, current)
- current = ""
- } else if r != '\r' {
- current += string(r)
- }
- }
- if current != "" {
- lines = append(lines, current)
- }
- return lines
-}
-
-func trimSpace(s string) string {
- // Simple trim implementation
- start := 0
- end := len(s)
-
- // Trim from start
- for start < end && isSpace(rune(s[start])) {
- start++
- }
-
- // Trim from end
- for end > start && isSpace(rune(s[end-1])) {
- end--
- }
-
- return s[start:end]
-}
-
-func isSpace(r rune) bool {
- return r == ' ' || r == '\t' || r == '\n' || r == '\r'
-}
-
-func generateAnkiCSV() error {
- // Create Anki generator
- gen := anki.NewGenerator(&anki.GeneratorOptions{
- OutputPath: filepath.Join(outputDir, "anki_import.csv"),
- MediaFolder: outputDir,
- IncludeHeaders: true,
- AudioFormat: audioFormat,
- })
-
- // Generate cards from output directory
- if err := gen.GenerateFromDirectory(outputDir); err != nil {
- return fmt.Errorf("failed to generate cards: %w", err)
- }
-
- // Generate CSV
- if err := gen.GenerateCSV(); err != nil {
- return fmt.Errorf("failed to generate CSV: %w", err)
- }
-
- // Print stats
- total, withAudio, withImages := gen.Stats()
- fmt.Printf(" Generated %d cards (%d with audio, %d with images)\n",
- total, withAudio, withImages)
-
- return nil
-}
-
-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")
-}
-
-func main() {
- if err := rootCmd.Execute(); err != nil {
- os.Exit(1)
- }
-} \ No newline at end of file
diff --git a/go.mod b/go.mod
index 2a00223..185ca8f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,20 +1,23 @@
-module codeberg.org/snonux/bulg
+module codeberg.org/snonux/totalrecall
go 1.24.4
require (
+ github.com/sashabaranov/go-openai v1.40.5
+ github.com/spf13/cobra v1.9.1
+ github.com/spf13/viper v1.20.1
+)
+
+require (
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
- github.com/sashabaranov/go-openai v1.40.5 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
- github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
- github.com/spf13/viper v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
diff --git a/go.sum b/go.sum
index 58daf75..6f00a23 100644
--- a/go.sum
+++ b/go.sum
@@ -1,15 +1,27 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
@@ -29,6 +41,8 @@ github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@@ -40,5 +54,7 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/config/doc.go b/internal/config/doc.go
index b616b99..1e67a8a 100644
--- a/internal/config/doc.go
+++ b/internal/config/doc.go
@@ -1,3 +1,3 @@
-// Package config provides configuration management for the bulg
+// Package config provides configuration management for the totalrecall
// application using viper for flexible configuration options.
package config \ No newline at end of file