feat: remove legacy configuration
This commit is contained in:
+130
-203
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+21
-85
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+32
-147
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user