Merge pull request #2 from nasedkinpv/main
feat: add Groq Whisper API support for transcription and translation
This commit is contained in:
@@ -32,3 +32,4 @@ go.work.sum
|
|||||||
hyprvoice
|
hyprvoice
|
||||||
hyprvoice-*
|
hyprvoice-*
|
||||||
tmp/*
|
tmp/*
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
|||||||
|
|
||||||
- **Wayland desktop** (Hyprland, Niri, GNOME, KDE, etc.)
|
- **Wayland desktop** (Hyprland, Niri, GNOME, KDE, etc.)
|
||||||
- **PipeWire audio system** with tools
|
- **PipeWire audio system** with tools
|
||||||
- **OpenAI API key** (for transcription)
|
- **API key for transcription**: OpenAI API key or Groq API key (Groq offers faster processing and free tier)
|
||||||
|
|
||||||
**System packages** (automatically installed with AUR package):
|
**System packages** (automatically installed with AUR package):
|
||||||
|
|
||||||
@@ -216,6 +216,59 @@ Configuration is stored in `~/.config/hyprvoice/config.toml` and can also be edi
|
|||||||
|
|
||||||
Hyprvoice supports multiple transcription backends:
|
Hyprvoice supports multiple transcription backends:
|
||||||
|
|
||||||
|
#### OpenAI Whisper API
|
||||||
|
|
||||||
|
Cloud-based transcription using OpenAI's Whisper API:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "sk-..." # Or set OPENAI_API_KEY environment variable
|
||||||
|
language = "" # Empty for auto-detect, or "en", "es", "fr", etc.
|
||||||
|
model = "whisper-1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- High-quality transcription
|
||||||
|
- Supports 50+ languages
|
||||||
|
- Auto-detection or specify language for better accuracy
|
||||||
|
|
||||||
|
#### Groq Whisper API (Transcription)
|
||||||
|
|
||||||
|
Fast cloud-based transcription using Groq's Whisper API:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transcription]
|
||||||
|
provider = "groq-transcription"
|
||||||
|
api_key = "gsk_..." # Or set GROQ_API_KEY environment variable
|
||||||
|
language = "" # Empty for auto-detect, or "en", "es", "fr", etc.
|
||||||
|
model = "whisper-large-v3" # Or "whisper-large-v3-turbo" for faster processing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Ultra-fast transcription (significantly faster than OpenAI)
|
||||||
|
- Same Whisper model quality
|
||||||
|
- Supports 50+ languages
|
||||||
|
- Free tier available with generous limits
|
||||||
|
|
||||||
|
#### Groq Translation API
|
||||||
|
|
||||||
|
Fast translation of audio to English using Groq's Whisper API:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transcription]
|
||||||
|
provider = "groq-translation"
|
||||||
|
api_key = "gsk_..." # Or set GROQ_API_KEY environment variable
|
||||||
|
language = "es" # Optional: hint source language for better accuracy
|
||||||
|
model = "whisper-large-v3-turbo"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Translates any language audio → English text
|
||||||
|
- Ultra-fast processing
|
||||||
|
- Language field hints at source language (improves accuracy)
|
||||||
|
- Always outputs English regardless of input language
|
||||||
|
|
||||||
#### Generated Configuration Example
|
#### Generated Configuration Example
|
||||||
|
|
||||||
The daemon automatically creates `~/.config/hyprvoice/config.toml` with helpful comments:
|
The daemon automatically creates `~/.config/hyprvoice/config.toml` with helpful comments:
|
||||||
@@ -237,10 +290,10 @@ The daemon automatically creates `~/.config/hyprvoice/config.toml` with helpful
|
|||||||
|
|
||||||
# Speech Transcription Configuration
|
# Speech Transcription Configuration
|
||||||
[transcription]
|
[transcription]
|
||||||
provider = "openai" # Transcription service ("openai" only currently supported)
|
provider = "openai" # Transcription service: "openai", "groq-transcription", or "groq-translation"
|
||||||
api_key = "" # OpenAI API key (or set OPENAI_API_KEY environment variable)
|
api_key = "" # API key (or set OPENAI_API_KEY/GROQ_API_KEY environment variable)
|
||||||
language = "" # Language code (empty for auto-detect, "en", "it", "es", "fr", etc.)
|
language = "" # Language code (empty for auto-detect, "en", "it", "es", "fr", etc.)
|
||||||
model = "whisper-1" # OpenAI model name ("whisper-1" recommended)
|
model = "whisper-1" # Model: OpenAI="whisper-1", Groq="whisper-large-v3" or "whisper-large-v3-turbo"
|
||||||
|
|
||||||
# Text Injection Configuration
|
# Text Injection Configuration
|
||||||
[injection]
|
[injection]
|
||||||
@@ -373,6 +426,7 @@ journalctl --user -u hyprvoice.service -f
|
|||||||
| Audio capture | ✅ | Efficient PipeWire integration |
|
| Audio capture | ✅ | Efficient PipeWire integration |
|
||||||
| Desktop notifications | ✅ | Status feedback via notify-send |
|
| Desktop notifications | ✅ | Status feedback via notify-send |
|
||||||
| OpenAI transcription | ✅ | HTTP API integration |
|
| OpenAI transcription | ✅ | HTTP API integration |
|
||||||
|
| Groq transcription | ✅ | Fast Whisper API with transcription and translation |
|
||||||
| Text injection | ✅ | Clipboard + wtype with fallback |
|
| Text injection | ✅ | Clipboard + wtype with fallback |
|
||||||
| Configuration system | ✅ | TOML-based user settings with hot-reload |
|
| Configuration system | ✅ | TOML-based user settings with hot-reload |
|
||||||
| Interactive setup | ✅ | `hyprvoice configure` wizard for easy setup |
|
| Interactive setup | ✅ | `hyprvoice configure` wizard for easy setup |
|
||||||
|
|||||||
+94
-7
@@ -131,7 +131,8 @@ func configureCmd() *cobra.Command {
|
|||||||
Short: "Interactive configuration setup",
|
Short: "Interactive configuration setup",
|
||||||
Long: `Interactive configuration wizard for hyprvoice.
|
Long: `Interactive configuration wizard for hyprvoice.
|
||||||
This will guide you through setting up:
|
This will guide you through setting up:
|
||||||
- OpenAI API key for transcription
|
- Transcription provider (OpenAI or Groq)
|
||||||
|
- API keys and model selection
|
||||||
- Audio and text injection preferences
|
- Audio and text injection preferences
|
||||||
- Notification settings`,
|
- Notification settings`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
@@ -157,8 +158,81 @@ func runInteractiveConfig() error {
|
|||||||
fmt.Println("📝 Transcription Configuration")
|
fmt.Println("📝 Transcription Configuration")
|
||||||
fmt.Println("------------------------------")
|
fmt.Println("------------------------------")
|
||||||
|
|
||||||
// OpenAI API Key
|
// Provider selection
|
||||||
fmt.Printf("OpenAI API Key (current: %s, leave empty to use OPENAI_API_KEY env var): ", maskAPIKey(cfg.Transcription.APIKey))
|
fmt.Println("Select transcription provider:")
|
||||||
|
fmt.Println(" 1. openai - OpenAI Whisper API (cloud-based)")
|
||||||
|
fmt.Println(" 2. groq-transcription - Groq Whisper API (fast transcription)")
|
||||||
|
fmt.Println(" 3. groq-translation - Groq Whisper API (translate to English)")
|
||||||
|
fmt.Printf("Provider [1-3] (current: %s): ", cfg.Transcription.Provider)
|
||||||
|
if scanner.Scan() {
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
cfg.Transcription.Provider = "openai"
|
||||||
|
case "2":
|
||||||
|
cfg.Transcription.Provider = "groq-transcription"
|
||||||
|
case "3":
|
||||||
|
cfg.Transcription.Provider = "groq-translation"
|
||||||
|
case "openai", "groq-transcription", "groq-translation":
|
||||||
|
cfg.Transcription.Provider = input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model selection based on provider
|
||||||
|
if cfg.Transcription.Provider == "openai" {
|
||||||
|
fmt.Println("\nOpenAI Model:")
|
||||||
|
fmt.Printf("Model (current: %s): ", cfg.Transcription.Model)
|
||||||
|
if scanner.Scan() {
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
if input != "" {
|
||||||
|
cfg.Transcription.Model = input
|
||||||
|
} else if cfg.Transcription.Model == "" {
|
||||||
|
cfg.Transcription.Model = "whisper-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cfg.Transcription.Provider == "groq-transcription" {
|
||||||
|
fmt.Println("\nGroq Transcription Model:")
|
||||||
|
fmt.Println(" 1. whisper-large-v3 - Standard model")
|
||||||
|
fmt.Println(" 2. whisper-large-v3-turbo - Faster model")
|
||||||
|
fmt.Printf("Model [1-2] (current: %s): ", cfg.Transcription.Model)
|
||||||
|
if scanner.Scan() {
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
cfg.Transcription.Model = "whisper-large-v3"
|
||||||
|
case "2":
|
||||||
|
cfg.Transcription.Model = "whisper-large-v3-turbo"
|
||||||
|
case "whisper-large-v3", "whisper-large-v3-turbo":
|
||||||
|
cfg.Transcription.Model = input
|
||||||
|
case "":
|
||||||
|
if cfg.Transcription.Model == "" {
|
||||||
|
cfg.Transcription.Model = "whisper-large-v3-turbo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cfg.Transcription.Provider == "groq-translation" {
|
||||||
|
fmt.Println("\nGroq Translation Model:")
|
||||||
|
fmt.Println(" Note: Translation only supports whisper-large-v3 (turbo not available)")
|
||||||
|
fmt.Printf("Model (current: %s, press Enter for whisper-large-v3): ", cfg.Transcription.Model)
|
||||||
|
if scanner.Scan() {
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
if input == "" || input == "whisper-large-v3" || input == "1" {
|
||||||
|
cfg.Transcription.Model = "whisper-large-v3"
|
||||||
|
} else {
|
||||||
|
fmt.Println(" Warning: Only whisper-large-v3 is supported for translation. Using whisper-large-v3.")
|
||||||
|
cfg.Transcription.Model = "whisper-large-v3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Key (provider-aware)
|
||||||
|
var envVarName string
|
||||||
|
if cfg.Transcription.Provider == "openai" {
|
||||||
|
envVarName = "OPENAI_API_KEY"
|
||||||
|
} else {
|
||||||
|
envVarName = "GROQ_API_KEY"
|
||||||
|
}
|
||||||
|
fmt.Printf("\nAPI Key (current: %s, leave empty to use %s env var): ", maskAPIKey(cfg.Transcription.APIKey), envVarName)
|
||||||
if scanner.Scan() {
|
if scanner.Scan() {
|
||||||
input := strings.TrimSpace(scanner.Text())
|
input := strings.TrimSpace(scanner.Text())
|
||||||
if input != "" {
|
if input != "" {
|
||||||
@@ -167,7 +241,12 @@ func runInteractiveConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Language
|
// Language
|
||||||
fmt.Printf("Language (empty for auto-detect, current: %s): ", cfg.Transcription.Language)
|
if cfg.Transcription.Provider == "groq-translation" {
|
||||||
|
fmt.Printf("\nSource language hint (empty for auto-detect, current: %s): ", cfg.Transcription.Language)
|
||||||
|
fmt.Println("\n Note: Translation always outputs English. Language hints at source audio language.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\nLanguage (empty for auto-detect, current: %s): ", cfg.Transcription.Language)
|
||||||
|
}
|
||||||
if scanner.Scan() {
|
if scanner.Scan() {
|
||||||
input := strings.TrimSpace(scanner.Text())
|
input := strings.TrimSpace(scanner.Text())
|
||||||
cfg.Transcription.Language = input
|
cfg.Transcription.Language = input
|
||||||
@@ -305,10 +384,10 @@ func saveConfig(cfg *config.Config) error {
|
|||||||
|
|
||||||
# Speech Transcription Configuration
|
# Speech Transcription Configuration
|
||||||
[transcription]
|
[transcription]
|
||||||
provider = "%s" # Transcription service ("openai" only currently supported)
|
provider = "%s" # Transcription service: "openai", "groq-transcription", or "groq-translation"
|
||||||
api_key = "%s" # OpenAI API key (or set OPENAI_API_KEY environment variable)
|
api_key = "%s" # API key (or set OPENAI_API_KEY/GROQ_API_KEY environment variable)
|
||||||
language = "%s" # Language code (empty for auto-detect, "en", "it", "es", "fr", etc.)
|
language = "%s" # Language code (empty for auto-detect, "en", "it", "es", "fr", etc.)
|
||||||
model = "%s" # OpenAI model name ("whisper-1" recommended)
|
model = "%s" # Model: OpenAI="whisper-1", Groq="whisper-large-v3" or "whisper-large-v3-turbo"
|
||||||
|
|
||||||
# Text Injection Configuration
|
# Text Injection Configuration
|
||||||
[injection]
|
[injection]
|
||||||
@@ -327,8 +406,16 @@ func saveConfig(cfg *config.Config) error {
|
|||||||
# - "type": Direct typing via wtype only
|
# - "type": Direct typing via wtype only
|
||||||
# - "fallback": Try typing first, fallback to clipboard if it fails
|
# - "fallback": Try typing first, fallback to clipboard if it fails
|
||||||
#
|
#
|
||||||
|
# Provider explanations:
|
||||||
|
# - "openai": OpenAI Whisper API (cloud-based, requires OPENAI_API_KEY)
|
||||||
|
# - "groq-transcription": Groq Whisper API for transcription (fast, requires GROQ_API_KEY)
|
||||||
|
# Models: whisper-large-v3 or whisper-large-v3-turbo
|
||||||
|
# - "groq-translation": Groq Whisper API for translation to English (always outputs English text)
|
||||||
|
# Models: whisper-large-v3 only (turbo not supported for translation)
|
||||||
|
#
|
||||||
# Language codes: Use empty string ("") for automatic detection, or specific codes like:
|
# Language codes: Use empty string ("") for automatic detection, or specific codes like:
|
||||||
# "en" (English), "it" (Italian), "es" (Spanish), "fr" (French), "de" (German), etc.
|
# "en" (English), "it" (Italian), "es" (Spanish), "fr" (French), "de" (German), etc.
|
||||||
|
# For groq-translation, the language field hints at the source audio language for better accuracy.
|
||||||
`,
|
`,
|
||||||
cfg.Recording.SampleRate,
|
cfg.Recording.SampleRate,
|
||||||
cfg.Recording.Channels,
|
cfg.Recording.Channels,
|
||||||
|
|||||||
@@ -69,8 +69,14 @@ func (c *Config) ToTranscriberConfig() transcriber.Config {
|
|||||||
Model: c.Transcription.Model,
|
Model: c.Transcription.Model,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for API key in environment variables if not in config
|
||||||
if config.APIKey == "" {
|
if config.APIKey == "" {
|
||||||
config.APIKey = os.Getenv("OPENAI_API_KEY")
|
switch c.Transcription.Provider {
|
||||||
|
case "openai":
|
||||||
|
config.APIKey = os.Getenv("OPENAI_API_KEY")
|
||||||
|
case "groq-transcription", "groq-translation":
|
||||||
|
config.APIKey = os.Getenv("GROQ_API_KEY")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -110,7 +116,10 @@ func (c *Config) Validate() error {
|
|||||||
if c.Transcription.Provider == "" {
|
if c.Transcription.Provider == "" {
|
||||||
return fmt.Errorf("invalid transcription.provider: empty")
|
return fmt.Errorf("invalid transcription.provider: empty")
|
||||||
}
|
}
|
||||||
if c.Transcription.Provider == "openai" {
|
|
||||||
|
// Validate provider-specific settings
|
||||||
|
switch c.Transcription.Provider {
|
||||||
|
case "openai":
|
||||||
apiKey := c.Transcription.APIKey
|
apiKey := c.Transcription.APIKey
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
apiKey = os.Getenv("OPENAI_API_KEY")
|
apiKey = os.Getenv("OPENAI_API_KEY")
|
||||||
@@ -123,7 +132,50 @@ func (c *Config) Validate() error {
|
|||||||
if c.Transcription.Language != "" && !isValidLanguageCode(c.Transcription.Language) {
|
if c.Transcription.Language != "" && !isValidLanguageCode(c.Transcription.Language) {
|
||||||
return fmt.Errorf("invalid transcription.language: %s (use empty string for auto-detect or ISO-639-1 codes like 'en', 'es', 'fr')", c.Transcription.Language)
|
return fmt.Errorf("invalid transcription.language: %s (use empty string for auto-detect or ISO-639-1 codes like 'en', 'es', 'fr')", c.Transcription.Language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "groq-transcription":
|
||||||
|
apiKey := c.Transcription.APIKey
|
||||||
|
if apiKey == "" {
|
||||||
|
apiKey = os.Getenv("GROQ_API_KEY")
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
return fmt.Errorf("Groq API key required: not found in config (transcription.api_key) or environment variable (GROQ_API_KEY)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate language code if provided (empty string means auto-detect)
|
||||||
|
if c.Transcription.Language != "" && !isValidLanguageCode(c.Transcription.Language) {
|
||||||
|
return fmt.Errorf("invalid transcription.language: %s (use empty string for auto-detect or ISO-639-1 codes like 'en', 'es', 'fr')", c.Transcription.Language)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Groq model
|
||||||
|
validGroqModels := map[string]bool{"whisper-large-v3": true, "whisper-large-v3-turbo": true}
|
||||||
|
if c.Transcription.Model != "" && !validGroqModels[c.Transcription.Model] {
|
||||||
|
return fmt.Errorf("invalid model for groq-transcription: %s (must be whisper-large-v3 or whisper-large-v3-turbo)", c.Transcription.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "groq-translation":
|
||||||
|
apiKey := c.Transcription.APIKey
|
||||||
|
if apiKey == "" {
|
||||||
|
apiKey = os.Getenv("GROQ_API_KEY")
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
return fmt.Errorf("Groq API key required: not found in config (transcription.api_key) or environment variable (GROQ_API_KEY)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// For translation, language field hints at source language (output is always English)
|
||||||
|
if c.Transcription.Language != "" && !isValidLanguageCode(c.Transcription.Language) {
|
||||||
|
return fmt.Errorf("invalid transcription.language: %s (use empty string for auto-detect or ISO-639-1 codes like 'en', 'es', 'fr')", c.Transcription.Language)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Groq translation model - only whisper-large-v3 is supported (no turbo)
|
||||||
|
if c.Transcription.Model != "" && c.Transcription.Model != "whisper-large-v3" {
|
||||||
|
return fmt.Errorf("invalid model for groq-translation: %s (must be whisper-large-v3, turbo version not supported for translation)", c.Transcription.Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported transcription.provider: %s (must be openai, groq-transcription, or groq-translation)", c.Transcription.Provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Transcription.Model == "" {
|
if c.Transcription.Model == "" {
|
||||||
return fmt.Errorf("invalid transcription.model: empty")
|
return fmt.Errorf("invalid transcription.model: empty")
|
||||||
}
|
}
|
||||||
@@ -237,10 +289,10 @@ func SaveDefaultConfig() error {
|
|||||||
|
|
||||||
# Speech Transcription Configuration
|
# Speech Transcription Configuration
|
||||||
[transcription]
|
[transcription]
|
||||||
provider = "openai" # Transcription service ("openai" only currently supported)
|
provider = "openai" # Transcription service: "openai", "groq-transcription", or "groq-translation"
|
||||||
api_key = "" # OpenAI API key (or set OPENAI_API_KEY environment variable)
|
api_key = "" # API key (or set OPENAI_API_KEY/GROQ_API_KEY environment variable)
|
||||||
language = "" # Language code (empty for auto-detect, "en", "it", "es", "fr", etc.)
|
language = "" # Language code (empty for auto-detect, "en", "it", "es", "fr", etc.)
|
||||||
model = "whisper-1" # OpenAI model name ("whisper-1" recommended)
|
model = "whisper-1" # Model: OpenAI="whisper-1", Groq="whisper-large-v3" or "whisper-large-v3-turbo"
|
||||||
|
|
||||||
# Text Injection Configuration
|
# Text Injection Configuration
|
||||||
[injection]
|
[injection]
|
||||||
@@ -259,8 +311,16 @@ func SaveDefaultConfig() error {
|
|||||||
# - "type": Direct typing via wtype only
|
# - "type": Direct typing via wtype only
|
||||||
# - "fallback": Try typing first, fallback to clipboard if it fails
|
# - "fallback": Try typing first, fallback to clipboard if it fails
|
||||||
#
|
#
|
||||||
|
# Provider explanations:
|
||||||
|
# - "openai": OpenAI Whisper API (cloud-based, requires OPENAI_API_KEY)
|
||||||
|
# - "groq-transcription": Groq Whisper API for transcription (fast, requires GROQ_API_KEY)
|
||||||
|
# Models: whisper-large-v3 or whisper-large-v3-turbo
|
||||||
|
# - "groq-translation": Groq Whisper API for translation to English (always outputs English text)
|
||||||
|
# Models: whisper-large-v3 only (turbo not supported for translation)
|
||||||
|
#
|
||||||
# Language codes: Use empty string ("") for automatic detection, or specific codes like:
|
# Language codes: Use empty string ("") for automatic detection, or specific codes like:
|
||||||
# "en" (English), "it" (Italian), "es" (Spanish), "fr" (French), "de" (German), etc.
|
# "en" (English), "it" (Italian), "es" (Spanish), "fr" (French), "de" (German), etc.
|
||||||
|
# For groq-translation, the language field hints at the source audio language for better accuracy.
|
||||||
`
|
`
|
||||||
|
|
||||||
if _, err := file.WriteString(configContent); err != nil {
|
if _, err := file.WriteString(configContent); err != nil {
|
||||||
|
|||||||
@@ -788,3 +788,244 @@ func TestConfig_Validate_RecordingBufferSizes(t *testing.T) {
|
|||||||
t.Errorf("Validate() should have failed with invalid recording buffer sizes")
|
t.Errorf("Validate() should have failed with invalid recording buffer sizes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_GroqTranscription(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "groq-transcription",
|
||||||
|
APIKey: "gsk-test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-large-v3",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Validate() should have passed with valid groq-transcription config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_GroqTranslation(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "groq-translation",
|
||||||
|
APIKey: "gsk-test-key",
|
||||||
|
Language: "es",
|
||||||
|
Model: "whisper-large-v3", // Translation only supports non-turbo
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Validate() should have passed with valid groq-translation config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_GroqInvalidModel(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "groq-transcription",
|
||||||
|
APIKey: "gsk-test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "invalid-model",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Validate() should have failed with invalid Groq model")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_GroqWithoutAPIKey(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "groq-transcription",
|
||||||
|
APIKey: "", // No API key
|
||||||
|
Model: "whisper-large-v3",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure environment variable is not set
|
||||||
|
originalAPIKey := os.Getenv("GROQ_API_KEY")
|
||||||
|
os.Unsetenv("GROQ_API_KEY")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey != "" {
|
||||||
|
os.Setenv("GROQ_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Validate() should have failed without Groq API key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_GroqWithEnvVarAPIKey(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "groq-transcription",
|
||||||
|
APIKey: "", // No API key in config
|
||||||
|
Model: "whisper-large-v3",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable
|
||||||
|
originalAPIKey := os.Getenv("GROQ_API_KEY")
|
||||||
|
os.Setenv("GROQ_API_KEY", "gsk-env-api-key")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey == "" {
|
||||||
|
os.Unsetenv("GROQ_API_KEY")
|
||||||
|
} else {
|
||||||
|
os.Setenv("GROQ_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Validate() should have passed with Groq API key from environment: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable
|
||||||
|
originalAPIKey := os.Getenv("GROQ_API_KEY")
|
||||||
|
os.Setenv("GROQ_API_KEY", "gsk-env-api-key")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey == "" {
|
||||||
|
os.Unsetenv("GROQ_API_KEY")
|
||||||
|
} else {
|
||||||
|
os.Setenv("GROQ_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
transcriberConfig := config.ToTranscriberConfig()
|
||||||
|
|
||||||
|
if transcriberConfig.APIKey != "gsk-env-api-key" {
|
||||||
|
t.Errorf("Expected APIKey from env var 'gsk-env-api-key', got %s", transcriberConfig.APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_GroqTranslation_RejectsTurbo(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "groq-translation",
|
||||||
|
APIKey: "gsk-test-key",
|
||||||
|
Language: "es",
|
||||||
|
Model: "whisper-large-v3-turbo", // Turbo not supported for translation
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Validate() should have rejected whisper-large-v3-turbo for groq-translation")
|
||||||
|
}
|
||||||
|
if err != nil && err.Error() != "invalid model for groq-translation: whisper-large-v3-turbo (must be whisper-large-v3, turbo version not supported for translation)" {
|
||||||
|
t.Errorf("Unexpected error message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroqTranscriptionAdapter implements TranscriptionAdapter for Groq Whisper API
|
||||||
|
type GroqTranscriptionAdapter struct {
|
||||||
|
client *openai.Client
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroqTranscriptionAdapter(config Config) *GroqTranscriptionAdapter {
|
||||||
|
clientConfig := openai.DefaultConfig(config.APIKey)
|
||||||
|
clientConfig.BaseURL = "https://api.groq.com/openai/v1"
|
||||||
|
client := openai.NewClientWithConfig(clientConfig)
|
||||||
|
|
||||||
|
return &GroqTranscriptionAdapter{
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GroqTranscriptionAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
if len(audioData) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert raw PCM to WAV format
|
||||||
|
wavData, err := convertToWAV(audioData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("convert to WAV: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create transcription request
|
||||||
|
req := openai.AudioRequest{
|
||||||
|
Model: a.config.Model,
|
||||||
|
Reader: bytes.NewReader(wavData),
|
||||||
|
FilePath: "audio.wav",
|
||||||
|
Language: a.config.Language,
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := a.client.CreateTranscription(ctx, req)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("groq-transcription-adapter: API call failed after %v: %v", duration, err)
|
||||||
|
return "", fmt.Errorf("groq transcription: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("groq-transcription-adapter: transcribed %d bytes in %v: %q", len(audioData), duration, resp.Text)
|
||||||
|
return resp.Text, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroqTranslationAdapter implements TranscriptionAdapter for Groq Translation API
|
||||||
|
// Translates audio to English text. The Language field in config hints at the source language.
|
||||||
|
type GroqTranslationAdapter struct {
|
||||||
|
client *openai.Client
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGroqTranslationAdapter(config Config) *GroqTranslationAdapter {
|
||||||
|
clientConfig := openai.DefaultConfig(config.APIKey)
|
||||||
|
clientConfig.BaseURL = "https://api.groq.com/openai/v1"
|
||||||
|
client := openai.NewClientWithConfig(clientConfig)
|
||||||
|
|
||||||
|
return &GroqTranslationAdapter{
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GroqTranslationAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
if len(audioData) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert raw PCM to WAV format
|
||||||
|
wavData, err := convertToWAV(audioData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("convert to WAV: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create translation request
|
||||||
|
// Note: Translation always outputs English, regardless of target language
|
||||||
|
// The Language field in the request hints at the source audio language for better accuracy
|
||||||
|
req := openai.AudioRequest{
|
||||||
|
Model: a.config.Model,
|
||||||
|
Reader: bytes.NewReader(wavData),
|
||||||
|
FilePath: "audio.wav",
|
||||||
|
Language: a.config.Language, // Source language hint
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := a.client.CreateTranslation(ctx, req)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("groq-translation-adapter: API call failed after %v: %v", duration, err)
|
||||||
|
return "", fmt.Errorf("groq translation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("groq-translation-adapter: translated %d bytes in %v: %q", len(audioData), duration, resp.Text)
|
||||||
|
return resp.Text, nil
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package transcriber
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,7 +30,7 @@ func (a *OpenAIAdapter) Transcribe(ctx context.Context, audioData []byte) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert raw PCM to WAV format
|
// Convert raw PCM to WAV format
|
||||||
wavData, err := a.convertToWAV(audioData)
|
wavData, err := convertToWAV(audioData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("convert to WAV: %w", err)
|
return "", fmt.Errorf("convert to WAV: %w", err)
|
||||||
}
|
}
|
||||||
@@ -56,39 +55,3 @@ func (a *OpenAIAdapter) Transcribe(ctx context.Context, audioData []byte) (strin
|
|||||||
log.Printf("openai-adapter: transcribed %d bytes in %v: %q", len(audioData), duration, resp.Text)
|
log.Printf("openai-adapter: transcribed %d bytes in %v: %q", len(audioData), duration, resp.Text)
|
||||||
return resp.Text, nil
|
return resp.Text, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToWAV converts raw 16-bit PCM audio to WAV format
|
|
||||||
func (a *OpenAIAdapter) convertToWAV(rawAudio []byte) ([]byte, error) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
const sampleRate = 16000
|
|
||||||
const channels = 1
|
|
||||||
const bitsPerSample = 16
|
|
||||||
const byteRate = sampleRate * channels * bitsPerSample / 8
|
|
||||||
const blockAlign = channels * bitsPerSample / 8
|
|
||||||
|
|
||||||
dataSize := len(rawAudio)
|
|
||||||
fileSize := 36 + dataSize
|
|
||||||
|
|
||||||
// WAV header
|
|
||||||
buf.WriteString("RIFF")
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint32(fileSize))
|
|
||||||
buf.WriteString("WAVE")
|
|
||||||
|
|
||||||
// fmt chunk
|
|
||||||
buf.WriteString("fmt ")
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint32(16)) // fmt chunk size
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint16(1)) // PCM format
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint16(channels)) // number of channels
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint32(sampleRate)) // sample rate
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint32(byteRate)) // byte rate
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint16(blockAlign)) // block align
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint16(bitsPerSample)) // bits per sample
|
|
||||||
|
|
||||||
// data chunk
|
|
||||||
buf.WriteString("data")
|
|
||||||
binary.Write(&buf, binary.LittleEndian, uint32(dataSize))
|
|
||||||
buf.Write(rawAudio)
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// convertToWAV converts raw 16-bit PCM audio to WAV format
|
||||||
|
func convertToWAV(rawAudio []byte) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
const sampleRate = 16000
|
||||||
|
const channels = 1
|
||||||
|
const bitsPerSample = 16
|
||||||
|
const byteRate = sampleRate * channels * bitsPerSample / 8
|
||||||
|
const blockAlign = channels * bitsPerSample / 8
|
||||||
|
|
||||||
|
dataSize := len(rawAudio)
|
||||||
|
fileSize := 36 + dataSize
|
||||||
|
|
||||||
|
// WAV header
|
||||||
|
buf.WriteString("RIFF")
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(fileSize))
|
||||||
|
buf.WriteString("WAVE")
|
||||||
|
|
||||||
|
// fmt chunk
|
||||||
|
buf.WriteString("fmt ")
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(16)) // fmt chunk size
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(1)) // PCM format
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(channels)) // number of channels
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(sampleRate)) // sample rate
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(byteRate)) // byte rate
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(blockAlign)) // block align
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint16(bitsPerSample)) // bits per sample
|
||||||
|
|
||||||
|
// data chunk
|
||||||
|
buf.WriteString("data")
|
||||||
|
binary.Write(&buf, binary.LittleEndian, uint32(dataSize))
|
||||||
|
buf.Write(rawAudio)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
@@ -39,6 +39,18 @@ func NewTranscriber(config Config) (Transcriber, error) {
|
|||||||
}
|
}
|
||||||
adapter = NewOpenAIAdapter(config)
|
adapter = NewOpenAIAdapter(config)
|
||||||
|
|
||||||
|
case "groq-transcription":
|
||||||
|
if config.APIKey == "" {
|
||||||
|
return nil, fmt.Errorf("Groq API key required")
|
||||||
|
}
|
||||||
|
adapter = NewGroqTranscriptionAdapter(config)
|
||||||
|
|
||||||
|
case "groq-translation":
|
||||||
|
if config.APIKey == "" {
|
||||||
|
return nil, fmt.Errorf("Groq API key required")
|
||||||
|
}
|
||||||
|
adapter = NewGroqTranslationAdapter(config)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported provider: %s", config.Provider)
|
return nil, fmt.Errorf("unsupported provider: %s", config.Provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,46 @@ func TestNewTranscriber(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "valid groq-transcription config",
|
||||||
|
config: Config{
|
||||||
|
Provider: "groq-transcription",
|
||||||
|
APIKey: "gsk-test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-large-v3",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groq-transcription config without api key",
|
||||||
|
config: Config{
|
||||||
|
Provider: "groq-transcription",
|
||||||
|
APIKey: "",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-large-v3",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid groq-translation config",
|
||||||
|
config: Config{
|
||||||
|
Provider: "groq-translation",
|
||||||
|
APIKey: "gsk-test-key",
|
||||||
|
Language: "es",
|
||||||
|
Model: "whisper-large-v3-turbo",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groq-translation config without api key",
|
||||||
|
config: Config{
|
||||||
|
Provider: "groq-translation",
|
||||||
|
APIKey: "",
|
||||||
|
Language: "es",
|
||||||
|
Model: "whisper-large-v3-turbo",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unsupported provider",
|
name: "unsupported provider",
|
||||||
config: Config{
|
config: Config{
|
||||||
|
|||||||
Reference in New Issue
Block a user