Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,96 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user