Skip to content

Keep remote speech active across config profile switches - #74

Open
LeonarddeR wants to merge 2 commits into
mainfrom
configProfileSwitch
Open

Keep remote speech active across config profile switches#74
LeonarddeR wants to merge 2 commits into
mainfrom
configProfileSwitch

Conversation

@LeonarddeR

Copy link
Copy Markdown
Owner

Problem

When NVDA on the remote desktop server starts with a synthesizer other than Remote Speech configured, SynthDetector activates the remote synth by assigning synthDriverHandler._curSynth directly, deliberately leaving config.conf["speech"]["synth"] alone.

NVDA''s synthDriverHandler.handlePostConfigProfileSwitch assumes _curSynth always mirrors config:

if conf["synth"] != _curSynth.name or config.conf["audio"]["outputDevice"] != _audioOutputDevice:
    ...
    setSynth(conf["synth"])
    return

So every configuration profile switch cancelled and terminated the remote synth and loaded the configured one. Nothing recovered it either: event_gainFocus triggers a rescan without force, and _queueBgScan bails out unless remote speech is the configured synthesizer. Only a lock state change forced a rescan.

Setting Remote Speech as the configured synthesizer would fix the mismatch, but writing real config is not acceptable here.

Fix

For as long as a SynthDetector is alive, it takes NVDA''s handler over and skips the synthesizer reload while remote speech is active without being configured.

Both the extension point registration and the module attribute are replaced. Action.register stores a reference to the function object itself, so patching the module attribute alone would leave the registered handler untouched, while only re-registering would leave the three direct calls in speech.manager (profile triggers inside a speech sequence) unhandled. Our handler is moved to the start of the registration order; NVDA registers its own second, and an OrderedDict cannot re-insert at an index.

The original handler and its registration are restored on terminate(), which is reached both when server mode is disabled and when the setting below is switched off.

The suppressed branch simply returns. RemoteDriver.loadSettings only touches _localSettings, i.e. fallbackSynth, and _fallback reads that value straight out of config.conf["speech"]["remote"] rather than off the driver, so there is nothing to re-sync.

Braille is unaffected: the remote display is auto-detected through NVDA''s standard mechanism with config.conf["braille"]["display"] == "auto", which brailleHandler.handlePostConfigProfileSwitch already preserves via _lastRequestedDisplayName.

Setting renamed

"Automatically recover remote speech after connection loss" is now "Automatically switch to remote speech when available", since the option governs activation, recovery and surviving profile switches. The config key recoverRemoteSpeech is unchanged, so existing configurations keep their value.

Testing

prek run --all-files passes (ruff, ty, 202 unit tests). synthDetect cannot be imported by the test harness, so this path is verified manually against a real session: connect with eSpeak configured on the server, confirm speech moves to the client, then activate a profile manually and by focusing an app, and confirm speech stays remote and the log shows no Loaded synthDriver espeak. Disconnecting still falls back per fallbackSynth, and turning the option off restores NVDA''s stock behaviour.

🤖 Generated with Claude Code

SynthDetector swaps the remote synth into synthDriverHandler._curSynth without
touching config.conf["speech"]["synth"], but NVDA's
synthDriverHandler.handlePostConfigProfileSwitch reloads the configured
synthesizer whenever it does not match the active one. Every profile switch
therefore terminated the remote synth, and nothing brought it back: the rescan
on focus change bails out unless remote speech is the configured synthesizer.

While a SynthDetector is alive, take NVDA's handler over, both on
config.post_configProfileSwitch and as the module attribute, so the calls from
speech.manager are covered too, and skip the reload while remote speech is
active without being configured. The original handler and registration are put
back on terminate.

Also broaden the wording of the setting that governs this, as it now covers
more than recovery after a connection loss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 12:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ensures Remote Speech (server-side) remains active across NVDA configuration profile switches when it was activated opportunistically (without changing the configured synthesizer), by intercepting NVDA’s post-profile-switch synth reload logic while SynthDetector is active.

Changes:

  • Take over NVDA’s post-config-profile-switch synth handler while SynthDetector is alive, skipping reload when Remote Speech is active but not configured.
  • Restore the original handler and extension-point registration on SynthDetector.terminate().
  • Rename the UI/readme setting label to reflect broader behavior (“switch to remote speech when available”).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
readme.md Updates the setting name/description to match new behavior (activation + persistence across profile switches).
addon/globalPlugins/rdAccess/synthDetect.py Adds handler takeover/restore to prevent synth reload during profile switches while Remote Speech is opportunistically active.
addon/globalPlugins/rdAccess/settingsPanel.py Updates the translated label text for the existing recoverRemoteSpeech setting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +72
original = synthDriverHandler.handlePostConfigProfileSwitch
if not config.post_configProfileSwitch.unregister(original):
log.debugWarning("NVDA's synthesizer profile switch handler was not registered")
self._nvdaHandlePostConfigProfileSwitch = original
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@LeonarddeR
LeonarddeR enabled auto-merge (squash) July 29, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants