Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
pull_request:
branches: [main]

# Least privilege by default; no job needs write access.
permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -17,12 +21,12 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.12"]
python: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # hatch-vcs needs git history
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python }}
cache: pip
Expand All @@ -40,26 +44,45 @@ jobs:
run: pytest -m "not slow and not gpu" --cov=cortex_score --cov-report=xml
- name: Upload coverage
if: matrix.python == '3.11'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-xml
path: coverage.xml

packaging:
name: build + twine check
name: build + twine check + wheel smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- run: |
- name: Build + metadata check
run: |
python -m pip install --upgrade pip build twine
python -m build
python -m twine check dist/*
- uses: actions/upload-artifact@v4
- name: Install built wheel and smoke-test (no torch)
run: |
python -m pip install dist/*.whl
cd "$RUNNER_TEMP" # import the installed wheel, not the source tree
python -c "
import sys
import numpy as np
import cortex_score
assert 'torch' not in sys.modules
from cortex_score import score_from_predictions
r = score_from_predictions(np.random.randn(4, 20484).astype('float32'), model_revision='ci-wheel')
assert len(r.networks) == 5
print('wheel smoke OK', cortex_score.__version__)
"
- name: Wheel-content packaging tests
run: |
pip install -e ".[dev]"
pytest tests/integration/test_packaging.py -m slow
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dist
path: dist/
Expand All @@ -68,10 +91,10 @@ jobs:
name: importable with no torch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- name: Install base only (no [gpu-deps])
Expand Down
32 changes: 26 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,54 @@ on:
tags:
- "v*.*.*"

# Default to read-only; the publish job opts into id-token below.
permissions:
contents: read

jobs:
build-and-publish:
name: build + publish to PyPI
runs-on: ubuntu-latest
permissions:
id-token: write # trusted publishing
id-token: write # trusted publishing (OIDC); no API token stored
contents: read
environment:
name: pypi
url: https://pypi.org/p/cortex-score
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # hatch-vcs reads the tag history

- uses: actions/setup-python@v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"

- name: Install build tooling
run: |
python -m pip install --upgrade pip build twine
run: python -m pip install --upgrade pip build twine

- name: Build sdist + wheel
run: python -m build

- name: Verify metadata
run: python -m twine check dist/*

- name: Smoke-test the built wheel before publishing
run: |
python -m pip install dist/*.whl
cd "$RUNNER_TEMP" # import the installed wheel, not the source tree
python -c "
import sys
import numpy as np
import cortex_score
assert 'torch' not in sys.modules
from cortex_score import score_from_predictions
r = score_from_predictions(np.random.randn(4, 20484).astype('float32'), model_revision='release-smoke')
assert len(r.networks) == 5
print('release wheel smoke OK', cortex_score.__version__)
"

- name: Publish to PyPI via trusted publishing
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
attestations: true
36 changes: 31 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ This project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.1.0] - 2026-06-10

First public release. The CPU-only postprocessing tier
(`score_from_predictions` / `score_from_prediction_bundle`) and the
`ScoreResult` JSON contract (`SCHEMA_VERSION = "1.0"`) are stable.

### Added
- Initial package scaffold (Epics 0-8):
- Bundled atlas data (Schaefer-400 + Yeo-17 + 5-network rollup) with
Expand All @@ -20,16 +26,36 @@ This project follows [Semantic Versioning](https://semver.org/).
ergonomic, requires explicit scientific assumptions.
- `score(video_path, runner=None)` — full pipeline.
- `CortexScorer` class for batch reuse.
- Two-tier cache: prediction cache + score cache, atomic writes,
cache_manifest.json, invalidation matrix.
- Two-tier cache infrastructure (prediction cache + score cache, atomic
writes, cache_manifest.json, invalidation matrix). NOTE: this is
plumbing for a future release — the scoring path does not read or write
it yet, so `cache info` reports empty until caching is wired in v0.1.1.
- Typer CLI under `[cli]` extra: `doctor`, `score`, `from-predictions`,
`schema`, `cache info`, `cache clear`.

### Fixed (pre-release hardening)
- `result_id` is now the SHA-256 of the result's own canonical JSON (with
`result_id` blanked), so it is reproducible from the serialized artifact.
The previous hand-built hash payload disagreed with the serialized form
(`+00:00` vs `Z` datetimes), making the documented audit hash unverifiable.
- MZ3 scalar-overlay export now emits the real NiiVue format (uint16 magic
`0x5A4D`, `attr=8`/isSCALAR, 16-byte header, gzip). The earlier port wrote
a header NiiVue rejected at the magic check.
- `score_from_predictions` rejects 1-D/non-finite inputs and unsupported
meshes with clear errors at the boundary (`UnsupportedMeshError`) instead
of an opaque `IndexError` / a raw traceback through the CLI.
- Version is read from installed distribution metadata
(`importlib.metadata`); the build no longer ships a generated `_version.py`.

### Removed
- The no-op `score --no-cache` flag (it silently did nothing). It will
return when the cache is wired into the scoring path.
- TRIBE v2 adapter under `[gpu-deps]` extra (TRIBE itself installed
from `requirements/tribev2-gpu.txt`, pinned to commit
`34f52344e5ba96660fac877393e1954e399d3ef3`).
- 118-test suite at 88.66% coverage with property tests, schema
snapshot, cache invalidation matrix, packaging smoke, and
import-without-GPU gate.
- 135-test suite at ~90% coverage with property tests, schema
snapshot, cache invalidation matrix, packaging smoke, MZ3 format
round-trip, result_id verifiability, and import-without-GPU gate.

### Notes
- The package source is MIT-licensed. The bundled atlases ship under
Expand Down
5 changes: 4 additions & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ cff-version: 1.2.0
title: "cortex-score"
message: "If you use this software, please cite both this package and the underlying TRIBE v2 brain-encoding model."
type: software
version: "0.1.0"
date-released: "2026-06-10"
license: MIT
authors:
- given-names: Madhav
family-names: Chauhan
repository-code: "https://github.com/madhavcodez/cortex-score"
url: "https://github.com/madhavcodez/cortex-score"
abstract: >-
cortex-score packages the TRIBE v2 brain-encoding pipeline as an installable
Python library that summarizes predicted cortical responses for any video
Expand All @@ -17,7 +21,6 @@ keywords:
- neuroimaging
- video-analysis
- tribe
license: MIT
references:
- type: software
title: "TRIBE v2"
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ a model trained on fMRI scans of people watching videos.
> viewer. Treat them as a creative signal, not a clinical one.

I built this to make a brain-encoding model usable from a few lines of Python.
It's a pre-release (v0.1) — not on PyPI yet.
It's an early release (v0.1): the CPU-only postprocessing tier is stable, and the
JSON output contract is locked behind `SCHEMA_VERSION = "1.0"`.

**Useful for:**

Expand Down Expand Up @@ -115,7 +116,7 @@ sequenceDiagram
RUN-->>API: PredictionBundle
API->>POST: aggregate → Yeo-17 → z-score
POST-->>SCH: 5 networks + metrics
SCH->>SCH: result_id = sha256(body)
SCH->>SCH: result_id = sha256(canonical JSON, id blanked)
SCH-->>U: ScoreResult JSON
```

Expand Down Expand Up @@ -211,6 +212,7 @@ class MyRunner:
| `MissingOptionalDependencyError` | `[gpu-deps]` / `tribev2` missing when `score()` is called without a runner |
| `MissingExternalToolError` | `ffmpeg` / `uvx` absent on PATH at TRIBE-load time |
| `IncompatiblePredictionShapeError` | `preds.shape[1]` doesn't match the mesh's vertex count |
| `UnsupportedMeshError` | A `mesh=` other than `fsaverage5` was requested (subclasses `ValueError` and `CortexScoreError`) |
| `AtlasMismatchError` | Bundled atlas SHA-256 disagrees with `data/manifest.json` (corrupted wheel) |

## Output
Expand All @@ -220,7 +222,7 @@ Every score is a self-describing JSON object. The contract is locked behind

| Field | What it gives you |
|---|---|
| `result_id` | SHA-256 of the payload — a stable id for caches, audit logs, dedup |
| `result_id` | SHA-256 of the result's own canonical JSON (with `result_id` blanked) — reproducible from the JSON alone, so it verifies as a stable id for caches, audit logs, dedup |
| `provenance.model_revision` | Which TRIBE v2 commit produced the numbers |
| `atlas.*_sha256` | Fingerprints of the exact Schaefer / Yeo / network-group data used |
| `normalization.scope` | `within_video` by default — two clips aren't comparable on the same axis unless you opt into a reference distribution |
Expand Down Expand Up @@ -272,7 +274,7 @@ embedded in `network_groups.json` and exposed via `NetworkScore.color`.
- **platformdirs** — XDG / `%LOCALAPPDATA%` cache dirs, override via `CORTEX_SCORE_CACHE_DIR`
- **Bundled atlas** — Schaefer 2018 + Yeo 2011 on fsaverage5, SHA-256 fingerprinted (~337 KB)
- **Encoder** — TRIBE v2 @ `34f52344` (Llama 3.2-3B + V-JEPA2 + W2V-BERT), pinned to commit
- **Tests** — `pytest` + `hypothesis`, 125 tests at 87.95% coverage, `ruff` + `mypy --strict` clean
- **Tests** — `pytest` + `hypothesis`, 135 tests at ~90% coverage, `ruff` + `mypy --strict` clean

## Licenses

Expand Down
65 changes: 65 additions & 0 deletions docs/install-gpu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# GPU install (full `score()` pipeline)

The base `cortex-score` install is **CPU-only** — it covers
`score_from_predictions` / `score_from_prediction_bundle`, which take a
prediction tensor you already have. To run the full `score("clip.mp4")`
pipeline you also need TRIBE v2 and its GPU stack.

TRIBE v2 is **not** a declared dependency of `cortex-score`: PyPI rejects
published metadata that contains direct-URL (Git) dependencies, so it must
be installed separately. `cortex-score[gpu-deps]` declares the *compatible
environment* (torch / transformers / moviepy versions), and the pinned
TRIBE commit is installed from a requirements file.

## Requirements

- A CUDA-capable GPU (TRIBE v2 weights are ~12 GB; plan for ≥16 GB VRAM).
- `ffmpeg` and `uvx` (from [uv](https://github.com/astral-sh/uv)) on `PATH` —
TRIBE's preprocessing shells out to them.
- A Hugging Face account with access to the gated Llama 3.2-3B weights.

## Install

```bash
# 1. Base package + the TRIBE-compatible GPU dependency matrix
pip install "cortex-score[gpu-deps]"

# 2. TRIBE v2 itself, pinned to the tested commit
pip install -r requirements/tribev2-gpu.txt

# 3. External tools (example: Debian/Ubuntu)
sudo apt-get install -y ffmpeg
curl -LsSf https://astral.sh/uv/install.sh | sh # provides `uvx`

# 4. Authenticate for the gated model weights
huggingface-cli login
```

`requirements/tribev2-gpu.txt` pins TRIBE v2 to commit
`34f52344e5ba96660fac877393e1954e399d3ef3`, which matches
`cortex_score.runners.tribev2.TRIBEV2_PINNED_REVISION`. Bumping one
requires bumping the other and re-running the GPU smoke test.

## Verify

```bash
cortex-score doctor
```

`doctor` reports Python, `cortex-score`, torch (+ CUDA), tribev2, ffmpeg,
uvx, the Hugging Face token, and the cache directory. Every row should read
`ok` (or report what to install) before you run `score()`.

## Run

```python
from cortex_score import score

result = score("clip.mp4")
result.save("clip.score.json")
```

> TRIBE v2 is licensed **CC-BY-NC-4.0**. Scores produced through the full
> `score()` path inherit the non-commercial restriction; it is emitted as a
> runtime warning on first load and recorded in every
> `ScoreResult.license_restrictions`.
Loading
Loading