Add RSVP continuous speech controller - #130
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds automatic speech recognition support to RSVP reading trials, including speech-mode handling, microphone/STT lifecycle management, simulation, and cleanup.
Changes:
- Adds RSVP speech runtime, mode, and controller logic.
- Integrates speech capture with RSVP timing and response handling.
- Adds lifecycle, simulation, and stimulus-generation tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Summary |
|---|---|
threshold.js |
Integrates automatic speech trials; critical completion, cleanup sequencing, and instruction issues remain. |
tests/rsvpSpeechRuntime.test.ts |
Tests speech runtime lifecycle and simulation. |
tests/rsvpSpeechMode.test.ts |
Tests response-mode resolution. |
tests/rsvpSpeechController.test.ts |
Tests speech controller lifecycle. |
tests/onStimulusGeneratedRsvp.test.ts |
Tests response-mode stimulus generation. |
components/rsvpSpeech/rsvpSpeechRuntime.ts |
Manages speech trial runtime state. |
components/rsvpSpeech/rsvpSpeechMode.ts |
Resolves RSVP response modes. |
components/rsvpSpeech/rsvpSpeechController.ts |
Controls microphone and provider lifecycle. |
components/rsvpReading.js |
Coordinates speech capture with RSVP timing; automatic completion remains unresolved. |
components/onStimulusGenerated.ts |
Supports mode-aware stimulus setup. |
components/lifetime.js |
Handles speech cleanup on quit and unload. |
components/keypad.js |
Restricts keypad handling by response mode. |
components/global.js |
Stores shared speech runtime state. |
Suppressed comments (7)
components/rsvpReading.js:668
allowProviderFinalization()only sets a flag; it does not request a provider commit. If VAD emits its commit while the targets are still on screen (for example, when the participant pauses), this call leaves that text buffered with no later commit, so the controller waits until its duration/finalization timeout. At the final target offset, force a provider commit or otherwise finalize the buffered transcript.
typeof rsvpReadingTargetSets.current === "undefined" &&
rsvpReadingTargetSets.upcoming.length === 0 &&
isRsvpReadingAutomaticSpeechResponseMode(rsvpReadingResponse.responseType)
)
allowRsvpSpeechProviderFinalization();
components/rsvpReading.js:603
- Even if the automatic trial is made to exit, this path never persists its speech result.
_rsvpReading_trialRoutineEndrecordsphraseIdentificationResponse, which automatic mode never fills, andgetRsvpSpeechResult()has no production caller, so the transcript is lost from experiment data. Add an automatic-response data field and define its scoring/timing semantics before trial end.
} else if (
isRsvpReadingExperimenterResponseMode(responseMode) &&
!rsvpReadingResponse.displayStatus
) {
// Else create some subtle feedback that the scientist can use
components/rsvpSpeech/rsvpSpeechController.ts:135
- Optional chaining does not validate runtime types here: values such as
utteranceId: 1,languageCode: {}, ortargetWords: "cat"still call.trim()/.map()and throw a rawTypeErrorbefore the structured configuration checks run. Validate the input types before normalization so callers consistently receiveRsvpSpeechControllerErrorwithinvalidConfiguration.
const utteranceId = configuration.utteranceId?.trim();
const languageCode = configuration.languageCode?.trim();
const targetWords = configuration.targetWords?.map((word) => word.trim());
components/rsvpSpeech/rsvpSpeechRuntime.ts:318
- The completion handler stores the result only in
rsvpSpeechRuntime; no production caller readsgetRsvpSpeechResult, while_rsvpReading_trialRoutineEndserializesphraseIdentificationResponse(which is never populated for automatic mode). Thus the captured transcript is not written to experiment data or converted intorsvpReadingResponsesBool/QUEST responses. Integrate the speech result into trial-end data/scoring before it is cleared.
runtime.result = result;
runtime.status = "completed";
removePageHideListener();
threshold.js:6612
prepareRsvpSpeechTrialcatches configuration/provider failures and resolvesfalse, but this await discards that result. The trial then proceeds withruntime.status === "failed"; the later capture/finalization calls returnfalse, and because manual responses are disabled in automatic mode the trial cannot complete. Handle afalseresult here by aborting/skipping and reporting the failed trial before continuing.
if (rsvpSpeechPreparationPromise) await rsvpSpeechPreparationPromise;
threshold.js:6211
- The simulation branch only puts the speech runtime into ready/capturing/finalizing states; it never calls
injectRsvpSpeechTranscriptForSimulation. After the final target, the result remains undefined, while the simulated participant has no answer controls and automatic mode ignores its space key, so simulated automatic-speech trials cannot complete. Inject a deterministic transcript at the finalization point and route it through the same completion/scoring path.
prepareSimulatedRsvpSpeechTrial({
threshold.js:7939
- This is the only new speech cleanup in the trial loop, but it is reached only when
toShowCursor()is handled at the top of the routine. The continuous-gaze failure path callsskipTrial()and returnsScheduler.Event.NEXTearlier (threshold.js:8629–8633), bypassing this block and leaving the microphone/provider active through the skipped trial. Move cleanup to common trial-end/skip handling or add it to that early return.
isRsvpReadingAutomaticSpeechResponseMode(
rsvpReadingResponse.responseType,
)
)
void closeActiveRsvpSpeechTrial();
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+664
to
+668
| typeof rsvpReadingTargetSets.current === "undefined" && | ||
| rsvpReadingTargetSets.upcoming.length === 0 && | ||
| isRsvpReadingAutomaticSpeechResponseMode(rsvpReadingResponse.responseType) | ||
| ) | ||
| allowRsvpSpeechProviderFinalization(); |
| status.block_condition, | ||
| psychoJS, | ||
| rsvpReadingResponse.responseType === "silent", | ||
| rsvpReadingResponse.responseType, |
| rsvpReadingResponse.responseType, | ||
| ) | ||
| ) | ||
| void closeActiveRsvpSpeechTrial(); |
Comment on lines
+6097
to
+6103
| rsvpReadingResponse.responseType = resolveRsvpReadingResponseMode({ | ||
| responseSpokenBool: paramReader.read("responseSpokenBool", BC), | ||
| responseSpokenToExperimenterBool: paramReader.read( | ||
| "responseSpokenToExperimenterBool", | ||
| BC, | ||
| ), | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the RSVP continuous speech controller for automatic speech responses.
What changed
responseSpokenBool.Testing