From a105c8c5a86321e8ec5269c943303763343dbc9c Mon Sep 17 00:00:00 2001 From: leonardotrapani Date: Sun, 1 Feb 2026 02:09:37 +0100 Subject: [PATCH] add runtime language-model compatibility check with fallback to auto-detect --- internal/transcriber/transcriber.go | 17 +++++ internal/transcriber/transcriber_test.go | 82 ++++++++++++++++++++++++ progress.txt | 12 +++- tasks/prd.jsonc | 2 +- 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/internal/transcriber/transcriber.go b/internal/transcriber/transcriber.go index a99b6e9..0fe4d9d 100644 --- a/internal/transcriber/transcriber.go +++ b/internal/transcriber/transcriber.go @@ -3,9 +3,12 @@ package transcriber import ( "context" "fmt" + "log" "strings" + "github.com/leonardotrapani/hyprvoice/internal/language" "github.com/leonardotrapani/hyprvoice/internal/models/whisper" + "github.com/leonardotrapani/hyprvoice/internal/notify" "github.com/leonardotrapani/hyprvoice/internal/provider" "github.com/leonardotrapani/hyprvoice/internal/recording" ) @@ -95,6 +98,20 @@ func NewTranscriber(config Config) (Transcriber, error) { return nil, fmt.Errorf("model %s is not a transcription model", config.Model) } + // runtime language-model compatibility check with fallback + // primary validation happens at config time (hard error), this is a safety net + if config.Language != "" && !model.SupportsLanguage(config.Language) { + langName := language.FromCode(config.Language).Name + log.Printf("warning: model %s does not support language %s, falling back to auto-detect", model.ID, langName) + + // send desktop notification to alert user + notifier := notify.NewDesktop(nil) + notifier.Error(fmt.Sprintf("Model %s does not support %s. Using auto-detect.", model.Name, langName)) + + // override language to auto for this session + config.Language = "" + } + // streaming models use StreamingTranscriber if model.Streaming { var streamingAdapter StreamingAdapter diff --git a/internal/transcriber/transcriber_test.go b/internal/transcriber/transcriber_test.go index 76e374e..43b93a3 100644 --- a/internal/transcriber/transcriber_test.go +++ b/internal/transcriber/transcriber_test.go @@ -994,3 +994,85 @@ func TestStreamingTranscriber_GetFinalTranscriptionSafe(t *testing.T) { t.Errorf("Stop() error = %v", err) } } + +func TestNewTranscriber_LanguageFallback(t *testing.T) { + // test that incompatible language falls back to auto-detect (no error) + // distil-whisper-large-v3-en only supports English + config := Config{ + Provider: "groq-transcription", + APIKey: "test-key", + Language: "es", // Spanish not supported by English-only model + Model: "distil-whisper-large-v3-en", + } + + // should succeed (fallback to auto), not error + transcriber, err := NewTranscriber(config) + if err != nil { + t.Errorf("NewTranscriber() should fall back to auto, got error: %v", err) + return + } + + if transcriber == nil { + t.Errorf("NewTranscriber() returned nil transcriber") + } +} + +func TestNewTranscriber_AutoLanguageNoFallback(t *testing.T) { + // test that auto language never triggers warning/fallback + config := Config{ + Provider: "groq-transcription", + APIKey: "test-key", + Language: "", // auto + Model: "distil-whisper-large-v3-en", + } + + transcriber, err := NewTranscriber(config) + if err != nil { + t.Errorf("NewTranscriber() error = %v", err) + return + } + + if transcriber == nil { + t.Errorf("NewTranscriber() returned nil transcriber") + } +} + +func TestNewTranscriber_CompatibleLanguageNoFallback(t *testing.T) { + // test that compatible language works normally + config := Config{ + Provider: "groq-transcription", + APIKey: "test-key", + Language: "en", // English supported by English-only model + Model: "distil-whisper-large-v3-en", + } + + transcriber, err := NewTranscriber(config) + if err != nil { + t.Errorf("NewTranscriber() error = %v", err) + return + } + + if transcriber == nil { + t.Errorf("NewTranscriber() returned nil transcriber") + } +} + +func TestNewTranscriber_MultilingualModelAllLanguages(t *testing.T) { + // test that multilingual model accepts any language without fallback + config := Config{ + Provider: "groq-transcription", + APIKey: "test-key", + Language: "es", // Spanish + Model: "whisper-large-v3", // multilingual + } + + transcriber, err := NewTranscriber(config) + if err != nil { + t.Errorf("NewTranscriber() error = %v", err) + return + } + + if transcriber == nil { + t.Errorf("NewTranscriber() returned nil transcriber") + } +} diff --git a/progress.txt b/progress.txt index b0be25d..df01fa9 100644 --- a/progress.txt +++ b/progress.txt @@ -458,4 +458,14 @@ Started: Sun Feb 1 12:22:47 AM CET 2026 - LLM validation also refactored to use registry - Removed old hardcoded `isValidLanguageCode()` function - Updated tests: replaced TestIsValidLanguageCode with TestValidateModelLanguageCompatibility -- All tests passing, typecheck passes \ No newline at end of file +- All tests passing, typecheck passes + +### Task 42: Add runtime language-model compatibility check with fallback +- Updated `internal/transcriber/transcriber.go` NewTranscriber() +- Added runtime check after model lookup: `if config.Language != "" && !model.SupportsLanguage(config.Language)` +- Logs warning with model ID and language name +- Sends desktop notification via `notify.NewDesktop(nil).Error(...)` alerting user of fallback +- Overrides `config.Language = ""` (auto) for this transcription session +- This is a safety net for manually-edited configs; primary validation is at config-time (hard error) +- Added 4 tests: LanguageFallback, AutoLanguageNoFallback, CompatibleLanguageNoFallback, MultilingualModelAllLanguages +- All tests passing with -race flag, typecheck passes \ No newline at end of file diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc index 2c8dec0..afa29cd 100644 --- a/tasks/prd.jsonc +++ b/tasks/prd.jsonc @@ -992,7 +992,7 @@ "NewTranscriber with compatible language works normally", "Typecheck passes" ], - "passes": false + "passes": true }, { "title": "Add DEEPGRAM_API_KEY env var support",