97 lines
3.1 KiB
Go
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
|
|
}
|