Add isolated Remote speech synthesis and typed rendering

Preserve literal whitespace and resolved spelling/language state across complete Remote speech assemblies. Isolate cancellation from local status and echo, retire handles on configuration changes or rendering failure, and cover metadata preparation cancellation with private SSIP fixtures.

Verified focused application and backend race tests, ten repetitions of isolated-client/rendering cases, shared speech regressions, affected-package vet, reader build and Tentacles checks. Incoming mailbox integration, local priority and live audible acceptance remain pending.
This commit is contained in:
ak47 committed 2026-11-04 19:56:38 -05:00
1 parent 622d2f96a1
commit ac431dd514
9 files changed
+1370 -11

No files matched your search

+116 -2
View File
@@ -138,6 +138,120 @@ warnings remain in place.
## Verification and remaining work
### Isolated synthesis prerequisite
The configured software backend now has an internal `open_remote_voice`
primitive. It lazily creates one independent Speech Dispatcher speaking/control
pair using the last successfully committed default voice settings. Ordinary
reader startup creates no additional connections. Hardware and unconnected or
closed backends reject acquisition; another acquisition is rejected until the
previous handle has finished closing. This is not a lease authorizer and is not
attached to the production incoming mailbox.
The handle offers plain-text `SpeakAndWait`, scoped `Cancel`, and idempotent
`Close`. Speech remains outstanding through END/CANCEL, rather than just queue
acknowledgement. Caller cancellation affects only the isolated client. Acquisition
context cancellation after a successful open does not retire that handle: a
future lease owner must explicitly close it on revoke, policy loss and shutdown.
Core user interruption cancels both local output and this separate client.
A successful settings commit retires the old handle; it never silently adopts
the replacement configuration. Failed preparation preserves it. Closing an old
handle cannot cancel its replacement. Configuration updates now also retain the
committed settings snapshot when reusing a software backend, so subsequent
acquisition uses the current module and default voice rather than startup values.
Private Unix-socket SSIP tests keep local status and custom echo queued while
remote cancellation, caller cancellation and handle closure run. They check
independent client identities, END/CANCEL pacing, connection reuse, exclusive
ownership, configuration success/failure and stale cleanup. These model protocol
ownership; they do not establish real Speech Dispatcher scheduling, audible
cancellation, or priority arbitration. The original plain-text primitive retains
ordinary normalization. The typed renderer below adds whitespace and
character/language handling; pause and NVDA priority still need stream policy.
No command invokes this primitive yet. The rendering prerequisite below adds
typed utterances; binding them to the bounded reader mailbox still requires
delivery-time lease/focus checks and local priority. Keep mailbox budgets until
delivery/discard and retire queued work on cancellation. No live Remote speech
acceptance test is available from these prerequisites alone.
### Literal utterance rendering prerequisite
`prepare_remote_utterance` now validates a complete speak assembly, retaining its
single priority for a future stream owner. It rejects non-speak effects, mixed
priorities and assemblies beyond the existing 64-frame/256-KiB limits. It joins
adjacent text with the same language/spelling state across IPC fragment
boundaries without adding separators. An end marker or metadata change creates
a synthesis boundary; repeated empty end markers produce no speech. It neither
owns nor releases mailbox budget and cannot authorize presentation.
The isolated handle's `SpeakUtteranceAndWait` renders that prepared sequence
through a new bounded `speech.Client.SpeakFragmentsAndWait` path. Every literal
fragment is checked before backend I/O. Invalid UTF-8, NUL, C0 controls other than
tab/CR/LF, DEL and invalid language tokens reject the whole sequence without a
spoken prefix. Spaces and tabs remain intact; CR, LF and CRLF become SSIP CRLF
line boundaries. Every dot-prefixed line is escaped, including a lone dot.
`SSML_MODE off` makes markup and command-looking text literal data. This changes
no ordinary local-text normalization.
Each fragment uses `SPELLING on/off` and its resolved language. Before rendering,
the client reads its effective default language, output module and symbolic voice
category; unsupported/malformed defaults fail closed. It restores the original
module after every language change, because Speech Dispatcher language mappings
may change modules. Remote language overrides clear a configured named voice and
use the captured symbolic category. If a named voice has no category, explicit
remote languages use SSIP category MALE1; default-language parts restore the
configured voice name. Unknown categories without a configured named voice are
rejected. The accepted module/default-language tokens are limited to 64 ASCII
letters, digits, hyphens or underscores. Backend acceptance of a language request
does not establish that the module can synthesize that language.
The speaking connection remains owned through each END/CANCEL and the complete
sequence. Success restores default voice/language and spelling off. A backend
failure or cancellation discards the remaining suffix and retires the isolated
handle, cancelling only its client. A replacement requires explicit acquisition;
there is no automatic retry or replay. Atomic IPC assembly does not make several
backend fragments atomic: a later backend rejection can follow an already-spoken
earlier fragment.
Only one typed assembly can render at a time. Scoped cancellation cancels its
context as well as outstanding synthesis, so a cancel during a stalled metadata
query cannot be lost before the first SPEAK. It never waits for metadata I/O in
the caller. The renderer completes cleanup before returning ownership.
This implementation follows the [SSIP speech, spelling and language
contract](https://github.com/brailcom/speechd/blob/master/doc/ssip.texi) and checks
default-query/module-switch behavior against the upstream
[parser](https://github.com/brailcom/speechd/blob/master/src/server/parse.c) and
[settings implementation](https://github.com/brailcom/speechd/blob/master/src/server/set.c).
It makes no NVDA priority or pause decision. A production owner still has to
apply priority once, recheck original-lease/focus/mode policy, yield to local
output, process cancel/pause while synthesis is outstanding, and retain mailbox
budget until completion/discard. The source Remote command remains relay-only
for incoming speech; no active or installed reader was replaced.
### Automated evidence
For the isolated synthesis prerequisite, focused application/backend race checks
passed, including ten repetitions of the new cases. Existing speech regression
checks, affected-package vet, a separate reader build and Tentacles `make check`
also passed. No installed or active reader was replaced, and no audible or
physical-input acceptance was performed.
The literal rendering increment passed the complete speech-backend race suite
and ten repetitions of the isolated-client/rendering race cases. Private SSIP
fixtures cover exact whitespace, mixed CR/LF, dot-only/command-looking lines,
markup as literal text, repeated-character spelling, language/module/default
restoration, named-voice fallback, long split text, end boundaries, assembly
limits, invalid later fragments, unavailable defaults, rejected settings and
remote/caller/configuration cancellation with local status/echo retained. The
stalled-metadata cancellation case also rejects overlapping typed assemblies
and verifies that neither a prefix nor suffix reaches synthesis. The
existing speech regression harness, affected-package vet, separate reader build
and Tentacles `make check` passed. These are protocol fixtures, without real
Speech Dispatcher, audio, physical input or Braille acceptance.
Run from this checkout:
```sh
@@ -172,8 +286,8 @@ host/wire/both-SDK race suites (host: about 127 seconds), focused application
speech/Remote/Tentacle race checks, affected-package vet, a separate reader
build, the existing speech regression harness and Tentacles `make check`.
Next: add incoming speech ownership, delivery-time policy, local priority and
remote-only cancellation. Explicit status detachment is implemented; physical
Next: bind the isolated renderer to incoming speech ownership, delivery-time
policy, local priority and stream cancellation. Explicit status detachment is implemented; physical
input capture/release and ordinary reading while connected still need acceptance.
Review the remaining output producers before assigning exportable origins.
Atomic IPC assembly is implemented; it does not establish backend