Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Annotation Output Data Format

Status: v0.2 design (supersedes the schema/annotation_schema.json v0.1 draft; reconciles it with the Phase 1 catalogs). Scope: the on-disk format the Phase 2 pipeline emits for one stimulus (one movie or one audio story), and the MATLAB reader interface that loads it.

This document defines: (1) the container format and why; (2) the timeline / alignment convention; (3) the hierarchical feature layout; (4) per-feature (per-channel) metadata; (5) how nulls and reserved human-annotation slots are represented; (6) a worked 2-timepoint example; (7) the MATLAB reader interface. It is the authoritative spec; schema/annotation_schema.json is the machine-checkable encoding of it, and matlab/*.m is the reference reader.


1. Format recommendation: HDF5 canonical + JSON sidecar manifest

1.1 The requirement, sized

A single feature-rich movie produces, per second:

  • ~hundreds of scalar channels (luminance, RMS contrast, F0, flow magnitude, valence, surprisal …);
  • several high-dimensional vector channels — AudioSet tags (527-dim), CLIP/SigLIP probe sets (tens–hundreds of prompts), Places365 (365-dim), DINO/CLIP/LLM embeddings (384–4096-dim), pymoten motion energy (hundreds–thousands-dim), pose keypoints (17–133 × 3);
  • categorical / label channels (scene category, speaker id, topic id, dialogue act, narrative stage);
  • sparse-event channels (cuts, beats, turning points) carried as both a per-bin flag and an onset list.

A 2-hour movie at 1 Hz is 7200 timepoints. With a few thousand embedding dimensions per timepoint across several encoders, the dense payload is tens of MB to low GB per stimulus. Inline-array JSON (the v0.1 draft) is unworkable at that size: it is ~3–5× larger than binary, parses slowly, holds everything in memory, and has no native typed n-D array. So JSON stays the readable metadata layer, not the bulk container.

1.2 Decision

Canonical container: HDF5 (.h5), one file per stimulus. Sidecar: a human-readable JSON manifest (.manifest.json) holding the same hierarchy and all metadata but no bulk numeric arrays — pointers (data_ref) into the .h5 instead.

Rationale, against the requirements:

RequirementHow HDF5 + JSON sidecar meets it
Second-by-second, configurable rateOne shared time axis dataset; every channel is [n_samples × …] aligned to it.
Semantically readable, coherent, hierarchicalHDF5 groups are a filesystem-like hierarchy; the JSON sidecar mirrors it and is the human-readable view.
Irrelevant features → null/NaNFloat datasets use NaN; an applicable attribute + a fill value make “not measured” explicit and self-describing.
Reserved human-annotation slotsA top-level /human/ group, pre-created empty with the same shape contract; never written by machines.
Loadable into MATLABHDF5 is a first-class MATLAB format (h5read, h5info, h5readatt) with no toolbox required; the JSON sidecar loads via jsondecode.
Reproducible provenancePer-channel attributes (model, version, native rate, units, resample op) live next to the data; global provenance in /.
Scales to dense embeddingsChunked + gzip-compressed datasets; lazy/partial reads; no whole-file parse.

Parquet is a strong alternative for the scalar/tabular slice (columnar, great with pandas/Arrow), but it is awkward for ragged high-dimensional per-timepoint vectors and variable-length event lists, and MATLAB support (parquetread) is newer and tabular-only. We therefore choose HDF5 as the single canonical container and optionally emit a flat *_scalars.parquet projection of just the scalar channels as a convenience export (see §3.5).

Pure-JSON profile (small/demo only). For tiny clips, tests, and documentation, the entire payload may be inlined into the JSON file (the v0.1 layout). The reader auto-detects: if a channel has inline value, it uses it; if it has data_ref only, it reads the .h5. This keeps the v0.1 example valid while making the dense case feasible.

1.3 On-disk artifacts per stimulus

annotations/output/<stimulus_id>/
  <stimulus_id>.h5             # canonical: time axis + all feature datasets + attributes
  <stimulus_id>.manifest.json  # readable hierarchy + metadata + data_ref pointers (no bulk arrays)
  <stimulus_id>_scalars.parquet  # OPTIONAL flat export of scalar channels (convenience)

Self-contained alternative: a single .h5 is sufficient on its own (it carries all metadata as attributes); the sidecar JSON exists for grep/diff/readability and for the pure-JSON profile.


2. Timeline & alignment convention

2.1 The common grid

There is exactly one common time grid per file, defined by three numbers and materialized as one dataset:

/time/rate_hz        scalar   e.g. 1.0   (configurable; default 1 Hz)
/time/t_start_sec    scalar   e.g. 0.0   (time of the *center* of sample 0; see §2.2)
/time/n_samples      scalar   e.g. 7200
/time/time_sec       [n_samples] float64   center timestamp of each grid bin (always written, even
                                           though derivable, so MATLAB/Python never recompute it)

Invariant: time_sec[i] == t_start_sec + i / rate_hz. Every feature dataset has leading dimension n_samples and shares this axis — that is what makes the whole file a single aligned matrix family.

A file may also declare an alternate grid (e.g. a 0.5 Hz coarse grid, or a per-TR grid for a specific fMRI study) under /time/alt/<name>/…; channels resampled to an alternate grid point at it via a grid attribute. Default and common case: a single grid.

2.2 Bin convention (center-referenced, half-open)

Grid bin i covers the continuous interval

[ t_start_sec + (i - 0.5)/rate_hz ,  t_start_sec + (i + 0.5)/rate_hz )

and time_sec[i] is its center. All resampling (§2.4) maps native samples into these bins. Center- referencing (not left-edge) keeps a feature’s grid time aligned to the middle of the interval it summarizes, which is the convention most neuroimaging analyses expect and avoids a systematic half-bin lag. The choice is recorded once in /time/bin_reference = "center".

2.3 Native rates feeding the grid

Each extractor has a native rate (from the Phase 1 catalogs), one of:

Native rateExamplesTypical native step
frameluminance, optical flow, CLIP/DINO probes, depth, saliency, pose1/fps (e.g. 25 Hz, or the analysis 2 fps)
subsecondlibrosa/openSMILE LLDs, Praat F0, CREPE, audio taggers on short hops~10–100 Hz
secondwindowed VLM/affect/action windows1 Hz
windowsliding clip models (VideoMAE 16-frame, CLAP 10 s)window-dependent
utteranceASR segments, dialogue acts, per-utterance affectirregular
wordsurprisal, lexical norms, POS, LLM hidden statesirregular (from forced alignment)
shot / sceneper-shot VLM captions, scene labels, situational tagsirregular
eventcuts, beats, turning points, event-segment boundariesirregular/sparse

The native rate is preserved as metadata on every channel (native_rate_hz, which may be a number or one of the strings above), and the raw pre-resampling output is cached (per DEPLOYMENT_FEASIBILITY §2.6) so the grid can be regenerated at a different rate without re-running models.

2.4 Resampling onto the grid (per dtype)

One resampling rule per channel, chosen by dtype and recorded in the resample attribute. These match DEPLOYMENT_FEASIBILITY §2.7:

Channel kindDefault resample op (resample)Notes
Continuous scalar (luminance, F0, flow, valence)meanAnti-aliased / area average of native samples within the bin. Optionally also emit _std, _max companion channels.
Probability / score vector (AudioSet 527, CLIP probes, emotion posteriors)meanElement-wise mean-pool within bin.
Embedding vector (DINO/CLIP/LLM/Qwen3)meanMean-pool of native vectors in bin (record embed_pool="mean").
Categorical / label (scene, speaker, topic, dialogue act, narrative stage)modeMost-frequent native label in bin; ties broken by longest dwell. Companion *_change boundary flag emitted.
bool / presence (speech present, face present)any or fracany = ≥1 native true in bin; or frac = fraction of bin true (record which).
Sparse event (cut, beat, turning point)count + onset listPer-bin integer count/flag and an exact onset-time list (§5.3).
Per-word feature (surprisal, concreteness, hidden state)meanAssign by word timestamp to its bin, average within bin; companion word_rate/word_onset channels.

A native sample with no bin (out of range) is dropped; a bin with no native sample is NaN (continuous) / empty-label (categorical) / 0 (count) — see §5.

2.5 Why a single grid (not per-feature native arrays in the file)

Keeping every channel on one shared, regular grid is what makes the file a coherent design matrix: MATLAB loads it straight into a timetable, cross-feature correlations and PCA (Phase 4) need no re-interpolation, and “inapplicable = NaN of the right length” gives a constant output shape across all stimuli (the README’s core principle). Native-rate arrays still exist — in the cache, and optionally archived under /native/<channel>/… for power users — but the grid is the contract.


3. Hierarchical feature layout

3.1 Top-level structure (HDF5 groups == hierarchy)

/                                      (root; global attributes = schema_version, ids, provenance)
├── time/                              the common grid (§2)
├── stimulus/                          (attrs: id, title, modality, duration_sec, source, media_file, sha256)
├── features/                          MACHINE annotations, mirroring the Phase 1 semantic hierarchy
│   ├── visual/
│   │   ├── low_level_static/          luminance, rms_contrast, colorfulness, edge_density, fft_slope, clutter…
│   │   ├── high_level_static/         scene_category, object_presence, clip_probe…, places365, panoptic_fractions
│   │   ├── faces_bodies_gaze/         n_faces, au_intensity, gaze_yaw/pitch, head_pose, pose_keypoints, expression
│   │   ├── dynamic_motion/            flow_magnitude, camera_motion, residual_motion, motion_energy, cut (event)
│   │   ├── action/                    action_posteriors, action_probe, action_segment (event)
│   │   └── saliency_aesthetics_depth/ saliency_entropy, depth_mean, fg_fraction, aesthetic, quality, memorability
│   ├── audio/
│   │   ├── low_level/                 rms, loudness, spectral_centroid, mfcc, chroma, f0, onset_strength, tempo
│   │   ├── high_level/                audioset_tags, clap_probe, scene, speech_music_noise, key, beat (event)
│   │   └── speech/                    asr_text, speaker_id, vad, speech_rate, prosody_f0, ser_arousal/valence/dominance
│   ├── language/
│   │   ├── lexical/                   word, lemma, pos, freq_zipf, concreteness, aoa, valence_norm, emolex_*, surprisal
│   │   ├── syntax/                    dep_depth, clauses_per_tunit, tree_depth, coref_chain_len, dialogue_act, readability
│   │   └── semantics_discourse/       embedding, coherence, drift, novelty, topic_id, topic_vector, narrative_stage, turning_point (event)
│   ├── social/                        n_agents, characters_present, active_speaker, mutual_gaze, joint_attention,
│   │                                  proximity, interaction_type, dominance, affiliation, tom_intention
│   ├── situation/                     location, time_of_day, indoor_outdoor, script_label, event_id, event_boundary (event),
│   │                                  space/time/causation/intention/protagonist (Event-Indexing dims)
│   └── affect/
│       ├── depicted/                  face_valence/arousal, voice_v/a/d, text_valence, music_v/a, categorical_emotion
│       └── elicited/                  induced_valence, induced_arousal, fear_flag   (separate stream from depicted)
├── human/                             RESERVED for later human annotation (§5.4); same shape contract; empty at emit
│   ├── visual/ … affect/              (mirrors features/ subgroups; created empty)
│   └── _free/                         free-form human channels not in the machine taxonomy
└── provenance/                        per-class model registry, env locks, params hashes (§4.3)

The top-level classes and subclasses are exactly the Phase 1 hierarchy (Visual / Audio / Language / Social / Situation / Affect). A program traverses groups; a human reads the JSON sidecar, which has the identical tree.

3.2 A leaf = one channel = one HDF5 dataset + attributes

Each leaf feature is one dataset named by the channel, shaped [n_samples] (scalar/label/bool/event) or [n_samples × D] (vector/embedding) or [n_samples × K × C] (e.g. pose K keypoints × C coords), with metadata carried as HDF5 attributes on that dataset (§4). The dataset’s HDF5 path is its hierarchical path, e.g. /features/visual/low_level_static/luminance.

3.3 Dtype encodings

dtypeHDF5 storageNull encoding
scalarfloat64 [n]NaN
vectorfloat32/float64 [n × D]whole row NaN (not measured)
boolint8 [n] (0/1)-1 (or float NaN if stored as float)
categoricalint32 [n] code + categories attr (string array)code -1<undefined>
labelvariable-length UTF-8 string [n]empty string "" ↔ not measured
eventsee §5.3 (per-bin count int32 [n] + onset list group)count 0
text (e.g. ASR words)variable-length UTF-8 string [n]""

Categorical channels store integer codes + a categories attribute (the label vocabulary) so MATLAB reconstructs a categorical array directly and analyses stay numeric; label/text channels store strings directly for the free-vocabulary cases (ASR words, LLM free-text tags).

3.4 Vector channels carry a component axis

A [n × D] vector dataset gets a components attribute: a length-D string array naming each column (e.g. AudioSet class names, CLIP prompt strings, MFCC indices, keypoint names). Embeddings whose dimensions are not individually meaningful set components = [] and rely on dim/model metadata. This makes every vector self-describing without an external codebook.

3.5 Optional flat scalar export

For quick tabular work, the pipeline may also emit <id>_scalars.parquet: one row per timepoint, one column per scalar channel, column names = the slash-path with /__ (visual__low_level_static__luminance), plus a time_sec column. This is a lossy convenience projection (no vectors/labels/events); the .h5 remains authoritative.


4. Per-feature (per-channel) metadata

4.1 Required + optional attributes on every channel

Carried as HDF5 attributes on the dataset (and as JSON keys in the sidecar). Required marked R.

AttributeTypeMeaning
dtype Rstringone of §3.3.
applicable Rboolfalse ⇒ feature does not apply to this stimulus modality; value is all-null (§5.2).
unitsstringe.g. "0-1", "Hz", "dB", "bits", "1-9 (Warriner)", "-1..1", "deg". ""/absent for unitless or categorical.
model Rstringproducing tool/model, e.g. "scikit-image", "SigLIP2-so400m", "faster-whisper-large-v3".
version Rstringmodel checkpoint + code version, e.g. "0.24.0", "siglip2-so400m-patch14-384".
native_rate_hz Rnumber or stringnumeric Hz, or one of `frame
resample Rstringop used to map native→grid (§2.4): `mean
componentsstring[]column names for vector dtypes (§3.4); empty for opaque embeddings.
categoriesstring[]label vocabulary for categorical (code↔label map); index = code.
dimintD for vector/embedding channels.
gridstring"default" or an alternate-grid name (§2.1).
tierstringprovenance: `cpu
params_hashstringcontent hash of model params/config that produced this channel.
notesstringfree text (e.g. “null where no speech present”).
confidence_refstringoptional path to a companion [n] confidence channel.

4.2 Companion channels (uncertainty & dispersion)

Where a model exposes it, a channel X may have siblings:

  • X_conf — per-timepoint confidence/probability (e.g. ASR avg_logprob, detection score, voicing prob);
  • X_std, X_max — within-bin dispersion when many native samples collapsed into one grid bin;
  • X_change — boundary/onset flag for categorical/label channels.

These are ordinary channels with their own metadata; the parent points to them via confidence_ref.

4.3 Global provenance (/provenance + root attributes)

/  (root attributes)
   schema_version   = "0.2"
   pipeline_version = "<git-describe>"
   generated_utc    = "2026-06-13T12:00:00Z"
   common_grid_rate_hz = 1.0

/provenance/
   models/        attr-per-model: name → {version, tier, env, params_hash, citation}
   env_locks/     names of the locked envs used (core-cv, torch-vision, speech, llm, …)
   stimulus_sha256
   transcript_source = "whisperx" | "supplied"

This satisfies the README’s “reproducible provenance”: every channel ties to a model entry, which ties to a locked environment and a params hash.


5. Nulls, applicability, and reserved human slots

5.1 Three distinct “missing” states — never conflated

StateMeaningEncoding
Not applicablefeature class can’t apply to this modality (visual on audio-only story)applicable=false attr and value all-NaN/""/-1 for full length
Not measured hereapplicable, but no native sample fell in this bin (e.g. silence → no word, no speech)per-element NaN/""/code=-1/count=0; applicable=true
Measured zero / absentthe model ran and reports zero/absence (e.g. 0 faces detected, loudness ≈ 0)the actual numeric value (0, low float) — not null

The applicable flag plus the element-wise fill value lets downstream analysis distinguish “not measured” from “measured zero” — the README’s explicit requirement. Float channels use IEEE NaN (reads into MATLAB as NaN natively); categorical uses code -1; labels/text use ""; events use 0.

5.2 Inapplicable features are still present, full-length, all-null

When a stimulus is audio-only, every visual/* channel is still emitted with the correct [n_samples × …] shape, filled with NaN/""/-1, and applicable=false. This guarantees a constant output shape and hierarchy across the whole corpus, so cross-stimulus matrices, PCA, and the Phase-4 design tool never have to special-case which classes a given stimulus has.

Modality → applicable-class gate (from IMPLEMENTATION_PLAN “applicability rule”):

Stimulus modalityApplicable top-level classes
audiovisualvisual, audio, language, social, situation, affect (all)
video-onlyvisual, situation, affect.depicted, social (no speech-derived language/affect)
audio-onlyaudio, language, social*, situation, affect (no visual)
text-onlylanguage, situation, social*, affect (text branch only)

(*social/situation degrade to the subset derivable from the available modalities; per-channel applicable flags carry the exact truth.)

5.3 Sparse events

An event channel is stored as a per-bin count dataset plus an onset group holding exact times:

/features/visual/dynamic_motion/cut            int32 [n]   per-bin cut count (resample="count")
/features/visual/dynamic_motion/cut__onsets/
    time_sec     [m] float64      exact onset times (m = total events)
    value        [m] (optional)   per-event payload (e.g. transition type, confidence)

So an event is both griddable (the [n] count/flag, usable as a regressor) and exact (the onset list, for precise timing). Beats, turning points, scene boundaries, action segments follow the same pattern; segment-type events add a paired __offsets list.

5.4 Reserved human-annotation slots (/human/)

A top-level /human/ group mirrors the /features/ subgroup tree but is created empty at machine emit time — no datasets, just the group skeleton — plus a /human/_free/ group for human channels that don’t fit the machine taxonomy. Properties:

  • Same channel contract. A later human (or human-in-the-loop tool) writes a channel into /human/<class>/<sub>/<name> using the identical schema: [n_samples] aligned to /time, with dtype, applicable, model="human", version=<rater id / protocol>, resample, notes.
  • Never auto-populated. Machine runs only (re)create the empty skeleton; they never write under /human/, so re-running the pipeline never clobbers human work. (Writers should append, and the pipeline must not overwrite an existing .h5’s /human/ group — emit a new file or merge.)
  • Discoverable. /human has an attribute populated = false until a human channel is added; the reader exposes ann.human as an (initially empty) struct of the same shape as ann.features.
  • Provenance for humans. A human channel records rater id, instructions/protocol version, and date in its attributes exactly as a model channel records model/version, so human and machine annotations are first-class and equally traceable.

In the JSON sidecar / pure-JSON profile, this is the human_annotations object: empty {} at emit, later filled with the same hierarchical shape as features.


6. Worked example (~2 timepoints)

Below is the JSON sidecar view (the readable layer) for an audiovisual clip at 1 Hz, showing two grid timepoints and a representative channel of each kind. Bulk vectors point into the .h5 via data_ref; small channels inline value for readability. A null at t=0 for a language feature shows “applicable but not measured” (no speech yet); the whole affect/elicited example shows a separate stream from depicted affect.

{
  "schema_version": "0.2",
  "stimulus": {
    "id": "demo_movie_001", "title": "Demo clip", "modality": "audiovisual",
    "duration_sec": 2.0, "source": "user-supplied",
    "media_file": "data/movies/demo_movie_001.mp4", "sha256": "ab12…"
  },
  "time": {
    "rate_hz": 1.0, "t_start_sec": 0.0, "n_samples": 2,
    "bin_reference": "center", "time_sec": [0.0, 1.0]
  },
  "features": {
    "visual": {
      "low_level_static": {
        "luminance": {
          "dtype": "scalar", "applicable": true, "units": "0-1",
          "model": "scikit-image", "version": "0.24.0",
          "native_rate_hz": 25, "resample": "mean",
          "value": [0.41, 0.43]
        }
      },
      "high_level_static": {
        "scene_category": {
          "dtype": "categorical", "applicable": true,
          "model": "SigLIP2-so400m", "version": "siglip2-so400m-patch14-384",
          "native_rate_hz": "frame", "resample": "mode",
          "categories": ["kitchen", "hallway", "street"],
          "value": [0, 1]                        // codes -> "kitchen", "hallway"
        },
        "clip_probe": {
          "dtype": "vector", "applicable": true, "dim": 3,
          "model": "SigLIP2-so400m", "version": "siglip2-so400m-patch14-384",
          "native_rate_hz": "frame", "resample": "mean",
          "components": ["a kitchen", "a person cooking", "an empty hallway"],
          "data_ref": "/features/visual/high_level_static/clip_probe"   // [2 x 3] in .h5
        }
      },
      "dynamic_motion": {
        "cut": {
          "dtype": "event", "applicable": true, "units": "count",
          "model": "TransNetV2", "version": "1.0",
          "native_rate_hz": "frame", "resample": "count",
          "value": [0, 1],                         // one cut landed in bin t=1
          "onsets": { "time_sec": [1.04], "value": ["hard"] }
        }
      }
    },
    "audio": {
      "low_level": {
        "loudness": {
          "dtype": "scalar", "applicable": true, "units": "LUFS-rel",
          "model": "librosa", "version": "0.10.2",
          "native_rate_hz": 100, "resample": "mean",
          "value": [0.62, 0.71]
        }
      },
      "high_level": {
        "audioset_tags": {
          "dtype": "vector", "applicable": true, "dim": 527,
          "model": "BEATs", "version": "iter3+",
          "native_rate_hz": "window", "resample": "mean",
          "components": ["Speech", "Music", "..."],         // 527 names (truncated)
          "data_ref": "/features/audio/high_level/audioset_tags"   // [2 x 527] in .h5
        }
      }
    },
    "language": {
      "lexical": {
        "valence_norm": {
          "dtype": "scalar", "applicable": true, "units": "1-9 (Warriner)",
          "model": "Warriner-norms", "version": "2013",
          "native_rate_hz": "word", "resample": "mean",
          "value": [null, 6.2],                    // t=0 NaN: no speech in that bin (not measured)
          "notes": "null where no word onset falls in the bin"
        }
      },
      "semantics_discourse": {
        "embedding": {
          "dtype": "vector", "applicable": true, "dim": 1024,
          "model": "Qwen3-Embedding-0.6B", "version": "0.6B",
          "native_rate_hz": "utterance", "resample": "mean",
          "components": [],                         // opaque embedding dims
          "data_ref": "/features/language/semantics_discourse/embedding"  // [2 x 1024]
        },
        "narrative_stage": {
          "dtype": "categorical", "applicable": true,
          "model": "Llama-3.1-8B-Instruct", "version": "4bit",
          "native_rate_hz": "scene", "resample": "mode",
          "categories": ["setup", "conflict", "climax", "resolution"],
          "value": [0, 0]                           // both bins "setup"
        }
      }
    },
    "social": {
      "n_agents": {
        "dtype": "scalar", "applicable": true, "units": "count",
        "model": "InsightFace-buffalo_l", "version": "0.7",
        "native_rate_hz": "frame", "resample": "mean",
        "value": [2.0, 2.0]                          // measured 2 (not null)
      }
    },
    "affect": {
      "depicted": {
        "face_valence": {
          "dtype": "scalar", "applicable": true, "units": "-1..1",
          "model": "HSEmotion-EfficientNet-B2", "version": "va_mtl",
          "native_rate_hz": "frame", "resample": "mean",
          "value": [0.10, 0.30]
        }
      },
      "elicited": {
        "induced_valence": {
          "dtype": "scalar", "applicable": true, "units": "-1..1",
          "model": "LIRIS-ACCEDE-regressor", "version": "1.0",
          "native_rate_hz": "second", "resample": "mean",
          "notes": "viewer-elicited; distinct stream from affect.depicted",
          "value": [0.05, 0.12]
        }
      }
    }
  },
  "human_annotations": {},     // empty skeleton mirrors features/ (see §5.4)
  "provenance": {
    "pipeline_version": "0.2.0", "generated_utc": "2026-06-13T12:00:00Z",
    "models": {
      "SigLIP2-so400m": { "version": "siglip2-so400m-patch14-384", "tier": "gpu", "env": "torch-vision" },
      "BEATs": { "version": "iter3+", "tier": "gpu", "env": "audio-tag" }
      /* … */
    }
  }
}

For an audio-only story, the entire features.visual subtree is still present and full-length, with every channel applicable=false and value all-NaN (scalars) / -1 codes (categoricals) / "" (labels). Shape is identical to the audiovisual file; only applicable and the fill values differ.


7. MATLAB reader interface

MATLAB reads HDF5 natively (h5read, h5info, h5readatt) and JSON via jsondecodeno toolbox required for the core reader. The reference implementation lives in matlab/.

Implementation status. IMPLEMENTED and verified: readAnnotations (.h5, folder, or JSON), getFeature, featuresToTimetable(ann) (no name-value options; scalar/bool/event channels only, not-applicable channels as NaN), plus readAnnotationCorpus, analyzeCorpus, selectStimulusSet, refreshAnalysis, annotationMovieViewer. The signatures below marked (planned)listFeatures, getFeatureMatrix, writeHumanChannel, the "Lazy" option, and featuresToTimetable’s name-value options — are design targets, not yet implemented.

High-level signatures:

% --- Load -----------------------------------------------------------------
ann = readAnnotations(path)
%   READANNOTATIONS  Load one annotation into a MATLAB struct.
%   PATH may be the .h5, the .manifest.json, or the stimulus folder.
%   If given JSON-with-data_ref, reads bulk arrays from the sibling .h5;
%   if given pure-JSON, uses inline values; if given the .h5, reads everything
%   from it (metadata from attributes). Returns:
%     ann.stimulus  struct   (id, title, modality, duration_sec, source, sha256)
%     ann.time      struct   (rate_hz, t_start_sec, n_samples, bin_reference)
%     ann.time_sec  double[n] column   (the common grid; always materialized)
%     ann.features  struct   nested groups mirroring the hierarchy; each leaf is
%                            a channel struct (see below)
%     ann.human     struct   same shape as features (empty until humans populate)
%     ann.provenance struct
%   JSON null / HDF5 fill -> NaN (numeric), "" (label/text), <undefined> (categorical).

% A leaf channel struct:
%   ch.value         numeric [n] | [n x D] | categorical [n] | string [n] | int [n]
%   ch.dtype, ch.applicable, ch.units, ch.model, ch.version,
%   ch.native_rate_hz, ch.resample, ch.components (string[]), ch.categories,
%   ch.notes, ch.onsets (struct .time_sec/.value for event dtypes)

% --- Navigate / select ----------------------------------------------------
ch = getFeature(ann, "visual/low_level_static/luminance")
%   GETFEATURE  Retrieve one channel by hierarchical (slash) path. Errors if the
%   path is not a leaf channel. Works for features/ and human/ paths.

paths = listFeatures(ann, namevalue)
%   LISTFEATURES  Return the slash-paths of all channels, filterable by:
%     "Class","visual" | "Dtype","scalar" | "Applicable",true |
%     "Modality",... | "Pattern","*valence*"
%   Use to discover what a file contains without manual traversal.

% --- Reshape for analysis -------------------------------------------------
tt = featuresToTimetable(ann, namevalue)
%   FEATURESTOTIMETABLE  Collect channels into a MATLAB timetable on the common
%   grid (RowTimes = seconds(ann.time_sec)). Options:
%     "Dtypes",["scalar","bool"]   which kinds to include (default scalars)
%     "Class","audio"              restrict to one branch
%     "ExpandVectors",true         explode [n x D] vectors into D named columns
%     "IncludeNaNApplicable",false drop all-NaN inapplicable channels
%   Variable names are the slash-path with "/"->"__". Vector/label/event channels
%   are skipped unless ExpandVectors / explicit Dtypes request them.

M = getFeatureMatrix(ann, paths)
%   GETFEATUREMATRIX  Stack a list of channel paths into one [n x P] numeric
%   matrix (scalars) for PCA / correlation / the Phase-4 design tool; returns the
%   column->path map and propagates NaN.

% --- Corpus level (Phase 4) ----------------------------------------------
C = readAnnotationCorpus(folder, namevalue)
%   READANNOTATIONCORPUS  Load many annotation files into a struct array (or a
%   stacked timetable keyed by stimulus_id), enforcing the constant-shape
%   contract so cross-stimulus matrices align. Options mirror featuresToTimetable.

% --- Write-back (human-in-the-loop) --------------------------------------
writeHumanChannel(path, "social/interaction_type", values, namevalue)
%   WRITEHUMANCHANNEL  Append a human annotation channel under /human/ in the .h5
%   (and update the JSON sidecar), validating length == n_samples and stamping
%   model="human", version=<RaterId>, plus protocol/date attributes. Never
%   touches /features/.

Design notes for the reader:

  • No toolbox dependency for read: h5read/h5info/h5readatt + jsondecode are base MATLAB. timetable/categorical are base MATLAB. (Parquet export read needs no extra toolbox via parquetread, which is base since R2019a.)
  • Lazy option. readAnnotations(...,"Lazy",true) reads only metadata + the time axis and defers bulk dataset reads until getFeature/getFeatureMatrix touches them — important for the multi-GB embedding channels.
  • Categorical round-trips. Integer-code + categories attribute reconstructs a MATLAB categorical directly, so code -1 becomes <undefined> and grid math stays numeric.
  • Constant shape guarantee is what lets readAnnotationCorpus stack heterogeneous stimuli without per-file branching: inapplicable channels are present and NaN, so the corpus matrix is rectangular.

8. Summary of decisions

  1. HDF5 canonical, one file per stimulus, with a readable JSON sidecar manifest; optional flat *_scalars.parquet export; a pure-JSON profile for small/demo files. All load into MATLAB with no toolbox.
  2. One shared, configurable common grid (default 1 Hz), center-referenced half-open bins, materialized time_sec; every channel shares the n_samples leading axis. Native rates preserved as metadata; raw native outputs cached for re-gridding.
  3. Hierarchical groups mirroring the Phase 1 semantic hierarchy (visual / audio / language / social / situation / affect, with subclasses); each leaf is one dataset + self-describing attributes.
  4. Per-channel metadata: model, version, native rate, resample op, units, components/categories, tier, params hash; global provenance ties channels to locked environments.
  5. Three explicit missing-states — not-applicable (applicable=false, all-null), not-measured (element NaN/""/-1/0), measured-zero (real value) — never conflated.
  6. Reserved /human/ group mirroring the machine tree, empty at emit, never auto-clobbered, with identical channel contract and human-rater provenance.
  7. MATLAB reader (readAnnotations, getFeature, listFeatures, featuresToTimetable, getFeatureMatrix, readAnnotationCorpus, writeHumanChannel) loading the file into structs and timetables on the common grid.