feat: refactor
This commit is contained in:
@@ -41,6 +41,10 @@ func editProviders(cfg *config.Config, onboarding bool) error {
|
||||
if onboarding {
|
||||
exitLabel = "Next"
|
||||
}
|
||||
|
||||
// track if we should default to "back" (Next) after configuring a provider
|
||||
defaultToExit := false
|
||||
|
||||
for {
|
||||
var options []huh.Option[string]
|
||||
for _, name := range AllProviders {
|
||||
@@ -48,7 +52,11 @@ func editProviders(cfg *config.Config, onboarding bool) error {
|
||||
}
|
||||
options = append(options, huh.NewOption(exitLabel, "back"))
|
||||
|
||||
var selected string
|
||||
selected := ""
|
||||
if defaultToExit {
|
||||
selected = "back"
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
@@ -77,6 +85,7 @@ func editProviders(cfg *config.Config, onboarding bool) error {
|
||||
cfg.Providers = make(map[string]config.ProviderConfig)
|
||||
}
|
||||
cfg.Providers[selected] = config.ProviderConfig{APIKey: apiKey}
|
||||
defaultToExit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,8 +244,37 @@ func editTranscription(cfg *config.Config, configuredProviders []string) ([]stri
|
||||
}
|
||||
|
||||
cfg.Transcription.Model = selectedModel
|
||||
// language is now set in the Language menu (cfg.General.Language)
|
||||
// cfg.Transcription.Language can still be used as override but not set here
|
||||
|
||||
// set streaming mode based on model capabilities
|
||||
model, err := provider.GetModel(registryName, selectedModel)
|
||||
if err == nil {
|
||||
if model.SupportsBothModes() {
|
||||
// model supports both: ask user
|
||||
useStreaming := cfg.Transcription.Streaming
|
||||
streamingForm := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("Enable streaming mode?").
|
||||
Description("This model supports both batch and streaming modes").
|
||||
Affirmative("Yes, use streaming (real-time)").
|
||||
Negative("No, use batch (after recording)").
|
||||
Value(&useStreaming),
|
||||
),
|
||||
).WithTheme(getTheme())
|
||||
|
||||
if err := streamingForm.Run(); err != nil {
|
||||
return configuredProviders, err
|
||||
}
|
||||
cfg.Transcription.Streaming = useStreaming
|
||||
} else if model.SupportsStreaming {
|
||||
// streaming-only model
|
||||
cfg.Transcription.Streaming = true
|
||||
fmt.Println(StyleSuccess.Render("Streaming mode enabled (this model only supports streaming)"))
|
||||
} else {
|
||||
// batch-only model
|
||||
cfg.Transcription.Streaming = false
|
||||
}
|
||||
}
|
||||
|
||||
return configuredProviders, nil
|
||||
}
|
||||
@@ -292,24 +321,8 @@ func getTranscriptionModelOptions(configProvider string, currentLang string) []h
|
||||
|
||||
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]
|
||||
|
||||
// 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 {
|
||||
for _, m := range models {
|
||||
label := buildModelLabel(m, currentLang)
|
||||
if m.Local && registryName == "whisper-cpp" {
|
||||
if whisper.IsInstalled(m.ID) {
|
||||
@@ -321,15 +334,6 @@ func getTranscriptionModelOptions(configProvider string, currentLang string) []h
|
||||
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))
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -354,10 +358,13 @@ func buildModelLabel(m provider.Model, currentLang string) string {
|
||||
label += fmt.Sprintf(" [%s]", m.LocalInfo.Size)
|
||||
}
|
||||
|
||||
// append streaming tag
|
||||
if m.Streaming {
|
||||
// append mode capabilities
|
||||
if m.SupportsBothModes() {
|
||||
label += " [batch+streaming]"
|
||||
} else if m.SupportsStreaming {
|
||||
label += " [streaming]"
|
||||
}
|
||||
// batch-only models don't need a tag (it's the default)
|
||||
|
||||
// append language warning if model doesn't support current language
|
||||
if currentLang != "" && !m.SupportsLanguage(currentLang) {
|
||||
|
||||
@@ -1,115 +1,100 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/leonardotrapani/hyprvoice/internal/provider"
|
||||
)
|
||||
|
||||
func TestGetTranscriptionModelOptions_GroupsModels(t *testing.T) {
|
||||
// test elevenlabs - has both batch and streaming
|
||||
func TestGetTranscriptionModelOptions_ShowsCapabilities(t *testing.T) {
|
||||
// test elevenlabs - has batch-only and streaming-only models
|
||||
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
|
||||
}
|
||||
}
|
||||
// should have 3 models: scribe_v1, scribe_v2, scribe_v2_realtime
|
||||
if len(options) != 3 {
|
||||
t.Errorf("expected 3 options for elevenlabs, got %d", len(options))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// verify models show capability tags
|
||||
for _, opt := range options {
|
||||
model, _, _ := provider.FindModelByID(opt.Value)
|
||||
if model == nil {
|
||||
continue // unknown model
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
if model.SupportsStreaming && !model.SupportsBatch {
|
||||
// streaming-only should have [streaming] tag
|
||||
if !strings.Contains(opt.Key, "[streaming]") {
|
||||
t.Errorf("streaming-only model %s should have [streaming] tag in label: %s", opt.Value, opt.Key)
|
||||
}
|
||||
} else if model.SupportsBothModes() {
|
||||
// both modes should have [batch+streaming] tag
|
||||
if !strings.Contains(opt.Key, "[batch+streaming]") {
|
||||
t.Errorf("both-modes model %s should have [batch+streaming] tag in label: %s", opt.Value, opt.Key)
|
||||
}
|
||||
}
|
||||
// batch-only models don't need a tag
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTranscriptionModelOptions_NoHeadersForSingleType(t *testing.T) {
|
||||
// test groq - batch only (no streaming models)
|
||||
options := getTranscriptionModelOptions("groq-transcription", "")
|
||||
func TestGetTranscriptionModelOptions_NoHeadersAnymore(t *testing.T) {
|
||||
// we removed batch/streaming section headers
|
||||
options := getTranscriptionModelOptions("elevenlabs", "")
|
||||
|
||||
for _, opt := range options {
|
||||
if opt.Value == "" {
|
||||
t.Errorf("expected no headers for provider with only one model type, got: %s", opt.Key)
|
||||
t.Errorf("should not have headers anymore, got: %s", opt.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTranscriptionModelOptions_OpenAI_GroupsCorrectly(t *testing.T) {
|
||||
func TestGetTranscriptionModelOptions_OpenAI_ShowsCapabilities(t *testing.T) {
|
||||
options := getTranscriptionModelOptions("openai", "")
|
||||
|
||||
var batchHeaderIdx, streamingHeaderIdx int
|
||||
batchHeaderIdx = -1
|
||||
streamingHeaderIdx = -1
|
||||
// OpenAI has 3 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe
|
||||
if len(options) != 3 {
|
||||
t.Errorf("expected 3 options for openai, got %d", len(options))
|
||||
}
|
||||
|
||||
for i, opt := range options {
|
||||
if opt.Value == "" {
|
||||
if opt.Key == "─── Batch ───" {
|
||||
batchHeaderIdx = i
|
||||
}
|
||||
if opt.Key == "─── Streaming ───" {
|
||||
streamingHeaderIdx = i
|
||||
// gpt-4o-transcribe and gpt-4o-mini-transcribe should have [batch+streaming]
|
||||
for _, opt := range options {
|
||||
if strings.Contains(opt.Value, "gpt-4o") {
|
||||
if !strings.Contains(opt.Key, "[batch+streaming]") {
|
||||
t.Errorf("gpt-4o model %s should have [batch+streaming] tag: %s", opt.Value, opt.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
func TestGetTranscriptionModelOptions_Deepgram_ShowsBothModes(t *testing.T) {
|
||||
options := getTranscriptionModelOptions("deepgram", "")
|
||||
|
||||
// Deepgram has 2 models: nova-3, nova-2 - both support batch+streaming
|
||||
if len(options) != 2 {
|
||||
t.Errorf("expected 2 options for deepgram, got %d", len(options))
|
||||
}
|
||||
|
||||
// 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++
|
||||
for _, opt := range options {
|
||||
if !strings.Contains(opt.Key, "[batch+streaming]") {
|
||||
t.Errorf("deepgram model %s should have [batch+streaming] tag: %s", opt.Value, opt.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if batchCount < 3 {
|
||||
t.Errorf("expected at least 3 batch models for OpenAI, got %d", batchCount)
|
||||
func TestGetTranscriptionModelOptions_Groq_BatchOnly(t *testing.T) {
|
||||
// test groq - batch only (no streaming models)
|
||||
options := getTranscriptionModelOptions("groq-transcription", "")
|
||||
|
||||
// should have 2 models: whisper-large-v3, whisper-large-v3-turbo
|
||||
if len(options) != 2 {
|
||||
t.Errorf("expected 2 options for groq, got %d", len(options))
|
||||
}
|
||||
if streamingCount < 1 {
|
||||
t.Errorf("expected at least 1 streaming model for OpenAI, got %d", streamingCount)
|
||||
|
||||
// batch-only models should not have any mode tags
|
||||
for _, opt := range options {
|
||||
if strings.Contains(opt.Key, "[streaming]") || strings.Contains(opt.Key, "[batch]") {
|
||||
t.Errorf("batch-only model should not have mode tags: %s", opt.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,26 +39,31 @@ func runFreshInstall(cfg *config.Config) (*ConfigureResult, error) {
|
||||
return &ConfigureResult{Cancelled: true}, nil
|
||||
}
|
||||
|
||||
// 4. Keywords
|
||||
// 4. Language selection
|
||||
if err := editLanguage(cfg); err != nil {
|
||||
return &ConfigureResult{Cancelled: true}, nil
|
||||
}
|
||||
|
||||
// 5. Keywords
|
||||
keywords, err := inputKeywords(cfg.Keywords)
|
||||
if err != nil {
|
||||
return &ConfigureResult{Cancelled: true}, nil
|
||||
}
|
||||
cfg.Keywords = keywords
|
||||
|
||||
// 5. Injection backends
|
||||
// 6. Injection backends
|
||||
backends, err := selectBackends(cfg.Injection.Backends)
|
||||
if err != nil {
|
||||
return &ConfigureResult{Cancelled: true}, nil
|
||||
}
|
||||
cfg.Injection.Backends = backends
|
||||
|
||||
// 6. Notifications - same screen as menu
|
||||
// 7. Notifications - same screen as menu
|
||||
if err := editNotifications(cfg); err != nil {
|
||||
return &ConfigureResult{Cancelled: true}, nil
|
||||
}
|
||||
|
||||
// 7. Advanced settings prompt
|
||||
// 8. Advanced settings prompt
|
||||
wantAdvanced, err := askAdvancedSettings()
|
||||
if err != nil {
|
||||
return &ConfigureResult{Cancelled: true}, nil
|
||||
|
||||
Reference in New Issue
Block a user