345 lines
16 KiB
Go
345 lines
16 KiB
Go
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 onboarding or 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")
|
|
}
|
|
|
|
// 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 {
|
|
return err
|
|
}
|
|
|
|
file, err := os.Create(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create config file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
configContent := `# Hyprvoice Configuration
|
|
# This file is automatically generated with defaults.
|
|
# Edit values as needed - changes are applied immediately without daemon restart.
|
|
# Keywords help both transcription and LLM understand domain-specific terms
|
|
# Add names, technical terms, or brand names that might be misheard
|
|
keywords = []
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Provider API Keys
|
|
# Configure API keys for each provider you want to use.
|
|
# Keys can also be set via environment variables: OPENAI_API_KEY, GROQ_API_KEY, etc.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
[providers.openai]
|
|
api_key = "" # OpenAI API key (or set OPENAI_API_KEY env var)
|
|
|
|
[providers.groq]
|
|
api_key = "" # Groq API key (or set GROQ_API_KEY env var)
|
|
|
|
# Uncomment to configure additional providers:
|
|
# [providers.mistral]
|
|
# api_key = "" # Mistral API key (or set MISTRAL_API_KEY env var)
|
|
# [providers.elevenlabs]
|
|
# api_key = "" # ElevenLabs API key (or set ELEVENLABS_API_KEY env var)
|
|
# [providers.deepgram]
|
|
# api_key = "" # Deepgram API key (or set DEEPGRAM_API_KEY env var)
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Audio Recording
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
[recording]
|
|
sample_rate = 16000 # Audio sample rate in Hz (16000 recommended for speech)
|
|
channels = 1 # Number of audio channels (1 = mono, 2 = stereo)
|
|
format = "s16" # Audio format (s16 = 16-bit signed integers)
|
|
buffer_size = 8192 # Internal buffer size in bytes (larger = less CPU, more latency)
|
|
device = "" # PipeWire audio device (empty = use default microphone)
|
|
channel_buffer_size = 30 # Audio frame buffer size (frames to buffer)
|
|
timeout = "5m" # Maximum recording duration (e.g., "30s", "2m", "5m")
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Speech Transcription
|
|
# Converts audio to text using speech-to-text APIs
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
[transcription]
|
|
provider = "openai" # "openai", "groq-transcription", "mistral-transcription", "elevenlabs", "whisper-cpp"
|
|
model = "whisper-1" # Model: OpenAI="whisper-1", Groq="whisper-large-v3", Mistral="voxtral-mini-latest", ElevenLabs="scribe_v1"
|
|
language = "" # ISO 639-1 code (e.g., en, es, de). Empty for auto-detect.
|
|
threads = 0 # CPU threads for local transcription (0 = auto: uses NumCPU-1)
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# LLM Post-Processing (Recommended)
|
|
# Cleans up transcribed text: removes stutters, adds punctuation, fixes grammar
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
[llm]
|
|
enabled = true # Enable LLM post-processing (highly recommended)
|
|
provider = "openai" # "openai" or "groq" (must have API key configured above)
|
|
model = "gpt-4o-mini" # OpenAI: "gpt-4o-mini", Groq: "llama-3.3-70b-versatile"
|
|
|
|
[llm.post_processing]
|
|
remove_stutters = true # Remove "um", "uh", repeated words
|
|
add_punctuation = true # Add proper punctuation
|
|
fix_grammar = true # Fix grammatical errors
|
|
remove_filler_words = true # Remove "like", "you know", "basically"
|
|
|
|
[llm.custom_prompt]
|
|
enabled = false # Enable custom instructions for LLM
|
|
prompt = "" # Additional instructions (e.g., "Format as bullet points")
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Text Injection
|
|
# How transcribed text is inserted into applications
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
[injection]
|
|
backends = ["ydotool", "wtype", "clipboard"] # Ordered fallback chain (tries each until one succeeds)
|
|
ydotool_timeout = "5s" # Timeout for ydotool commands
|
|
wtype_timeout = "5s" # Timeout for wtype commands
|
|
clipboard_timeout = "3s" # Timeout for clipboard operations
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Desktop Notifications
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
[notifications]
|
|
enabled = true # Enable desktop notifications
|
|
type = "desktop" # "desktop", "log", or "none"
|
|
|
|
# Custom notification messages (optional - defaults shown below)
|
|
# Uncomment and modify to customize notification text
|
|
# [notifications.messages]
|
|
# [notifications.messages.recording_started]
|
|
# title = "Hyprvoice"
|
|
# body = "Recording Started"
|
|
# [notifications.messages.transcribing]
|
|
# title = "Hyprvoice"
|
|
# body = "Recording Ended... Transcribing"
|
|
# [notifications.messages.llm_processing]
|
|
# title = "Hyprvoice"
|
|
# body = "Processing..."
|
|
# [notifications.messages.config_reloaded]
|
|
# title = "Hyprvoice"
|
|
# body = "Config Reloaded"
|
|
# [notifications.messages.operation_cancelled]
|
|
# title = "Hyprvoice"
|
|
# body = "Operation Cancelled"
|
|
# [notifications.messages.recording_aborted]
|
|
# body = "Recording Aborted"
|
|
# [notifications.messages.injection_aborted]
|
|
# body = "Injection Aborted"
|
|
#
|
|
# Emoji-only example (for minimal pill-style notifications):
|
|
# [notifications.messages.recording_started]
|
|
# title = ""
|
|
# body = "..."
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Reference: Provider Details
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
#
|
|
# Transcription providers:
|
|
# - "openai": OpenAI Whisper API (cloud-based, excellent accuracy)
|
|
# - "groq-transcription": Groq Whisper API (very fast, models: whisper-large-v3, whisper-large-v3-turbo)
|
|
# - "mistral-transcription": Mistral Voxtral API (excellent for European languages, model: voxtral-mini-latest)
|
|
# - "elevenlabs": ElevenLabs Scribe API (99 languages, models: scribe_v1, scribe_v2)
|
|
#
|
|
# LLM providers (for post-processing):
|
|
# - "openai": GPT models (gpt-4o-mini recommended for cost/quality balance)
|
|
# - "groq": Fast inference (llama-3.3-70b-versatile recommended)
|
|
#
|
|
# Injection backends:
|
|
# - "ydotool": Uses ydotool (requires ydotoold daemon). Best for Chromium/Electron apps.
|
|
# - "wtype": Uses wtype for Wayland. May have issues with some Chromium apps.
|
|
# - "clipboard": Copies to clipboard only (most reliable, requires manual paste).
|
|
#
|
|
# Language codes: "" (auto-detect), "en", "it", "es", "fr", "de", "pt", etc.
|
|
# Language is configured per transcription model - only supported languages are shown during setup.
|
|
`
|
|
|
|
if _, err := file.WriteString(configContent); err != nil {
|
|
return fmt.Errorf("failed to write config content: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|