Feat(tts) alt+0 to replay last message in the chat

This commit is contained in:
Grail Finder
2026-02-18 13:15:40 +03:00
parent 5548991f5c
commit f40f09390b
3 changed files with 59 additions and 41 deletions

View File

@@ -13,10 +13,9 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"regexp"
"strings" "strings"
"time"
"sync" "sync"
"time"
google_translate_tts "github.com/GrailFinder/google-translate-tts" google_translate_tts "github.com/GrailFinder/google-translate-tts"
"github.com/GrailFinder/google-translate-tts/handlers" "github.com/GrailFinder/google-translate-tts/handlers"
@@ -31,43 +30,8 @@ var (
TTSFlushChan = make(chan bool, 1) TTSFlushChan = make(chan bool, 1)
TTSDoneChan = make(chan bool, 1) TTSDoneChan = make(chan bool, 1)
// endsWithPunctuation = regexp.MustCompile(`[;.!?]$`) // 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 { type Orator interface {
Speak(text string) error Speak(text string) error
Stop() Stop()
@@ -157,7 +121,7 @@ func (o *KokoroOrator) readroutine() {
} }
continue // if only one (often incomplete) sentence; wait for next chunk continue // if only one (often incomplete) sentence; wait for next chunk
} }
cleanedText := cleanText(sentence.Text) cleanedText := models.CleanText(sentence.Text)
if cleanedText == "" { if cleanedText == "" {
continue // Skip empty text after cleaning continue // Skip empty text after cleaning
} }
@@ -186,7 +150,7 @@ func (o *KokoroOrator) readroutine() {
// flush remaining text // flush remaining text
o.mu.Lock() o.mu.Lock()
remaining := o.textBuffer.String() remaining := o.textBuffer.String()
remaining = cleanText(remaining) remaining = models.CleanText(remaining)
o.textBuffer.Reset() o.textBuffer.Reset()
o.mu.Unlock() o.mu.Unlock()
if remaining == "" { if remaining == "" {
@@ -389,7 +353,7 @@ func (o *GoogleTranslateOrator) readroutine() {
} }
continue // if only one (often incomplete) sentence; wait for next chunk continue // if only one (often incomplete) sentence; wait for next chunk
} }
cleanedText := cleanText(sentence.Text) cleanedText := models.CleanText(sentence.Text)
if cleanedText == "" { if cleanedText == "" {
continue // Skip empty text after cleaning continue // Skip empty text after cleaning
} }
@@ -417,7 +381,7 @@ func (o *GoogleTranslateOrator) readroutine() {
} }
o.mu.Lock() o.mu.Lock()
remaining := o.textBuffer.String() remaining := o.textBuffer.String()
remaining = cleanText(remaining) remaining = models.CleanText(remaining)
o.textBuffer.Reset() o.textBuffer.Reset()
o.mu.Unlock() o.mu.Unlock()
if remaining == "" { if remaining == "" {

View File

@@ -1,8 +1,49 @@
package models package models
import (
"regexp"
"strings"
)
type AudioFormat string type AudioFormat string
const ( const (
AFWav AudioFormat = "wav" AFWav AudioFormat = "wav"
AFMP3 AudioFormat = "mp3" 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
}

13
tui.go
View File

@@ -83,6 +83,7 @@ var (
[yellow]Ctrl+l[white]: show model selection popup to choose current model [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+k[white]: switch tool use (recommend tool use to llm after user msg)
[yellow]Ctrl+a[white]: interrupt tts (needs tts server) [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+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+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 [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 { if event.Key() == tcell.KeyCtrlA && cfg.TTS_ENABLED {
TTSDoneChan <- true 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 { if event.Key() == tcell.KeyCtrlW {
// INFO: continue bot/text message // INFO: continue bot/text message
// without new role // without new role