Feat(tts) alt+0 to replay last message in the chat
This commit is contained in:
46
extra/tts.go
46
extra/tts.go
@@ -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 == "" {
|
||||||
|
|||||||
@@ -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
13
tui.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user