summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2026-02-18 13:15:40 +0300
committerGrail Finder <wohilas@gmail.com>2026-02-18 13:15:40 +0300
commitf40f09390b7ccf365b41fa1cc134432537b50cad (patch)
treea3eec3dae4af0405830eb0eac13cac2f2f0cbebb
parent5548991f5c488032e682fe3a410e747a72c23b90 (diff)
Feat(tts) alt+0 to replay last message in the chatHEADmaster
-rw-r--r--extra/tts.go46
-rw-r--r--models/extra.go41
-rw-r--r--tui.go13
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