feat: refactor
This commit is contained in:
@@ -689,22 +689,22 @@ func TestValidateModelLanguageCompatibility(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "auto language always passes",
|
||||
provider: "groq",
|
||||
model: "distil-whisper-large-v3-en",
|
||||
provider: "whisper-cpp",
|
||||
model: "base.en",
|
||||
langCode: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "english model supports english",
|
||||
provider: "groq",
|
||||
model: "distil-whisper-large-v3-en",
|
||||
provider: "whisper-cpp",
|
||||
model: "base.en",
|
||||
langCode: "en",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "english model rejects spanish",
|
||||
provider: "groq",
|
||||
model: "distil-whisper-large-v3-en",
|
||||
provider: "whisper-cpp",
|
||||
model: "base.en",
|
||||
langCode: "es",
|
||||
wantErr: true,
|
||||
errContains: "does not support Spanish (es)",
|
||||
@@ -2079,9 +2079,8 @@ func TestConfig_Validate_GeneralLanguage(t *testing.T) {
|
||||
t.Run("general.language validated against model", func(t *testing.T) {
|
||||
config := baseConfig()
|
||||
config.General.Language = "es"
|
||||
config.Transcription.Provider = "groq-transcription"
|
||||
config.Transcription.Model = "distil-whisper-large-v3-en" // english-only model
|
||||
config.Transcription.APIKey = "gsk-test-key"
|
||||
config.Transcription.Provider = "whisper-cpp"
|
||||
config.Transcription.Model = "base.en" // english-only model
|
||||
|
||||
err := config.Validate()
|
||||
if err == nil {
|
||||
@@ -2096,9 +2095,8 @@ func TestConfig_Validate_GeneralLanguage(t *testing.T) {
|
||||
config := baseConfig()
|
||||
config.General.Language = "en" // compatible
|
||||
config.Transcription.Language = "es" // override with incompatible
|
||||
config.Transcription.Provider = "groq-transcription"
|
||||
config.Transcription.Model = "distil-whisper-large-v3-en" // english-only model
|
||||
config.Transcription.APIKey = "gsk-test-key"
|
||||
config.Transcription.Provider = "whisper-cpp"
|
||||
config.Transcription.Model = "base.en" // english-only model
|
||||
|
||||
err := config.Validate()
|
||||
if err == nil {
|
||||
@@ -2113,9 +2111,8 @@ func TestConfig_Validate_GeneralLanguage(t *testing.T) {
|
||||
config := baseConfig()
|
||||
config.General.Language = "es" // would be incompatible
|
||||
config.Transcription.Language = "en" // override with compatible
|
||||
config.Transcription.Provider = "groq-transcription"
|
||||
config.Transcription.Model = "distil-whisper-large-v3-en" // english-only model
|
||||
config.Transcription.APIKey = "gsk-test-key"
|
||||
config.Transcription.Provider = "whisper-cpp"
|
||||
config.Transcription.Model = "base.en" // english-only model
|
||||
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
@@ -2126,9 +2123,8 @@ func TestConfig_Validate_GeneralLanguage(t *testing.T) {
|
||||
t.Run("auto language always passes", func(t *testing.T) {
|
||||
config := baseConfig()
|
||||
config.General.Language = "" // auto
|
||||
config.Transcription.Provider = "groq-transcription"
|
||||
config.Transcription.Model = "distil-whisper-large-v3-en" // english-only model
|
||||
config.Transcription.APIKey = "gsk-test-key"
|
||||
config.Transcription.Provider = "whisper-cpp"
|
||||
config.Transcription.Model = "base.en" // english-only model
|
||||
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
|
||||
+14
-35
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/leonardotrapani/hyprvoice/internal/injection"
|
||||
"github.com/leonardotrapani/hyprvoice/internal/provider"
|
||||
"github.com/leonardotrapani/hyprvoice/internal/recording"
|
||||
"github.com/leonardotrapani/hyprvoice/internal/transcriber"
|
||||
)
|
||||
@@ -22,11 +23,12 @@ func (c *Config) ToRecordingConfig() recording.Config {
|
||||
|
||||
func (c *Config) ToTranscriberConfig() transcriber.Config {
|
||||
config := transcriber.Config{
|
||||
Provider: c.Transcription.Provider,
|
||||
Language: c.resolveEffectiveLanguage(),
|
||||
Model: c.Transcription.Model,
|
||||
Keywords: c.Keywords,
|
||||
Threads: c.Transcription.Threads,
|
||||
Provider: c.Transcription.Provider,
|
||||
Language: c.resolveEffectiveLanguage(),
|
||||
Model: c.Transcription.Model,
|
||||
Keywords: c.Keywords,
|
||||
Threads: c.Transcription.Threads,
|
||||
Streaming: c.Transcription.Streaming,
|
||||
}
|
||||
|
||||
config.APIKey = c.resolveAPIKeyForProvider(c.Transcription.Provider)
|
||||
@@ -44,29 +46,12 @@ func (c *Config) resolveEffectiveLanguage() string {
|
||||
}
|
||||
|
||||
// resolveAPIKeyForProvider returns the API key for a provider from multiple sources
|
||||
func (c *Config) resolveAPIKeyForProvider(provider string) string {
|
||||
providerName := provider
|
||||
envVar := ""
|
||||
switch provider {
|
||||
case "openai":
|
||||
providerName = "openai"
|
||||
envVar = "OPENAI_API_KEY"
|
||||
case "groq-transcription", "groq-translation":
|
||||
providerName = "groq"
|
||||
envVar = "GROQ_API_KEY"
|
||||
case "mistral-transcription":
|
||||
providerName = "mistral"
|
||||
envVar = "MISTRAL_API_KEY"
|
||||
case "elevenlabs":
|
||||
providerName = "elevenlabs"
|
||||
envVar = "ELEVENLABS_API_KEY"
|
||||
case "deepgram":
|
||||
providerName = "deepgram"
|
||||
envVar = "DEEPGRAM_API_KEY"
|
||||
}
|
||||
func (c *Config) resolveAPIKeyForProvider(providerName string) string {
|
||||
baseName := provider.BaseProviderName(providerName)
|
||||
envVar := provider.EnvVarForProvider(providerName)
|
||||
|
||||
if c.Providers != nil {
|
||||
if pc, ok := c.Providers[providerName]; ok && pc.APIKey != "" {
|
||||
if pc, ok := c.Providers[baseName]; ok && pc.APIKey != "" {
|
||||
return pc.APIKey
|
||||
}
|
||||
}
|
||||
@@ -106,17 +91,11 @@ func (c *Config) ToLLMConfig() LLMAdapterConfig {
|
||||
}
|
||||
|
||||
// resolveAPIKeyForLLMProvider returns the API key for an LLM provider
|
||||
func (c *Config) resolveAPIKeyForLLMProvider(provider string) string {
|
||||
envVar := ""
|
||||
switch provider {
|
||||
case "openai":
|
||||
envVar = "OPENAI_API_KEY"
|
||||
case "groq":
|
||||
envVar = "GROQ_API_KEY"
|
||||
}
|
||||
func (c *Config) resolveAPIKeyForLLMProvider(providerName string) string {
|
||||
envVar := provider.EnvVarForProvider(providerName)
|
||||
|
||||
if c.Providers != nil {
|
||||
if pc, ok := c.Providers[provider]; ok && pc.APIKey != "" {
|
||||
if pc, ok := c.Providers[providerName]; ok && pc.APIKey != "" {
|
||||
return pc.APIKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,193 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Save writes the config to the config file with formatted TOML output
|
||||
func Save(cfg *Config) error {
|
||||
configPath, err := GetConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// Header
|
||||
sb.WriteString(`# Hyprvoice Configuration
|
||||
# Generated by hyprvoice configure
|
||||
# Changes are applied immediately without daemon restart.
|
||||
|
||||
`)
|
||||
|
||||
// Keywords (must be before any table definitions in TOML)
|
||||
if len(cfg.Keywords) > 0 {
|
||||
sb.WriteString("# Keywords help transcription and LLM spell names/terms correctly\n")
|
||||
sb.WriteString("keywords = [")
|
||||
for i, kw := range cfg.Keywords {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%q", kw))
|
||||
}
|
||||
sb.WriteString("]\n\n")
|
||||
}
|
||||
|
||||
// General section
|
||||
sb.WriteString(`# General Settings
|
||||
[general]
|
||||
`)
|
||||
sb.WriteString(fmt.Sprintf(" language = %q\n", cfg.General.Language))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Providers section
|
||||
if len(cfg.Providers) > 0 {
|
||||
sb.WriteString("# API Keys for providers\n")
|
||||
for name, pc := range cfg.Providers {
|
||||
sb.WriteString(fmt.Sprintf("[providers.%s]\n", name))
|
||||
sb.WriteString(fmt.Sprintf(" api_key = %q\n", pc.APIKey))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Recording
|
||||
sb.WriteString(`# Audio Recording Configuration
|
||||
[recording]
|
||||
`)
|
||||
sb.WriteString(fmt.Sprintf(" sample_rate = %d\n", cfg.Recording.SampleRate))
|
||||
sb.WriteString(fmt.Sprintf(" channels = %d\n", cfg.Recording.Channels))
|
||||
sb.WriteString(fmt.Sprintf(" format = %q\n", cfg.Recording.Format))
|
||||
sb.WriteString(fmt.Sprintf(" buffer_size = %d\n", cfg.Recording.BufferSize))
|
||||
sb.WriteString(fmt.Sprintf(" device = %q\n", cfg.Recording.Device))
|
||||
sb.WriteString(fmt.Sprintf(" channel_buffer_size = %d\n", cfg.Recording.ChannelBufferSize))
|
||||
sb.WriteString(fmt.Sprintf(" timeout = %q\n", cfg.Recording.Timeout.String()))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Transcription
|
||||
sb.WriteString(`# Speech Transcription Configuration
|
||||
[transcription]
|
||||
`)
|
||||
sb.WriteString(fmt.Sprintf(" provider = %q\n", cfg.Transcription.Provider))
|
||||
sb.WriteString(fmt.Sprintf(" language = %q\n", cfg.Transcription.Language))
|
||||
sb.WriteString(fmt.Sprintf(" model = %q\n", cfg.Transcription.Model))
|
||||
sb.WriteString(fmt.Sprintf(" streaming = %v\n", cfg.Transcription.Streaming))
|
||||
sb.WriteString(fmt.Sprintf(" threads = %d\n", cfg.Transcription.Threads))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// LLM
|
||||
sb.WriteString(`# LLM Post-Processing Configuration
|
||||
[llm]
|
||||
`)
|
||||
sb.WriteString(fmt.Sprintf(" enabled = %v\n", cfg.LLM.Enabled))
|
||||
if cfg.LLM.Provider != "" {
|
||||
sb.WriteString(fmt.Sprintf(" provider = %q\n", cfg.LLM.Provider))
|
||||
}
|
||||
if cfg.LLM.Model != "" {
|
||||
sb.WriteString(fmt.Sprintf(" model = %q\n", cfg.LLM.Model))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString(" [llm.post_processing]\n")
|
||||
sb.WriteString(fmt.Sprintf(" remove_stutters = %v\n", cfg.LLM.PostProcessing.RemoveStutters))
|
||||
sb.WriteString(fmt.Sprintf(" add_punctuation = %v\n", cfg.LLM.PostProcessing.AddPunctuation))
|
||||
sb.WriteString(fmt.Sprintf(" fix_grammar = %v\n", cfg.LLM.PostProcessing.FixGrammar))
|
||||
sb.WriteString(fmt.Sprintf(" remove_filler_words = %v\n", cfg.LLM.PostProcessing.RemoveFillerWords))
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString(" [llm.custom_prompt]\n")
|
||||
sb.WriteString(fmt.Sprintf(" enabled = %v\n", cfg.LLM.CustomPrompt.Enabled))
|
||||
if cfg.LLM.CustomPrompt.Prompt != "" {
|
||||
sb.WriteString(fmt.Sprintf(" prompt = %q\n", cfg.LLM.CustomPrompt.Prompt))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Injection
|
||||
sb.WriteString(`# Text Injection Configuration
|
||||
[injection]
|
||||
`)
|
||||
sb.WriteString(" backends = [")
|
||||
for i, b := range cfg.Injection.Backends {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%q", b))
|
||||
}
|
||||
sb.WriteString("]\n")
|
||||
sb.WriteString(fmt.Sprintf(" ydotool_timeout = %q\n", cfg.Injection.YdotoolTimeout.String()))
|
||||
sb.WriteString(fmt.Sprintf(" wtype_timeout = %q\n", cfg.Injection.WtypeTimeout.String()))
|
||||
sb.WriteString(fmt.Sprintf(" clipboard_timeout = %q\n", cfg.Injection.ClipboardTimeout.String()))
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Notifications
|
||||
sb.WriteString(`# Desktop Notification Configuration
|
||||
[notifications]
|
||||
`)
|
||||
sb.WriteString(fmt.Sprintf(" enabled = %v\n", cfg.Notifications.Enabled))
|
||||
sb.WriteString(fmt.Sprintf(" type = %q\n", cfg.Notifications.Type))
|
||||
|
||||
// Write custom messages if any
|
||||
msgs := cfg.Notifications.Messages
|
||||
if hasCustomMessages(msgs) {
|
||||
sb.WriteString("\n [notifications.messages]\n")
|
||||
if msgs.RecordingStarted.Title != "" || msgs.RecordingStarted.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.recording_started]\n")
|
||||
sb.WriteString(fmt.Sprintf(" title = %q\n", msgs.RecordingStarted.Title))
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.RecordingStarted.Body))
|
||||
}
|
||||
if msgs.Transcribing.Title != "" || msgs.Transcribing.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.transcribing]\n")
|
||||
sb.WriteString(fmt.Sprintf(" title = %q\n", msgs.Transcribing.Title))
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.Transcribing.Body))
|
||||
}
|
||||
if msgs.LLMProcessing.Title != "" || msgs.LLMProcessing.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.llm_processing]\n")
|
||||
sb.WriteString(fmt.Sprintf(" title = %q\n", msgs.LLMProcessing.Title))
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.LLMProcessing.Body))
|
||||
}
|
||||
if msgs.ConfigReloaded.Title != "" || msgs.ConfigReloaded.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.config_reloaded]\n")
|
||||
sb.WriteString(fmt.Sprintf(" title = %q\n", msgs.ConfigReloaded.Title))
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.ConfigReloaded.Body))
|
||||
}
|
||||
if msgs.OperationCancelled.Title != "" || msgs.OperationCancelled.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.operation_cancelled]\n")
|
||||
sb.WriteString(fmt.Sprintf(" title = %q\n", msgs.OperationCancelled.Title))
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.OperationCancelled.Body))
|
||||
}
|
||||
if msgs.RecordingAborted.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.recording_aborted]\n")
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.RecordingAborted.Body))
|
||||
}
|
||||
if msgs.InjectionAborted.Body != "" {
|
||||
sb.WriteString(" [notifications.messages.injection_aborted]\n")
|
||||
sb.WriteString(fmt.Sprintf(" body = %q\n", msgs.InjectionAborted.Body))
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := file.WriteString(sb.String()); err != nil {
|
||||
return fmt.Errorf("failed to write config content: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasCustomMessages(msgs MessagesConfig) bool {
|
||||
return msgs.RecordingStarted.Title != "" || msgs.RecordingStarted.Body != "" ||
|
||||
msgs.Transcribing.Title != "" || msgs.Transcribing.Body != "" ||
|
||||
msgs.LLMProcessing.Title != "" || msgs.LLMProcessing.Body != "" ||
|
||||
msgs.ConfigReloaded.Title != "" || msgs.ConfigReloaded.Body != "" ||
|
||||
msgs.OperationCancelled.Title != "" || msgs.OperationCancelled.Body != "" ||
|
||||
msgs.RecordingAborted.Body != "" ||
|
||||
msgs.InjectionAborted.Body != ""
|
||||
}
|
||||
|
||||
// SaveDefaultConfig writes the default config template to the config file
|
||||
func SaveDefaultConfig() error {
|
||||
configPath, err := GetConfigPath()
|
||||
if err != nil {
|
||||
|
||||
@@ -62,11 +62,12 @@ type RecordingConfig struct {
|
||||
}
|
||||
|
||||
type TranscriptionConfig struct {
|
||||
Provider string `toml:"provider"`
|
||||
APIKey string `toml:"api_key"`
|
||||
Language string `toml:"language"`
|
||||
Model string `toml:"model"`
|
||||
Threads int `toml:"threads"` // CPU threads for local transcription (0 = auto: NumCPU-1)
|
||||
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
|
||||
Threads int `toml:"threads"` // CPU threads for local transcription (0 = auto: NumCPU-1)
|
||||
}
|
||||
|
||||
type InjectionConfig struct {
|
||||
|
||||
Reference in New Issue
Block a user