Files
hyprvoice/internal/tui/configure_llm.go
T

296 lines
7.8 KiB
Go

package tui
import (
"fmt"
"github.com/charmbracelet/huh"
"github.com/leonardotrapani/hyprvoice/internal/config"
"github.com/leonardotrapani/hyprvoice/internal/provider"
)
// buildLLMModelLabel creates the display label for an LLM model option
func buildLLMModelLabel(m provider.Model) string {
return fmt.Sprintf("%s (%s)", m.Name, m.Description)
}
// editLLM handles the LLM section edit with smart provider detection
func editLLM(cfg *config.Config, configuredProviders []string) ([]string, error) {
var llmProviders []string
for _, name := range configuredProviders {
p := provider.GetProvider(name)
if p != nil && len(provider.ModelsOfType(p, provider.LLM)) > 0 {
llmProviders = append(llmProviders, name)
}
}
postProcessing := cfg.LLM.PostProcessing
if !postProcessing.RemoveStutters && !postProcessing.AddPunctuation &&
!postProcessing.FixGrammar && !postProcessing.RemoveFillerWords {
postProcessing = config.LLMPostProcessingConfig{
RemoveStutters: true,
AddPunctuation: true,
FixGrammar: true,
RemoveFillerWords: true,
}
}
customPrompt := cfg.LLM.CustomPrompt
enableLLM := cfg.LLM.Enabled
enableDesc := "LLM improves transcription by fixing grammar, removing stutters, and cleaning up text. Recommended for weak voice models"
if cfg.LLM.Enabled {
enableDesc = fmt.Sprintf("Currently: enabled (%s/%s). %s", cfg.LLM.Provider, cfg.LLM.Model, enableDesc)
} else {
enableDesc = "Currently: disabled. " + enableDesc
}
enableForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Enable LLM Post-Processing? (Recommended)").
Description(enableDesc).
Affirmative("Yes (Recommended)").
Negative("No").
Value(&enableLLM),
),
).WithTheme(getTheme())
if err := enableForm.Run(); err != nil {
return configuredProviders, err
}
if !enableLLM {
cfg.LLM.Enabled = false
return configuredProviders, nil
}
var llmOptions []huh.Option[string]
for _, name := range llmProviders {
switch name {
case "openai":
llmOptions = append(llmOptions, huh.NewOption("OpenAI GPT", "openai"))
case "groq":
llmOptions = append(llmOptions, huh.NewOption("Groq Llama (fast)", "groq"))
}
}
unconfiguredLLM := getUnconfiguredLLMOptions(configuredProviders)
if len(unconfiguredLLM) > 0 {
llmOptions = append(llmOptions, unconfiguredLLM...)
}
if len(llmOptions) == 0 {
fmt.Println(StyleError.Render("No LLM providers available. Please configure OpenAI or Groq first."))
cfg.LLM.Enabled = false
return configuredProviders, nil
}
selectedProvider := cfg.LLM.Provider
if selectedProvider == "" && len(llmOptions) > 0 {
selectedProvider = llmOptions[0].Value
}
llmProviderDesc := "Choose which service to use for text post-processing"
if cfg.LLM.Provider != "" {
llmProviderDesc = fmt.Sprintf("Currently: %s/%s", cfg.LLM.Provider, cfg.LLM.Model)
}
providerForm := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("LLM Provider").
Description(llmProviderDesc).
Options(llmOptions...).
Value(&selectedProvider),
),
).WithTheme(getTheme())
if err := providerForm.Run(); err != nil {
return configuredProviders, err
}
configuredProviders = ensureProviderConfigured(cfg, selectedProvider, configuredProviders)
cfg.LLM.Provider = selectedProvider
modelOptions := getLLMModelOptions(selectedProvider)
selectedModel := cfg.LLM.Model
if selectedModel == "" && len(modelOptions) > 0 {
selectedModel = modelOptions[0].Value
}
llmModelDesc := ""
if cfg.LLM.Model != "" {
llmModelDesc = fmt.Sprintf("Currently: %s", cfg.LLM.Model)
}
modelForm := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("LLM Model").
Description(llmModelDesc).
Options(modelOptions...).
Value(&selectedModel),
),
).WithTheme(getTheme())
if err := modelForm.Run(); err != nil {
return configuredProviders, err
}
cfg.LLM.Model = selectedModel
var ppErr error
postProcessing, ppErr = selectPostProcessingOptions(postProcessing)
if ppErr != nil {
return configuredProviders, ppErr
}
cfg.LLM.PostProcessing = postProcessing
enableCustomPrompt := customPrompt.Enabled
customPromptText := customPrompt.Prompt
customPromptDesc := "Add extra instructions for the LLM"
if customPrompt.Enabled && customPrompt.Prompt != "" {
preview := customPrompt.Prompt
if len(preview) > 40 {
preview = preview[:40] + "..."
}
customPromptDesc = fmt.Sprintf("Currently: \"%s\"", preview)
} else {
customPromptDesc = "Currently: none. " + customPromptDesc
}
customForm := huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("Add custom prompt?").
Description(customPromptDesc).
Value(&enableCustomPrompt),
),
).WithTheme(getTheme())
if err := customForm.Run(); err != nil {
return configuredProviders, err
}
if enableCustomPrompt {
promptForm := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Custom Prompt").
Description("Additional instructions (e.g., 'Format as bullet points')").
Value(&customPromptText).
CharLimit(500),
),
).WithTheme(getTheme())
if err := promptForm.Run(); err != nil {
return configuredProviders, err
}
cfg.LLM.CustomPrompt.Enabled = true
cfg.LLM.CustomPrompt.Prompt = customPromptText
} else {
cfg.LLM.CustomPrompt.Enabled = false
}
cfg.LLM.Enabled = true
return configuredProviders, nil
}
// getUnconfiguredLLMOptions returns options for LLM providers not yet configured
func getUnconfiguredLLMOptions(configuredProviders []string) []huh.Option[string] {
configured := make(map[string]bool)
for _, p := range configuredProviders {
configured[p] = true
}
var options []huh.Option[string]
if !configured["openai"] {
options = append(options, huh.NewOption("OpenAI GPT (not configured)", "openai"))
}
if !configured["groq"] {
options = append(options, huh.NewOption("Groq Llama (not configured)", "groq"))
}
return options
}
func getLLMModelOptions(providerName string) []huh.Option[string] {
p := provider.GetProvider(providerName)
if p == nil {
return []huh.Option[string]{}
}
models := provider.ModelsOfType(p, provider.LLM)
var options []huh.Option[string]
for _, m := range models {
label := buildLLMModelLabel(m)
options = append(options, huh.NewOption(label, m.ID))
}
return options
}
// selectPostProcessingOptions shows a multi-select for LLM post-processing toggles
func selectPostProcessingOptions(current config.LLMPostProcessingConfig) (config.LLMPostProcessingConfig, error) {
type ppOption string
const (
optRemoveStutters ppOption = "stutters"
optAddPunctuation ppOption = "punctuation"
optFixGrammar ppOption = "grammar"
optRemoveFillerWords ppOption = "fillers"
)
options := []huh.Option[ppOption]{
huh.NewOption("Remove stutters (repeated words)", optRemoveStutters),
huh.NewOption("Add punctuation", optAddPunctuation),
huh.NewOption("Fix grammar", optFixGrammar),
huh.NewOption("Remove filler words (um, uh, like)", optRemoveFillerWords),
}
var selected []ppOption
if current.RemoveStutters {
selected = append(selected, optRemoveStutters)
}
if current.AddPunctuation {
selected = append(selected, optAddPunctuation)
}
if current.FixGrammar {
selected = append(selected, optFixGrammar)
}
if current.RemoveFillerWords {
selected = append(selected, optRemoveFillerWords)
}
form := huh.NewForm(
huh.NewGroup(
huh.NewMultiSelect[ppOption]().
Title("Post-Processing Options").
Description("Select which improvements to apply").
Options(options...).
Value(&selected),
),
).WithTheme(getTheme())
if err := form.Run(); err != nil {
return current, err
}
result := config.LLMPostProcessingConfig{}
for _, opt := range selected {
switch opt {
case optRemoveStutters:
result.RemoveStutters = true
case optAddPunctuation:
result.AddPunctuation = true
case optFixGrammar:
result.FixGrammar = true
case optRemoveFillerWords:
result.RemoveFillerWords = true
}
}
return result, nil
}