diff --git a/README.md b/README.md index 86aebb4..84d6a65 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ sudo apt install libnotify-bin # Ubuntu/Debian ``` - Verify Wayland compositor supports text input protocols -- Check injection mode in configuration (fallback mode is most robust) +- Check injection backends in configuration (fallback chain is most robust) **Clipboard issues:** diff --git a/docs/config.md b/docs/config.md index 329fe64..dc2834c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -34,7 +34,7 @@ Configuration is stored in `~/.config/hyprvoice/config.toml` and changes are app - [Text Injection](#text-injection) - [Notifications](#notifications) - [Example Configurations](#example-configurations) -- [Migration from Old Config Format](#migration-from-old-config-format) +- [Legacy Configs](#legacy-configs) ## Unified Provider System @@ -647,64 +647,16 @@ You can customize notification text via the `[notifications.messages]` section: model = "gpt-4o-mini" ``` -## Migration from Old Config Format +## Legacy Configs -### Language Configuration Change +Older config formats are no longer supported. If your config uses any of these fields, rerun onboarding to regenerate a supported config: -Language is now configured per transcription model in `[transcription].language`. If you had `[general].language` set, move it to the transcription section: +- `transcription.api_key` +- `injection.mode` +- `general.language` +- `transcription.provider = "groq-translation"` -**Old format:** - -```toml -[general] - language = "en" - -[transcription] - provider = "openai" - model = "whisper-1" -``` - -**New format:** - -```toml -[transcription] - provider = "openai" - model = "whisper-1" - language = "en" -``` - -Run `hyprvoice configure` to interactively update your config. - -### API Key Migration - -If you're upgrading from an older version with `transcription.api_key`: - -**Old format (still works):** - -```toml -[transcription] - provider = "openai" - api_key = "sk-..." # Legacy location - model = "whisper-1" -``` - -**New format (recommended):** - -```toml -[providers.openai] - api_key = "sk-..." # Unified location - -[transcription] - provider = "openai" - model = "whisper-1" - -[llm] - enabled = true - provider = "openai" - model = "gpt-4o-mini" -``` - -Run `hyprvoice configure` to interactively update your config to the new format. +Run `hyprvoice onboarding` to generate a new config, then `hyprvoice configure` for advanced settings. ## Configuration Hot-Reloading diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b8e66dd..dce412b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -26,10 +26,12 @@ func createTestConfig() *Config { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-api-key", Language: "", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-api-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -55,7 +57,6 @@ func createTestConfigWithInvalidValues() *Config { }, Transcription: TranscriptionConfig{ Provider: "", // Invalid - APIKey: "", // Invalid Model: "", // Invalid }, Injection: InjectionConfig{ @@ -98,9 +99,11 @@ func TestConfig_Validate(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -125,9 +128,11 @@ func TestConfig_Validate(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -152,9 +157,11 @@ func TestConfig_Validate(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"invalid"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -179,9 +186,11 @@ func TestConfig_Validate(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -206,10 +215,12 @@ func TestConfig_Validate(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -234,10 +245,12 @@ func TestConfig_Validate(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "invalid", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -312,9 +325,11 @@ buffer_size = 8192 channel_buffer_size = 30 timeout = "5m" +[providers.openai] +api_key = "test-key" + [transcription] provider = "openai" -api_key = "test-key" model = "whisper-1" [injection] @@ -362,8 +377,8 @@ type = "log"` } }) - // Test migration from legacy mode config - t.Run("migrates legacy mode=fallback to backends", func(t *testing.T) { + // Legacy configs should fail like missing config + t.Run("rejects legacy injection.mode", func(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") @@ -382,17 +397,12 @@ timeout = "5m" [transcription] provider = "openai" -api_key = "test-key" model = "whisper-1" [injection] mode = "fallback" wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` +clipboard_timeout = "3s"` err = os.WriteFile(configPath, []byte(legacyConfig), 0644) if err != nil { @@ -409,35 +419,70 @@ type = "log"` } }() - config, err := Load() - if err != nil { - t.Errorf("Load() error = %v", err) - return + _, err = Load() + if err == nil { + t.Fatalf("Load() should have failed for legacy injection.mode") } - - // Should have migrated to backends - expectedBackends := []string{"wtype", "clipboard"} - if len(config.Injection.Backends) != len(expectedBackends) { - t.Errorf("Expected %d backends, got %d", len(expectedBackends), len(config.Injection.Backends)) - } - for i, b := range expectedBackends { - if i < len(config.Injection.Backends) && config.Injection.Backends[i] != b { - t.Errorf("Expected backend[%d]=%s, got %s", i, b, config.Injection.Backends[i]) - } - } - - // Should have set default ydotool timeout - if config.Injection.YdotoolTimeout != 5*time.Second { - t.Errorf("Expected YdotoolTimeout=5s, got %v", config.Injection.YdotoolTimeout) - } - - // Verify it passes validation - if err := config.Validate(); err != nil { - t.Errorf("Migrated config is invalid: %v", err) + if !errors.Is(err, ErrConfigNotFound) { + t.Errorf("Load() error = %v, expected ErrConfigNotFound", err) } }) - t.Run("migrates legacy mode=clipboard to backends", func(t *testing.T) { + t.Run("rejects legacy general.language", func(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") + + err := os.MkdirAll(filepath.Dir(configPath), 0755) + if err != nil { + t.Fatalf("Failed to create config directory: %v", err) + } + + legacyConfig := `[general] +language = "en" + +[recording] +sample_rate = 16000 +channels = 1 +format = "s16" +buffer_size = 8192 +channel_buffer_size = 30 +timeout = "5m" + +[transcription] +provider = "openai" +model = "whisper-1" + +[injection] +backends = ["clipboard"] +ydotool_timeout = "5s" +wtype_timeout = "5s" +clipboard_timeout = "3s"` + + err = os.WriteFile(configPath, []byte(legacyConfig), 0644) + if err != nil { + t.Fatalf("Failed to create config file: %v", err) + } + + originalConfigDir := os.Getenv("XDG_CONFIG_HOME") + os.Setenv("XDG_CONFIG_HOME", tempDir) + defer func() { + if originalConfigDir == "" { + os.Unsetenv("XDG_CONFIG_HOME") + } else { + os.Setenv("XDG_CONFIG_HOME", originalConfigDir) + } + }() + + _, err = Load() + if err == nil { + t.Fatalf("Load() should have failed for legacy general.language") + } + if !errors.Is(err, ErrConfigNotFound) { + t.Errorf("Load() error = %v, expected ErrConfigNotFound", err) + } + }) + + t.Run("rejects legacy transcription.api_key", func(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") @@ -456,17 +501,14 @@ timeout = "5m" [transcription] provider = "openai" -api_key = "test-key" +api_key = "sk-old-style-key" model = "whisper-1" [injection] -mode = "clipboard" +backends = ["clipboard"] +ydotool_timeout = "5s" wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` +clipboard_timeout = "3s"` err = os.WriteFile(configPath, []byte(legacyConfig), 0644) if err != nil { @@ -483,23 +525,16 @@ type = "log"` } }() - config, err := Load() - if err != nil { - t.Errorf("Load() error = %v", err) - return + _, err = Load() + if err == nil { + t.Fatalf("Load() should have failed for legacy transcription.api_key") } - - expectedBackends := []string{"clipboard"} - if len(config.Injection.Backends) != len(expectedBackends) { - t.Errorf("Expected %d backends, got %d", len(expectedBackends), len(config.Injection.Backends)) - } - - if err := config.Validate(); err != nil { - t.Errorf("Migrated config is invalid: %v", err) + if !errors.Is(err, ErrConfigNotFound) { + t.Errorf("Load() error = %v, expected ErrConfigNotFound", err) } }) - t.Run("migrates legacy mode=type to backends", func(t *testing.T) { + t.Run("rejects legacy groq-translation provider", func(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") @@ -517,18 +552,14 @@ channel_buffer_size = 30 timeout = "5m" [transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" +provider = "groq-translation" +model = "whisper-large-v3" [injection] -mode = "type" +backends = ["clipboard"] +ydotool_timeout = "5s" wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` +clipboard_timeout = "3s"` err = os.WriteFile(configPath, []byte(legacyConfig), 0644) if err != nil { @@ -545,19 +576,12 @@ type = "log"` } }() - config, err := Load() - if err != nil { - t.Errorf("Load() error = %v", err) - return + _, err = Load() + if err == nil { + t.Fatalf("Load() should have failed for legacy groq-translation provider") } - - expectedBackends := []string{"wtype"} - if len(config.Injection.Backends) != len(expectedBackends) { - t.Errorf("Expected %d backends, got %d", len(expectedBackends), len(config.Injection.Backends)) - } - - if err := config.Validate(); err != nil { - t.Errorf("Migrated config is invalid: %v", err) + if !errors.Is(err, ErrConfigNotFound) { + t.Errorf("Load() error = %v, expected ErrConfigNotFound", err) } }) } @@ -643,8 +667,8 @@ func TestConfig_ConversionMethods(t *testing.T) { if transcriberConfig.Provider != config.Transcription.Provider { t.Errorf("Provider mismatch: got %s, want %s", transcriberConfig.Provider, config.Transcription.Provider) } - if transcriberConfig.APIKey != config.Transcription.APIKey { - t.Errorf("APIKey mismatch: got %s, want %s", transcriberConfig.APIKey, config.Transcription.APIKey) + if transcriberConfig.APIKey != config.Providers["openai"].APIKey { + t.Errorf("APIKey mismatch: got %s, want %s", transcriberConfig.APIKey, config.Providers["openai"].APIKey) } if transcriberConfig.Language != config.Transcription.Language { t.Errorf("Language mismatch: got %s, want %s", transcriberConfig.Language, config.Transcription.Language) @@ -777,7 +801,6 @@ func TestConfig_ToTranscriberConfig_WithEnvVar(t *testing.T) { config := &Config{ Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "", // Empty API key to test env var fallback Language: "en", Model: "whisper-1", }, @@ -805,10 +828,12 @@ func TestConfig_ToTranscriberConfig_WithoutEnvVar(t *testing.T) { config := &Config{ Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "config-api-key", // Config has API key Language: "en", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "config-api-key"}, + }, } // Ensure environment variable is not set @@ -873,7 +898,6 @@ func TestConfig_Validate_OpenAI_WithoutAPIKey(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "", // No API key Model: "whisper-1", }, Injection: InjectionConfig{ @@ -913,7 +937,6 @@ func TestConfig_Validate_OpenAI_WithEnvVarAPIKey(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "", // No API key in config Model: "whisper-1", }, Injection: InjectionConfig{ @@ -955,9 +978,11 @@ func TestConfig_Validate_RecordingTimeout(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -986,9 +1011,11 @@ func TestConfig_Validate_InjectionTimeouts(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 0, // Invalid timeout @@ -1017,9 +1044,11 @@ func TestConfig_Validate_RecordingBufferSizes(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -1048,10 +1077,12 @@ func TestConfig_Validate_GroqTranscription(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "groq-transcription", - APIKey: "gsk-test-key", Language: "en", Model: "whisper-large-v3", }, + Providers: map[string]ProviderConfig{ + "groq": {APIKey: "gsk-test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -1080,10 +1111,12 @@ func TestConfig_Validate_GroqInvalidModel(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "groq-transcription", - APIKey: "gsk-test-key", Language: "en", Model: "invalid-model", }, + Providers: map[string]ProviderConfig{ + "groq": {APIKey: "gsk-test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: time.Second, @@ -1112,7 +1145,6 @@ func TestConfig_Validate_GroqWithoutAPIKey(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "groq-transcription", - APIKey: "", // No API key Model: "whisper-large-v3", }, Injection: InjectionConfig{ @@ -1152,7 +1184,6 @@ func TestConfig_Validate_GroqWithEnvVarAPIKey(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "groq-transcription", - APIKey: "", // No API key in config Model: "whisper-large-v3", }, Injection: InjectionConfig{ @@ -1186,7 +1217,6 @@ func TestConfig_ToTranscriberConfig_GroqWithEnvVar(t *testing.T) { config := &Config{ Transcription: TranscriptionConfig{ Provider: "groq-transcription", - APIKey: "", // Empty API key to test env var fallback Language: "en", Model: "whisper-large-v3", }, @@ -1300,38 +1330,6 @@ func TestConfig_ProvidersMap(t *testing.T) { } } -func TestConfig_ProvidersMapFallbackToLegacy(t *testing.T) { - config := &Config{ - Recording: RecordingConfig{ - SampleRate: 16000, - Channels: 1, - Format: "s16", - BufferSize: 8192, - ChannelBufferSize: 30, - Timeout: time.Minute, - }, - Transcription: TranscriptionConfig{ - Provider: "openai", - APIKey: "sk-legacy-key", // Legacy field - Model: "whisper-1", - }, - Providers: map[string]ProviderConfig{}, // Empty providers map - Injection: InjectionConfig{ - Backends: []string{"clipboard"}, - YdotoolTimeout: 5 * time.Second, - WtypeTimeout: 5 * time.Second, - ClipboardTimeout: 3 * time.Second, - }, - Notifications: NotificationsConfig{Type: "log"}, - } - - // Should fall back to legacy transcription.api_key - transcriberConfig := config.ToTranscriberConfig() - if transcriberConfig.APIKey != "sk-legacy-key" { - t.Errorf("Expected APIKey from legacy field, got %s", transcriberConfig.APIKey) - } -} - func TestConfig_LLMConfig(t *testing.T) { config := &Config{ Recording: RecordingConfig{ @@ -1507,79 +1505,6 @@ func TestConfig_LLMValidation(t *testing.T) { }) } -func TestConfig_MigrateTranscriptionAPIKey(t *testing.T) { - tempDir := t.TempDir() - configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") - - err := os.MkdirAll(filepath.Dir(configPath), 0755) - if err != nil { - t.Fatalf("Failed to create config directory: %v", err) - } - - // Old-style config with api_key in transcription - oldConfig := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "sk-old-style-key" -model = "whisper-1" - -[injection] -backends = ["clipboard"] -ydotool_timeout = "5s" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -type = "log"` - - err = os.WriteFile(configPath, []byte(oldConfig), 0644) - if err != nil { - t.Fatalf("Failed to create config file: %v", err) - } - - originalConfigDir := os.Getenv("XDG_CONFIG_HOME") - os.Setenv("XDG_CONFIG_HOME", tempDir) - defer func() { - if originalConfigDir == "" { - os.Unsetenv("XDG_CONFIG_HOME") - } else { - os.Setenv("XDG_CONFIG_HOME", originalConfigDir) - } - }() - - config, err := Load() - if err != nil { - t.Errorf("Load() error = %v", err) - return - } - - // Should have migrated to providers map - if config.Providers == nil { - t.Fatal("Providers map should not be nil after migration") - } - if config.Providers["openai"].APIKey != "sk-old-style-key" { - t.Errorf("Expected migrated API key in providers.openai, got %s", config.Providers["openai"].APIKey) - } - - // Validation should pass - if err := config.Validate(); err != nil { - t.Errorf("Validate() should pass after migration: %v", err) - } - - // ToTranscriberConfig should resolve correctly - transcriberConfig := config.ToTranscriberConfig() - if transcriberConfig.APIKey != "sk-old-style-key" { - t.Errorf("Expected APIKey 'sk-old-style-key', got %s", transcriberConfig.APIKey) - } -} - func TestConfig_NewStyleConfig(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") @@ -1956,9 +1881,11 @@ func TestConfig_Validate_TranscriptionLanguage(t *testing.T) { }, Transcription: TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Model: "whisper-1", }, + Providers: map[string]ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: InjectionConfig{ Backends: []string{"clipboard"}, YdotoolTimeout: 5 * time.Second, diff --git a/internal/config/convert.go b/internal/config/convert.go index f320a2b..37a07db 100644 --- a/internal/config/convert.go +++ b/internal/config/convert.go @@ -41,7 +41,7 @@ func (c *Config) resolveEffectiveLanguage() string { return c.Transcription.Language } -// resolveAPIKeyForProvider returns the API key for a provider from multiple sources +// resolveAPIKeyForProvider returns the API key for a provider from config or env func (c *Config) resolveAPIKeyForProvider(providerName string) string { baseName := provider.BaseProviderName(providerName) envVar := provider.EnvVarForProvider(providerName) @@ -52,10 +52,6 @@ func (c *Config) resolveAPIKeyForProvider(providerName string) string { } } - if c.Transcription.APIKey != "" { - return c.Transcription.APIKey - } - if envVar != "" { return os.Getenv(envVar) } diff --git a/internal/config/load.go b/internal/config/load.go index 8adbd5b..575908f 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "runtime" - "time" "github.com/BurntSushi/toml" ) @@ -28,21 +27,6 @@ func GetConfigPath() (string, error) { return filepath.Join(hyprvoiceDir, "config.toml"), nil } -// legacyInjectionConfig for migration from old mode-based config -type legacyInjectionConfig struct { - Mode string `toml:"mode"` -} - -// legacyTranscriptionConfig for migration from old api_key in transcription -type legacyTranscriptionConfig struct { - APIKey string `toml:"api_key"` -} - -type legacyConfig struct { - Injection legacyInjectionConfig `toml:"injection"` - Transcription legacyTranscriptionConfig `toml:"transcription"` -} - func Load() (*Config, error) { configPath, err := GetConfigPath() if err != nil { @@ -57,30 +41,19 @@ func Load() (*Config, error) { log.Printf("Config: loading configuration from %s", configPath) var config Config - if _, err := toml.DecodeFile(configPath, &config); err != nil { + meta, err := toml.DecodeFile(configPath, &config) + if err != nil { return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err) } - - var legacy legacyConfig - toml.DecodeFile(configPath, &legacy) - - if len(config.Injection.Backends) == 0 { - config.migrateInjectionMode(legacy.Injection.Mode) - } - - if legacy.Transcription.APIKey != "" && config.Providers == nil { - config.migrateTranscriptionAPIKey(legacy.Transcription.APIKey) + if isLegacyConfig(meta, &config) { + log.Printf("Config: legacy configuration detected - run hyprvoice onboarding") + return nil, fmt.Errorf("%w: run hyprvoice onboarding", ErrConfigNotFound) } if config.Providers == nil { config.Providers = make(map[string]ProviderConfig) } - if config.Transcription.Provider == "groq-translation" { - log.Printf("Config: deprecated transcription.provider 'groq-translation' detected - using 'groq-transcription' instead") - config.Transcription.Provider = "groq-transcription" - } - config.applyLLMDefaults() config.applyThreadsDefault() @@ -88,6 +61,22 @@ func Load() (*Config, error) { return &config, nil } +func isLegacyConfig(meta toml.MetaData, config *Config) bool { + if meta.IsDefined("transcription", "api_key") { + return true + } + if meta.IsDefined("injection", "mode") { + return true + } + if meta.IsDefined("general", "language") { + return true + } + if config.Transcription.Provider == "groq-translation" { + return true + } + return false +} + // applyThreadsDefault sets default threads for local transcription if not explicitly set func (c *Config) applyThreadsDefault() { if c.Transcription.Threads == 0 { @@ -99,33 +88,6 @@ func (c *Config) applyThreadsDefault() { } } -// migrateTranscriptionAPIKey migrates old transcription.api_key to providers map -func (c *Config) migrateTranscriptionAPIKey(apiKey string) { - if c.Providers == nil { - c.Providers = make(map[string]ProviderConfig) - } - - providerName := c.Transcription.Provider - switch providerName { - case "openai": - c.Providers["openai"] = ProviderConfig{APIKey: apiKey} - case "groq-transcription": - c.Providers["groq"] = ProviderConfig{APIKey: apiKey} - case "mistral-transcription": - c.Providers["mistral"] = ProviderConfig{APIKey: apiKey} - case "elevenlabs": - c.Providers["elevenlabs"] = ProviderConfig{APIKey: apiKey} - default: - if len(apiKey) > 3 && apiKey[:3] == "sk-" { - c.Providers["openai"] = ProviderConfig{APIKey: apiKey} - } else if len(apiKey) > 4 && apiKey[:4] == "gsk_" { - c.Providers["groq"] = ProviderConfig{APIKey: apiKey} - } - } - - log.Printf("Config: migrated transcription.api_key to providers map. Run 'hyprvoice configure' to update config format.") -} - // applyLLMDefaults sets default values for LLM config func (c *Config) applyLLMDefaults() { pp := &c.LLM.PostProcessing @@ -136,29 +98,3 @@ func (c *Config) applyLLMDefaults() { pp.RemoveFillerWords = true } } - -// migrateInjectionMode converts old mode field to new backends array -func (c *Config) migrateInjectionMode(mode string) { - switch mode { - case "clipboard": - c.Injection.Backends = []string{"clipboard"} - log.Printf("Config: migrated injection.mode='clipboard' to backends=['clipboard']") - case "type": - c.Injection.Backends = []string{"wtype"} - log.Printf("Config: migrated injection.mode='type' to backends=['wtype']") - case "fallback": - c.Injection.Backends = []string{"wtype", "clipboard"} - log.Printf("Config: migrated injection.mode='fallback' to backends=['wtype', 'clipboard']") - default: - c.Injection.Backends = []string{"ydotool", "wtype", "clipboard"} - if mode != "" { - log.Printf("Config: unknown injection.mode='%s', using default backends", mode) - } - } - - if c.Injection.YdotoolTimeout == 0 { - c.Injection.YdotoolTimeout = 5 * time.Second - } - - log.Printf("Config: legacy 'mode' config detected - please update your config.toml to use 'backends' instead") -} diff --git a/internal/config/save.go b/internal/config/save.go index 45de1a1..84481fe 100644 --- a/internal/config/save.go +++ b/internal/config/save.go @@ -198,11 +198,6 @@ func SaveDefaultConfig() error { configContent := `# Hyprvoice Configuration # This file is automatically generated with defaults. # Edit values as needed - changes are applied immediately without daemon restart. -# -# MIGRATION NOTE: If upgrading from an older version, your transcription.api_key -# will be automatically migrated to the new [providers.X] format. Run 'hyprvoice configure' -# to update your config file structure. - # Keywords help both transcription and LLM understand domain-specific terms # Add names, technical terms, or brand names that might be misheard keywords = [] diff --git a/internal/config/types.go b/internal/config/types.go index 8784425..898009a 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -63,7 +63,6 @@ type RecordingConfig struct { type TranscriptionConfig struct { Provider string `toml:"provider"` - APIKey string `toml:"api_key"` Language string `toml:"language"` Model string `toml:"model"` Streaming bool `toml:"streaming"` // use streaming mode if model supports it diff --git a/internal/config/validate.go b/internal/config/validate.go index 8bc58f4..1354dc6 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -78,7 +78,7 @@ func (c *Config) Validate() error { apiKey := c.resolveAPIKeyForProvider(c.Transcription.Provider) if apiKey == "" { envVar := envVarForProvider(registryName) - return fmt.Errorf("%s API key required: not found in config (providers.%s.api_key, transcription.api_key) or environment variable (%s)", + return fmt.Errorf("%s API key required: not found in config (providers.%s.api_key) or environment variable (%s)", strings.Title(registryName), registryName, envVar) } } diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go index e06f2f8..3454ed7 100644 --- a/internal/daemon/daemon_test.go +++ b/internal/daemon/daemon_test.go @@ -13,6 +13,31 @@ import ( "github.com/leonardotrapani/hyprvoice/internal/pipeline" ) +const testConfigContent = `[recording] +sample_rate = 16000 +channels = 1 +format = "s16" +buffer_size = 8192 +channel_buffer_size = 30 +timeout = "5m" + +[providers.openai] +api_key = "test-key" + +[transcription] +provider = "openai" +model = "whisper-1" + +[injection] +backends = ["ydotool", "wtype", "clipboard"] +ydotool_timeout = "5s" +wtype_timeout = "5s" +clipboard_timeout = "3s" + +[notifications] +enabled = true +type = "log"` + func TestNew(t *testing.T) { // Set up a temporary config directory tempDir := t.TempDir() @@ -29,27 +54,7 @@ func TestNew(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() @@ -89,27 +94,7 @@ func TestDaemon_Status(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() @@ -140,27 +125,7 @@ func TestDaemon_Toggle(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() @@ -195,27 +160,7 @@ func TestDaemon_Handle(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() @@ -288,27 +233,7 @@ func TestDaemon_OnConfigReload(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() @@ -339,27 +264,7 @@ func TestDaemon_StopPipeline(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() @@ -402,27 +307,7 @@ func TestDaemon_Handle_Commands(t *testing.T) { // Create a basic config file configPath := filepath.Join(tempDir, "hyprvoice", "config.toml") os.MkdirAll(filepath.Dir(configPath), 0755) - configContent := `[recording] -sample_rate = 16000 -channels = 1 -format = "s16" -buffer_size = 8192 -channel_buffer_size = 30 -timeout = "5m" - -[transcription] -provider = "openai" -api_key = "test-key" -model = "whisper-1" - -[injection] -mode = "fallback" -wtype_timeout = "5s" -clipboard_timeout = "3s" - -[notifications] -enabled = true -type = "log"` + configContent := testConfigContent os.WriteFile(configPath, []byte(configContent), 0644) daemon, err := New() diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 3873c82..4a23369 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -21,10 +21,12 @@ func TestNew(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -59,10 +61,12 @@ func TestPipeline_Status(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -105,10 +109,12 @@ func TestPipeline_GetActionCh(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -149,10 +155,12 @@ func TestPipeline_GetErrorCh(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -193,10 +201,12 @@ func TestPipeline_Stop(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -230,10 +240,12 @@ func TestPipeline_Run(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -340,10 +352,12 @@ func TestPipeline_ConcurrentAccess(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, WtypeTimeout: 5 * time.Second, @@ -391,10 +405,12 @@ func TestPipeline_WithMocks(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"clipboard"}, ClipboardTimeout: 3 * time.Second, @@ -452,7 +468,6 @@ func TestPipeline_WithMocks_LLMProcessing(t *testing.T) { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-key", Language: "en", Model: "whisper-1", }, diff --git a/internal/provider/openai.go b/internal/provider/openai.go index 53aa3db..66c2fc6 100644 --- a/internal/provider/openai.go +++ b/internal/provider/openai.go @@ -48,13 +48,11 @@ func (p *OpenAIProvider) Models() []Model { Description: "High quality transcription with GPT-4o", Type: Transcription, SupportsBatch: true, - SupportsStreaming: true, + SupportsStreaming: false, Local: false, AdapterType: AdapterOpenAI, - StreamingAdapter: AdapterOpenAIRealtime, SupportedLanguages: allLangs, Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"}, - StreamingEndpoint: &EndpointConfig{BaseURL: "wss://api.openai.com", Path: "/v1/realtime"}, DocsURL: docsURL, }, { @@ -63,13 +61,24 @@ func (p *OpenAIProvider) Models() []Model { Description: "Fast transcription with GPT-4o Mini", Type: Transcription, SupportsBatch: true, - SupportsStreaming: true, + SupportsStreaming: false, Local: false, AdapterType: AdapterOpenAI, - StreamingAdapter: AdapterOpenAIRealtime, SupportedLanguages: allLangs, Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"}, - StreamingEndpoint: &EndpointConfig{BaseURL: "wss://api.openai.com", Path: "/v1/realtime"}, + DocsURL: docsURL, + }, + { + ID: "gpt-4o-realtime-preview", + Name: "GPT-4o Realtime Preview", + Description: "Real-time streaming transcription with GPT-4o", + Type: Transcription, + SupportsBatch: false, + SupportsStreaming: true, + Local: false, + AdapterType: AdapterOpenAIRealtime, + SupportedLanguages: allLangs, + Endpoint: &EndpointConfig{BaseURL: "wss://api.openai.com", Path: "/v1/realtime"}, DocsURL: docsURL, }, // LLM models diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index be5fd8f..6ddae43 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -177,9 +177,9 @@ func TestModelsOfType(t *testing.T) { trans := ModelsOfType(p, Transcription) llm := ModelsOfType(p, LLM) - // OpenAI has 3 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe - if len(trans) != 3 { - t.Errorf("ModelsOfType(Transcription) = %d, want 3", len(trans)) + // OpenAI has 4 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, gpt-4o-realtime-preview + if len(trans) != 4 { + t.Errorf("ModelsOfType(Transcription) = %d, want 4", len(trans)) } // OpenAI has 2 LLM models: gpt-4o-mini, gpt-4o if len(llm) != 2 { @@ -295,32 +295,32 @@ func TestValidateModelLanguage_ErrorFormat(t *testing.T) { } func TestOpenAIStreamingModels(t *testing.T) { - // gpt-4o-transcribe supports both batch and streaming - m, err := GetModel("openai", "gpt-4o-transcribe") + // gpt-4o-realtime-preview is streaming-only + m, err := GetModel("openai", "gpt-4o-realtime-preview") if err != nil { - t.Fatalf("GetModel('openai', 'gpt-4o-transcribe') error: %v", err) + t.Fatalf("GetModel('openai', 'gpt-4o-realtime-preview') error: %v", err) } - if !m.SupportsBatch { - t.Error("gpt-4o-transcribe should have SupportsBatch=true") + if m.SupportsBatch { + t.Error("gpt-4o-realtime-preview should have SupportsBatch=false") } if !m.SupportsStreaming { - t.Error("gpt-4o-transcribe should have SupportsStreaming=true") + t.Error("gpt-4o-realtime-preview should have SupportsStreaming=true") } - if !m.SupportsBothModes() { - t.Error("gpt-4o-transcribe should support both modes") + if m.SupportsBothModes() { + t.Error("gpt-4o-realtime-preview should not support both modes") } - if m.StreamingAdapter != "openai-realtime" { - t.Errorf("gpt-4o-transcribe StreamingAdapter=%q, want 'openai-realtime'", m.StreamingAdapter) + if m.AdapterType != "openai-realtime" { + t.Errorf("gpt-4o-realtime-preview AdapterType=%q, want 'openai-realtime'", m.AdapterType) } - if m.StreamingEndpoint == nil { - t.Fatal("gpt-4o-transcribe should have StreamingEndpoint set") + if m.Endpoint == nil { + t.Fatal("gpt-4o-realtime-preview should have Endpoint set") } - if m.StreamingEndpoint.BaseURL != "wss://api.openai.com" { - t.Errorf("gpt-4o-transcribe StreamingEndpoint.BaseURL=%q, want 'wss://api.openai.com'", m.StreamingEndpoint.BaseURL) + if m.Endpoint.BaseURL != "wss://api.openai.com" { + t.Errorf("gpt-4o-realtime-preview Endpoint.BaseURL=%q, want 'wss://api.openai.com'", m.Endpoint.BaseURL) } // default model should still be whisper-1 diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 8cbd52b..71dab1b 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -31,10 +31,12 @@ func TestConfig() *config.Config { }, Transcription: config.TranscriptionConfig{ Provider: "openai", - APIKey: "test-api-key", Language: "", Model: "whisper-1", }, + Providers: map[string]config.ProviderConfig{ + "openai": {APIKey: "test-api-key"}, + }, Injection: config.InjectionConfig{ Backends: []string{"ydotool", "wtype", "clipboard"}, YdotoolTimeout: 5 * time.Second, @@ -61,7 +63,6 @@ func TestConfigWithInvalidValues() *config.Config { }, Transcription: config.TranscriptionConfig{ Provider: "", // Invalid - APIKey: "", // Invalid Model: "", // Invalid }, Injection: config.InjectionConfig{ diff --git a/internal/transcriber/adapter_elevenlabs.go b/internal/transcriber/adapter_elevenlabs.go index 680c7b4..5abb4d3 100644 --- a/internal/transcriber/adapter_elevenlabs.go +++ b/internal/transcriber/adapter_elevenlabs.go @@ -45,18 +45,6 @@ func NewElevenLabsAdapter(endpoint *provider.EndpointConfig, apiKey, model, lang } } -// NewElevenLabsAdapterFromConfig creates an adapter using the legacy Config struct -// for backwards compatibility during migration -func NewElevenLabsAdapterFromConfig(config Config) *ElevenLabsAdapter { - return NewElevenLabsAdapter( - &provider.EndpointConfig{BaseURL: "https://api.elevenlabs.io", Path: "/v1/speech-to-text"}, - config.APIKey, - config.Model, - config.Language, - config.Keywords, - ) -} - // Transcribe sends audio to ElevenLabs API for transcription func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) { if len(audioData) == 0 { diff --git a/internal/transcriber/adapter_elevenlabs_test.go b/internal/transcriber/adapter_elevenlabs_test.go index cdbe914..fc16de5 100644 --- a/internal/transcriber/adapter_elevenlabs_test.go +++ b/internal/transcriber/adapter_elevenlabs_test.go @@ -40,34 +40,6 @@ func TestNewElevenLabsAdapter(t *testing.T) { } } -func TestNewElevenLabsAdapterFromConfig(t *testing.T) { - config := Config{ - Provider: "elevenlabs", - APIKey: "test-api-key", - Language: "en", - Model: "scribe_v1", - } - - adapter := NewElevenLabsAdapterFromConfig(config) - - if adapter == nil { - t.Fatalf("NewElevenLabsAdapterFromConfig() returned nil") - } - - if adapter.apiKey != "test-api-key" { - t.Errorf("APIKey not set correctly, got: %s", adapter.apiKey) - } - - if adapter.model != "scribe_v1" { - t.Errorf("Model not set correctly, got: %s", adapter.model) - } - - // should use default endpoint - if adapter.endpoint.BaseURL != "https://api.elevenlabs.io" { - t.Errorf("Default endpoint BaseURL not set correctly, got: %s", adapter.endpoint.BaseURL) - } -} - func TestElevenLabsAdapter_Transcribe_EmptyAudio(t *testing.T) { endpoint := &provider.EndpointConfig{ BaseURL: "https://api.elevenlabs.io", diff --git a/internal/transcriber/adapter_openai.go b/internal/transcriber/adapter_openai.go index 6e76074..47e8460 100644 --- a/internal/transcriber/adapter_openai.go +++ b/internal/transcriber/adapter_openai.go @@ -51,12 +51,6 @@ func NewOpenAIAdapter(endpoint *provider.EndpointConfig, apiKey, model, lang str } } -// NewOpenAIAdapterFromConfig creates an adapter using the legacy Config struct -// This is for backwards compatibility during migration -func NewOpenAIAdapterFromConfig(config Config) *OpenAIAdapter { - return NewOpenAIAdapter(nil, config.APIKey, config.Model, config.Language, config.Keywords, "openai") -} - func (a *OpenAIAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) { if len(audioData) == 0 { return "", nil diff --git a/internal/transcriber/transcriber_test.go b/internal/transcriber/transcriber_test.go index edcc43f..19f5330 100644 --- a/internal/transcriber/transcriber_test.go +++ b/internal/transcriber/transcriber_test.go @@ -162,7 +162,7 @@ func TestNewTranscriber(t *testing.T) { Provider: "openai", APIKey: "test-key", Language: "en", - Model: "gpt-4o-transcribe", + Model: "gpt-4o-realtime-preview", Streaming: true, }, wantErr: false, @@ -635,34 +635,6 @@ func TestOpenAIAdapter_Creation(t *testing.T) { } } -func TestOpenAIAdapterFromConfig(t *testing.T) { - config := Config{ - Provider: "openai", - APIKey: "sk-test-key", - Model: "whisper-1", - Language: "en", - Keywords: []string{"test"}, - } - - adapter := NewOpenAIAdapterFromConfig(config) - if adapter == nil { - t.Errorf("NewOpenAIAdapterFromConfig() returned nil") - return - } - - if adapter.model != config.Model { - t.Errorf("model = %q, want %q", adapter.model, config.Model) - } - - if adapter.language != config.Language { - t.Errorf("language = %q, want %q", adapter.language, config.Language) - } - - if adapter.providerName != "openai" { - t.Errorf("providerName = %q, want %q", adapter.providerName, "openai") - } -} - // MockStreamingAdapter implements StreamingAdapter for testing type MockStreamingAdapter struct { StartFunc func(ctx context.Context, language string) error diff --git a/internal/tui/configure_transcription_test.go b/internal/tui/configure_transcription_test.go index 12307c3..f23e791 100644 --- a/internal/tui/configure_transcription_test.go +++ b/internal/tui/configure_transcription_test.go @@ -52,16 +52,21 @@ func TestGetTranscriptionModelOptions_NoHeadersAnymore(t *testing.T) { func TestGetTranscriptionModelOptions_OpenAI_ShowsCapabilities(t *testing.T) { options := getTranscriptionModelOptions("openai") - // OpenAI has 3 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe - if len(options) != 3 { - t.Errorf("expected 3 options for openai, got %d", len(options)) + // OpenAI has 4 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, gpt-4o-realtime-preview + if len(options) != 4 { + t.Errorf("expected 4 options for openai, got %d", len(options)) } - // gpt-4o-transcribe and gpt-4o-mini-transcribe should mention batch+streaming + // gpt-4o-realtime-preview should mention streaming for _, opt := range options { - if strings.Contains(opt.ID, "gpt-4o") { - if !strings.Contains(opt.Desc, "batch+streaming") { - t.Errorf("gpt-4o model %s should mention batch+streaming: %s", opt.ID, opt.Desc) + switch opt.ID { + case "gpt-4o-realtime-preview": + if !strings.Contains(opt.Desc, "streaming") { + t.Errorf("gpt-4o-realtime-preview should mention streaming: %s", opt.Desc) + } + case "gpt-4o-transcribe", "gpt-4o-mini-transcribe": + if strings.Contains(opt.Desc, "streaming") { + t.Errorf("batch-only model %s should not mention streaming: %s", opt.ID, opt.Desc) } } } diff --git a/internal/tui/flows.go b/internal/tui/flows.go index 46e6e0f..8c6105d 100644 --- a/internal/tui/flows.go +++ b/internal/tui/flows.go @@ -25,11 +25,14 @@ const ( menuAdvanced = "advanced" menuSave = "save" menuDiscard = "discard" + repoURL = "https://github.com/leonardotrapani/hyprvoice" ) func newWelcomeScreen(state *wizardState) screen { desc := append([]string{}, LogoLines()...) desc = append(desc, "", "Voice-powered typing for Wayland/Hyprland.", "Let's set up your configuration.") + desc = append(desc, "Consider starring the project on GitHub ⭐") + desc = append(desc, repoURL) s := newInfoScreen(state, "Hyprvoice Onboarding", desc, func() screen { return onboardingVoiceProviderScreen(state) }, func() screen { @@ -103,7 +106,7 @@ func newMenuScreen(state *wizardState) screen { state.result = &ConfigureResult{Cancelled: true} return nil }) - screen.footer = "enter select • esc cancel • / filter" + screen.footer = fmt.Sprintf("enter select • esc cancel • / filter\nconsider starring the project on github ⭐\n%s", repoURL) return screen } diff --git a/internal/tui/screens.go b/internal/tui/screens.go index 5893cb9..ead68bc 100644 --- a/internal/tui/screens.go +++ b/internal/tui/screens.go @@ -48,7 +48,8 @@ func (s *listScreen) Init() tea.Cmd { func (s *listScreen) Update(msg tea.Msg) (screen, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - s.list.SetSize(msg.Width-4, msg.Height-8) + extraFooterLines := strings.Count(s.footer, "\n") + s.list.SetSize(msg.Width-4, msg.Height-8-extraFooterLines) case tea.KeyMsg: switch msg.String() { case "enter":