4 Commits

Author SHA1 Message Date
Grail Finder
850ca103e5 Enha: update model status color 2026-02-22 19:06:45 +03:00
Grail Finder
b7b5fcbf79 Fix: tts skiping over sentences 2026-02-22 18:27:32 +03:00
Grail Finder
1e13c7796d Fix: character specific context tts 2026-02-22 17:45:09 +03:00
Grail Finder
9a727b21ad Enha: simplify status line 2026-02-22 17:27:24 +03:00
4 changed files with 51 additions and 42 deletions

View File

@@ -92,8 +92,6 @@ func (o *KokoroOrator) stoproutine() {
func (o *KokoroOrator) readroutine() { func (o *KokoroOrator) readroutine() {
tokenizer, _ := english.NewSentenceTokenizer(nil) tokenizer, _ := english.NewSentenceTokenizer(nil)
// var sentenceBuf bytes.Buffer
// var remainder strings.Builder
for { for {
select { select {
case chunk := <-TTSTextChan: case chunk := <-TTSTextChan:
@@ -106,24 +104,28 @@ func (o *KokoroOrator) readroutine() {
continue continue
} }
text := o.textBuffer.String() text := o.textBuffer.String()
o.mu.Unlock()
sentences := tokenizer.Tokenize(text) sentences := tokenizer.Tokenize(text)
o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences)) o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences))
for i, sentence := range sentences { if len(sentences) <= 1 {
if i == len(sentences)-1 { // last sentence
o.mu.Lock()
o.textBuffer.Reset()
_, err := o.textBuffer.WriteString(sentence.Text)
o.mu.Unlock() o.mu.Unlock()
if err != nil {
o.logger.Warn("failed to write to stringbuilder", "error", err)
continue continue
} }
continue // if only one (often incomplete) sentence; wait for next chunk completeSentences := sentences[:len(sentences)-1]
remaining := sentences[len(sentences)-1].Text
o.textBuffer.Reset()
o.textBuffer.WriteString(remaining)
o.mu.Unlock()
for _, sentence := range completeSentences {
o.mu.Lock()
interrupted := o.interrupt
o.mu.Unlock()
if interrupted {
return
} }
cleanedText := models.CleanText(sentence.Text) cleanedText := models.CleanText(sentence.Text)
if cleanedText == "" { if cleanedText == "" {
continue // Skip empty text after cleaning continue
} }
o.logger.Debug("calling Speak with sentence", "sent", cleanedText) o.logger.Debug("calling Speak with sentence", "sent", cleanedText)
if err := o.Speak(cleanedText); err != nil { if err := o.Speak(cleanedText); err != nil {
@@ -338,24 +340,28 @@ func (o *GoogleTranslateOrator) readroutine() {
continue continue
} }
text := o.textBuffer.String() text := o.textBuffer.String()
o.mu.Unlock()
sentences := tokenizer.Tokenize(text) sentences := tokenizer.Tokenize(text)
o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences)) o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences))
for i, sentence := range sentences { if len(sentences) <= 1 {
if i == len(sentences)-1 { // last sentence
o.mu.Lock()
o.textBuffer.Reset()
_, err := o.textBuffer.WriteString(sentence.Text)
o.mu.Unlock() o.mu.Unlock()
if err != nil {
o.logger.Warn("failed to write to stringbuilder", "error", err)
continue continue
} }
continue // if only one (often incomplete) sentence; wait for next chunk completeSentences := sentences[:len(sentences)-1]
remaining := sentences[len(sentences)-1].Text
o.textBuffer.Reset()
o.textBuffer.WriteString(remaining)
o.mu.Unlock()
for _, sentence := range completeSentences {
o.mu.Lock()
interrupted := o.interrupt
o.mu.Unlock()
if interrupted {
return
} }
cleanedText := models.CleanText(sentence.Text) cleanedText := models.CleanText(sentence.Text)
if cleanedText == "" { if cleanedText == "" {
continue // Skip empty text after cleaning continue
} }
o.logger.Debug("calling Speak with sentence", "sent", cleanedText) o.logger.Debug("calling Speak with sentence", "sent", cleanedText)
if err := o.Speak(cleanedText); err != nil { if err := o.Speak(cleanedText); err != nil {

View File

@@ -108,14 +108,17 @@ func refreshChatDisplay() {
} }
} }
// stopTTSIfNotForUser: character specific context, not meant fot the human to hear
func stopTTSIfNotForUser(msg *models.RoleMsg) { func stopTTSIfNotForUser(msg *models.RoleMsg) {
if strings.Contains(cfg.CurrentAPI, "/chat") || !cfg.CharSpecificContextEnabled {
return
}
viewingAs := cfg.UserRole viewingAs := cfg.UserRole
if cfg.WriteNextMsgAs != "" { if cfg.WriteNextMsgAs != "" {
viewingAs = cfg.WriteNextMsgAs viewingAs = cfg.WriteNextMsgAs
} }
// stop tts if msg is not for user // stop tts if msg is not for user
if cfg.CharSpecificContextEnabled && if !slices.Contains(msg.KnownTo, viewingAs) && cfg.TTS_ENABLED {
!slices.Contains(msg.KnownTo, viewingAs) && cfg.TTS_ENABLED {
TTSDoneChan <- true TTSDoneChan <- true
} }
} }
@@ -354,13 +357,17 @@ func makeStatusLine() string {
} }
// Get model color based on load status for local llama.cpp models // Get model color based on load status for local llama.cpp models
modelColor := getModelColor() modelColor := getModelColor()
statusLine := fmt.Sprintf(statusLineTempl, boolColors[botRespMode], botRespMode, activeChatName, statusLine := fmt.Sprintf(statusLineTempl, boolColors[botRespMode], activeChatName,
boolColors[cfg.ToolUse], cfg.ToolUse, modelColor, chatBody.Model, boolColors[cfg.SkipLLMResp], boolColors[cfg.ToolUse], modelColor, chatBody.Model, boolColors[cfg.SkipLLMResp],
cfg.SkipLLMResp, cfg.CurrentAPI, boolColors[isRecording], isRecording, persona, cfg.CurrentAPI, persona, botPersona)
botPersona) if cfg.STT_ENABLED {
recordingS := fmt.Sprintf(" | [%s:-:b]voice recording[-:-:-] (ctrl+r)",
boolColors[isRecording])
statusLine += recordingS
}
// completion endpoint // completion endpoint
if !strings.Contains(cfg.CurrentAPI, "chat") { if !strings.Contains(cfg.CurrentAPI, "chat") {
roleInject := fmt.Sprintf(" | role injection [%s:-:b]%v[-:-:-] (alt+7)", boolColors[injectRole], injectRole) roleInject := fmt.Sprintf(" | [%s:-:b]role injection[-:-:-] (alt+7)", boolColors[injectRole])
statusLine += roleInject statusLine += roleInject
} }
return statusLine + imageInfo + shellModeInfo return statusLine + imageInfo + shellModeInfo

View File

@@ -13,7 +13,7 @@ var (
selectedIndex = int(-1) selectedIndex = int(-1)
shellMode = false shellMode = false
thinkingCollapsed = false thinkingCollapsed = false
statusLineTempl = "help (F12) | llm turn: [%s:-:b]%v[-:-:-] (F6) | chat: [orange:-:b]%s[-:-:-] (F1) |tool-use: [%s:-:b]%v[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [%s:-:b]%v[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | voice recording: [%s:-:b]%v[-:-:-] (ctrl+r) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)" statusLineTempl = "help (F12) | [%s:-:b]llm writes[-:-:-] (F6 to interrupt) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)"
focusSwitcher = map[tview.Primitive]tview.Primitive{} focusSwitcher = map[tview.Primitive]tview.Primitive{}
) )

View File

@@ -61,14 +61,11 @@ func showModelSelectionPopup() {
modelListWidget.SetCurrentItem(currentModelIndex) modelListWidget.SetCurrentItem(currentModelIndex)
} }
modelListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) { modelListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
// Strip "(loaded)" suffix if present for local llama.cpp models
modelName := strings.TrimPrefix(mainText, "(loaded) ") modelName := strings.TrimPrefix(mainText, "(loaded) ")
// Update the model in both chatBody and config
chatBody.Model = modelName chatBody.Model = modelName
cfg.CurrentModel = chatBody.Model cfg.CurrentModel = chatBody.Model
// Remove the popup page
pages.RemovePage("modelSelectionPopup") pages.RemovePage("modelSelectionPopup")
// Update the status line to reflect the change updateCachedModelColor()
updateStatusLine() updateStatusLine()
}) })
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
@@ -162,10 +159,9 @@ func showAPILinkSelectionPopup() {
chatBody.Model = newModelList[0] chatBody.Model = newModelList[0]
cfg.CurrentModel = chatBody.Model cfg.CurrentModel = chatBody.Model
} }
// Remove the popup page
pages.RemovePage("apiLinkSelectionPopup") pages.RemovePage("apiLinkSelectionPopup")
// Update the parser and status line to reflect the change
choseChunkParser() choseChunkParser()
updateCachedModelColor()
updateStatusLine() updateStatusLine()
}) })
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {