Files
hyprvoice/progress.txt
T

494 lines
30 KiB
Plaintext

# Ralph Progress Log
Started: Sun Feb 1 12:22:47 AM CET 2026
---
## Completed
### Task 1: Create language package with core types and helpers
- Created `internal/language/language.go` with Language struct, Auto constant
- Implemented FromCode, List, Codes, AllLanguageCodes, IsValidCode
- Full 57 language list from OpenAI Whisper
- All tests passing, typecheck passes
### Task 16: Update config.ToTranscriberConfig to work with new architecture
- Added `Threads int` field to `TranscriptionConfig` in types.go (for local transcription CPU threads)
- Added `Threads int` field to `transcriber.Config` struct
- Updated `ToTranscriberConfig()` to pass Threads from config
- Updated config template in save.go with `threads = 0` and comment explaining auto-detection (NumCPU-1)
- Added whisper-cpp to provider list in config template
- Config package doesn't import provider - factory handles model lookup
- All tests passing, typecheck passes
### Task 3: Create Model type with full metadata
- Created `internal/provider/model.go`
- ModelType enum: Transcription, LLM
- Model struct: ID, Name, Description, Type, Streaming, Local, AdapterType, SupportedLanguages, Endpoint, LocalInfo
- EndpointConfig: BaseURL, Path
- LocalModelInfo: Filename, Size, DownloadURL
- Helper methods: NeedsDownload(), IsStreaming(), SupportsLanguage(code), SupportsAllLanguages()
- SupportsLanguage("") always returns true (auto always allowed)
- All tests passing, typecheck passes
### Task 4: Refactor Provider interface to return Models
- Updated `internal/provider/provider.go` Provider interface
- Replaced old methods with: Models() []Model, DefaultModel(t ModelType) string, IsLocal() bool
- Added package-level helpers:
- GetModel(providerName, modelID string) (*Model, error)
- ModelsOfType(p Provider, t ModelType) []Model
- FindModelByID(modelID string) (*Model, Provider, error)
- ModelsForLanguage(p Provider, t ModelType, langCode string) []Model
- ValidateModelLanguage(providerName, modelID, langCode string) error
- Updated all providers (openai, groq, mistral, elevenlabs) with full Model metadata
- Updated TUI files to use ModelsOfType instead of old SupportsTranscription/SupportsLLM
- Added comprehensive tests for all new helper functions
- All tests passing, typecheck passes
### Task 5: Define BatchAdapter and StreamingAdapter interfaces
- Renamed `TranscriptionAdapter` to `BatchAdapter` in transcriber.go
- Updated all adapters (openai, groq, mistral, elevenlabs) to reference BatchAdapter in comments
- Updated SimpleTranscriber to use BatchAdapter
- Updated test mocks (MockTranscriptionAdapter -> MockBatchAdapter)
- Created `internal/transcriber/streaming.go` with:
- `TranscriptionResult` struct: Text, IsFinal, Error fields
- `StreamingAdapter` interface: Start, SendChunk, Results, Close methods
- All tests passing, typecheck passes
### Task 6: Create StreamingTranscriber wrapper
- Created `internal/transcriber/streaming_transcriber.go`
- StreamingTranscriber struct with: adapter, language, finalText builder, mutex, ctx/cancel, WaitGroup
- Start() creates cancelable context, starts adapter, spawns 2 goroutines
- Goroutine 1: reads frames from channel, calls adapter.SendChunk()
- Goroutine 2: reads from adapter.Results(), accumulates final results with space separator
- Stop() cancels context, waits for goroutines, closes adapter
- GetFinalTranscription() returns accumulated text with mutex protection
- Added MockStreamingAdapter and comprehensive tests
- Tests verify: start/stop, result accumulation, partial result filtering, error handling, concurrent access
- All tests passing with -race flag, typecheck passes
### Task 7: Write tests for Model, Provider, and interfaces
- Created `internal/provider/model_test.go`
- TestModel_NeedsDownload: local with LocalInfo = true, cloud = false, nil = false
- TestModel_IsStreaming: returns Streaming field value
- TestModel_SupportsLanguage: multilingual supports all, english-only supports en, auto always true
- TestModel_SupportsAllLanguages: true when 57 languages, false otherwise
- TestModelType_Constants: Transcription=0, LLM=1
- TestEndpointConfig_Fields, TestLocalModelInfo_Fields: struct fields accessible
- TestModel_AllFields: comprehensive struct field test
- provider_test.go already had GetModel, ModelsOfType, FindModelByID, ModelsForLanguage, ValidateModelLanguage tests
- All tests passing, typecheck passes
### Task 9: Migrate Groq provider to new Model structure
- Implementation was already complete from previous work
- Verified 6 models: 3 transcription (whisper-large-v3, whisper-large-v3-turbo, distil-whisper-large-v3-en) + 3 LLM
- All models use AdapterType='openai' (Groq is OpenAI-compatible)
- Endpoint.BaseURL='https://api.groq.com/openai' for all
- distil-whisper-large-v3-en correctly has SupportedLanguages=['en'] (English only)
- Multilingual models have all 57 language codes
- All verification items confirmed working
### Task 10: Migrate Mistral provider to new Model structure
- Implementation was already complete from previous work
- Verified 2 models: voxtral-mini-latest, voxtral-mini-2507
- All models use AdapterType='openai' (Mistral transcription is OpenAI-compatible)
- Endpoint.BaseURL='https://api.mistral.ai' with Path='/v1/audio/transcriptions'
- SupportedLanguages set to all 57 language codes (multilingual per Mistral docs)
- Researched Mistral API docs - language parameter is optional, no specific list of restrictions
- All tests passing, typecheck passes
### Task 11: Migrate ElevenLabs provider to new Model structure
- Added 4 models: 2 batch (scribe_v1, scribe_v2) + 2 streaming (scribe_v1-streaming, scribe_v2-streaming)
- Batch models: AdapterType='elevenlabs', Streaming=false, Endpoint.BaseURL='https://api.elevenlabs.io'
- Streaming models: AdapterType='elevenlabs-streaming', Streaming=true, Endpoint.BaseURL='wss://api.elevenlabs.io'
- Researched ElevenLabs docs: Scribe supports 90+ languages, including all 57 from our master list
- SupportedLanguages set to all 57 language codes
- Added TestElevenLabsProvider test verifying all requirements
- All tests passing, typecheck passes
### Task 12: Create consolidated OpenAI-compatible BatchAdapter
- Refactored `internal/transcriber/adapter_openai.go` to be configurable
- New constructor: `NewOpenAIAdapter(endpoint *EndpointConfig, apiKey, model, lang string, keywords []string, providerName string)`
- Removed hardcoded base URL, now uses `endpoint.BaseURL + "/v1"` when endpoint provided
- Added `NewOpenAIAdapterFromConfig(config Config)` for backward compatibility during migration
- Language code converted to provider format via `language.ToProviderFormat(lang, providerName)`
- Log messages now include provider name for better debugging
- Added tests: `TestOpenAIAdapter_Creation`, `TestOpenAIAdapterFromConfig`
- Updated factory to use `NewOpenAIAdapterFromConfig` for now (will be updated in Task 15)
- All tests passing, typecheck passes
### Task 13: Remove redundant Groq and Mistral transcription adapters
- Deleted `internal/transcriber/adapter_groq_transcription.go`
- Deleted `internal/transcriber/adapter_mistral.go`
- KEPT `adapter_groq_translation.go` (uses CreateTranslation, different from CreateTranscription)
- Updated `transcriber.go` factory to use consolidated OpenAI adapter for groq-transcription and mistral-transcription
- Both now use `NewOpenAIAdapter` with their respective endpoints
- All tests passing, typecheck passes
### Task 14: Update ElevenLabs BatchAdapter to use EndpointConfig
- Refactored `internal/transcriber/adapter_elevenlabs.go` to use EndpointConfig
- New constructor: `NewElevenLabsAdapter(endpoint *EndpointConfig, apiKey, model, lang string)`
- Uses `endpoint.BaseURL + endpoint.Path` for URL (no hardcoded URL)
- Language converted via `language.ToProviderFormat(a.language, "elevenlabs")`
- Kept `xi-api-key` header for ElevenLabs-specific auth
- Added `NewElevenLabsAdapterFromConfig` for backward compatibility
- Updated factory to use `NewElevenLabsAdapterFromConfig`
- Updated tests for new constructor signature
- All tests passing, typecheck passes
### Task 15: Update transcriber factory to use Model metadata
- Refactored `NewTranscriber()` to look up Model via `provider.GetModel()`
- Added `mapConfigProviderToRegistryName()` to map config provider names (e.g., "groq-transcription") to registry names (e.g., "groq")
- Factory now switches on `model.AdapterType` instead of provider name
- Special case: "groq-translation" still uses dedicated `GroqTranslationAdapter` (uses CreateTranslation API)
- For "openai" adapter type: creates `OpenAIAdapter` with model's endpoint config
- For "elevenlabs" adapter type: creates `ElevenLabsAdapter` with model's endpoint config
- Streaming models return clear error: "streaming model %s not supported yet (coming soon)"
- Empty model now uses provider's default transcription model
- Added tests for streaming model rejection and unknown model error
- All tests passing, typecheck passes
### Task 17: Write tests for transcriber factory
- Tests already exist in `internal/transcriber/transcriber_test.go` from Task 15
- Verified test coverage:
- `TestNewTranscriber/valid_openai_config` - creates OpenAIAdapter for openai
- `TestNewTranscriber/valid_groq-transcription_config` - creates OpenAIAdapter for groq
- `TestNewTranscriber/valid_elevenlabs_config_with_scribe_v1` - creates ElevenLabsAdapter
- `TestNewTranscriber/unsupported_provider` - returns error for unknown provider
- `TestNewTranscriber/unknown_model_returns_error` - returns error for unknown model
- `TestNewTranscriber/streaming_model_returns_error` - returns error for streaming model
- `go test ./internal/transcriber/...` passes
- Typecheck passes
### Task 18: Create dependency checker for whisper-cli
- Created `internal/deps/deps.go`
- Status struct: Installed bool, Path string, Version string
- CheckWhisperCli() uses exec.LookPath, tries --version (whisper-cli doesn't support it, but handles gracefully)
- CheckFFmpeg() same pattern, version extraction works
- Both return Installed=false when binary not found, no errors thrown
- All tests passing, typecheck passes
### Task 19: Create whisper model info and download management
- Created `internal/models/whisper/models.go`
- ModelInfo struct: ID, Name, Filename, Size, SizeBytes, Multilingual
- 9 models: 4 english-only (tiny.en, base.en, small.en, medium.en) + 5 multilingual (tiny, base, small, medium, large-v3)
- GetModelsDir() returns `~/.local/share/hyprvoice/models/whisper/` (expanded)
- GetModelPath(name) returns full path to model file
- GetDownloadURL(name) returns HuggingFace URL
- GetModel(id) returns ModelInfo pointer
- ListModels(), ListMultilingualModels(), ListEnglishOnlyModels() helpers
- Created `internal/models/whisper/registry.go`
- IsInstalled(modelID) checks if model file exists
- ListInstalled() returns all installed model IDs
- Download(ctx, modelID, progressFn) downloads from HuggingFace with progress callback
- Remove(modelID) deletes model file
- GetInstalledPath(modelID) returns path or error if not installed
- Download uses temp file + rename for atomicity, respects context cancellation
- All tests passing, typecheck passes
### Task 20: Create WhisperCppAdapter implementing BatchAdapter
- Created `internal/transcriber/adapter_whisper_cpp.go`
- WhisperCppAdapter struct with modelPath, language, threads fields
- Constructor: `NewWhisperCppAdapter(modelPath, lang string, threads int)`
- Transcribe() implementation:
- Returns empty string for empty audio (no error)
- Checks whisper-cli exists via exec.LookPath
- Checks model file exists via os.Stat
- Converts raw PCM to WAV using existing convertToWAV helper
- Writes to temp file in os.TempDir() with unique timestamp
- Uses defer os.Remove(tmpFile) for cleanup
- Converts language via language.ToProviderFormat(lang, "whisper-cpp")
- Executes: whisper-cli -m {modelPath} -l {lang} -nt -np -f {tempfile}
- Adds -t {threads} flag if threads > 0
- Respects context cancellation
- Parses stdout for transcription text
- Created comprehensive test file adapter_whisper_cpp_test.go
- Tests: interface implementation, empty audio, missing model, language, threads, context cancellation
- All tests passing, typecheck passes
### Task 21: Create whisper-cpp Provider
- Created `internal/provider/whisper_cpp.go` implementing Provider interface
- Name() returns 'whisper-cpp', RequiresAPIKey() returns false, IsLocal() returns true
- Models() returns 9 whisper models from whisper.ListModels()
- English-only models (*.en) have SupportedLanguages=['en']
- Multilingual models have SupportedLanguages with all 57 language codes
- Each model has: Type=Transcription, AdapterType='whisper-cpp', Local=true, LocalInfo with Filename/Size/DownloadURL
- No Endpoint (local CLI, not HTTP)
- DefaultModel(Transcription) returns 'base.en'
- Registered in provider.init()
- Comprehensive test file created: whisper_cpp_test.go
- All tests passing, typecheck passes
### Task 22: Wire whisper-cpp into transcriber factory
- Added `case "whisper-cpp"` to NewTranscriber() switch on model.AdapterType
- Imports whisper package to get model path via `whisper.GetModelPath(config.Model)`
- Creates `NewWhisperCppAdapter(modelPath, config.Language, config.Threads)`
- Returns error if whisper model ID is unknown
- Added tests for whisper-cpp factory cases: valid config, no API key required, unknown model error
- All tests passing, typecheck passes
### Task 23: Update config for local transcription
- Added `applyThreadsDefault()` to config.Load() - sets Threads to max(1, NumCPU-1) when 0
- Added whisper-cpp case to config validation (no API key required)
- Validates whisper model names: tiny.en, base.en, small.en, medium.en, tiny, base, small, medium, large-v3
- Validates language codes for whisper-cpp same as other providers
- Note: Threads field, ToTranscriberConfig, and template were already done in Task 16
- Added comprehensive tests for whisper-cpp validation and threads auto-detection
- All tests passing, typecheck passes
### Task 24: Add model list CLI command
- Created `modelCmd()` returning cobra.Command with Use: 'model'
- Created `modelListCmd()` subcommand with Use: 'list'
- Added `--provider` flag to filter by provider name
- Added `--type` flag to filter by 'transcription' or 'llm'
- Iterates all providers sorted alphabetically, gets Models(), filters by type
- For local models: shows [x] if installed via whisper.IsInstalled(), [ ] if not
- Shows: Model ID, Description, [streaming] tag if applicable, [size] for local models
- Groups output by provider with headers
- All tests passing, typecheck passes
### Task 25: Add model download CLI command
- Created `modelDownloadCmd()` subcommand with Use: 'download <model-name>'
- Uses `provider.FindModelByID()` to search all providers for model
- Checks `model.NeedsDownload()` - if false, prints 'cloud model, does not require download'
- Checks `whisper.IsInstalled()` - if true, prints 'already installed at {path}'
- Downloads with progress callback showing percentage (10%, 20%, ...)
- Prints success message with full model path
- Tested: cloud model rejection, unknown model error, download with progress, already installed
- All tests passing, typecheck passes
### Task 26: Add model remove CLI command
- Created `modelRemoveCmd()` subcommand with Use: 'remove <model-name>'
- Uses `provider.FindModelByID()` to find model across all providers
- Cloud models: prints 'nothing to remove'
- Not installed: returns error 'model is not installed'
- Installed: calls `whisper.Remove()`, prints success message
- All verification scenarios tested, typecheck passes
### Task 27: Refactor TUI to use Model metadata for descriptions
- Refactored `getTranscriptionModelOptions()` to use `provider.ModelsOfType()` instead of hardcoded switch
- Added `currentLang` parameter to show language compatibility warnings
- Created `buildModelLabel()` helper: formats "Name (Description)", adds [size] for local, [streaming] for streaming models
- Created `mapConfigProviderToRegistry()` to map config provider names (groq-transcription, mistral-transcription) to registry names
- Refactored `getLLMModelOptions()` to use `provider.ModelsOfType()` instead of hardcoded switch
- Created `buildLLMModelLabel()` helper for LLM model formatting
- Added `getLangName()` helper to get human-readable language name from code
- Added language import to configure_transcription.go
- All tests passing, typecheck passes
### Task 28: Add local provider options to TUI with dependency check
- Added `LocalProviders` list and "whisper-cpp" to providerDisplayNames in configure.go
- Updated editTranscription() to show whisper-cpp option first
- Added deps.CheckWhisperCli() check to show warning if whisper-cli not installed
- Shows disabled option "(whisper-cli not found)" with install instructions when binary missing
- Local providers skip ensureProviderConfigured() (no API key needed)
- Updated getTranscriptionModelOptions() to show [x]/[ ] prefix for installed status
- Added download confirmation dialog after selecting uninstalled model
- Download shows progress percentage (10%, 20%, ...)
- All tests passing, typecheck passes
### Task 29: Add language picker to TUI using language package
- Created `internal/tui/languages.go` with `getLanguageOptions()` function
- Takes optional `*provider.Model` to show compatibility warnings for non-supported languages
- First option is "Auto-detect (Recommended)" with empty value
- Languages formatted as "Name - NativeName (code)" when native name differs
- English-only models (*.en) show "(not supported by current model)" for non-English languages
- Updated `editTranscription()` to use `huh.NewSelect` with `Filtering(true)` instead of text input
- Pass current model to `getLanguageOptions()` for compatibility warnings
- Language code saved to config, not display name
- All 57 languages + Auto = 58 options total
- All tests passing, typecheck passes
### Task 30: Add TUI validation for language-model compatibility on save
- Added validation check in `editTranscription()` before saving config
- Uses `provider.ValidateModelLanguage(registryName, selectedModel, selectedLanguage)`
- If validation fails: shows error with message and options (change model, select auto-detect, choose supported language)
- Shows confirm dialog "Try again?" - if yes, recursively calls `editTranscription()` to let user fix
- Config only saved AFTER validation passes (no save on cancel)
- Leverages existing `ValidateModelLanguage` which returns error with supported languages list
- All tests passing, typecheck passes
### Task 31: Create ElevenLabs StreamingAdapter
- Created `internal/transcriber/adapter_elevenlabs_streaming.go`
- Added gorilla/websocket dependency
- ElevenLabsStreamingAdapter struct: endpoint, apiKey, model, language, conn, resultsCh, mutex, ctx/cancel, WaitGroup
- Start(): connects to wss://api.elevenlabs.io/v1/speech-to-text/realtime with xi-api-key header
- Query params: model_id, language_code, audio_format=pcm_16000, commit_strategy=vad
- Language conversion via language.ToProviderFormat(lang, "elevenlabs")
- SendChunk(): sends input_audio_chunk JSON message with base64-encoded audio
- readLoop goroutine: parses session_started, partial_transcript, committed_transcript messages
- Handles all ElevenLabs error types (auth_error, quota_exceeded, rate_limited, etc.)
- Close(): cancels context, sends close frame, waits for reader goroutine
- Comprehensive tests with mock WebSocket server
- All tests passing with -race flag, typecheck passes
### Task 32: Add reconnection logic to ElevenLabs StreamingAdapter
- Added `maxRetries` (default 3) and `retryDelays` (1s, 2s, 4s) fields
- Created `connectLocked()` helper extracted from Start() for reuse
- Created `reconnect()` method with exponential backoff:
- Attempts up to maxRetries connections
- Waits retryDelays[i] between attempts
- Closes old connection before reconnecting
- Sends notification error to resultsCh on successful reconnect
- Updated `readLoop()` to call reconnect() on read errors
- Updated `SendChunk()` to call reconnect() on write errors, then retry chunk
- After max retries exhausted, sends final error and closes channel
- Added tests: ReconnectOnReadError, ReconnectNotifiesClient, MaxRetriesExhausted, ReconnectExponentialBackoff
- All tests passing with -race flag, typecheck passes
### Task 33: Create Deepgram Provider
- Created `internal/provider/deepgram.go` implementing Provider interface
- Researched Deepgram docs: Nova-3 and Nova-2 are main models, both streaming-only
- Models: nova-3, nova-3-general, nova-2, nova-2-general (all Streaming=true)
- Nova-3 supports 42 languages from our list (ar, be, bs, bg, ca, hr, cs, da, nl, en, et, fi, fr, de, el, hi, hu, id, it, ja, kn, ko, lv, lt, mk, ms, mr, no, pl, pt, ro, ru, sr, sk, sl, es, sv, tl, ta, tr, uk, vi)
- Nova-2 supports 33 languages (subset of nova-3)
- All models have AdapterType='deepgram', Endpoint.BaseURL='wss://api.deepgram.com'
- DefaultModel(Transcription) returns 'nova-3'
- Registered in provider.init()
- Comprehensive test file created: deepgram_test.go
- All tests passing, typecheck passes
### Task 34: Create Deepgram StreamingAdapter
- Created `internal/transcriber/adapter_deepgram.go`
- DeepgramAdapter struct: endpoint, apiKey, model, language, conn, resultsCh, mutex, ctx/cancel, WaitGroup
- Start(): connects to wss://api.deepgram.com/v1/listen with Authorization: Token header
- Query params: model, language, encoding=linear16, sample_rate=16000, channels=1, interim_results=true, smart_format=true, punctuate=true
- Language conversion via language.ToProviderFormat(lang, "deepgram")
- SendChunk(): sends raw binary audio (websocket.BinaryMessage, not base64 like ElevenLabs)
- readLoop goroutine: parses Metadata, Results (interim + final), Error, UtteranceEnd, SpeechStarted messages
- Close(): cancels context, sends close frame, waits for reader goroutine
- Added reconnection logic (maxRetries=3, exponential backoff 1s, 2s, 4s) matching ElevenLabs pattern
- Comprehensive tests with mock WebSocket server
- All tests passing with -race flag, typecheck passes
### Task 35: Add reconnection logic to Deepgram StreamingAdapter
- Verified reconnection logic already in place from Task 34
- maxRetries=3 with retryDelays [1s, 2s, 4s] (exponential backoff)
- reconnect() method attempts re-establish with backoff, respects context cancellation
- readLoop calls reconnect() on read errors, readLoop calls reconnect() after failed reads
- SendChunk() calls reconnect() on write errors and retries the chunk
- Sends notification error to resultsCh on successful reconnect
- All tests passing with -race flag, typecheck passes
### Task 36: Add OpenAI Realtime model to OpenAI provider
- Added `gpt-4o-realtime-preview` model to OpenAI provider's Models()
- Type=Transcription, Streaming=true, AdapterType='openai-realtime'
- Endpoint.BaseURL='wss://api.openai.com', Path='/v1/realtime'
- SupportedLanguages=language.AllLanguageCodes() (all 57 languages)
- DefaultModel(Transcription) unchanged (still returns 'whisper-1')
- Added TestOpenAIRealtimeModel test verifying all properties
- Updated TestModelsOfType to expect 4 transcription models for OpenAI
- All tests passing, typecheck passes
### Task 37: Create OpenAI Realtime StreamingAdapter
- Created `internal/transcriber/adapter_openai_realtime.go`
- OpenAIRealtimeAdapter struct: endpoint, apiKey, model, language, conn, resultsCh, mu, ctx/cancel, WaitGroup
- Start(): connects to wss://api.openai.com/v1/realtime?model=X with Bearer auth and OpenAI-Beta header
- Sends session.update to configure transcription-only mode (modalities=['text'], input_audio_format='pcm16')
- Enables input_audio_transcription with gpt-4o-transcribe model
- Uses server_vad turn detection for automatic speech detection
- SendChunk(): resamples audio from 16kHz to 24kHz, sends input_audio_buffer.append with base64 audio
- readLoop goroutine: parses conversation.item.input_audio_transcription.delta (partial) and .completed (final)
- Handles error events, speech_started, speech_stopped, session events
- Close(): cancels context, sends close frame, waits for reader goroutine
- Added resample16to24() for 16kHz to 24kHz PCM conversion using linear interpolation
- Added reconnection logic (maxRetries=3, exponential backoff 1s, 2s, 4s) - same pattern as ElevenLabs/Deepgram
- Comprehensive tests with mock WebSocket server
- All tests passing with -race flag, typecheck passes
### Task 38: Add reconnection logic to OpenAI Realtime StreamingAdapter
- Implemented as part of Task 37 (same commit)
- maxRetries=3 with retryDelays [1s, 2s, 4s] (exponential backoff)
- reconnect() method re-establishes connection and calls configureSession()
- readLoop calls reconnect() on read errors
- SendChunk() calls reconnect() on write errors and retries the chunk
- Sends notification error to resultsCh on successful reconnect
- Context cancellation stops reconnection attempts (checked in reconnect loop)
- TestOpenAIRealtimeAdapter_Reconnection verifies behavior
- All tests passing with -race flag, typecheck passes
### Task 39: Update factory to create streaming transcribers
- Updated `NewTranscriber()` in internal/transcriber/transcriber.go
- Added streaming model check: `if model.Streaming {...}`
- For streaming models, creates appropriate StreamingAdapter based on AdapterType:
- `elevenlabs-streaming` -> `NewElevenLabsStreamingAdapter()`
- `deepgram` -> `NewDeepgramAdapter()`
- `openai-realtime` -> `NewOpenAIRealtimeAdapter()`
- Wraps streaming adapter in `NewStreamingTranscriber(adapter, config.Language)`
- Updated tests: streaming models now succeed (not error)
- Added tests for deepgram and openai-realtime streaming models
- All tests passing with -race flag, typecheck passes
### Task 43: Add DEEPGRAM_API_KEY env var support
- Added `case "deepgram"` to `resolveAPIKeyForProvider()` in convert.go
- Maps to providerName="deepgram" and envVar="DEEPGRAM_API_KEY"
- Updated config template in save.go with commented deepgram section
- Added "deepgram" to AllProviders and providerDisplayNames in configure.go for TUI
- All tests passing, typecheck passes
### Task 40: Write tests for streaming adapters
- Tests already existed in comprehensive form across multiple files (implemented with tasks 31-39)
- Verified test coverage in:
- `adapter_elevenlabs_streaming_test.go` (744 lines): Start, SendChunk, Results, Error, Language, Close, Reconnect logic
- `adapter_deepgram_test.go` (435 lines): Creation, URL building, Results, Binary audio, Errors, Context
- `adapter_openai_realtime_test.go` (545 lines): Start, SendChunk, Transcription, Errors, Reconnect, Close, resample
- `transcriber_test.go` (StreamingTranscriber tests): Accumulation, Errors, Context cancellation, Concurrent access
- Tests verify:
- StreamingTranscriber accumulates final results (TestStreamingTranscriber_AccumulatesResults)
- Error handling (TestStreamingTranscriber_HandlesErrors, adapter error tests)
- Context cancellation (TestStreamingTranscriber_ContextCancellation, TestDeepgramAdapter_ContextCancellation)
- Concurrent GetFinalTranscription safety (TestStreamingTranscriber_GetFinalTranscriptionSafe)
- Reconnection logic with exponential backoff (multiple reconnect tests)
- Close cleanup (TestElevenLabsStreamingAdapter_Close, TestOpenAIRealtimeAdapter_Close)
- `go test -race ./internal/transcriber/...` passes with no race conditions
- All tests passing, typecheck passes
### Task 41: Update config validation to use provider registry
- Refactored `internal/config/validate.go` to use provider registry
- Added `mapConfigProviderToRegistryName()` to map config names to registry names
- Added `envVarForProvider()` helper for error messages
- Provider validation now uses `provider.GetProvider()` instead of hardcoded switch
- Model validation now uses `provider.GetModel()` to verify model exists
- API key validation uses `p.RequiresAPIKey()` - local providers (whisper-cpp) skip this check
- Language validation: warns for unrecognized codes (log.Printf) but doesn't error
- Added `ValidateModelLanguageCompatibility(registryProvider, modelID, langCode)`:
- Returns nil for auto language ("")
- Checks `model.SupportsLanguage(langCode)`
- Returns error with model name, language, and truncated list of supported languages
- LLM validation also refactored to use registry
- Removed old hardcoded `isValidLanguageCode()` function
- Updated tests: replaced TestIsValidLanguageCode with TestValidateModelLanguageCompatibility
- 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
### Task 44: Update README with new architecture
- Updated Features section: added local transcription, streaming, 57 language support, Deepgram Nova
- Added "## Local Transcription" section with whisper.cpp setup, model table, configuration example
- Added "## Streaming Transcription" section with provider table, config examples
- Added "Model Management" subsection under Quick Reference with `hyprvoice model list/download/remove`
- Updated provider list: OpenAI, Groq, Mistral, ElevenLabs, Deepgram + whisper.cpp
- Updated Development Status table: all items now complete (local, streaming, model mgmt, language validation)
- Updated Project Structure: added deps/, language/, models/whisper/ packages
- Updated File Locations: added models directory path
- Typecheck passes
### Task 45: Create docs/providers.md comparison guide
- Created comprehensive provider comparison documentation
- Transcription providers table: OpenAI, Groq, Mistral, ElevenLabs, Deepgram, whisper-cpp with Type/Models/Languages/Streaming/Speed/Quality/Cost
- Individual provider sections with models list and "Best for" recommendations
- LLM providers table: OpenAI and Groq models
- Decision flowchart for choosing a provider (privacy -> streaming -> speed -> accuracy)
- Quick recommendations table for common use cases
- Language support section: full 57-language list, English-only models clearly marked, Deepgram subset languages
- Streaming vs Batch explanation with use cases
- Local vs Cloud comparison with pros/cons and when-to-choose guidelines
- Typecheck passes