+47
-12
@@ -132,7 +132,7 @@ 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:
|
||||||
- Transcription provider (OpenAI or Groq)
|
- Transcription provider (OpenAI, Groq, or Mistral)
|
||||||
- API keys and model selection
|
- API keys and model selection
|
||||||
- Audio and text injection preferences
|
- Audio and text injection preferences
|
||||||
- Notification settings`,
|
- Notification settings`,
|
||||||
@@ -162,10 +162,11 @@ func runInteractiveConfig() error {
|
|||||||
// Provider selection
|
// Provider selection
|
||||||
for {
|
for {
|
||||||
fmt.Println("Select transcription provider:")
|
fmt.Println("Select transcription provider:")
|
||||||
fmt.Println(" 1. openai - OpenAI Whisper API (cloud-based)")
|
fmt.Println(" 1. openai - OpenAI Whisper API (cloud-based)")
|
||||||
fmt.Println(" 2. groq-transcription - Groq Whisper API (fast transcription)")
|
fmt.Println(" 2. groq-transcription - Groq Whisper API (fast transcription)")
|
||||||
fmt.Println(" 3. groq-translation - Groq Whisper API (translate to English)")
|
fmt.Println(" 3. groq-translation - Groq Whisper API (translate to English)")
|
||||||
fmt.Printf("Provider [1-3] (current: %s): ", cfg.Transcription.Provider)
|
fmt.Println(" 4. mistral-transcription - Mistral Voxtral API (excellent for European languages)")
|
||||||
|
fmt.Printf("Provider [1-4] (current: %s): ", cfg.Transcription.Provider)
|
||||||
if !scanner.Scan() {
|
if !scanner.Scan() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -180,10 +181,12 @@ func runInteractiveConfig() error {
|
|||||||
cfg.Transcription.Provider = "groq-transcription"
|
cfg.Transcription.Provider = "groq-transcription"
|
||||||
case "3":
|
case "3":
|
||||||
cfg.Transcription.Provider = "groq-translation"
|
cfg.Transcription.Provider = "groq-translation"
|
||||||
case "openai", "groq-transcription", "groq-translation":
|
case "4":
|
||||||
|
cfg.Transcription.Provider = "mistral-transcription"
|
||||||
|
case "openai", "groq-transcription", "groq-translation", "mistral-transcription":
|
||||||
cfg.Transcription.Provider = input
|
cfg.Transcription.Provider = input
|
||||||
default:
|
default:
|
||||||
fmt.Println("❌ Error: invalid provider. Please enter 1, 2, 3 or provider name.")
|
fmt.Println("❌ Error: invalid provider. Please enter 1-4 or provider name.")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -245,13 +248,43 @@ func runInteractiveConfig() error {
|
|||||||
}
|
}
|
||||||
fmt.Println("❌ Error: only whisper-large-v3 is supported for translation.")
|
fmt.Println("❌ Error: only whisper-large-v3 is supported for translation.")
|
||||||
}
|
}
|
||||||
|
case "mistral-transcription":
|
||||||
|
for {
|
||||||
|
fmt.Println("\nMistral Voxtral Model:")
|
||||||
|
fmt.Println(" 1. voxtral-mini-latest - Recommended (latest version)")
|
||||||
|
fmt.Println(" 2. voxtral-mini-2507 - Pinned version")
|
||||||
|
fmt.Printf("Model [1-2] (current: %s): ", cfg.Transcription.Model)
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
cfg.Transcription.Model = "voxtral-mini-latest"
|
||||||
|
case "2":
|
||||||
|
cfg.Transcription.Model = "voxtral-mini-2507"
|
||||||
|
case "voxtral-mini-latest", "voxtral-mini-2507":
|
||||||
|
cfg.Transcription.Model = input
|
||||||
|
case "":
|
||||||
|
if cfg.Transcription.Model == "" || !strings.HasPrefix(cfg.Transcription.Model, "voxtral") {
|
||||||
|
cfg.Transcription.Model = "voxtral-mini-latest"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Println("❌ Error: invalid model. Please enter 1, 2 or model name.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Key (provider-aware)
|
// API Key (provider-aware)
|
||||||
var envVarName string
|
var envVarName string
|
||||||
if cfg.Transcription.Provider == "openai" {
|
switch cfg.Transcription.Provider {
|
||||||
|
case "openai":
|
||||||
envVarName = "OPENAI_API_KEY"
|
envVarName = "OPENAI_API_KEY"
|
||||||
} else {
|
case "mistral-transcription":
|
||||||
|
envVarName = "MISTRAL_API_KEY"
|
||||||
|
default:
|
||||||
envVarName = "GROQ_API_KEY"
|
envVarName = "GROQ_API_KEY"
|
||||||
}
|
}
|
||||||
fmt.Printf("\nAPI Key (current: %s, leave empty to use %s env var): ", maskAPIKey(cfg.Transcription.APIKey), envVarName)
|
fmt.Printf("\nAPI Key (current: %s, leave empty to use %s env var): ", maskAPIKey(cfg.Transcription.APIKey), envVarName)
|
||||||
@@ -575,10 +608,10 @@ func saveConfig(cfg *config.Config) error {
|
|||||||
|
|
||||||
# Speech Transcription Configuration
|
# Speech Transcription Configuration
|
||||||
[transcription]
|
[transcription]
|
||||||
provider = "%s" # Transcription service: "openai", "groq-transcription", or "groq-translation"
|
provider = "%s" # Transcription service: "openai", "groq-transcription", "groq-translation", or "mistral-transcription"
|
||||||
api_key = "%s" # API key (or set OPENAI_API_KEY/GROQ_API_KEY environment variable)
|
api_key = "%s" # API key (or set OPENAI_API_KEY/GROQ_API_KEY/MISTRAL_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" # Model: OpenAI="whisper-1", Groq="whisper-large-v3" or "whisper-large-v3-turbo"
|
model = "%s" # Model: OpenAI="whisper-1", Groq="whisper-large-v3", Mistral="voxtral-mini-latest"
|
||||||
|
|
||||||
# Text Injection Configuration
|
# Text Injection Configuration
|
||||||
[injection]
|
[injection]
|
||||||
@@ -600,6 +633,8 @@ func saveConfig(cfg *config.Config) error {
|
|||||||
# Models: whisper-large-v3 or whisper-large-v3-turbo
|
# Models: whisper-large-v3 or whisper-large-v3-turbo
|
||||||
# - "groq-translation": Groq Whisper API for translation to English (always outputs English text)
|
# - "groq-translation": Groq Whisper API for translation to English (always outputs English text)
|
||||||
# Models: whisper-large-v3 only (turbo not supported for translation)
|
# Models: whisper-large-v3 only (turbo not supported for translation)
|
||||||
|
# - "mistral-transcription": Mistral Voxtral API (excellent for European languages, requires MISTRAL_API_KEY)
|
||||||
|
# Models: voxtral-mini-latest or voxtral-mini-2507
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ func (c *Config) ToTranscriberConfig() transcriber.Config {
|
|||||||
config.APIKey = os.Getenv("OPENAI_API_KEY")
|
config.APIKey = os.Getenv("OPENAI_API_KEY")
|
||||||
case "groq-transcription", "groq-translation":
|
case "groq-transcription", "groq-translation":
|
||||||
config.APIKey = os.Getenv("GROQ_API_KEY")
|
config.APIKey = os.Getenv("GROQ_API_KEY")
|
||||||
|
case "mistral-transcription":
|
||||||
|
config.APIKey = os.Getenv("MISTRAL_API_KEY")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +223,28 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("invalid model for groq-translation: %s (must be whisper-large-v3, turbo version not supported for translation)", c.Transcription.Model)
|
return fmt.Errorf("invalid model for groq-translation: %s (must be whisper-large-v3, turbo version not supported for translation)", c.Transcription.Model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "mistral-transcription":
|
||||||
|
apiKey := c.Transcription.APIKey
|
||||||
|
if apiKey == "" {
|
||||||
|
apiKey = os.Getenv("MISTRAL_API_KEY")
|
||||||
|
}
|
||||||
|
if apiKey == "" {
|
||||||
|
return fmt.Errorf("Mistral API key required: not found in config (transcription.api_key) or environment variable (MISTRAL_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 Mistral model
|
||||||
|
validMistralModels := map[string]bool{"voxtral-mini-latest": true, "voxtral-mini-2507": true}
|
||||||
|
if c.Transcription.Model != "" && !validMistralModels[c.Transcription.Model] {
|
||||||
|
return fmt.Errorf("invalid model for mistral-transcription: %s (must be voxtral-mini-latest or voxtral-mini-2507)", c.Transcription.Model)
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported transcription.provider: %s (must be openai, groq-transcription, or groq-translation)", c.Transcription.Provider)
|
return fmt.Errorf("unsupported transcription.provider: %s (must be openai, groq-transcription, groq-translation, or mistral-transcription)", c.Transcription.Provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Transcription.Model == "" {
|
if c.Transcription.Model == "" {
|
||||||
@@ -390,10 +412,10 @@ func SaveDefaultConfig() error {
|
|||||||
|
|
||||||
# Speech Transcription Configuration
|
# Speech Transcription Configuration
|
||||||
[transcription]
|
[transcription]
|
||||||
provider = "openai" # Transcription service: "openai", "groq-transcription", or "groq-translation"
|
provider = "openai" # Transcription service: "openai", "groq-transcription", "groq-translation", or "mistral-transcription"
|
||||||
api_key = "" # API key (or set OPENAI_API_KEY/GROQ_API_KEY environment variable)
|
api_key = "" # API key (or set OPENAI_API_KEY/GROQ_API_KEY/MISTRAL_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" # Model: OpenAI="whisper-1", Groq="whisper-large-v3" or "whisper-large-v3-turbo"
|
model = "whisper-1" # Model: OpenAI="whisper-1", Groq="whisper-large-v3", Mistral="voxtral-mini-latest"
|
||||||
|
|
||||||
# Text Injection Configuration
|
# Text Injection Configuration
|
||||||
[injection]
|
[injection]
|
||||||
@@ -455,6 +477,8 @@ func SaveDefaultConfig() error {
|
|||||||
# Models: whisper-large-v3 or whisper-large-v3-turbo
|
# Models: whisper-large-v3 or whisper-large-v3-turbo
|
||||||
# - "groq-translation": Groq Whisper API for translation to English (always outputs English text)
|
# - "groq-translation": Groq Whisper API for translation to English (always outputs English text)
|
||||||
# Models: whisper-large-v3 only (turbo not supported for translation)
|
# Models: whisper-large-v3 only (turbo not supported for translation)
|
||||||
|
# - "mistral-transcription": Mistral Voxtral API (excellent for European languages, requires MISTRAL_API_KEY)
|
||||||
|
# Models: voxtral-mini-latest or voxtral-mini-2507
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MistralAdapter implements TranscriptionAdapter for Mistral Voxtral API
|
||||||
|
type MistralAdapter struct {
|
||||||
|
client *openai.Client
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMistralAdapter(config Config) *MistralAdapter {
|
||||||
|
clientConfig := openai.DefaultConfig(config.APIKey)
|
||||||
|
clientConfig.BaseURL = "https://api.mistral.ai/v1"
|
||||||
|
client := openai.NewClientWithConfig(clientConfig)
|
||||||
|
|
||||||
|
return &MistralAdapter{
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MistralAdapter) 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("mistral-adapter: API call failed after %v: %v", duration, err)
|
||||||
|
return "", fmt.Errorf("mistral transcription: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("mistral-adapter: transcribed %d bytes in %v: %q", len(audioData), duration, resp.Text)
|
||||||
|
return resp.Text, nil
|
||||||
|
}
|
||||||
@@ -51,6 +51,12 @@ func NewTranscriber(config Config) (Transcriber, error) {
|
|||||||
}
|
}
|
||||||
adapter = NewGroqTranslationAdapter(config)
|
adapter = NewGroqTranslationAdapter(config)
|
||||||
|
|
||||||
|
case "mistral-transcription":
|
||||||
|
if config.APIKey == "" {
|
||||||
|
return nil, fmt.Errorf("Mistral API key required")
|
||||||
|
}
|
||||||
|
adapter = NewMistralAdapter(config)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported provider: %s", config.Provider)
|
return nil, fmt.Errorf("unsupported provider: %s", config.Provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,26 @@ func TestNewTranscriber(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "valid mistral-transcription config",
|
||||||
|
config: Config{
|
||||||
|
Provider: "mistral-transcription",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "de",
|
||||||
|
Model: "voxtral-mini-latest",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mistral-transcription config without api key",
|
||||||
|
config: Config{
|
||||||
|
Provider: "mistral-transcription",
|
||||||
|
APIKey: "",
|
||||||
|
Language: "de",
|
||||||
|
Model: "voxtral-mini-latest",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unsupported provider",
|
name: "unsupported provider",
|
||||||
config: Config{
|
config: Config{
|
||||||
|
|||||||
Reference in New Issue
Block a user