diff --git a/.ai/context.md b/.ai/context.md new file mode 100644 index 00000000..4d11452f --- /dev/null +++ b/.ai/context.md @@ -0,0 +1,399 @@ +# ReproStim Project AI Context/Memory File + +## Project Overview + +**ReproStim** is a comprehensive video capture and recording suite designed for neuroimaging and psychology experiments. It provides experimenters with a complete, high-fidelity audio-visual record of stimulus presentation during fMRI scanning sessions as part of the **ReproFlow** ecosystem for reproducible neuroimaging data collection. + +### Core Mission +- Automatic capture of all audio/video stimuli presented to research subjects +- Integration with BIDS (Brain Imaging Data Structure) datasets +- Minimal effort required from end-users (runs silently in background) +- Enhanced experimental reproducibility and data loss prevention +- Recovery of experimental data in case of irregularities or scanner glitches + +### Architecture Overview +``` +Hardware Layer (Magewell USB Capture Device) + ↓ +Capture Services (C++ videocapture/screencapture) + ↓ +Python Analysis Tools (QR parsing, audio codes, video audit) + ↓ +BIDS Integration & Documentation +``` + +## Project Structure + +### Main Directories + +#### `src/reprostim/` - Python Package (Core) +Main Python package with CLI tools and analysis utilities. + +**Key Modules:** +- **cli/** - Command-line interface (Click-based with DYMGroup for suggestions) + - `entrypoint.py` - Main CLI dispatcher + - `cmd_qr_parse.py` - Parse QR codes from `.mkv` videos (PARSE/INFO modes) + - `cmd_timesync_stimuli.py` - PsychoPy integration for QR/audio code generation + - `cmd_detect_noscreen.py` - Detect no-signal/rainbow frames with fixup capabilities + - `cmd_list_displays.py` - List available GUI displays (cross-platform) + - `cmd_monitor_displays.py` - Monitor display connection status with callbacks + - `cmd_video_audit.py` - Comprehensive video analysis (incremental/full/force modes) + - `cmd_echo.py` - Simple echo command for testing + +- **qr/** - QR code processing and time synchronization + - `qr_parse.py` - Parse `.mkv` files, extract QR codes, audio codes, metadata → JSONL + - `timesync_stimuli.py` - PsychoPy-based MRI/BIRCH/Magewell synchronization + - `disp_mon.py` - Cross-platform display monitoring (Linux/macOS/Windows) + - `psychopy.py` - PsychoPy framework integration utilities + - `video_audit.py` - Comprehensive video analysis with multiple audit sources + +- **audio/** - Audio codec generation + - `audiocodes.py` - FSK/NFE codecs with Reed-Solomon error correction + - Supports PsychoPy audio backends (sounddevice, PTB) + - CRC8 checksum implementation for data validation + +- **capture/** - Video capture utilities + - `nosignal.py` - Rainbow/no-signal frame detection (multi-algorithm: has_rainbow, has_rainbow2) + - VideoInfo Pydantic model for structured metadata + - Video fixup capabilities using ffmpeg for truncated/invalid-timing videos + +#### `src/reprostim-capture/` - C++ Video Capture Suite +CMake-based C++ project with 3 main executables. + +**Structure:** +``` +src/reprostim-capture/ +├── CMakeLists.txt # Main build configuration (C++20) +├── capturelib/ # Core capture library +│ ├── include/reprostim/ # Headers (CaptureLib, CaptureLog, CaptureApp) +│ ├── src/ # Implementation files +│ └── test/ # Catch2-based C++ tests +├── screencapture/ # Screen capture utility +├── videocapture/ # Magewell USB video capture +├── rectrigger/ # Trigger server/client +└── 3rdparty/ # Magewell SDK (versions 3.3.1.0 and 3.3.1.1313) +``` + +**Key Components:** +- **CaptureLib**: Core library with threading, REST API, ReproMon integration, logging +- **ScreenCapture**: Desktop/screen recording utility +- **VideoCapture**: Magewell USB device capture with configuration support +- **Dependencies**: libMWCapture (Magewell SDK), libusb, wxWidgets, portaudio + +#### `tests/` - Test Suite +``` +tests/ +├── audio/ +│ └── test_audiocodes.py # Audio codec tests (CRC8) +└── data/ + ├── reprostim-videos/ # Test video samples + └── nosignal/ # No-signal detection test data +``` + +**Testing Approach:** +- **Python**: pytest with pytest-cov (coverage), pytest-mock (mocking), pytest-xdist (parallel) +- **C++**: Catch2 framework (v2/v3 compatible) +- **CI/CD**: GitHub Actions workflows, container-based integration tests + +#### `docs/` - Documentation +Sphinx + MyST (Markdown support) documentation. + +**Structure:** +``` +docs/source/ +├── conf.py # Sphinx configuration +├── index.rst # Main documentation index +├── intro/ # Introduction docs +├── install/ # Installation guide (Markdown) +├── cli/ # CLI command documentation (auto-generated) +├── api/ # Auto-generated API reference +├── dev/ # Development guide +├── notes/ # Technical notes (disp_mon, psychopy, VAAPI, ffmpeg) +├── changes/ # Changelog/release notes +└── _static/ # Static assets +``` + +**Special Features:** +- Auto-documentation via sphinx-click +- Mermaid diagram support +- Read the Docs integration (`.readthedocs.yaml`) +- SVG-based ReproFlow diagram +- Platform-specific notes (Linux, macOS, Windows) + +#### `containers/` - Container Definitions +Docker and Singularity container support for deployment. + +**Structure:** +``` +containers/repronim-reprostim/ +├── Dockerfile.repronim-reprostim +├── Singularity.repronim-reprostim +├── generate_container.sh # Template generation +├── build_docker.sh +├── build_singularity.sh +├── run_reprostim.sh +├── build_reprostim.sh # Install script (PsychoPy + reprostim) +└── README.md +``` + +**Container Strategy:** +- **Base**: Neurodebian (bookworm) +- **PsychoPy**: Version 2025.2.0 via psychopy_linux_installer +- **Python**: 3.10 with all optional dependencies +- **Modes**: CI/CD mode (install from worktree) vs default mode (install from PyPI) +- **Features**: Development overlay support for debugging + +#### `tools/` - CI/CD and Configuration Scripts +``` +tools/ +├── ci/ # CI/CD scripts +│ ├── build_reprostim_container.sh +│ ├── run_reprostim_container.sh +│ ├── test_reprostim_container.sh +│ ├── test_reprostim_timesync-stimuli.sh +│ └── test_reprostim_events.sh +└── reproiner-config.sh # ReproInner configuration +``` + +## Technology Stack + +### Python Dependencies +- **Click 8.1.7+**: CLI framework with did-you-mean suggestions +- **PsychoPy**: Psychology experiment framework (optional) +- **OpenCV (cv2) 4.9.0+**: Video processing and QR code detection +- **Pyzbar 0.1.9+**: QR code scanning +- **Pydantic 2.7.1+**: Data validation and serialization +- **Numpy 1.26.4+**: Numerical operations +- **SciPy 1.14.1+**: Signal processing +- **sounddevice 0.5.1+**: Audio I/O +- **pygame 2.6.1+**: Display monitoring +- **PyAudio 0.2.14+**: Audio interface +- **reedsolo 1.7.0+**: Reed-Solomon error correction +- **psutil**: System monitoring + +### C++ Stack +- **C++20**: Modern C++ standard +- **CMake 3.10+**: Build system +- **Magewell SDK 3.3.1**: USB capture device API +- **Catch2**: C++ testing framework (v2/v3) +- **libusb**: USB device interaction + +### Documentation & Testing +- **Sphinx + MyST**: Documentation generation +- **pytest**: Python testing framework +- **pytest-cov**: Code coverage +- **pytest-mock**: Test mocking +- **pytest-xdist**: Parallel test execution + +## Installation Options + +```bash +# Core package +pip install reprostim + +# With audio codec support +pip install reprostim[audio] + +# With display monitoring (platform-specific) +pip install reprostim[disp_mon] + +# With PsychoPy integration +pip install reprostim[psychopy] + +# Everything +pip install reprostim[all,disp_mon] +``` + +## Build Systems + +### Python Package +```bash +# Build system: hatchling with versioningit +# Version source: Git tags → src/reprostim/_version.py +# Distribution: PyPI (reprostim) and Conda-Forge + +pip install -e . +``` + +### C++ Build +```bash +cd src/reprostim-capture +mkdir build && cd build +cmake .. -DCTEST_ENABLED=ON +make +make test +sudo make install # Installs to CMAKE_INSTALL_PREFIX/bin +``` + +### Container Build +```bash +cd containers/repronim-reprostim +./generate_container.sh +./build_docker.sh +# or +./build_singularity.sh +``` + +## Workflow and Data Flow + +``` +Live Experiment Session + ↓ +Magewell USB Capture Device (Hardware) + ↓ +reprostim-videocapture (C++) → Records .mkv videos + ↓ +reprostim timesync-stimuli (Python/PsychoPy) + ├─ Generates QR codes on screen (video) + └─ Generates audio codes (FSK/NFE) + ↓ +Recorded Videos contain: Video + QR codes + Audio codes + ↓ +reprostim qr-parse → Extract QR/audio metadata (JSONL) + ↓ +reprostim video-audit → Analyze all videos, generate videos.tsv + ↓ +reprostim detect-noscreen → Check for no-signal frames + ↓ +Integrated with BIDS datasets for archival +``` + +## Important Files + +| File | Purpose | +|------|---------| +| `pyproject.toml` | Python package configuration, dependencies, build metadata | +| `CMakeLists.txt` | C++ build configuration (reprostim-capture) | +| `README.md` | Project overview, quick start guide | +| `CHANGELOG.md` | Release notes and version history | +| `overview.md` | Detailed project scope and architecture | +| `src/reprostim/__init__.py` | Logger initialization, package entry point | +| `src/reprostim/cli/entrypoint.py` | Main CLI dispatcher | +| `src/reprostim/qr/qr_parse.py` | Video QR code extraction (JSONL output) | +| `src/reprostim/audio/audiocodes.py` | FSK/NFE audio codec generation | +| `src/reprostim/capture/nosignal.py` | No-signal frame detection algorithm | +| `.readthedocs.yaml` | Read the Docs build configuration | +| `.github/workflows/` | GitHub Actions CI/CD pipeline definitions | + +## Configuration Files + +| Config File | Role | +|-------------|------| +| `pyproject.toml` | Package metadata, dependencies, build backend (hatchling), tool configs | +| `CMakeLists.txt` | C++ build configuration, compiler flags, dependencies | +| `.readthedocs.yaml` | Read the Docs build environment, Python version | +| `.pre-commit-config.yaml` | Pre-commit hooks for code quality | +| `.codespellrc` | Spell checking configuration | +| `REUSE.toml` | SPDX license compliance configuration | +| `containers/*/Dockerfile*` | Container image definitions | +| `src/reprostim-capture/version.txt` | Version string for C++ build | + +## Git Information + +- **Current Branch**: `enh-perms` (enhancement for permissions) +- **Main Branch**: `master` (use for PRs) +- **Recent Commits**: Focus on permissions fixes, container generation, QR/nosignal features +- **License**: MIT +- **Version**: 0.7.x (Alpha development status) + +## Team and Community + +- **Organization**: ReproNim (Reproducible Neuroimaging) +- **Core Contributors**: + - Yaroslav Halchenko (lead) + - Vadim Melnik (active development) + - Horea Christian + - Andy Connolly +- **Project Links**: + - GitHub: https://github.com/ReproNim/reprostim + - Documentation: Read the Docs (configured) + - PyPI: reprostim package + +## Development Guidelines + +### Code Quality +- Pre-commit hooks configured (see `.pre-commit-config.yaml`) +- Spell checking via codespell +- SPDX license compliance (REUSE.toml) +- Shellcheck for bash scripts (disable SC2086, SC2034 in build_reprostim.sh) + +### Testing +- Run Python tests: `pytest tests/` +- Run C++ tests: `cd build && make test` +- CI tests: `tools/ci/test_reprostim_container.sh` + +### Documentation +- Build docs: `cd docs && make html` +- Auto-generated CLI docs via sphinx-click +- Technical notes in `docs/source/notes/` + +### Version Management +- Version source: Git tags +- Auto-generated: `src/reprostim/_version.py` via versioningit +- C++ version: `src/reprostim-capture/version.txt` + +## Common Tasks + +### Running CLI Commands +```bash +# Parse QR codes from video +reprostim qr-parse video.mkv + +# Run PsychoPy time synchronization +reprostim timesync-stimuli + +# Detect no-signal frames +reprostim detect-noscreen video.mkv + +# Audit all videos +reprostim video-audit /path/to/videos + +# List displays +reprostim list-displays + +# Monitor displays +reprostim monitor-displays +``` + +### Building Containers +```bash +cd containers/repronim-reprostim + +# Generate templates +./generate_container.sh + +# Build Docker (default mode - PyPI install) +./build_docker.sh + +# Build for CI (install from worktree) +./build_docker.sh ci + +# Build Singularity +./build_singularity.sh +``` + +### C++ Capture Tools +```bash +# After building and installing: +reprostim-videocapture -V # Check version +reprostim-screencapture --help # Screen capture help +``` + +## Notes and Caveats + +1. **PsychoPy Version**: Container uses 2025.2.0 (latest) with Python 3.10 +2. **Display Monitoring**: Platform-specific dependencies (pygame, pyglet, pyudev for Linux; quartz for macOS) +3. **Magewell SDK**: 3rdparty directory contains SDK versions 3.3.1.0 and 3.3.1.1313 +4. **BIDS Integration**: Project designed to work within BIDS dataset structure +5. **Permissions**: Recent work on `/opt` permissions (enh-perms branch) +6. **Container Modes**: CI mode installs from worktree, default mode from PyPI + +## Recent Development Focus (from CHANGELOG) + +- Video audit tool creation +- PsychoPy integration improvements +- Container support (Docker/Singularity) +- Display monitoring capabilities +- Audio codec improvements (FSK/NFE) +- Permissions fixes in container builds +- QR code and nosignal detection enhancements \ No newline at end of file diff --git a/REUSE.toml b/REUSE.toml index 45d9d7b0..6d798a48 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -2,6 +2,7 @@ version=1 [[annotations]] path = [ + ".ai/**", ".github/**", "*.md", "*.toml", diff --git a/containers/repronim-reprostim/generate_container.sh b/containers/repronim-reprostim/generate_container.sh index 003c9cf7..00dff281 100755 --- a/containers/repronim-reprostim/generate_container.sh +++ b/containers/repronim-reprostim/generate_container.sh @@ -5,65 +5,43 @@ set -eu thisdir=$(dirname "$0") -PYTHON_VERSION=3.10 +MODE=${1:-default} +PYTHON_VERSION=3.10 PSYCHOPY_VERSION=2025.2.0 -PSYCHOPY_INSTALL_DIR=/opt/psychopy -PSYCHOPY_VENV_NAME=psychopy_${PSYCHOPY_VERSION}_py${PYTHON_VERSION} -PSYCHOPY_HOME=${PSYCHOPY_INSTALL_DIR}/${PSYCHOPY_VENV_NAME} -PSYCHOPY_VENV_BIN=${PSYCHOPY_HOME}/.venv/bin - REPROSTIM_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.1") # Decided to go without version to make diff easier to analyze etc REPROSTIM_SUFFIX=repronim-reprostim # -${REPROSTIM_VERSION} -REPROSTIM_GIT_HOME=$(git rev-parse --show-toplevel) REPROSTIM_HOME=/opt/reprostim +REPROSTIM_GIT_HOME=$(git rev-parse --show-toplevel) REPROSTIM_CAPTURE_ENABLED="${REPROSTIM_CAPTURE_ENABLED:-0}" REPROSTIM_CAPTURE_PACKAGES_DEV="" -REPROSTIM_CAPTURE_PACKAGES_RUNTIME="" -REPROSTIM_CAPTURE_BUILD="echo 'ReproStim capture build is disabled'" -REPROSTIM_CAPTURE_CLEAN="echo 'ReproStim capture clean is disabled'" +REPROSTIM_CAPTURE_PACKAGES_RUNTIME="mc" +# include capture packages if enabled, two subsets: dev and runtime +# dev packages are needed only during build time +# runtime packages are needed during runtime and build time if [[ "$REPROSTIM_CAPTURE_ENABLED" == "1" ]]; then REPROSTIM_CAPTURE_PACKAGES_DEV="libyaml-cpp-dev libspdlog-dev catch2 libv4l-dev libudev-dev libopencv-dev libcurl4-openssl-dev nlohmann-json3-dev cmake g++" - REPROSTIM_CAPTURE_PACKAGES_RUNTIME="libyaml-cpp0.7 libfmt9" - REPROSTIM_CAPTURE_BUILD="cd \"$REPROSTIM_HOME/src/reprostim-capture\"; mkdir build; cd build; cmake ..; make; cd ..; cmake --install build; rm -rf \"$REPROSTIM_HOME/src/reprostim-capture/build\"; reprostim-videocapture -V" - REPROSTIM_CAPTURE_CLEAN="apt-get remove --purge -y $REPROSTIM_CAPTURE_PACKAGES_DEV && apt-get autoremove -y" - # Extend with packages hold if runtime packages env exist - if [[ -n "$REPROSTIM_CAPTURE_PACKAGES_RUNTIME" ]]; then - REPROSTIM_CAPTURE_CLEAN="apt-mark manual $REPROSTIM_CAPTURE_PACKAGES_RUNTIME && apt-mark hold $REPROSTIM_CAPTURE_PACKAGES_RUNTIME && $REPROSTIM_CAPTURE_CLEAN" - fi -fi - - -MODE=${1:-default} - - -if [[ "$MODE" == "ci" ]]; then - echo "Running in CI/CD mode, install reprostim from current worktree" - REPROSTIM_COPY="${REPROSTIM_GIT_HOME} ${REPROSTIM_HOME}" - REPROSTIM_RUN_INSTALL="${PSYCHOPY_VENV_BIN}/pip install ${REPROSTIM_HOME}[all,disp_mon]" -else - echo "Running in default mode, install reprostim from PyPI" - REPROSTIM_COPY="" - REPROSTIM_RUN_INSTALL="${PSYCHOPY_VENV_BIN}/pip install reprostim[all,disp_mon]==${REPROSTIM_VERSION}" + REPROSTIM_CAPTURE_PACKAGES_RUNTIME="mc libyaml-cpp0.7 libfmt9" fi - - generate() { - if [[ "$1" == docker && "$MODE" == "ci" ]]; then - REPROSTIM_COPY="reprostim ${REPROSTIM_HOME}" + # Somehow --copy source differs between docker and singularity + REPROSTIM_COPY_SRC="${REPROSTIM_GIT_HOME}" + if [[ "$1" == docker ]]; then + REPROSTIM_COPY_SRC="reprostim" fi - if [[ "$1" == singularity && "$MODE" == "ci" ]]; then - REPROSTIM_COPY="${REPROSTIM_GIT_HOME} ${REPROSTIM_HOME}" - fi + # copy the setup script to /opt/setup_container.sh + REPROSTIM_COPY_ARGS=( "--copy" "${REPROSTIM_COPY_SRC}/containers/repronim-reprostim/setup_container.sh" "/opt/setup_container.sh" ) - if [[ -n "${REPROSTIM_COPY:-}" ]]; then - REPROSTIM_COPY_ARG="--copy ${REPROSTIM_COPY}" - else - REPROSTIM_COPY_ARG="" + # optionally copy the current worktree into the container + # for CI mode to build reprostim package from current code + # rather than from PyPI + if [[ "$MODE" == "ci" ]]; then + # add another --copy pair for CI + REPROSTIM_COPY_ARGS+=( "--copy" "${REPROSTIM_COPY_SRC}" "${REPROSTIM_HOME}" ) fi # shellcheck disable=SC2034 @@ -87,15 +65,9 @@ generate() { vim wget strace time ncdu gnupg curl procps pigz less tree python3 python3-pip \ "${REPROSTIM_CAPTURE_PACKAGES_RUNTIME}" \ "${REPROSTIM_CAPTURE_PACKAGES_DEV}" \ - --run "git clone https://github.com/wieluk/psychopy_linux_installer/ /opt/psychopy-installer; cd /opt/psychopy-installer; git checkout tags/v2.2.3" \ - --run "/opt/psychopy-installer/psychopy_linux_installer --install-dir=${PSYCHOPY_INSTALL_DIR} --venv-name=${PSYCHOPY_VENV_NAME} --psychopy-version=${PSYCHOPY_VERSION} --additional-packages=psychopy_bids==2025.1.2,psychopy-mri-emulator==0.0.2 --python-version=${PYTHON_VERSION} --wxpython-version=4.2.3 -v -f" \ - ${REPROSTIM_COPY_ARG} \ - --run "${REPROSTIM_RUN_INSTALL}" \ - --run "bash -c 'ln -s ${PSYCHOPY_HOME}/start_psychopy /usr/local/bin/psychopy'" \ - --run "bash -c 'b=\$(ls ${PSYCHOPY_VENV_BIN}/python3); echo -e \"#!/bin/sh\n\$b \\\"\\\$@\\\"\" >| /usr/local/bin/python3; chmod a+x /usr/local/bin/python3'" \ - --entrypoint python3 \ - --run "bash -c '$REPROSTIM_CAPTURE_BUILD'" \ - --run "bash -c '$REPROSTIM_CAPTURE_CLEAN'" + "${REPROSTIM_COPY_ARGS[@]}" \ + --run "bash -c 'chmod a+rX /opt/setup_container.sh && PYTHON_VERSION=\"${PYTHON_VERSION}\" PSYCHOPY_VERSION=\"${PSYCHOPY_VERSION}\" REPROSTIM_VERSION=\"${REPROSTIM_VERSION}\" REPROSTIM_HOME=\"${REPROSTIM_HOME}\" REPROSTIM_GIT_HOME=\"${REPROSTIM_GIT_HOME}\" MODE=\"${MODE}\" REPROSTIM_CAPTURE_ENABLED=\"${REPROSTIM_CAPTURE_ENABLED}\" REPROSTIM_CAPTURE_PACKAGES_DEV=\"${REPROSTIM_CAPTURE_PACKAGES_DEV}\" REPROSTIM_CAPTURE_PACKAGES_RUNTIME=\"${REPROSTIM_CAPTURE_PACKAGES_RUNTIME}\" /opt/setup_container.sh'" \ + --entrypoint python3 # --user=reproin \ } diff --git a/containers/repronim-reprostim/run_reprostim_ci.sh b/containers/repronim-reprostim/run_reprostim_ci.sh index 44d2d406..e1ac01c4 100755 --- a/containers/repronim-reprostim/run_reprostim_ci.sh +++ b/containers/repronim-reprostim/run_reprostim_ci.sh @@ -26,10 +26,11 @@ elif [[ "$REPROSTIM_CONTAINER_TYPE" == "singularity" ]]; then REPROSTIM_CONTAINER_IMAGE="${REPROSTIM_CONTAINER_IMAGE:-./repronim-reprostim-${REPROSTIM_VERSION}.sing}" fi -# Calculate entry point +# Calculate entry point and command to run inside container REPROSTIM_CONTAINER_ENTRYPOINT="" -if [ "$REPROSTIM_CONTAINER_RUN_MODE" = "reprostim-videocapture" ]; then - REPROSTIM_CONTAINER_APP="reprostim-videocapture" +if [ "$REPROSTIM_CONTAINER_RUN_MODE" = "reprostim-videocapture" ] || [ "$REPROSTIM_CONTAINER_RUN_MODE" = "psychopy" ]; then + REPROSTIM_CONTAINER_APP="$REPROSTIM_CONTAINER_RUN_MODE" + # clear/remove python entrypoint for standalone apps if [ "$REPROSTIM_CONTAINER_TYPE" = "docker" ]; then REPROSTIM_CONTAINER_ENTRYPOINT="--entrypoint=" fi @@ -54,6 +55,19 @@ log " [IMAGE] : ${REPROSTIM_CONTAINER_IMAGE}" log " [OVERLAY] : ${REPROSTIM_OVERLAY}" log " [ARGS] : $*" +# +# Prepare XAUTHORITY path (fallback to $HOME/.Xauthority) +# also before running docker container with psychopy UI allow +# access to X server with: +# +# xhost +local:root +# or +# xhost +local:docker +# +# and optionally to quiet Podman (when "Emulate Docker CLI" listed in logs): +# sudo touch /etc/containers/nodocker +# +XAUTHORITY_HOST="${XAUTHORITY:-$HOME/.Xauthority}" if [[ "$REPROSTIM_CONTAINER_TYPE" == "docker" ]]; then docker run --rm -i ${DOCKER_TTY} \ @@ -61,6 +75,8 @@ if [[ "$REPROSTIM_CONTAINER_TYPE" == "docker" ]]; then -v "${REPROSTIM_PATH}:${REPROSTIM_PATH}" \ -w "${REPROSTIM_PATH}" \ --env DISPLAY=$DISPLAY \ + -v "${XAUTHORITY_HOST}:${XAUTHORITY_HOST}:ro" \ + -v /tmp/.X11-unix:/tmp/.X11-unix \ ${REPROSTIM_OVERLAY} ${REPROSTIM_CONTAINER_IMAGE} \ ${REPROSTIM_CONTAINER_APP} "$@" elif [[ "$REPROSTIM_CONTAINER_TYPE" == "singularity" ]]; then diff --git a/containers/repronim-reprostim/setup_container.sh b/containers/repronim-reprostim/setup_container.sh new file mode 100755 index 00000000..3c16dd8f --- /dev/null +++ b/containers/repronim-reprostim/setup_container.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# shellcheck disable=SC2086,SC2034 + +# This is internal script used by Docker/Singularity to setup the container +# and it's automatically copied into the container image under /opt/setup_container.sh +# location. It is not meant to be run directly by users. +# +# Note: env variables passed explicitly from generate_container.sh script: +# $MODE +# $PYTHON_VERSION +# $PSYCHOPY_VERSION +# $REPROSTIM_VERSION +# $REPROSTIM_HOME +# $REPROSTIM_GIT_HOME +# $REPROSTIM_CAPTURE_ENABLED +# $REPROSTIM_CAPTURE_PACKAGES_DEV +# $REPROSTIM_CAPTURE_PACKAGES_RUNTIME + +set -eu + +thisdir=$(dirname "$0") + +PSYCHOPY_INSTALL_DIR=/opt/psychopy +PSYCHOPY_VENV_NAME=psychopy_${PSYCHOPY_VERSION}_py${PYTHON_VERSION} +PSYCHOPY_HOME=${PSYCHOPY_INSTALL_DIR}/${PSYCHOPY_VENV_NAME} +PSYCHOPY_VENV_BIN=${PSYCHOPY_HOME}/.venv/bin + + +# Install psychopy_linux_installer from GitHub +echo "Install psychopy_linux_installer from GitHub..." +git clone --branch v2.2.4 --depth 1 https://github.com/wieluk/psychopy_linux_installer/ /opt/psychopy-installer +cd /opt/psychopy-installer + +# Install PsychoPy via psychopy_linux_installer +echo "Install PsychoPy v${PSYCHOPY_VERSION} via psychopy_linux_installer..." +/opt/psychopy-installer/psychopy_linux_installer --install-dir=${PSYCHOPY_INSTALL_DIR} --venv-name=${PSYCHOPY_VENV_NAME} --psychopy-version=${PSYCHOPY_VERSION} --additional-packages=psychopy_bids==2025.1.2,psychopy-mri-emulator==0.0.2 --python-version=${PYTHON_VERSION} --wxpython-version=4.2.3 --cleanup -v -f +# Create symlink to psychopy executable +ln -sf "${PSYCHOPY_HOME}/start_psychopy" /usr/local/bin/psychopy + +# Install reprostim package from PyPI or from current worktree in CI mode +if [[ "$MODE" == "ci" ]]; then + echo "Running in CI/CD mode, install reprostim from current worktree" + "${PSYCHOPY_VENV_BIN}/pip" install "${REPROSTIM_HOME}[all,disp_mon]" +else + echo "Running in default mode, install reprostim from PyPI" + "${PSYCHOPY_VENV_BIN}/pip" install "reprostim[all,disp_mon]==${REPROSTIM_VERSION}" +fi + +# Create symlink to python3 in psychopy venv as default python3 +echo "Creating symlink to python3 in psychopy venv as default python3" +b=$(ls "${PSYCHOPY_VENV_BIN}/python3") +echo -e "#!/bin/sh\n$b \"\$@\"" >| /usr/local/bin/python3 +chmod a+x /usr/local/bin/python3 + +if [[ "$REPROSTIM_CAPTURE_ENABLED" == "1" ]]; then + # Build reprostim-capture from source + echo "Building reprostim-capture from source..." + cd "${REPROSTIM_HOME}/src/reprostim-capture" + mkdir build + cd build + cmake .. + make + cd .. + + # Install the built binary + echo "Installing reprostim-capture..." + cmake --install build + + # Clean build files + rm -rf "${REPROSTIM_HOME}/src/reprostim-capture/build" + + # Verify installation + echo "Verifying reprostim-capture installation..." + reprostim-videocapture -V + + # Cleanup reprostim-capture packages + # + # Keep runtime packages marked as manual to avoid their removal + if [[ -n "$REPROSTIM_CAPTURE_PACKAGES_RUNTIME" ]]; then + echo "Marking reprostim-capture runtime packages as manual to avoid their removal: ${REPROSTIM_CAPTURE_PACKAGES_RUNTIME}" + apt-mark manual $REPROSTIM_CAPTURE_PACKAGES_RUNTIME + apt-mark hold $REPROSTIM_CAPTURE_PACKAGES_RUNTIME + fi + + # Remove dev packages + echo "Removing reprostim-capture development packages: ${REPROSTIM_CAPTURE_PACKAGES_DEV}" + apt-get remove --purge -y ${REPROSTIM_CAPTURE_PACKAGES_DEV} + apt-get autoremove -y +fi + +# Remove psychopy-installer +echo "Removing psychopy-installer..." +rm -rf /opt/psychopy-installer + +# Remove unnecessary dirs/files from the copied worktree +echo "Cleaning up copied reprostim worktree..." +rm -rf "${REPROSTIM_HOME}/.ai" +rm -rf "${REPROSTIM_HOME}/.git" +rm -rf "${REPROSTIM_HOME}/.github" +rm -rf "${REPROSTIM_HOME}/.idea" +rm -rf "${REPROSTIM_HOME}/.pytest_cache" +rm -rf "${REPROSTIM_HOME}/Events" +rm -rf "${REPROSTIM_HOME}/Parsing" +rm -rf "${REPROSTIM_HOME}/QRCoding" +rm -rf "${REPROSTIM_HOME}/dist" +rm -rf "${REPROSTIM_HOME}/docs" +rm -rf "${REPROSTIM_HOME}/examples/exp-alpha" +rm -rf "${REPROSTIM_HOME}/temp" +rm -rf "${REPROSTIM_HOME}/venv" +rm -rf "${REPROSTIM_HOME}/.gitignore" + +# Configure permissions +echo "Configuring permissions to allow non-root users to run PsychoPy and ReproStim..." +if [[ -d /opt ]]; then + chmod a+rX -R /opt || true +fi +if [[ -d /root/.psychopy3 ]]; then + chmod a+rX -R /root/.psychopy3 || true +fi + +echo "Container setup completed: Python v${PYTHON_VERSION} + PsychoPy v${PSYCHOPY_VERSION} + ReproStim v${REPROSTIM_VERSION}"