Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update NVDA Controller client to support speaking SSML sequences #15734

Merged
merged 27 commits into from
Nov 23, 2023

Conversation

LeonarddeR
Copy link
Collaborator

@LeonarddeR LeonarddeR commented Nov 2, 2023

Link to issue number:

Fixes #11028
Fixes #5638

Summary of the issue:

The NVDA Controller client has been stable for a long time, but it lacked support for modern speech features, such as priority and callbacks.

Description of user facing changes

None.

Description of development approach

Added the following functions to the controller client:

  • nvdaController_getProcessId: To get the process id (PID) of the current instance of NVDA the controller client is using.
  • nvdaController_speakSsml: To instruct NVDA to speak according to the given SSML. This function also supports:
    • Providing the symbol level.
    • Providing the priority of speech to be spoken.
    • speaking both synchronously (blockking) and asynchronously (instant return).
  • nvdaController_setOnSsmlMarkReachedCallback: To register a callback of type onSsmlMarkReachedFuncType that is called in synchronous mode for every <mark /> tag encountered in the SSML sequence provided to nvdaController_speakSsml.

Testing strategy:

Updated python example. Note that this doesn't work very well with Espeak, but that's more of espeaks fault. This is best tested with OneCore.

Known issues with pull request:

  • NVDA Helper Remote exports functions to behave like the NVDA Controller client. It looks like these exports are used nowhere in the wild. I kept this for now, but am tempted to remove it from NVDAHelperRemote.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

@LeonarddeR LeonarddeR requested a review from a team as a code owner November 2, 2023 17:35
@LeonarddeR LeonarddeR requested a review from seanbudd November 2, 2023 17:35
@LeonarddeR LeonarddeR force-pushed the nvdaControllerClient branch from 690511c to cb0bf83 Compare November 2, 2023 17:39
@LeonarddeR LeonarddeR force-pushed the nvdaControllerClient branch from cb0bf83 to 1b0ff0a Compare November 2, 2023 17:41
@seanbudd seanbudd marked this pull request as draft November 3, 2023 02:46
@LeonarddeR LeonarddeR marked this pull request as ready for review November 4, 2023 08:59
- Providing the symbol level.
- Providing the priority of speech to be spoken.
- Speaking both synchronously (blocking) and asynchronously (instant return).
- The exported ``_nvdaController_onSsmlMarkReached`` pointer can be assigned to a callback function that is called in synchronous mode for every ``<mark />`` tag encountered in the SSML sequence.
Copy link
Contributor

Choose a reason for hiding this comment

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

It is very likely my lack of understanding of how RPC rather than a real problem, but looking at this I cannot figure out how two applications wanting to provide their own callback for onSsmlMarkReached can do so. Since it seems to be necessary to assign the callback to the global pointer, it appears that, to be on the safe side, application developer should assign the old value of onSsmlMarkReached to a temporary variable before passing any speech to NVDA, assign their own callback, speak whatever has to be spoken, and restore the old value afterwards. Would it be possible to pass pointer to onSsmlMarkReached as an additional parameter to nvdaController_speakSsml instead of using the single global callback?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it appears that, to be on the safe side, application developer should assign the old value of onSsmlMarkReached to a temporary variable before passing any speech to NVDA, assign their own callback, speak whatever has to be spoken, and restore the old value afterwards.

That's not necessary. The old value is nullptr anyway and the internal nvdaController_onSsmlMarkReached only calls the exported pointer when it is set correctly.

Would it be possible to pass pointer to onSsmlMarkReached as an additional parameter to nvdaController_speakSsml instead of using the single global callback?

That would be a lot nicer indeed, though I don't think we'd be able to pass the function pointer over RPC.

I'll have a look into this.

Copy link
Contributor

Choose a reason for hiding this comment

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

it appears that, to be on the safe side, application developer should assign the old value of onSsmlMarkReached to a temporary variable before passing any speech to NVDA, assign their own callback, speak whatever has to be spoken, and restore the old value afterwards.

That's not necessary. The old value is nullptr anyway and the internal nvdaController_onSsmlMarkReached only calls the exported pointer when it is set correctly.

Backing up and restoring the old function pointer seems to still be necessary, without doing so I see no way in which two applications can provide their own callback. Without backing up and restoring, the last assigned callback will always be used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Every application can provide its own callback because the callback is stored in the user space of the client. The server, NVDA in this case, doesn't know anything about what the callback does. A callback is just RPC in the opposite direction.

@lukaszgo1
Copy link
Contributor

Note that this doesn't work very well with Espeak, but that's more of espeaks fault. This is best tested with OneCore.

Is this caused by issues in eSpeak, or our driver for it. In other words should this be reported against NVDA (if so is there an existing issue), or eSpeak (same question)?

Version 1.0 supports all past and present versions of NVDA, including 2024.1.

Would it be possible to create a system test bundling version 1.0 of controller client, which ensures that the current NVDA can indeed speak through it?

Version 1.1 is linked in the readme.

I assume you mend version 1.0 here?

@LeonarddeR
Copy link
Collaborator Author

Note that this doesn't work very well with Espeak, but that's more of espeaks fault. This is best tested with OneCore.

Is this caused by issues in eSpeak, or our driver for it. In other words should this be reported against NVDA (if so is there an existing issue), or eSpeak (same question)?

I recall that @jcsteh bumped into issues with Espeak indexing during speech refactor, but I"m not sure. I'm pretty sure it is an issue in ESpeak though.

Version 1.0 supports all past and present versions of NVDA, including 2024.1.

Would it be possible to create a system test bundling version 1.0 of controller client, which ensures that the current NVDA can indeed speak through it?

I'm not very comfortable with writing system tests honestly. I guess that this can be a unit test though.

Version 1.1 is linked in the readme.

I assume you mend version 1.0 here?

Correct, sorry.

@LeonarddeR LeonarddeR marked this pull request as draft November 6, 2023 08:38
@lukaszgo1
Copy link
Contributor

Would it be possible to create a system test bundling version 1.0 of controller client, which ensures that the current NVDA can indeed speak through it?

I'm not very comfortable with writing system tests honestly. I guess that this can be a unit test though.

Doing this as an unit test seems tricky as you need to have NVDAHelper and speech initialized. In system tests the entire screen reader is running, so it appears to be much easier.

@LeonarddeR
Copy link
Collaborator Author

I found out a nice way to avoid the API breakage. I will update the description, changelog and docs accoordingly later this week.

@AppVeyorBot
Copy link

See test results for failed build of commit 420e50f977

@LeonarddeR LeonarddeR marked this pull request as ready for review November 7, 2023 06:47
@AppVeyorBot
Copy link

See test results for failed build of commit fd7a608ac8

Copy link
Collaborator Author

@LeonarddeR LeonarddeR left a comment

Choose a reason for hiding this comment

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

I'm sorry, I had pending comments I forgot to post yesterday.

@LeonarddeR
Copy link
Collaborator Author

Thanks for approving @seanbudd. Is this still blocked by something?

@seanbudd seanbudd added the conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. label Nov 21, 2023

prefixSpeechCommand = None
markCallable = None
if not asynchronous:
Copy link
Member

Choose a reason for hiding this comment

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

Am I write in understanding that if the function is called with asynchronous as True, the mark callback will not be ever fired?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is correct. As per the docs about callbacks:

The callback function ... lets the server query the client for information in the context of the original call. Callbacks are special cases of remote calls that execute as part of a single thread. A callback is issued in the context of a remote call. Any remote procedure defined as part of the same interface as the static callback function can call the callback function.

So in other words, a callback can only be called during a call. Calling the callback as part of an async call that was already completed wouldn't work.

@seanbudd seanbudd merged commit 709c3d9 into nvaccess:master Nov 23, 2023
@nvaccessAuto nvaccessAuto added this to the 2024.1 milestone Nov 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. Controller
Projects
None yet
6 participants