A full run on the example dataset¶
This walkthrough takes a real example dataset (stimulus audio and fMRIPrep outputs) through the whole hypline pipeline: phonemic and syntactic features and denoised BOLD, then a fitted encoding model joining the two sides, using one command per step. By the end you will have run hypline end to end and seen exactly what each step reads and writes.
It assumes you have hypline installed (see Installation, including FFmpeg for transcription). No prior hypline experience is needed, but skim The hypline dataset layout first if a path or filename below is ever unclear; this tutorial shows the layout in action rather than re-explaining it.
Expect about 30 minutes start to finish. A few steps dominate: the
one-time ~2.8 GB dataset download, transcription (step 2),
a few minutes on CPU with the small model, and the encoding fit
(step 6), a whole-brain ridge model that runs ~4–5
minutes per subject on CPU. Generating syntactic features
(step 3) also triggers a one-time ~560 MB
spaCy model download. Every other step runs in seconds. Transcription and
the fit both run on CPU so the tutorial works anywhere; if you have a GPU,
--device cuda makes them far faster.
1. Get the example dataset¶
Download the example dataset and unpack it. Throughout this tutorial we call the
unpacked dataset root data/.
# TODO: replace with the published Zenodo record on release
curl -L -o hypline-tutorial-data.zip "<ZENODO-DOI>"
unzip hypline-tutorial-data.zip -d data/
The dataset is a BIDS-style tree for one dyad:
two partners (sub-031 and sub-032) who held a conversation while both were
scanned. It already contains the inputs hypline needs: stimulus audio under
stimuli/, raw events and BOLD under each sub-*/, and fMRIPrep outputs under
derivatives/fmriprep/.
What the example dataset covers
It is a faithful subset of a real hyperscanning study (about 2.8 GB), trimmed so it is small enough to download and run quickly:
- Two of the study's five runs are included (
run-1,run-2). The per-run file set and the dyad structure are otherwise complete. - Audio is released for the reading-condition (R) trials only, for
privacy. The
events.tsvfiles still list every trial, so each run's audio covers only a subset of its events (see step 2).
Otherwise the dataset mirrors the structure and design of the original study.
Every command below takes data/ as its only positional argument and discovers
its inputs from the directory layout; you never pass individual file paths.
2. Transcribe the audio¶
transcribe turns each stimulus .wav into a word-level transcript using a
Whisper speech-recognition model.
hypline transcribe data/ --audio-ext .wav --model small
Transcribing dyad-030_ses-1_task-conv_run-1_trial-1_audio.wav
Transcribing dyad-030_ses-1_task-conv_run-1_trial-3_audio.wav
Transcribing dyad-030_ses-1_task-conv_run-2_trial-1_audio.wav
Transcribing dyad-030_ses-1_task-conv_run-2_trial-3_audio.wav
(Log lines are abridged here; a first run also prints a one-time model download and voice-activity-detection messages.)
Why --model small
small keeps this tutorial quick while producing usable transcripts: a few
minutes on a laptop CPU, with a modest one-time model download. It still mis-hears
the occasional word, which is fine here, since you are learning the workflow
rather than analyzing the transcripts. For a real analysis, omit --model to use
the default large-v2, which is more accurate still but a multi-GB download and
much slower on CPU (pass --device cuda if you have a GPU).
Only four files are transcribed, not one per run. These are the
reading-condition subset from step 1: each run's
audio covers only its R trials (trial-1, trial-3), so transcripts exist for
those trials and not the others. The run's events.tsv still describes every
trial; hypline transcribes whatever audio is present.
The transcripts land beside the audio, under a new transcript/ subdirectory:
data/stimuli/dyad-030/ses-1/transcript/
├── dyad-030_ses-1_task-conv_run-1_trial-1_transcript.csv
├── dyad-030_ses-1_task-conv_run-1_trial-3_transcript.csv
├── dyad-030_ses-1_task-conv_run-2_trial-1_transcript.csv
└── dyad-030_ses-1_task-conv_run-2_trial-3_transcript.csv
Each CSV is one row per word, with its timing and the partner who spoke it:
word,start_time,end_time,confidence_score,turn_sub
Thank,5.714,6.095,0.368,031
you.,6.195,6.416,0.326,031
These transcripts are dyad-keyed (dyad-030), because the conversation
belongs to the pair rather than to either partner. See
Subject vs. dyad for why.
Check
ls data/stimuli/dyad-030/ses-1/transcript/ lists four _transcript.csv
files, and each opens with the word,start_time,end_time,… header above. If
no transcripts appear, the audio was not found: confirm you passed --audio-ext
.wav and that data/ is the unpacked dataset root.
3. Generate features¶
featuregen reads those transcripts and computes features: per-word
representations that become predictors in the encoding model. We generate two
families here (phonemic and syntactic), so the encoding fit later has a
band for each.
Start with featuregen phonemic, which computes a phonemic feature for each
transcript.
hypline featuregen phonemic data/
Generating phonemic features for dyad-030_ses-1_task-conv_run-1_trial-1_transcript.csv
...
Generating phonemic confounds for dyad-030_ses-1_task-conv_run-1_trial-1_feat-phonemic.parquet
...
By default this step also generates the matching phonemic confounds
(speech-onset and speech-rate regressors derived from the same features), so you
get both in one command. Pass --skip-confoundgen to suppress that, or run
confoundgen phonemic on its own later.
Two new areas appear, both dyad-keyed:
data/
├── features/dyad-030/ses-1/phonemic/
│ └── dyad-030_ses-1_task-conv_run-1_trial-1_feat-phonemic.parquet # … one per transcript (4)
└── confounds/dyad-030/ses-1/
├── phonemic-onset/
│ └── dyad-030_ses-1_task-conv_run-1_trial-1_conf-phonemic_desc-onset.parquet # … (4)
└── phonemic-rate/
└── dyad-030_ses-1_task-conv_run-1_trial-1_conf-phonemic_desc-rate.parquet # … (4)
The two confound flavors live in their own subdirectories because they are
desc variants of the same conf-phonemic kind — see
Variants with desc.
Now add a second family. featuregen syntactic reads the same transcripts and
computes per-token part-of-speech, dependency, and stopword features:
hypline featuregen syntactic data/
Generating syntactic features for dyad-030_ses-1_task-conv_run-1_trial-1_transcript.csv
...
The first run downloads a spaCy language model (~560 MB, one time) before it begins. Unlike phonemic, syntactic generates no confounds — just the features:
data/features/dyad-030/ses-1/syntactic/
└── dyad-030_ses-1_task-conv_run-1_trial-1_feat-syntactic.parquet # … one per transcript (4)
That completes the stimulus branch: from audio to the features (and phonemic confounds) the encoding model uses as predictors.
Check
features/ now holds four feat-phonemic.parquet and four
feat-syntactic.parquet files, one of each per transcript from step 2, plus
four files in each of the phonemic-onset/ and phonemic-rate/ confound
subdirectories.
4. Denoise the BOLD¶
The other branch cleans the BOLD signal, the encoding model's target.
denoise reads fMRIPrep's preprocessed BOLD and regresses out nuisance signals
you select from fMRIPrep's own confounds table.
hypline denoise data/ \
--columns trans_x,trans_y,trans_z,rot_x,rot_y,rot_z,cosine \
--space MNI152NLin2009cAsym
Denoising starting: sub-031_ses-1_task-conv_run-1_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
Denoising complete: sub-031_ses-1_task-conv_run-1_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
...
Here --columns names confound columns from fMRIPrep's table: the six head-motion
parameters (trans_*, rot_*) plus cosine, a prefix that expands to every
cosine-drift regressor. The example dataset's fMRIPrep outputs are volumetric
(MNI152NLin2009cAsym), so we name that space explicitly — --space defaults to
the surface fsaverage6, and your own surface data needs no --space at all.
(Omitting --columns entirely would fall back to hypline's default
Speer et al. 2024 confound set — see the
denoise reference.)
This step is sub-keyed: it processes each partner's brain (sub-031,
sub-032) independently, so all four run × subject combinations are denoised.
The output goes to hypline's own derivatives tree, leaving fMRIPrep's untouched:
data/derivatives/hypline/sub-031/ses-1/func/
├── sub-031_ses-1_task-conv_run-1_space-MNI152NLin2009cAsym_desc-denoised_bold.nii.gz
└── sub-031_ses-1_task-conv_run-1_space-MNI152NLin2009cAsym_desc-denoised_bold.json
The same pair is written for each run and subject — sub-031 and sub-032,
run-1 and run-2.
Each denoised BOLD carries a .json sidecar recording exactly how it was made
(the desc-preproc source it came from, the resolved regressor columns, and the
hypline version), so the result is reproducible. See the
denoise reference for CompCor selectors, custom
nuisance/ regressors, and surface spaces.
Check
derivatives/hypline/ now holds a desc-denoised .nii.gz + .json pair
for each subject and run: eight files total (2 subjects × 2 runs × 2). If
the command logged No subjects found, check that derivatives/fmriprep/
unpacked correctly under data/.
5. (Optional) Add a custom nuisance regressor¶
So far denoise pulled every regressor from fMRIPrep's confounds table via
--columns. The other channel is custom nuisance files under nuisance/:
run-level regressors you supply yourself that fMRIPrep never produced (e.g.
physiological recordings). The example dataset ships a small set so you can try
this path:
data/nuisance/sub-031/ses-1/demo/
└── sub-031_ses-1_task-conv_run-1_nuis-demo_timeseries.tsv # … one per subject × run (4)
These are synthetic
The shipped nuis-demo files hold synthetic placeholder regressors
(demo_regressor1, demo_regressor2) rather than real signals, so the tutorial
can exercise --custom-sources without needing physiological data. In a real
analysis you author these yourself; the
denoise reference documents the
nuisance/ file format under --custom-sources.
Re-run denoise adding the custom source. --custom-sources names the
nuisance/<kind>/ directory and --custom-columns selects columns from it; the
two go together:
hypline denoise data/ \
--columns trans_x,trans_y,trans_z,rot_x,rot_y,rot_z,cosine \
--space MNI152NLin2009cAsym \
--custom-sources demo \
--custom-columns demo_regressor1,demo_regressor2 \
--force
The synthetic regressors are now stacked with the fMRIPrep columns into one
regressor matrix. We pass --force because step 4 already
wrote desc-denoised outputs; without it, denoise skips files that exist. The
.json sidecar now also records custom_sources and custom_columns, so the
result stays reproducible.
Check
The same eight desc-denoised files are rewritten, and each sidecar's
custom_columns lists demo_regressor1, demo_regressor2. A
Nuisance column name collision across channels error means a custom column
name collides with a selected fMRIPrep column; rename or drop one.
6. Fit the encoding model¶
Both sides are now in place: the phonemic and syntactic features (the
predictors) and denoised BOLD (the target). encoding train joins them, fitting
a voxelwise ridge model per subject:
hypline encoding train data/ \
--data-filters task-conv \
--features phonemic,syntactic \
--bold-space MNI152NLin2009cAsym \
--desc v1 \
--fold-by run
Fitting starting: sub-031 fold 1/2 — training on 1 cells / … rows
Fitting starting: sub-031 fold 2/2 — training on 1 cells / … rows
Fitting complete: sub-031 (2 folds)
Fitting starting: sub-032 fold 1/2 — training on 1 cells / … rows
Fitting starting: sub-032 fold 2/2 — training on 1 cells / … rows
Fitting complete: sub-032 (2 folds)
--features phonemic,syntactic uses both feature families from step 3 as the
model's predictors — this is a banded ridge, so each family becomes its own
band with a separately tuned regularization strength. --data-filters task-conv
scopes the fit to the conv task. We pass --bold-space MNI152NLin2009cAsym to
match the volumetric BOLD denoised in step 4; --bold-space otherwise defaults
to the surface fsaverage6. --fold-by run cross-validates by run,
leave-one-out by default (n_folds='loo'), leaving one run out per fold. This is
the common setup, and the one that lets you score held-out data in
step 7.
With two runs, loo yields two folds. --desc v1 tags this model variant so its
output lands in its own subdirectory.
The fit is the slow step
This is the tutorial's one compute-heavy step: a whole-brain ridge over a
grid of regularization strengths, ~4–5 minutes per subject on CPU. The
Fitting starting/complete lines above mark each fold's progress, so a
long pause between them is normal. The fit runs on CPU so the tutorial works
anywhere; pass --device cuda if you have a GPU.
The step is sub-keyed like denoise, one model per brain. Outputs go to a
new results/ area:
data/results/sub-031/encodingModel-v1/
├── sub-031_result-encodingModel_desc-v1.joblib # the fitted model
└── sub-031_result-encodingModel_desc-v1.json # provenance sidecar
Check
results/ now holds an encodingModel-v1/ directory for sub-031 and
sub-032, each with a .joblib + .json pair. If the command logged
No subjects found, check that step 4 wrote desc-denoised BOLD.
Load the result back¶
The model saves as a .joblib blob you load back into Python for downstream
analysis, the same way read_feature reads a
feature file:
from hypline.encoding import load_artifact
artifact = load_artifact(
"data/results/sub-031/encodingModel-v1/sub-031_result-encodingModel_desc-v1.joblib"
)
artifact.recipe # the features, delays, alphas, and split the model was fit with
artifact.models # the fitted pipeline(s)
The fitted model is a starting point: the next two steps use it, scoring its predictions against a real brain.
7. Score a model within a subject¶
encoding analyze scores a model's predictions against a subject's actual BOLD.
It takes three subject roles, independent by design:
- target (
--target-sub) — whose real BOLD is the comparison, and whose speaking turns define the roles below. - model (
--model-sub) — whose trained weights are loaded. - source (
--source-sub) — whose features build the prediction inputs.
Start with the simplest case, all three the same subject: sub-031's own model,
scored against sub-031's own brain.
hypline encoding analyze data/ \
--target-sub 031 \
--model-sub self \
--model-desc v1 \
--desc selfeval
Analyzing: target sub-031, model sub-031, source sub-031 (OOS)
Analysis complete: target sub-031 — scored 2 folds
--model-desc v1 names the model from step 6, and
--desc selfeval tags this eval. We passed no --test-on, so analyze scores
each fold's held-out run, the run that fold did not train on. This is why
step 6 folded: a single unfolded model has no held-out data to score against
itself.
Each score is broken out by band and role. There is one band per feature
family you trained on (here phonemic and syntactic), so you can read each
family's contribution separately, plus an always-present screens_band holding
the task boxcars (you can ignore it). Role is derived from the target's turns in
the conversation: prod (the target is speaking), comp (the partner is
speaking, the target listening), and both (either). One eval thus reports how
well each feature band predicts the brain during production, comprehension, and
overall.
The eval lands in its own results/ subdirectory, keyed by the target subject:
data/results/sub-031/encodingEval-selfeval/
└── sub-031_result-encodingEval_desc-selfeval.nc # per-voxel scores (netCDF-4)
It is a self-describing netCDF-4 file; load it back as an
xarray.Dataset:
from hypline.encoding import load_eval
ds = load_eval(
"data/results/sub-031/encodingEval-selfeval/sub-031_result-encodingEval_desc-selfeval.nc"
)
ds["corr"].sel(band="phonemic", role="prod") # phonemic scores during the target's own speech
ds.attrs["model_sub"], ds.attrs["target_sub"] # provenance rides along
These scores are not [-1, 1] correlations
corr holds himalaya split scores (each feature band's own contribution
to the joint prediction), so a value is not a plain Pearson correlation and
can fall outside [-1, 1]. Read them as relative encoding scores rather than
accuracy fractions.
Check
results/sub-031/ gains an encodingEval-selfeval/ directory with one
.nc file, and the log reads scored 2 folds. An empty out-of-sample set
error means the model wasn't folded; re-run step 6 with --fold-by run.
8. Score across brains¶
The within-subject eval is the warm-up. What hypline is built for is cross-brain analysis: because the two partners shared one conversation, you can drive one partner's model with the partner's speech and score it against the other partner's brain. Same command, different subject wiring:
hypline encoding analyze data/ \
--target-sub 031 \
--model-sub partner \
--source-sub partner \
--model-desc v1 \
--desc crosseval
Analyzing: target sub-031, model sub-032, source sub-032 (OOS)
Analysis complete: target sub-031 — scored 2 folds
--target-sub 031 keeps sub-031's brain as the comparison, but --model-sub
partner and --source-sub partner swap in sub-032's model and features (hypline
resolves partner through participants.tsv). The output structure is identical
to step 7 (a .nc under encodingEval-crosseval/), so load_eval reads it the
same way.
This pairs the partner's model and speech, the cross-brain form used in Zada et
al. (2026). It is not the only one: pairing your own speech with the partner's
model (--source-sub self --model-sub partner) is often preferred, since it holds
the stimulus fixed and varies only the model. How the encoding model
works lays out the
choices side by side.
What this step demonstrates
This is how you run the cross-brain analysis that motivates hypline: the
mechanics of pointing one partner's model at the other's brain. It does not
demonstrate the effect, since two runs of --model small transcripts are far
too little data for the cross-brain scores to mean anything. On a full study
they carry the shared-representation signal; here they only confirm the
command runs end to end.
Check
results/sub-031/ now also holds encodingEval-crosseval/, and the log
shows model sub-032, source sub-032 — the partner's model and features
scored against sub-031's brain.
What you have now¶
data/ now holds a full encoding run, end to end:
| Side | Where | From |
|---|---|---|
| Predictors | features/dyad-030/…/{phonemic,syntactic}/ |
steps 2–3 |
| Target | derivatives/hypline/sub-*/…/func/ |
step 4 |
| Model | results/sub-*/encodingModel-v1/ |
step 6 |
| Eval | results/sub-031/encodingEval-*/ |
steps 7–8 |
Each command read only what the previous steps wrote — no file paths, just the
dataset root. To regenerate a step after changing an option, re-run it with
--force. Without it, hypline skips outputs that already exist.
Where to go next¶
- Add richer features — this run used phonemic and syntactic features, both
CPU-only. For LLM-derived semantic features (contextual word embeddings,
the representation central to Zada et al.), see
featuregen semanticand addsemanticto--features. - Process only some runs or conditions — Filter to specific runs or conditions.
- Regenerate outputs after a fix — Regenerate outputs.
- Per-command options — the Reference pages: transcribe · featuregen · confoundgen · denoise · encoding.