diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc new file mode 100644 index 0000000..1aea892 --- /dev/null +++ b/tasks/prd.jsonc @@ -0,0 +1,1088 @@ +{ + "project": "Hyprvoice Model Architecture Overhaul", + "description": "Refactor to Model as first-class entity with metadata, two adapter types (BatchAdapter/StreamingAdapter), consolidated adapter implementations, local transcription via whisper-cpp, streaming transcription, new cloud providers, and full language-model compatibility validation", + "tasks": [ + // ============================================================================ + // PHASE 1: FOUNDATION + // Model as first-class entity, language handling, adapter interfaces + // ============================================================================ + { + "title": "Create language package with core types and helpers", + "steps": [ + "Create internal/language/language.go", + "Define Language struct: Code string, Name string, NativeName string", + "Define Auto constant: Language{Code: '', Name: 'Auto-detect', NativeName: ''} - represents auto-detection", + "Implement FromCode(code string) Language - returns Auto if not found", + "Implement List() []Language - returns all supported languages", + "Implement Codes() []string - returns all language codes", + "Implement AllLanguageCodes() []string - alias for Codes(), used by models that support everything", + "Implement IsValidCode(code string) bool - returns true if code is known (including '' for auto)" + ], + "verify": [ + "FromCode('en') returns Language{Code: 'en', Name: 'English', NativeName: 'English'}", + "FromCode('invalid') returns Auto", + "IsValidCode('en') returns true", + "IsValidCode('invalid') returns false", + "IsValidCode('') returns true (auto is valid)", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add language list and provider-specific mappings", + "steps": [ + "Update internal/language/language.go with full language list", + "Master language list derived from OpenAI Whisper's 57 supported languages (source: https://platform.openai.com/docs/guides/speech-to-text#supported-languages)", + "Add all 57 languages: af/Afrikaans, ar/Arabic/العربية, hy/Armenian/Հայdelays, az/Azerbaijani/Azərbaycan, be/Belarusian/Беларуская, bs/Bosnian/Bosanski, bg/Bulgarian/Български, ca/Catalan/Català, zh/Chinese/中文, hr/Croatian/Hrvatski, cs/Czech/Čeština, da/Danish/Dansk, nl/Dutch/Nederlands, en/English, et/Estonian/Eesti, fi/Finnish/Suomi, fr/French/Français, gl/Galician/Galego, de/German/Deutsch, el/Greek/Ελληνικά, he/Hebrew/עברית, hi/Hindi/हिन्दी, hu/Hungarian/Magyar, is/Icelandic/Íslenska, id/Indonesian/Bahasa Indonesia, it/Italian/Italiano, ja/Japanese/日本語, kn/Kannada/ಕನ್ನಡ, kk/Kazakh/Қазақ, ko/Korean/한국어, lv/Latvian/Latviešu, lt/Lithuanian/Lietuvių, mk/Macedonian/Македонски, ms/Malay/Bahasa Melayu, mr/Marathi/मराठी, mi/Maori/Māori, ne/Nepali/नेपाली, no/Norwegian/Norsk, fa/Persian/فارسی, pl/Polish/Polski, pt/Portuguese/Português, ro/Romanian/Română, ru/Russian/Русский, sr/Serbian/Српски, sk/Slovak/Slovenčina, sl/Slovenian/Slovenščina, es/Spanish/Español, sw/Swahili/Kiswahili, sv/Swedish/Svenska, tl/Tagalog, ta/Tamil/தமிழ், th/Thai/ไทย, tr/Turkish/Türkçe, uk/Ukrainian/Українська, ur/Urdu/اردو, vi/Vietnamese/Tiếng Việt, cy/Welsh/Cymraeg", + "Implement ToProviderFormat(code string, providerName string) string - maps our code to provider-specific format", + "Provider mappings: whisper-cpp uses 'en'/'auto', some APIs use 'english', Deepgram uses 'en-US'", + "Each provider can handle Auto ('') differently via ToProviderFormat" + ], + "verify": [ + "List() returns 57 languages", + "Codes() returns []string of all 57 codes", + "AllLanguageCodes() returns all 57 codes for use by models", + "ToProviderFormat('en', 'whisper-cpp') returns 'en'", + "ToProviderFormat('en', 'deepgram') returns 'en-US'", + "ToProviderFormat('', 'openai') returns '' or appropriate auto value", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create Model type with full metadata", + "steps": [ + "Create internal/provider/model.go", + "Define ModelType enum: Transcription, LLM", + "Define Model struct with fields: ID string, Name string, Description string, Type ModelType, Streaming bool, Local bool, AdapterType string", + "Add SupportedLanguages []string field to Model - ALWAYS an explicit list of language codes, never nil", + "For models supporting all languages, use language.AllLanguageCodes() to populate the full list", + "For English-only models, use []string{'en'}", + "Each provider task must research API docs to determine exact supported languages", + "Define EndpointConfig struct: BaseURL string, Path string", + "Define LocalModelInfo struct: Filename string, Size string, DownloadURL string", + "Add Endpoint *EndpointConfig and LocalInfo *LocalModelInfo optional fields to Model", + "Add helper method Model.NeedsDownload() bool - returns LocalInfo != nil", + "Add helper method Model.IsStreaming() bool - returns Streaming field", + "Add helper method Model.SupportsLanguage(code string) bool - returns true if code is in SupportedLanguages OR code is '' (auto always allowed)", + "Add helper method Model.SupportsAllLanguages() bool - returns len(SupportedLanguages) == len(language.AllLanguageCodes())" + ], + "verify": [ + "Model struct has all fields: ID, Name, Description, Type, Streaming, Local, AdapterType, SupportedLanguages, Endpoint, LocalInfo", + "ModelType has Transcription and LLM constants", + "EndpointConfig has BaseURL and Path", + "LocalModelInfo has Filename, Size, DownloadURL", + "NeedsDownload() returns true when LocalInfo is set", + "SupportsLanguage('en') returns true for multilingual model", + "SupportsLanguage('es') returns false for English-only model with SupportedLanguages=['en']", + "SupportsLanguage('') returns true always (auto is always supported)", + "SupportsAllLanguages() returns true when model has all 57 languages", + "SupportsAllLanguages() returns false for English-only model", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Refactor Provider interface to return Models", + "steps": [ + "Update internal/provider/provider.go Provider interface", + "Replace TranscriptionModels() []string and LLMModels() []string with Models() []Model", + "Replace DefaultTranscriptionModel() and DefaultLLMModel() with DefaultModel(t ModelType) string", + "Keep: Name() string, RequiresAPIKey() bool, ValidateAPIKey(key string) bool", + "Add: IsLocal() bool method", + "Add package-level helper: GetModel(providerName, modelID string) (*Model, error)", + "Add package-level helper: ModelsOfType(p Provider, t ModelType) []Model", + "Add package-level helper: FindModelByID(modelID string) (*Model, Provider, error) - searches all providers", + "Add package-level helper: ModelsForLanguage(p Provider, t ModelType, langCode string) []Model - returns models that support given language (checks model.SupportsLanguage)", + "Add package-level helper: ValidateModelLanguage(providerName, modelID, langCode string) error - returns error with list of supported languages if model doesn't support the language", + "Update registry functions to work with new interface" + ], + "verify": [ + "Provider interface has Models() []Model method", + "Provider interface has DefaultModel(t ModelType) string method", + "Provider interface has IsLocal() bool method", + "GetModel returns correct model or error if not found", + "ModelsOfType filters models by type", + "FindModelByID finds model across all providers", + "ModelsForLanguage returns only models supporting given language", + "ModelsForLanguage with '' (auto) returns all models (auto always supported)", + "ValidateModelLanguage returns error listing supported languages for unsupported language", + "ValidateModelLanguage returns nil for '' (auto) on any model", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Define BatchAdapter and StreamingAdapter interfaces", + "steps": [ + "Update internal/transcriber/transcriber.go", + "Rename TranscriptionAdapter to BatchAdapter", + "Keep BatchAdapter interface: Transcribe(ctx context.Context, audioData []byte) (string, error)", + "Create internal/transcriber/streaming.go", + "Define StreamingAdapter interface: Start(ctx context.Context, language string) error, SendChunk(audio []byte) error, Results() <-chan TranscriptionResult, Close() error", + "Define TranscriptionResult struct: Text string, IsFinal bool, Error error", + "Both adapter types are used by Transcriber implementations (SimpleTranscriber, StreamingTranscriber)" + ], + "verify": [ + "BatchAdapter interface exists with Transcribe method", + "StreamingAdapter interface exists with Start, SendChunk, Results, Close methods", + "TranscriptionResult has Text, IsFinal, Error fields", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create StreamingTranscriber wrapper", + "steps": [ + "Create internal/transcriber/streaming_transcriber.go", + "Define StreamingTranscriber struct: adapter StreamingAdapter, finalText strings.Builder, mu sync.Mutex, ctx context.Context, cancel context.CancelFunc", + "Implement Start(ctx, frameCh <-chan recording.AudioFrame) (<-chan error, error)", + "Create internal cancelable context from parent ctx for coordinated shutdown", + "Goroutine 1: call adapter.Start(), loop reading frames with select on ctx.Done(), call adapter.SendChunk()", + "Goroutine 2: read from adapter.Results() with select on ctx.Done(), use mutex when writing to finalText builder", + "Use sync.WaitGroup to track goroutine completion", + "Implement Stop(ctx) error - call cancel(), wait for WaitGroup, then call adapter.Close()", + "Handle context cancellation gracefully - don't treat as error, complete with partial results", + "Implement GetFinalTranscription() (string, error) - lock mutex, return finalText.String()" + ], + "verify": [ + "StreamingTranscriber implements Transcriber interface", + "Start() begins streaming audio to adapter", + "Stop() returns final accumulated text", + "GetFinalTranscription() returns complete text", + "Context cancellation stops all goroutines cleanly", + "No race conditions (run with -race flag)", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Write tests for Model, Provider, and interfaces", + "steps": [ + "Create internal/provider/model_test.go", + "Test Model.NeedsDownload() returns true when LocalInfo set, false when nil", + "Test Model.IsStreaming() returns correct value", + "Test Model.SupportsLanguage('en') returns true for model with SupportedLanguages containing 'en'", + "Test Model.SupportsLanguage('es') returns false for English-only model with SupportedLanguages=['en']", + "Test Model.SupportsLanguage('') returns true for any model (auto always supported)", + "Test Model.SupportsAllLanguages() returns true when model has all 57 languages", + "Test Model.SupportsAllLanguages() returns false when model has subset of languages", + "Create internal/provider/provider_test.go", + "Test GetModel returns correct model for valid provider+model", + "Test GetModel returns error for unknown provider", + "Test GetModel returns error for unknown model", + "Test ModelsOfType filters correctly", + "Test FindModelByID finds model in any provider", + "Test ModelsForLanguage returns only compatible models", + "Test ModelsForLanguage with '' (auto) returns all models", + "Test ValidateModelLanguage returns error with supported languages list for incompatible language", + "Test ValidateModelLanguage returns nil for auto on any model" + ], + "verify": [ + "go test ./internal/provider/... passes", + "Model helper methods tested including explicit language support", + "GetModel edge cases tested", + "Language validation helpers tested with proper error messages", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 2: MIGRATE PROVIDERS TO NEW MODEL STRUCTURE + // ============================================================================ + { + "title": "Migrate OpenAI provider to new Model structure", + "steps": [ + "Update internal/provider/openai.go to implement new Provider interface", + "Research OpenAI API docs (https://platform.openai.com/docs/guides/speech-to-text#supported-languages) for exact language support", + "Implement Models() returning []Model with: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe (transcription), gpt-4o-mini, gpt-4o (LLM)", + "Each model has: ID, Name, Description, Type, AdapterType='openai', Endpoint with BaseURL='https://api.openai.com' and appropriate Path", + "Set SupportedLanguages=language.AllLanguageCodes() for whisper-1 (supports all 57 languages - this IS the source list)", + "Set SupportedLanguages=language.AllLanguageCodes() for gpt-4o-transcribe and gpt-4o-mini-transcribe (multilingual per docs)", + "LLM models: set SupportedLanguages=language.AllLanguageCodes() (LLMs are language-agnostic for prompting)", + "Implement DefaultModel(t ModelType) - returns 'whisper-1' for Transcription, 'gpt-4o-mini' for LLM", + "Implement IsLocal() returning false", + "Remove old TranscriptionModels(), LLMModels(), DefaultTranscriptionModel(), DefaultLLMModel() methods" + ], + "verify": [ + "OpenAIProvider.Models() returns 5 models with correct metadata", + "Each model has AdapterType='openai'", + "Each model has Endpoint with BaseURL and Path", + "All transcription models have SupportedLanguages with 57 language codes", + "whisper-1.SupportsAllLanguages() returns true", + "DefaultModel(Transcription) returns 'whisper-1'", + "DefaultModel(LLM) returns 'gpt-4o-mini'", + "IsLocal() returns false", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Migrate Groq provider to new Model structure", + "steps": [ + "Update internal/provider/groq.go to implement new Provider interface", + "Research Groq API docs (https://console.groq.com/docs/speech-to-text) for exact language support per model", + "Implement Models() returning transcription models: whisper-large-v3, whisper-large-v3-turbo, distil-whisper-large-v3-en", + "Add LLM models: llama-3.3-70b-versatile, llama-3.1-8b-instant, mixtral-8x7b-32768", + "All models use AdapterType='openai' (Groq is OpenAI-compatible)", + "Set Endpoint.BaseURL='https://api.groq.com/openai' for all models", + "Set SupportedLanguages=language.AllLanguageCodes() for whisper-large-v3 and whisper-large-v3-turbo (uses Whisper, same 57 languages)", + "Set SupportedLanguages=[]string{'en'} for distil-whisper-large-v3-en (English only - fastest but single language)", + "LLM models: set SupportedLanguages=language.AllLanguageCodes() (language-agnostic)", + "Implement DefaultModel(t ModelType) appropriately", + "Remove old methods" + ], + "verify": [ + "GroqProvider.Models() returns 6 models", + "All models have AdapterType='openai'", + "All models have Endpoint.BaseURL='https://api.groq.com/openai'", + "whisper-large-v3.SupportedLanguages has 57 codes", + "whisper-large-v3.SupportsAllLanguages() returns true", + "distil-whisper-large-v3-en.SupportedLanguages == ['en']", + "distil-whisper-large-v3-en.SupportsLanguage('es') returns false", + "distil-whisper-large-v3-en.SupportsLanguage('en') returns true", + "distil-whisper-large-v3-en.SupportsLanguage('') returns true (auto always supported)", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Migrate Mistral provider to new Model structure", + "steps": [ + "Update internal/provider/mistral.go to implement new Provider interface", + "Research Mistral API docs (https://docs.mistral.ai/) for exact Voxtral language support", + "Implement Models() returning transcription models: voxtral-mini-latest, voxtral-mini-2507", + "All models use AdapterType='openai' (Mistral transcription is OpenAI-compatible)", + "Set Endpoint.BaseURL='https://api.mistral.ai'", + "Set SupportedLanguages based on Voxtral docs - if docs list specific languages, use that list; if 'multilingual' use language.AllLanguageCodes()", + "Implement DefaultModel(t ModelType)", + "Remove old methods" + ], + "verify": [ + "MistralProvider.Models() returns 2 transcription models", + "All models have AdapterType='openai'", + "All models have explicit SupportedLanguages list (researched from docs)", + "Endpoint.BaseURL is 'https://api.mistral.ai'", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Migrate ElevenLabs provider to new Model structure", + "steps": [ + "Update internal/provider/elevenlabs.go to implement new Provider interface", + "Research ElevenLabs API docs (https://elevenlabs.io/docs/api-reference/speech-to-text) for exact Scribe language support", + "Implement Models() returning: scribe_v1, scribe_v2 (batch), scribe_v1-streaming, scribe_v2-streaming (streaming)", + "Batch models: AdapterType='elevenlabs', Streaming=false", + "Streaming models: AdapterType='elevenlabs-streaming', Streaming=true", + "Set Endpoint.BaseURL='https://api.elevenlabs.io'", + "Set SupportedLanguages to explicit list from ElevenLabs docs (reportedly 32 languages - get exact codes)", + "If ElevenLabs supports languages not in our master list, only include ones we have (intersection with language.AllLanguageCodes())", + "Implement DefaultModel(t ModelType) - returns 'scribe_v1'", + "Remove old methods" + ], + "verify": [ + "ElevenLabsProvider.Models() returns 4 models", + "scribe_v1 and scribe_v2 have Streaming=false, AdapterType='elevenlabs'", + "scribe_v1-streaming and scribe_v2-streaming have Streaming=true, AdapterType='elevenlabs-streaming'", + "All models have explicit SupportedLanguages list from docs (subset of our 57)", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 3: CONSOLIDATE BATCH ADAPTER IMPLEMENTATIONS + // Reduce duplication: OpenAI adapter handles OpenAI/Groq/Mistral + // ============================================================================ + { + "title": "Create consolidated OpenAI-compatible BatchAdapter", + "steps": [ + "Refactor internal/transcriber/adapter_openai.go to be configurable", + "Rename to OpenAICompatibleAdapter or keep as OpenAIAdapter", + "Constructor takes: endpoint EndpointConfig, apiKey string, model string, language string, keywords []string", + "Remove hardcoded base URL, use endpoint.BaseURL + endpoint.Path", + "Use language.ToProviderFormat(language, 'openai') for language parameter", + "Keep same HTTP request logic (multipart form, Authorization: Bearer header)", + "Keep same response parsing" + ], + "verify": [ + "OpenAIAdapter constructor accepts EndpointConfig", + "Adapter uses endpoint.BaseURL from config, not hardcoded", + "Language converted to provider format", + "Transcribe() works with OpenAI endpoint", + "Transcribe() works with Groq endpoint (different BaseURL)", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Remove redundant Groq and Mistral transcription adapters", + "steps": [ + "Delete internal/transcriber/adapter_groq_transcription.go (functionality merged into OpenAIAdapter)", + "Delete internal/transcriber/adapter_groq_translation.go or keep if translation is different", + "Delete internal/transcriber/adapter_mistral.go (functionality merged into OpenAIAdapter)", + "Update any imports that referenced these files", + "If groq-translation has different logic, keep as separate adapter with AdapterType='groq-translation'" + ], + "verify": [ + "adapter_groq_transcription.go is deleted", + "adapter_mistral.go is deleted", + "No broken imports", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Update ElevenLabs BatchAdapter to use EndpointConfig", + "steps": [ + "Update internal/transcriber/adapter_elevenlabs.go", + "Constructor takes: endpoint EndpointConfig, apiKey string, model string, language string", + "Use endpoint.BaseURL + endpoint.Path instead of hardcoded URL", + "Use language.ToProviderFormat(language, 'elevenlabs') for language parameter", + "Keep ElevenLabs-specific request format (different headers, body structure)", + "Keep ElevenLabs-specific response parsing" + ], + "verify": [ + "ElevenLabsAdapter constructor accepts EndpointConfig", + "Uses endpoint config for URL", + "Language converted to provider format", + "Still uses xi-api-key header (ElevenLabs-specific)", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Update transcriber factory to use Model metadata", + "steps": [ + "Update internal/transcriber/transcriber.go NewTranscriber()", + "Import provider package", + "Lookup model via provider.GetModel(config.Provider, config.Model)", + "Get adapter type from model.AdapterType", + "Get endpoint from model.Endpoint (may be nil for local)", + "Switch on adapterType instead of provider name", + "For 'openai': create OpenAIAdapter with model.Endpoint, config.APIKey, model.ID, config.Language, config.Keywords", + "For 'elevenlabs': create ElevenLabsAdapter with model.Endpoint", + "For streaming models (model.Streaming=true): return error for now (implemented later)", + "Remove old provider name switch cases" + ], + "verify": [ + "Factory looks up Model from provider", + "Factory switches on model.AdapterType", + "OpenAI, Groq, Mistral all create OpenAIAdapter with different endpoints", + "ElevenLabs creates ElevenLabsAdapter", + "Streaming models return clear error until implemented", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Update config.ToTranscriberConfig to work with new architecture", + "steps": [ + "Update internal/config/convert.go ToTranscriberConfig()", + "Keep existing fields: Provider, APIKey, Language, Model, Keywords", + "The factory will use provider.GetModel() to get endpoint config", + "Config doesn't need to know about endpoints - that's the factory's job", + "Ensure language is stored as our canonical code (e.g., 'en'), adapter converts to provider format", + "Add Threads field for local providers" + ], + "verify": [ + "ToTranscriberConfig returns all needed fields", + "Language stored as canonical code", + "Threads field included", + "Config doesn't import provider package (factory does)", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Write tests for transcriber factory", + "steps": [ + "Create internal/transcriber/transcriber_test.go", + "Test NewTranscriber creates OpenAIAdapter for openai provider", + "Test NewTranscriber creates OpenAIAdapter for groq provider (same adapter, different endpoint)", + "Test NewTranscriber creates ElevenLabsAdapter for elevenlabs provider", + "Test NewTranscriber returns error for unknown provider", + "Test NewTranscriber returns error for unknown model", + "Test NewTranscriber returns error for streaming model (until implemented)" + ], + "verify": [ + "go test ./internal/transcriber/... passes", + "Factory creates correct adapters for each provider", + "Error cases handled", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 4: LOCAL TRANSCRIPTION (whisper-cpp) + // ============================================================================ + { + "title": "Create dependency checker for whisper-cli", + "steps": [ + "Create internal/deps/deps.go", + "Define Status struct: Installed bool, Path string, Version string", + "Implement CheckWhisperCli() Status - uses exec.LookPath for 'whisper-cli'", + "If found, try to get version via 'whisper-cli --version' or similar", + "Return Status with Installed=false if not found (no error)", + "Add CheckFFmpeg() Status for audio conversion dependency" + ], + "verify": [ + "CheckWhisperCli() returns Installed=true and Path when whisper-cli exists", + "CheckWhisperCli() returns Installed=false when not in PATH", + "No errors thrown, just returns status", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create whisper model info and download management", + "steps": [ + "Create internal/models/whisper/models.go", + "Define available models as data: tiny.en (75MB), base.en (142MB), small.en (466MB), medium.en (1.5GB), tiny, base, small, medium, large-v3 (3GB)", + "Implement GetModelsDir() string - returns ~/.local/share/hyprvoice/models/whisper/", + "Implement GetModelPath(name string) string - returns full path to model file", + "Create internal/models/whisper/registry.go", + "Implement IsInstalled(name string) bool", + "Implement ListInstalled() []string", + "Implement Download(name string, onProgress func(downloaded, total int64)) error - downloads from HuggingFace", + "Implement Remove(name string) error", + "Download URL: https://huggingface.co/ggerganov/whisper.cpp/resolve/main/{filename}" + ], + "verify": [ + "GetModelsDir() returns expanded path (no ~)", + "GetModelPath('base.en') returns correct path", + "IsInstalled returns false for non-existent model", + "Download creates directory if needed and downloads with progress", + "Remove deletes the model file", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create WhisperCppAdapter implementing BatchAdapter", + "steps": [ + "Create internal/transcriber/adapter_whisper_cpp.go", + "Define WhisperCppAdapter struct: modelPath string, language string, threads int", + "Constructor takes these fields directly (no EndpointConfig since it's local CLI)", + "Use language.ToProviderFormat(language, 'whisper-cpp') for language parameter", + "Implement Transcribe(ctx context.Context, audioData []byte) (string, error)", + "Write audio to temp WAV file (use existing convertToWAV helper)", + "Execute: whisper-cli -m {modelPath} -l {language} -t {threads} -nt -np -f {tempfile}", + "Parse stdout for transcription text", + "Clean up temp file in defer", + "Return clear error if whisper-cli not found" + ], + "verify": [ + "WhisperCppAdapter implements BatchAdapter interface", + "Returns 'whisper-cli not found' error when binary missing", + "Returns error if model file missing", + "Language converted to whisper-cpp format", + "Cleans up temp files", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create whisper-cpp Provider", + "steps": [ + "Create internal/provider/whisper_cpp.go implementing Provider interface", + "Name() returns 'whisper-cpp'", + "RequiresAPIKey() returns false", + "IsLocal() returns true", + "Models() returns all whisper models with: Type=Transcription, AdapterType='whisper-cpp', Local=true", + "Set SupportedLanguages=[]string{'en'} for English-only models: tiny.en, base.en, small.en, medium.en", + "Set SupportedLanguages=language.AllLanguageCodes() for multilingual models: tiny, base, small, medium, large-v3 (same 57 languages as OpenAI Whisper)", + "Each model has LocalInfo with Filename, Size, DownloadURL", + "No Endpoint (local CLI, not HTTP)", + "DefaultModel(Transcription) returns 'base.en'", + "Register in provider.init()" + ], + "verify": [ + "provider.GetProvider('whisper-cpp') returns WhisperCppProvider", + "Models() returns 9 whisper models", + "Each model has Local=true and LocalInfo set", + "Each model has AdapterType='whisper-cpp'", + "English-only models (*.en) have SupportedLanguages=['en']", + "Multilingual models have SupportedLanguages with 57 codes", + "base.en.SupportsLanguage('es') returns false", + "base.en.SupportsLanguage('en') returns true", + "base.SupportsLanguage('es') returns true", + "base.SupportsAllLanguages() returns true", + "RequiresAPIKey() returns false", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Wire whisper-cpp into transcriber factory", + "steps": [ + "Update internal/transcriber/transcriber.go NewTranscriber()", + "Add case for AdapterType='whisper-cpp'", + "For whisper-cpp: get model path from whisper.GetModelPath(config.Model)", + "Create WhisperCppAdapter with modelPath, language, threads (from config, default 4)", + "Add Threads field to transcriber.Config struct", + "Update config.ToTranscriberConfig() to pass Threads from config" + ], + "verify": [ + "Factory creates WhisperCppAdapter for whisper-cpp models", + "Model path resolved from model name", + "Threads passed to adapter", + "Full flow works: config -> factory -> adapter -> transcription", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Update config for local transcription", + "steps": [ + "Add Threads int field to TranscriptionConfig in internal/config/types.go", + "Update config.Load() to detect CPU cores via runtime.NumCPU() and set Threads to max(1, NumCPU-1) to leave one core free", + "Only apply default if Threads is 0 (not explicitly set)", + "Update config validation to accept whisper-cpp provider without API key", + "Update config.ToTranscriberConfig() to include Threads", + "Update config template in save.go with threads field and comment explaining auto-detection" + ], + "verify": [ + "TranscriptionConfig has Threads field", + "Default Threads is runtime.NumCPU()-1 (minimum 1)", + "Explicitly set Threads value is preserved", + "Validation passes for whisper-cpp without API key", + "Config round-trips correctly with threads field", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 5: MODEL CLI COMMANDS + // ============================================================================ + { + "title": "Add model list CLI command", + "steps": [ + "Create modelCmd() in cmd/hyprvoice/main.go returning cobra.Command with Use: 'model'", + "Add modelListCmd() subcommand with Use: 'list'", + "Add --provider flag to filter by provider", + "Add --type flag: 'transcription', 'llm', or '' for all", + "Iterate all providers, get Models(), filter by type", + "For local models: check whisper.IsInstalled() and show checkmark if installed", + "Show: Model ID, Name, Description, Size (for local), [streaming] tag if applicable", + "Group by provider with headers" + ], + "verify": [ + "Running 'hyprvoice model list' shows all models grouped by provider", + "Running 'hyprvoice model list --type transcription' shows only transcription models", + "Running 'hyprvoice model list --provider whisper-cpp' shows only whisper models", + "Installed local models show checkmark", + "Output includes size for local models", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add model download CLI command", + "steps": [ + "Add modelDownloadCmd() subcommand with Use: 'download '", + "Use provider.FindModelByID() to search all providers for model", + "Check model.NeedsDownload() - if false, print 'model does not require download (cloud model)'", + "Check if already installed via whisper.IsInstalled()", + "If installed, print 'already installed at {path}'", + "Otherwise call whisper.Download() with progress bar (use pb or similar)", + "Print success message with model path" + ], + "verify": [ + "Running 'hyprvoice model download base.en' downloads whisper model", + "Shows progress during download", + "Shows 'already installed' if model exists", + "Shows error for unknown model name", + "Shows error for cloud models that don't need download", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add model remove CLI command", + "steps": [ + "Add modelRemoveCmd() subcommand with Use: 'remove '", + "Use provider.FindModelByID() to search all providers for model", + "Check model.NeedsDownload() - if false, print 'model is cloud-based, nothing to remove'", + "Check if installed via whisper.IsInstalled()", + "If not installed, print error 'model not installed'", + "Otherwise call whisper.Remove()", + "Print success message" + ], + "verify": [ + "Running 'hyprvoice model remove base.en' removes whisper model", + "Shows error if model not installed", + "Shows error for cloud models", + "Shows success message after removal", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 6: TUI IMPROVEMENTS + // Use Model metadata instead of hardcoded descriptions + // ============================================================================ + { + "title": "Refactor TUI to use Model metadata for descriptions", + "steps": [ + "Update internal/tui/configure_transcription.go getTranscriptionModelOptions(providerName string, currentLang string)", + "Instead of hardcoded switch, get provider via provider.GetProvider()", + "Call provider.ModelsOfType(p, provider.Transcription) to get models", + "Build label from model: fmt.Sprintf('%s (%s)', model.Name, model.Description)", + "For local models (model.Local): append ' [%s]' with model.LocalInfo.Size", + "For streaming models (model.Streaming): append ' [streaming]'", + "If currentLang != '' and !model.SupportsLanguage(currentLang): append ' (does not support %s)' with language name to label", + "Pass currentLang to getTranscriptionModelOptions() from editTranscription()", + "Do same for getLLMModelOptions() in configure_llm.go (though LLMs are language-agnostic for prompting)" + ], + "verify": [ + "Model options show Name and Description from Model struct", + "Local models show size in label", + "Streaming models show [streaming] in label", + "When Spanish language selected, base.en shows '(does not support Spanish)'", + "When auto selected, all models show without warnings", + "No more hardcoded descriptions in TUI", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add local provider options to TUI with dependency check", + "steps": [ + "Update editTranscription() provider options to include whisper-cpp", + "Before showing whisper-cpp, call deps.CheckWhisperCli()", + "If not installed, show as disabled with note: 'whisper-cli not found - install whisper.cpp'", + "When whisper-cpp selected, show model picker", + "Show installed models with checkmark prefix using whisper.IsInstalled()", + "If user selects uninstalled model, show confirm dialog: 'Download {name} ({size})?'", + "If confirmed, show progress during whisper.Download()" + ], + "verify": [ + "whisper-cpp appears in provider list", + "Warning shown if whisper-cli not installed", + "Model picker shows installed status", + "Download prompt appears for uninstalled models", + "Download completes with progress", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add language picker to TUI using language package", + "steps": [ + "Create internal/tui/languages.go with getLanguageOptions(currentModel *Model) []huh.Option[string]", + "Use language.List() to get all languages", + "First option: Auto-detect (Recommended) with value '' from language.Auto - always show as recommended", + "Format each as: fmt.Sprintf('%s - %s (%s)', lang.Name, lang.NativeName, lang.Code) or simpler if names match", + "If currentModel is not nil and !currentModel.SupportsLanguage(lang.Code), append ' (not supported by current model)' to label", + "Use huh.NewSelect with Filtering(true) to enable search through languages", + "Update editTranscription() to use filtered language dropdown instead of text input", + "Store language.Code in config, not display name", + "Pass current model to getLanguageOptions() so it can show compatibility warnings" + ], + "verify": [ + "Language dropdown shows 50+ options", + "Auto-detect (Recommended) is first option with value ''", + "Format shows native name where different", + "Search/filter works on language dropdown", + "If model is English-only, non-English languages show '(not supported by current model)'", + "Selecting language saves the Code to config", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add TUI validation for language-model compatibility on save", + "steps": [ + "Update internal/tui/configure_transcription.go editTranscription()", + "Before saving config, call config.ValidateModelLanguageCompatibility(provider, model, language)", + "If validation fails, show error dialog with message from validation", + "Error message should be: 'model {model} does not support language {lang}. Change model, select auto-detect, or choose: {supported_languages}'", + "Do not save config until user fixes the incompatibility", + "User can fix by: changing model, changing language to auto, or changing to supported language", + "After showing error, return to the form so user can make changes" + ], + "verify": [ + "Selecting English-only model + Spanish language shows error on save", + "Error dialog displays clear message with options", + "Config is not saved when validation fails", + "User can change model and save successfully", + "User can change language to auto and save successfully", + "User can change language to supported language and save successfully", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 7: STREAMING ADAPTER IMPLEMENTATIONS + // Each adapter is a separate task for right-sizing + // ============================================================================ + { + "title": "Create ElevenLabs StreamingAdapter", + "steps": [ + "Create internal/transcriber/adapter_elevenlabs_streaming.go", + "Define ElevenLabsStreamingAdapter struct: apiKey, model, language string, conn *websocket.Conn, resultsCh chan TranscriptionResult, mu sync.Mutex", + "Implement Start(ctx): connect to wss://api.elevenlabs.io/v1/speech-to-text/realtime with xi-api-key header", + "Use language.ToProviderFormat(language, 'elevenlabs') for language_code param", + "Set query params: model_id, language_code, audio_format=pcm_16000", + "Implement SendChunk(): send input_audio_chunk message with base64 audio", + "Implement Results(): return resultsCh, goroutine reads websocket and parses partial_transcript/committed_transcript", + "Implement Close(): close websocket cleanly with proper close frame", + "Use gorilla/websocket, respect ctx cancellation throughout" + ], + "verify": [ + "ElevenLabsStreamingAdapter implements StreamingAdapter interface", + "Start() connects to correct WebSocket URL", + "Language converted to provider format", + "SendChunk() sends properly formatted JSON", + "Results() channel receives partial and final transcripts", + "Close() terminates cleanly", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add reconnection logic to ElevenLabs StreamingAdapter", + "steps": [ + "Update internal/transcriber/adapter_elevenlabs_streaming.go", + "Add reconnection fields: maxRetries int, retryDelays []time.Duration (1s, 2s, 4s)", + "Implement reconnect() helper that attempts to re-establish WebSocket connection", + "On read error in Results() goroutine: attempt reconnection before giving up", + "On write error in SendChunk(): trigger reconnect, retry the chunk", + "On reconnection, send error to resultsCh with IsFinal=false to notify caller of brief interruption", + "After max retries exhausted, send final error and close channel" + ], + "verify": [ + "Reconnection attempted on connection loss (up to 3 times)", + "Exponential backoff between retries (1s, 2s, 4s)", + "Caller notified of reconnection via error in results channel", + "After max retries, final error sent and channel closed", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create Deepgram Provider", + "steps": [ + "Create internal/provider/deepgram.go implementing Provider interface", + "Research Deepgram API docs (https://developers.deepgram.com/docs/language) for exact Nova-2 language support", + "Name() returns 'deepgram', RequiresAPIKey() returns true, IsLocal() returns false", + "Models() returns: nova-2, nova-2-general, nova-2-meeting, nova-2-phonecall", + "All models: Type=Transcription, Streaming=true, AdapterType='deepgram'", + "Set SupportedLanguages to explicit list from Deepgram docs (intersection with our 57 languages)", + "Deepgram uses locale codes (en-US, en-GB) - map these to our base codes ('en') for SupportedLanguages", + "Set Endpoint.BaseURL='wss://api.deepgram.com'", + "Implement DefaultModel returning 'nova-2'", + "Register in provider.init()" + ], + "verify": [ + "provider.GetProvider('deepgram') returns DeepgramProvider", + "All Deepgram models have Streaming=true", + "All Deepgram models have explicit SupportedLanguages from docs", + "All models have AdapterType='deepgram'", + "RequiresAPIKey() returns true", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create Deepgram StreamingAdapter", + "steps": [ + "Create internal/transcriber/adapter_deepgram.go", + "Define DeepgramAdapter struct: apiKey, model, language string, conn *websocket.Conn, resultsCh chan TranscriptionResult", + "Implement Start(ctx): connect to wss://api.deepgram.com/v1/listen with Authorization: Token header", + "Use language.ToProviderFormat(language, 'deepgram') for language param (e.g., 'en' -> 'en-US')", + "Set query params: model, language, encoding=linear16, sample_rate=16000", + "Implement SendChunk(): send raw binary audio (not base64)", + "Implement Results(): goroutine reads websocket, parse JSON responses with is_final field", + "Implement Close(): send close message, close connection" + ], + "verify": [ + "DeepgramAdapter implements StreamingAdapter", + "Language converted to Deepgram format (en -> en-US style)", + "Connects with Token auth header", + "SendChunk sends binary audio", + "Parses interim and final results correctly", + "Close() terminates cleanly", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add reconnection logic to Deepgram StreamingAdapter", + "steps": [ + "Update internal/transcriber/adapter_deepgram.go", + "Add reconnection fields: maxRetries int, retryDelays []time.Duration (1s, 2s, 4s)", + "Implement reconnect() helper that attempts to re-establish WebSocket connection", + "On read error: attempt reconnection before giving up", + "On write error in SendChunk(): trigger reconnect, retry the chunk", + "On reconnection, send error to resultsCh with IsFinal=false to notify caller", + "After max retries exhausted, send final error and close channel", + "Respect context cancellation throughout" + ], + "verify": [ + "Reconnection attempted on connection loss (up to 3 times)", + "Exponential backoff between retries", + "Caller notified of reconnection via error in results channel", + "Context cancellation stops reconnection attempts", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add OpenAI Realtime model to OpenAI provider", + "steps": [ + "Update internal/provider/openai.go", + "Add gpt-realtime model to Models() return value", + "Set: Type=Transcription, Streaming=true, AdapterType='openai-realtime'", + "Set Endpoint.BaseURL='wss://api.openai.com' (WebSocket endpoint)", + "Set SupportedLanguages=language.AllLanguageCodes() (same as other OpenAI transcription models)", + "Keep DefaultModel unchanged (batch whisper-1 remains default)" + ], + "verify": [ + "OpenAIProvider.Models() now includes gpt-realtime", + "gpt-realtime has Streaming=true", + "gpt-realtime has AdapterType='openai-realtime'", + "gpt-realtime has WebSocket endpoint", + "DefaultModel(Transcription) still returns 'whisper-1'", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create OpenAI Realtime StreamingAdapter", + "steps": [ + "Create internal/transcriber/adapter_openai_realtime.go", + "Define OpenAIRealtimeAdapter struct: apiKey, model, language string, conn *websocket.Conn, resultsCh chan TranscriptionResult", + "Implement Start(ctx): connect to wss://api.openai.com/v1/realtime with Bearer auth header", + "Send session.update event to configure transcription mode", + "Implement SendChunk(): send input_audio_buffer.append events with base64 audio", + "Implement Results(): goroutine reads websocket, parse response.output_text.delta and .done events", + "Implement Close(): send session.close event, close connection" + ], + "verify": [ + "OpenAIRealtimeAdapter implements StreamingAdapter", + "Connects with correct Bearer auth", + "Session configured for transcription mode", + "SendChunk sends audio buffer events", + "Receives transcription delta and done events", + "Close() terminates cleanly", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add reconnection logic to OpenAI Realtime StreamingAdapter", + "steps": [ + "Update internal/transcriber/adapter_openai_realtime.go", + "Add reconnection fields: maxRetries int, retryDelays []time.Duration (1s, 2s, 4s)", + "Implement reconnect() helper that re-establishes WebSocket and re-sends session.update", + "On read/write errors: attempt reconnection before giving up", + "On reconnection, send error to resultsCh with IsFinal=false", + "After max retries exhausted, send final error and close channel", + "Respect context cancellation throughout" + ], + "verify": [ + "Reconnection attempted on connection loss (up to 3 times)", + "Session reconfigured after reconnection", + "Exponential backoff between retries", + "Context cancellation stops reconnection attempts", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Update factory to create streaming transcribers", + "steps": [ + "Update internal/transcriber/transcriber.go NewTranscriber()", + "After getting Model, check model.Streaming", + "If streaming: create appropriate StreamingAdapter based on AdapterType", + "Wrap in StreamingTranscriber and return", + "If not streaming: create BatchAdapter, wrap in SimpleTranscriber (existing behavior)", + "Add case 'elevenlabs-streaming' -> ElevenLabsStreamingAdapter", + "Add case 'deepgram' -> DeepgramAdapter", + "Add case 'openai-realtime' -> OpenAIRealtimeAdapter" + ], + "verify": [ + "Factory creates StreamingTranscriber for scribe_v1-streaming", + "Factory creates StreamingTranscriber for nova-2", + "Factory creates StreamingTranscriber for gpt-realtime", + "Factory creates SimpleTranscriber for scribe_v1 (batch)", + "Factory creates SimpleTranscriber for whisper-1", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Write tests for streaming adapters", + "steps": [ + "Create internal/transcriber/streaming_test.go", + "Test StreamingTranscriber accumulates final results correctly", + "Test StreamingTranscriber handles adapter errors", + "Test context cancellation stops StreamingTranscriber cleanly", + "Test concurrent access to GetFinalTranscription is safe", + "Mock WebSocket for unit testing adapters", + "Test ElevenLabsStreamingAdapter message format", + "Test DeepgramAdapter binary audio sending", + "Test OpenAIRealtimeAdapter session configuration", + "Test reconnection logic with simulated connection drops", + "Test Close() cleans up resources and goroutines" + ], + "verify": [ + "go test ./internal/transcriber/... passes", + "go test -race ./internal/transcriber/... passes (no race conditions)", + "Streaming accumulation tested", + "Context cancellation tested", + "Reconnection logic tested", + "Error handling tested", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 8: CONFIG AND VALIDATION UPDATES + // ============================================================================ + { + "title": "Update config validation to use provider registry", + "steps": [ + "Update internal/config/validate.go", + "For provider validation: use provider.GetProvider() instead of hardcoded list", + "For model validation: use provider.GetModel() to verify model exists", + "For API key validation: check provider.RequiresAPIKey() and provider.IsLocal()", + "Remove hardcoded provider and model lists from validation", + "Validate language using language.IsValidCode() - warn if not recognized but don't error", + "Add ValidateModelLanguageCompatibility(providerName, modelID, langCode string) error", + "Get model via provider.GetModel(), check model.SupportsLanguage(langCode)", + "If not supported, return error: 'model {model} does not support language {lang}. Either change model, select auto-detect, or choose a supported language: {model.SupportedLanguages[:10]}...' (truncate if many)", + "This validation runs at configure time (TUI save, CLI config set) and returns hard error" + ], + "verify": [ + "Validation uses provider registry", + "Unknown provider returns clear error", + "Unknown model returns clear error", + "Missing API key for cloud provider returns error", + "No API key required for local provider", + "Language validation warns but doesn't error for unknown language codes", + "Model-language incompatibility returns hard error with supported languages list", + "Error message includes model name, language, and list of supported languages (from model.SupportedLanguages)", + "Auto language ('') passes validation for any model", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add runtime language-model compatibility check with fallback", + "steps": [ + "Update internal/transcriber/transcriber.go NewTranscriber()", + "After looking up model via provider.GetModel(), check model.SupportsLanguage(config.Language)", + "If language not supported and language != '' (not auto):", + " - Log warning: 'model {model} does not support language {lang}, falling back to auto-detect'", + " - Send notification via internal/notify package (desktop notification)", + " - Override config.Language to '' (auto) for this transcription session", + "This allows runtime to proceed even if config was manually edited to invalid state", + "Configure-time validation is still the primary guard (hard error)", + "Runtime check is fallback safety net with user notification" + ], + "verify": [ + "NewTranscriber with incompatible language logs warning", + "NewTranscriber with incompatible language sends desktop notification", + "NewTranscriber with incompatible language falls back to auto-detect", + "Transcription still works after fallback", + "NewTranscriber with '' (auto) never triggers warning", + "NewTranscriber with compatible language works normally", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Add DEEPGRAM_API_KEY env var support", + "steps": [ + "Update internal/config/convert.go resolveAPIKeyForProvider()", + "Add case for 'deepgram' provider with DEEPGRAM_API_KEY env var", + "Update providers config section to include deepgram", + "Update config template in save.go with deepgram section" + ], + "verify": [ + "Deepgram API key resolved from config or DEEPGRAM_API_KEY env", + "Config template includes deepgram section", + "Typecheck passes" + ], + "passes": false + }, + // ============================================================================ + // PHASE 9: DOCUMENTATION + // Consolidated at the end - update all docs once architecture is stable + // ============================================================================ + { + "title": "Update README with new architecture", + "steps": [ + "Update Features section to mention: local transcription (whisper-cpp), streaming support", + "Add '## Local Transcription' section explaining whisper-cpp setup: install whisper.cpp, download model, configure", + "Add '## Streaming Transcription' section explaining streaming providers and models", + "Update provider list in Configuration section to include all providers", + "Add 'hyprvoice model list/download/remove' commands to Quick Reference", + "Update Development Status table with completed items", + "Update Architecture Overview if needed" + ], + "verify": [ + "README mentions local and streaming support", + "Local setup instructions are clear", + "Model commands documented", + "Provider list is complete and accurate", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Create docs/providers.md comparison guide", + "steps": [ + "Create docs/providers.md", + "Add Transcription Providers table: Provider, Type (Cloud/Local), Models, Language Support, Streaming Support, Speed, Quality, Cost, Notes", + "Language Support column: 'All' for multilingual, 'English only' for *.en models, specific count like '36 languages' where known", + "Add LLM Providers table with similar columns (language support less relevant for LLMs)", + "Add '## Choosing a Provider' section with decision flowchart or guide", + "Add '## Language Support' section explaining which models support which languages", + "Clearly list English-only models: tiny.en, base.en, small.en, medium.en, distil-whisper-large-v3-en", + "Recommend auto-detect for most users unless specific language needed", + "Add '## Streaming vs Batch' section explaining when to use each", + "Add '## Local vs Cloud' section with tradeoffs (privacy, latency, cost, setup)" + ], + "verify": [ + "Comparison tables are complete with Language Support column", + "All providers listed with accurate info", + "English-only models clearly marked", + "Language support section is comprehensive", + "Decision guide is helpful", + "File is well-formatted markdown", + "Typecheck passes" + ], + "passes": false + }, + { + "title": "Update docs/config.md with all providers and options", + "steps": [ + "Add whisper-cpp provider section with: provider, model, threads options", + "Add Deepgram provider section with: provider, model, api_key / DEEPGRAM_API_KEY", + "Document streaming models (scribe_v1-streaming, nova-2, gpt-realtime)", + "Document 'hyprvoice model list/download/remove' commands with examples", + "Add language configuration section explaining language codes and auto-detect", + "Document language-model compatibility: which models support which languages", + "Note that *.en models (base.en, tiny.en, distil-whisper-large-v3-en) are English only", + "Explain validation behavior: configure-time hard error, runtime warning + fallback to auto", + "Update examples throughout to reflect new Model-based architecture" + ], + "verify": [ + "All providers documented with all options", + "Model commands documented with examples", + "Streaming configuration documented", + "Language configuration documented with auto-detect recommendation", + "Language-model compatibility clearly explained", + "English-only models listed", + "Validation behavior documented", + "Examples are copy-paste ready", + "Typecheck passes" + ], + "passes": false + } + ] +}