add batch/streaming section headers to model picker
This commit is contained in:
@@ -123,7 +123,13 @@ func editTranscription(cfg *config.Config, configuredProviders []string) ([]stri
|
|||||||
modelOptions := getTranscriptionModelOptions(selectedProvider, effectiveLanguage)
|
modelOptions := getTranscriptionModelOptions(selectedProvider, effectiveLanguage)
|
||||||
selectedModel := cfg.Transcription.Model
|
selectedModel := cfg.Transcription.Model
|
||||||
if selectedModel == "" && len(modelOptions) > 0 {
|
if selectedModel == "" && len(modelOptions) > 0 {
|
||||||
selectedModel = modelOptions[0].Value
|
// skip header options (empty value) to find first real model
|
||||||
|
for _, opt := range modelOptions {
|
||||||
|
if opt.Value != "" {
|
||||||
|
selectedModel = opt.Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modelDesc := ""
|
modelDesc := ""
|
||||||
@@ -145,6 +151,11 @@ func editTranscription(cfg *config.Config, configuredProviders []string) ([]stri
|
|||||||
return configuredProviders, err
|
return configuredProviders, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if user selected a section header (empty value), re-prompt
|
||||||
|
if selectedModel == "" {
|
||||||
|
return editTranscription(cfg, configuredProviders)
|
||||||
|
}
|
||||||
|
|
||||||
// validate language-model compatibility before saving
|
// validate language-model compatibility before saving
|
||||||
registryName := mapConfigProviderToRegistry(selectedProvider)
|
registryName := mapConfigProviderToRegistry(selectedProvider)
|
||||||
if err := provider.ValidateModelLanguage(registryName, selectedModel, effectiveLanguage); err != nil {
|
if err := provider.ValidateModelLanguage(registryName, selectedModel, effectiveLanguage); err != nil {
|
||||||
@@ -280,12 +291,26 @@ func getTranscriptionModelOptions(configProvider string, currentLang string) []h
|
|||||||
}
|
}
|
||||||
|
|
||||||
models := provider.ModelsOfType(p, provider.Transcription)
|
models := provider.ModelsOfType(p, provider.Transcription)
|
||||||
|
|
||||||
|
// separate batch and streaming models
|
||||||
|
var batchModels, streamingModels []provider.Model
|
||||||
|
for _, m := range models {
|
||||||
|
if m.Streaming {
|
||||||
|
streamingModels = append(streamingModels, m)
|
||||||
|
} else {
|
||||||
|
batchModels = append(batchModels, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var options []huh.Option[string]
|
var options []huh.Option[string]
|
||||||
|
|
||||||
for _, m := range models {
|
// add batch models first (with header if we have both types)
|
||||||
|
hasBoth := len(batchModels) > 0 && len(streamingModels) > 0
|
||||||
|
if hasBoth && len(batchModels) > 0 {
|
||||||
|
options = append(options, huh.NewOption("─── Batch ───", ""))
|
||||||
|
}
|
||||||
|
for _, m := range batchModels {
|
||||||
label := buildModelLabel(m, currentLang)
|
label := buildModelLabel(m, currentLang)
|
||||||
|
|
||||||
// for local models, show installed status
|
|
||||||
if m.Local && registryName == "whisper-cpp" {
|
if m.Local && registryName == "whisper-cpp" {
|
||||||
if whisper.IsInstalled(m.ID) {
|
if whisper.IsInstalled(m.ID) {
|
||||||
label = "[x] " + label
|
label = "[x] " + label
|
||||||
@@ -293,7 +318,15 @@ func getTranscriptionModelOptions(configProvider string, currentLang string) []h
|
|||||||
label = "[ ] " + label
|
label = "[ ] " + label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
options = append(options, huh.NewOption(label, m.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// add streaming models (with header if we have both types)
|
||||||
|
if hasBoth && len(streamingModels) > 0 {
|
||||||
|
options = append(options, huh.NewOption("─── Streaming ───", ""))
|
||||||
|
}
|
||||||
|
for _, m := range streamingModels {
|
||||||
|
label := buildModelLabel(m, currentLang)
|
||||||
options = append(options, huh.NewOption(label, m.ID))
|
options = append(options, huh.NewOption(label, m.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetTranscriptionModelOptions_GroupsModels(t *testing.T) {
|
||||||
|
// test elevenlabs - has both batch and streaming
|
||||||
|
options := getTranscriptionModelOptions("elevenlabs", "")
|
||||||
|
|
||||||
|
// find headers
|
||||||
|
var batchHeaderIdx, streamingHeaderIdx int
|
||||||
|
batchHeaderIdx = -1
|
||||||
|
streamingHeaderIdx = -1
|
||||||
|
|
||||||
|
for i, opt := range options {
|
||||||
|
if opt.Value == "" {
|
||||||
|
if opt.Key == "─── Batch ───" {
|
||||||
|
batchHeaderIdx = i
|
||||||
|
}
|
||||||
|
if opt.Key == "─── Streaming ───" {
|
||||||
|
streamingHeaderIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if batchHeaderIdx == -1 {
|
||||||
|
t.Error("expected Batch header for provider with both types")
|
||||||
|
}
|
||||||
|
if streamingHeaderIdx == -1 {
|
||||||
|
t.Error("expected Streaming header for provider with both types")
|
||||||
|
}
|
||||||
|
if batchHeaderIdx >= streamingHeaderIdx {
|
||||||
|
t.Errorf("Batch header should come before Streaming header: batch=%d, streaming=%d", batchHeaderIdx, streamingHeaderIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify models are grouped correctly
|
||||||
|
for i, opt := range options {
|
||||||
|
if opt.Value == "" {
|
||||||
|
continue // skip headers
|
||||||
|
}
|
||||||
|
model, _, _ := provider.FindModelByID(opt.Value)
|
||||||
|
if model == nil {
|
||||||
|
continue // unknown model
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < streamingHeaderIdx && model.Streaming {
|
||||||
|
t.Errorf("streaming model %s found before streaming header", opt.Value)
|
||||||
|
}
|
||||||
|
if i > streamingHeaderIdx && !model.Streaming {
|
||||||
|
t.Errorf("batch model %s found after streaming header", opt.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTranscriptionModelOptions_NoHeadersForSingleType(t *testing.T) {
|
||||||
|
// test groq - batch only (no streaming models)
|
||||||
|
options := getTranscriptionModelOptions("groq-transcription", "")
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
if opt.Value == "" {
|
||||||
|
t.Errorf("expected no headers for provider with only one model type, got: %s", opt.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTranscriptionModelOptions_OpenAI_GroupsCorrectly(t *testing.T) {
|
||||||
|
options := getTranscriptionModelOptions("openai", "")
|
||||||
|
|
||||||
|
var batchHeaderIdx, streamingHeaderIdx int
|
||||||
|
batchHeaderIdx = -1
|
||||||
|
streamingHeaderIdx = -1
|
||||||
|
|
||||||
|
for i, opt := range options {
|
||||||
|
if opt.Value == "" {
|
||||||
|
if opt.Key == "─── Batch ───" {
|
||||||
|
batchHeaderIdx = i
|
||||||
|
}
|
||||||
|
if opt.Key == "─── Streaming ───" {
|
||||||
|
streamingHeaderIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI has 3 batch + 1 streaming
|
||||||
|
if batchHeaderIdx == -1 {
|
||||||
|
t.Error("expected Batch header for OpenAI")
|
||||||
|
}
|
||||||
|
if streamingHeaderIdx == -1 {
|
||||||
|
t.Error("expected Streaming header for OpenAI")
|
||||||
|
}
|
||||||
|
|
||||||
|
// count models (not headers) by position
|
||||||
|
batchCount := 0
|
||||||
|
streamingCount := 0
|
||||||
|
for i, opt := range options {
|
||||||
|
if opt.Value == "" {
|
||||||
|
continue // skip headers
|
||||||
|
}
|
||||||
|
if i > batchHeaderIdx && i < streamingHeaderIdx {
|
||||||
|
batchCount++
|
||||||
|
} else if i > streamingHeaderIdx {
|
||||||
|
streamingCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if batchCount < 3 {
|
||||||
|
t.Errorf("expected at least 3 batch models for OpenAI, got %d", batchCount)
|
||||||
|
}
|
||||||
|
if streamingCount < 1 {
|
||||||
|
t.Errorf("expected at least 1 streaming model for OpenAI, got %d", streamingCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -562,4 +562,13 @@ Started: Sun Feb 1 12:22:47 AM CET 2026
|
|||||||
- Removed `if m.Streaming { continue }` filter from `getTranscriptionModelOptions()`
|
- Removed `if m.Streaming { continue }` filter from `getTranscriptionModelOptions()`
|
||||||
- Streaming models now appear in model picker: scribe_v1-streaming, scribe_v2-streaming, nova-3, nova-2, gpt-4o-realtime-preview
|
- Streaming models now appear in model picker: scribe_v1-streaming, scribe_v2-streaming, nova-3, nova-2, gpt-4o-realtime-preview
|
||||||
- `buildModelLabel()` already adds `[streaming]` tag (lines 324-327)
|
- `buildModelLabel()` already adds `[streaming]` tag (lines 324-327)
|
||||||
|
- All tests passing, typecheck passes
|
||||||
|
|
||||||
|
### Task 8: Add streaming section header in model picker
|
||||||
|
- Updated `getTranscriptionModelOptions()` to separate batch and streaming models
|
||||||
|
- Added `─── Batch ───` and `─── Streaming ───` headers when provider has both types
|
||||||
|
- Headers use empty string value, selecting header re-prompts user
|
||||||
|
- Default selection skips headers to find first real model
|
||||||
|
- Providers with only one type (e.g., Groq=batch, Deepgram=streaming) show no headers
|
||||||
|
- Added unit tests: GroupsModels, NoHeadersForSingleType, OpenAI_GroupsCorrectly
|
||||||
- All tests passing, typecheck passes
|
- All tests passing, typecheck passes
|
||||||
+1
-1
@@ -130,7 +130,7 @@
|
|||||||
"Clear visual distinction between batch and streaming sections",
|
"Clear visual distinction between batch and streaming sections",
|
||||||
"Typecheck passes"
|
"Typecheck passes"
|
||||||
],
|
],
|
||||||
"passes": false
|
"passes": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Add docs URLs to provider models",
|
"title": "Add docs URLs to provider models",
|
||||||
|
|||||||
Reference in New Issue
Block a user