1089 lines
54 KiB
JSON
1089 lines
54 KiB
JSON
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
// ============================================================================
|
|
// 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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
// ============================================================================
|
|
// 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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
// ============================================================================
|
|
// 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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
// ============================================================================
|
|
// 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": true
|
|
},
|
|
{
|
|
"title": "Add model download CLI command",
|
|
"steps": [
|
|
"Add modelDownloadCmd() subcommand with Use: 'download <model-name>'",
|
|
"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": true
|
|
},
|
|
{
|
|
"title": "Add model remove CLI command",
|
|
"steps": [
|
|
"Add modelRemoveCmd() subcommand with Use: 'remove <model-name>'",
|
|
"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": true
|
|
},
|
|
// ============================================================================
|
|
// 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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
// ============================================================================
|
|
// 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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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": true
|
|
},
|
|
{
|
|
"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
|
|
}
|
|
]
|
|
}
|