RSVP (Rapid Serial Visual Presentation) word flasher with LSL (Lab Streaming Layer) marker streaming for EEG experiments. Displays words one at a time with frame-accurate timing and pushes timestamped markers to LSL for downstream EEG epoch-locking.
Built for the London Neurotech Hackathon 2026 as part of the Neural RLHF system — using EEG-derived preference signals (N400, P300) for real-time language model evaluation.
┌─────────────┐
--sentence │ RSVP Loop │ LSL "RSVPMarkers"
--llm claude ──┤ (PsychoPy) ├──► outlet ──► LabRecorder / EEG pipeline
│ 60Hz vsync │
└─────────────┘
Per word:
1. Set text, pre-draw to back buffer
2. win.flip() — blocks until vsync, word appears on screen
3. push_sample() — LSL marker pushed (sub-ms after flip)
4. Hold for remaining frames of word duration
Each word is shown for 500 ms followed by a 200 ms inter-word blank (~1.43 words/sec). Timing is frame-based, not clock-based, for sub-millisecond precision.
- Python 3.10 (required by psychopy-lib, pinned in
.python-version) - uv package manager
- Linux (X11) or Windows — macOS compositor introduces erratic frame timing
git clone <repo-url> && cd Neural_RLFH_frontend
# Install locked dependencies
uv sync
# Install PsychoPy without its heavy GUI deps (wxPython, PyQt6, psychtoolbox)
uv pip install psychopy-lib --no-depsuv run python -c "from psychopy import visual, core, event; from pylsl import StreamInfo, StreamOutlet; print('OK')"Warnings about missing psychtoolbox, wx, PyQt6, or ALSA are harmless and expected.
uv run rsvp --sentence "The model generated a surprisingly coherent response"# Requires ANTHROPIC_API_KEY or OPENAI_API_KEY in environment
uv run rsvp --llm claude --prompt "Explain in one sentence why the sky is blue."
uv run rsvp --llm chatgpt --prompt "What is neuroplasticity?"Claude responses are automatically constrained to a single sentence (≤20 words) for RSVP suitability. Missing or invalid API keys produce clear error messages instead of raw tracebacks.
uv run rsvp --no-fullscreen --sentence "Testing in a window"Always use fullscreen for real data collection — windowed mode adds compositor jitter.
uv run rsvp-receiverPrints every marker as it arrives and reports inter-word jitter statistics after each sentence.
uv run python lsl_drain.pyResolves the RSVPMarkers stream, drains all buffered samples, and exits. Useful for cleanup between runs.
All markers are pushed to the RSVPMarkers LSL stream (type Markers).
| Marker | When |
|---|---|
FIXATION |
Fixation cross onset |
SENTENCE_START |
Before word loop |
idx:word:total |
Each word onset (e.g., 3:neural:12) |
SENTENCE_END |
After word loop |
ABORTED |
User pressed Escape |
neural_rlfh_frontend/
__init__.py # Package marker, version string
config.py # All constants
cli.py # parse_args()
llm.py # fetch_from_claude, fetch_from_chatgpt, get_sentence
lsl.py # create_lsl_outlet, push_marker, receive_markers
display.py # create_window, measure_frame_rate, create_stimuli
presentation.py # show_fixation, present_words, present_chunks
timing.py # report_timing (post-hoc jitter stats)
main.py # main() entry point — orchestrates everything
Root-level rsvp_flasher.py and lsl_receiver.py are thin shims for backwards compatibility.
Defined in pyproject.toml:
| Command | Target | Description |
|---|---|---|
uv run rsvp |
neural_rlfh_frontend.main:main |
Run the RSVP flasher |
uv run rsvp-receiver |
neural_rlfh_frontend.lsl:receive_markers |
Monitor LSL markers |
All constants are in neural_rlfh_frontend/config.py:
| Constant | Default | Description |
|---|---|---|
WORD_DURATION_SEC |
0.5 |
Word display duration (seconds) |
BLANK_DURATION_SEC |
0.2 |
Inter-word blank duration (seconds) |
FIXATION_SEC |
0.5 |
Fixation cross duration (seconds) |
FONT_HEIGHT |
0.12 |
PsychoPy height units (~7 deg visual angle) |
FONT_COLOR |
white |
Stimulus color |
BG_COLOR |
black |
Background color |
FULLSCREEN |
True |
Fullscreen by default |
LSL_STREAM_NAME |
RSVPMarkers |
LSL stream name |
This package is a pure stimulus presenter. It outputs LSL markers consumed by:
- LabRecorder — records all LSL streams to XDF
- LSL sync hub — subscribes by stream name/type
- Streamlit dashboard — reads
RSVPMarkersfor live word display - MNE-Python — epochs EEG data around word onset markers (
tmin=-0.2, tmax=0.8)
- All timing is frame-based (vsync-locked
win.flip()calls), nevertime.sleep() - At 60 Hz: 30 frames per word (500 ms) + 12 frames blank (200 ms) = 700 ms per cycle
- LSL markers are pushed after
win.flip()returns (flip is what puts the pixel on screen) - The presentation loop runs entirely on the main thread (PsychoPy's OpenGL context is not thread-safe)
- Post-presentation timing report shows dropped frames and marker jitter
MIT