# 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 ' - 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 ' - 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