Files

97 lines
3.1 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"log/slog"
"strings"
"unicode"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/presentation"
"git.stormux.org/storm/cthulhu/internal/review"
)
func presentReadAllResult(logger *slog.Logger, speech speechEnqueuer, result review.Result, options presentation.Options) {
if result.ReadAllContext == nil || result.ReadAllContext.Err() != nil {
return
}
text := ""
var parts []readAllPart
if result.Err != nil {
logger.Warn("read all failed", "error", result.Err)
text = "Could not read from current location."
} else {
// Keep semantic roles audible in continuous reading, without firing all
// the page's sound cues before the speech has reached those objects.
options.RoleSoundPresentation = "sound_and_speech"
// Python reads each cell once in Say All rather than expanding every
// cell into the same complete row.
reading := config.DefaultSettings().Reading
if options.Reading != nil {
reading = *options.Reading
}
reading.ReadFullRowInDocumentTable, reading.ReadFullRowInSpreadSheet = false, false
options.Reading = &reading
options.ReadFullRowInGUITable = false
for index, line := range result.ReadAllLines {
if spoken := presentation.DocumentLineSpeechWithOptions(line, options); spoken.Text != "" {
var began func()
if result.ReadAllProgress != nil {
began = func() { result.ReadAllProgress(index) }
}
values := spoken.Chunks
if len(values) == 0 {
values = []string{spoken.Text}
}
var chunks []readAllPart
for _, value := range values {
chunks = append(chunks, readAllLineParts(value, began, options.SayAllStyle)...)
began = nil
}
chunks[0].Line = document_speech_line(line)
for index := range chunks {
chunks[index].VoiceType = document_line_voice(line)
}
chunks[0].Context = line.Context
parts = append(parts, chunks...)
}
}
}
if text != "" || len(parts) != 0 {
speech.Enqueue(speechRequest{Text: text, ReadAll: parts, Role: "system", Cause: "read-all", context: result.ReadAllContext})
}
}
// Keep each source line's first callback attached to its first spoken chunk.
// The review worker tracks line positions, so sentence mode splits within a
// line and never merges lines whose separate progress updates drive the caret.
func readAllLineParts(text string, began func(), style string) []readAllPart {
if style != "sentence" {
return []readAllPart{{Text: text, Began: began}}
}
runes := []rune(text)
var parts []readAllPart
start := 0
for index, value := range runes {
if value != '.' && value != '!' && value != '?' {
continue
}
if index+1 < len(runes) && !unicode.IsSpace(runes[index+1]) {
continue
}
chunk := strings.TrimSpace(string(runes[start : index+1]))
if chunk != "" {
parts = append(parts, readAllPart{Text: chunk, Began: began})
began = nil
}
start = index + 1
}
if tail := strings.TrimSpace(string(runes[start:])); tail != "" {
parts = append(parts, readAllPart{Text: tail, Began: began})
}
return parts
}