From f40f09390b7ccf365b41fa1cc134432537b50cad Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Wed, 18 Feb 2026 13:15:40 +0300 Subject: Feat(tts) alt+0 to replay last message in the chat --- extra/tts.go | 46 +++++----------------------------------------- models/extra.go | 41 +++++++++++++++++++++++++++++++++++++++++ tui.go | 13 +++++++++++++ 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/extra/tts.go b/extra/tts.go index dcc811e..21e8a4b 100644 --- a/extra/tts.go +++ b/extra/tts.go @@ -13,10 +13,9 @@ import ( "log/slog" "net/http" "os" - "regexp" "strings" - "time" "sync" + "time" google_translate_tts "github.com/GrailFinder/google-translate-tts" "github.com/GrailFinder/google-translate-tts/handlers" @@ -31,43 +30,8 @@ var ( TTSFlushChan = make(chan bool, 1) TTSDoneChan = make(chan bool, 1) // endsWithPunctuation = regexp.MustCompile(`[;.!?]$`) - threeOrMoreDashesRE = regexp.MustCompile(`-{3,}`) ) -// cleanText removes markdown and special characters that are not suitable for TTS -func cleanText(text string) string { - // Remove markdown-like characters that might interfere with TTS - text = strings.ReplaceAll(text, "*", "") // Bold/italic markers - text = strings.ReplaceAll(text, "#", "") // Headers - text = strings.ReplaceAll(text, "_", "") // Underline/italic markers - text = strings.ReplaceAll(text, "~", "") // Strikethrough markers - text = strings.ReplaceAll(text, "`", "") // Code markers - text = strings.ReplaceAll(text, "[", "") // Link brackets - text = strings.ReplaceAll(text, "]", "") // Link brackets - text = strings.ReplaceAll(text, "!", "") // Exclamation marks (if not punctuation) - // Remove HTML tags using regex - htmlTagRegex := regexp.MustCompile(`<[^>]*>`) - text = htmlTagRegex.ReplaceAllString(text, "") - // Split text into lines to handle table separators - lines := strings.Split(text, "\n") - var filteredLines []string - for _, line := range lines { - // Check if the line looks like a table separator (e.g., |----|, |===|, | - - - |) - // A table separator typically contains only |, -, =, and spaces - isTableSeparator := regexp.MustCompile(`^\s*\|\s*[-=\s]+\|\s*$`).MatchString(strings.TrimSpace(line)) - if !isTableSeparator { - // If it's not a table separator, remove vertical bars but keep the content - processedLine := strings.ReplaceAll(line, "|", "") - filteredLines = append(filteredLines, processedLine) - } - // If it is a table separator, skip it (don't add to filteredLines) - } - text = strings.Join(filteredLines, "\n") - text = threeOrMoreDashesRE.ReplaceAllString(text, "") - text = strings.TrimSpace(text) // Remove leading/trailing whitespace - return text -} - type Orator interface { Speak(text string) error Stop() @@ -157,7 +121,7 @@ func (o *KokoroOrator) readroutine() { } continue // if only one (often incomplete) sentence; wait for next chunk } - cleanedText := cleanText(sentence.Text) + cleanedText := models.CleanText(sentence.Text) if cleanedText == "" { continue // Skip empty text after cleaning } @@ -186,7 +150,7 @@ func (o *KokoroOrator) readroutine() { // flush remaining text o.mu.Lock() remaining := o.textBuffer.String() - remaining = cleanText(remaining) + remaining = models.CleanText(remaining) o.textBuffer.Reset() o.mu.Unlock() if remaining == "" { @@ -389,7 +353,7 @@ func (o *GoogleTranslateOrator) readroutine() { } continue // if only one (often incomplete) sentence; wait for next chunk } - cleanedText := cleanText(sentence.Text) + cleanedText := models.CleanText(sentence.Text) if cleanedText == "" { continue // Skip empty text after cleaning } @@ -417,7 +381,7 @@ func (o *GoogleTranslateOrator) readroutine() { } o.mu.Lock() remaining := o.textBuffer.String() - remaining = cleanText(remaining) + remaining = models.CleanText(remaining) o.textBuffer.Reset() o.mu.Unlock() if remaining == "" { diff --git a/models/extra.go b/models/extra.go index e1ca80f..5c60a26 100644 --- a/models/extra.go +++ b/models/extra.go @@ -1,8 +1,49 @@ package models +import ( + "regexp" + "strings" +) + type AudioFormat string const ( AFWav AudioFormat = "wav" AFMP3 AudioFormat = "mp3" ) + +var threeOrMoreDashesRE = regexp.MustCompile(`-{3,}`) + +// CleanText removes markdown and special characters that are not suitable for TTS +func CleanText(text string) string { + // Remove markdown-like characters that might interfere with TTS + text = strings.ReplaceAll(text, "*", "") // Bold/italic markers + text = strings.ReplaceAll(text, "#", "") // Headers + text = strings.ReplaceAll(text, "_", "") // Underline/italic markers + text = strings.ReplaceAll(text, "~", "") // Strikethrough markers + text = strings.ReplaceAll(text, "`", "") // Code markers + text = strings.ReplaceAll(text, "[", "") // Link brackets + text = strings.ReplaceAll(text, "]", "") // Link brackets + text = strings.ReplaceAll(text, "!", "") // Exclamation marks (if not punctuation) + // Remove HTML tags using regex + htmlTagRegex := regexp.MustCompile(`<[^>]*>`) + text = htmlTagRegex.ReplaceAllString(text, "") + // Split text into lines to handle table separators + lines := strings.Split(text, "\n") + var filteredLines []string + for _, line := range lines { + // Check if the line looks like a table separator (e.g., |----|, |===|, | - - - |) + // A table separator typically contains only |, -, =, and spaces + isTableSeparator := regexp.MustCompile(`^\s*\|\s*[-=\s]+\|\s*$`).MatchString(strings.TrimSpace(line)) + if !isTableSeparator { + // If it's not a table separator, remove vertical bars but keep the content + processedLine := strings.ReplaceAll(line, "|", "") + filteredLines = append(filteredLines, processedLine) + } + // If it is a table separator, skip it (don't add to filteredLines) + } + text = strings.Join(filteredLines, "\n") + text = threeOrMoreDashesRE.ReplaceAllString(text, "") + text = strings.TrimSpace(text) // Remove leading/trailing whitespace + return text +} diff --git a/tui.go b/tui.go index 0413a83..147d35c 100644 --- a/tui.go +++ b/tui.go @@ -83,6 +83,7 @@ var ( [yellow]Ctrl+l[white]: show model selection popup to choose current model [yellow]Ctrl+k[white]: switch tool use (recommend tool use to llm after user msg) [yellow]Ctrl+a[white]: interrupt tts (needs tts server) +[yellow]Alt+0[white]: replay last message via tts (needs tts server) [yellow]Ctrl+g[white]: open RAG file manager (load files for context retrieval) [yellow]Ctrl+y[white]: list loaded RAG files (view and manage loaded files) [yellow]Ctrl+q[white]: show user role selection popup to choose who sends next msg as @@ -1144,6 +1145,18 @@ func init() { if event.Key() == tcell.KeyCtrlA && cfg.TTS_ENABLED { TTSDoneChan <- true } + if event.Key() == tcell.KeyRune && event.Rune() == '0' && event.Modifiers()&tcell.ModAlt != 0 && cfg.TTS_ENABLED { + if len(chatBody.Messages) > 0 { + // Stop any currently playing TTS first + TTSDoneChan <- true + lastMsg := chatBody.Messages[len(chatBody.Messages)-1] + cleanedText := models.CleanText(lastMsg.Content) + if cleanedText != "" { + go orator.Speak(cleanedText) + } + } + return nil + } if event.Key() == tcell.KeyCtrlW { // INFO: continue bot/text message // without new role -- cgit v1.2.3