Files
hyprvoice/progress.txt
T

323 lines
19 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