reprostim.qr.bids_inject

Core logic for bids-inject: cross-reference ReproStim videos.tsv with BIDS _scans.tsv files to slice and inject per-acquisition video clips into a BIDS dataset.

See .ai/spec-bids-inject.md for the full specification.

Functions

do_main(paths, videos_tsv, recursive, match, ...)

Main entry point for the bids-inject command.

dt_bids_to_reprostim(dt, bids_tz, reprostim_tz)

Convert a naive BIDS datetime directly to a naive ReproStim datetime.

dt_bids_to_utc(dt, tz)

Convert a naive BIDS datetime to a naive UTC datetime.

dt_convert(dt, tz_from, tz_to)

Convert a naive datetime from one timezone context to another.

dt_parse_bids(s)

Parse a BIDS acq_time ISO 8601 string to a naive datetime.

dt_parse_dicom_time(value)

Parse a DICOM TM (Time) string into a datetime.time object.

dt_reprostim_to_bids(dt, reprostim_tz, bids_tz)

Convert a naive ReproStim datetime directly to a naive BIDS datetime.

dt_reprostim_to_utc(dt, tz)

Convert a naive ReproStim datetime to a naive UTC datetime.

dt_resolve_tz(name)

Resolve a timezone name string to a datetime.tzinfo object.

dt_time_to_sec(t)

Convert a datetime.time to total seconds since midnight.

dt_tz_label(name)

Return the current UTC offset for a timezone name, e.g. UTC-05:00.

dt_utc_to_bids(dt, tz)

Convert a naive UTC datetime to a naive BIDS-domain datetime.

dt_utc_to_reprostim(dt, tz)

Convert a naive UTC datetime to a naive ReproStim-domain datetime.

Classes

BiContext(*, dry_run, recursive, match, ...)

Context for bids-inject processing of scan records.

BiSummary(*, n_processed, n_injected, ...)

Mutable counters accumulating per-run injection statistics.

BufferPolicy(value)

Policy for handling buffer overflow beyond video boundaries.

LayoutMode(value)

Output file placement layout within the BIDS dataset.

MediaSuffix(value)

Recording-type suffix per BEP044:Stimuli.

OverwriteMode(value)

Policy for handling existing output files.

QrMode(value)

QR code-based timing refinement mode.

ScanMetadata(*, TaskName, ...)

Duration-relevant fields from a BIDS JSON sidecar file.

ScanRecord(*, filename, acq_time, extra, ...)

A single row from a BIDS _scans.tsv file with optionally expanded metadata.

ScansModel(*, path, records)

Parsed representation of a single BIDS *_scans.tsv file.

class reprostim.qr.bids_inject.BiContext(*, dry_run: bool, recursive: bool, match: str = '.*', videos_tsv: str | None = None, time_offset: float = 0.0, qr: ~reprostim.qr.bids_inject.QrMode = QrMode.NONE, buffer_before: str = '0', buffer_after: str = '0', buffer_policy: str = 'flexible', layout: ~reprostim.qr.bids_inject.LayoutMode = LayoutMode.NEARBY, overwrite: ~reprostim.qr.bids_inject.OverwriteMode = OverwriteMode.SKIP, reprostim_timezone: str = 'local', bids_timezone: str = 'local', lock: bool = True, verbose: bool = False, out_func: ~typing.Callable | None = None, summary: ~reprostim.qr.bids_inject.BiSummary = <factory>)[source]

Context for bids-inject processing of scan records.

bids_timezone: str
property bids_tz: tzinfo

Resolved tzinfo for bids_timezone (cached).

buffer_after: str
buffer_before: str
buffer_policy: str
dry_run: bool
layout: LayoutMode
lock: bool
match: str
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

out_func: Callable | None
overwrite: OverwriteMode
qr: QrMode
recursive: bool
reprostim_timezone: str
property reprostim_tz: tzinfo

Resolved tzinfo for reprostim_timezone (cached).

summary: BiSummary
time_offset: float
verbose: bool
videos_tsv: str | None
class reprostim.qr.bids_inject.BiSummary(*, n_processed: int = 0, n_injected: int = 0, n_skipped: int = 0, n_errors: int = 0, errors: ~typing.List[str] = <factory>)[source]

Mutable counters accumulating per-run injection statistics.

errors: List[str]
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

n_errors: int
n_injected: int
n_processed: int
n_skipped: int
class reprostim.qr.bids_inject.BufferPolicy(value)[source]

Policy for handling buffer overflow beyond video boundaries.

FLEXIBLE = 'flexible'
STRICT = 'strict'
class reprostim.qr.bids_inject.LayoutMode(value)[source]

Output file placement layout within the BIDS dataset.

NEARBY = 'nearby'
TOP_STIMULI = 'top-stimuli'
class reprostim.qr.bids_inject.MediaSuffix(value)[source]

Recording-type suffix per BEP044:Stimuli.

AUDIO = '_audio'
AUDIOVIDEO = '_audiovideo'
VIDEO = '_video'
class reprostim.qr.bids_inject.OverwriteMode(value)[source]

Policy for handling existing output files.

ALWAYS = 'always'
ERROR = 'error'
FORCE = 'force'
SKIP = 'skip'
class reprostim.qr.bids_inject.QrMode(value)[source]

QR code-based timing refinement mode.

AUTO = 'auto'
EMBED_EXISTING = 'embed-existing'
NONE = 'none'
PARSE = 'parse'
class reprostim.qr.bids_inject.ScanMetadata(*, TaskName: str | None = None, FrameAcquisitionDuration: float | None = None, AcquisitionTime: ~typing.List[str] | None = None, RepetitionTime: float | None = None, NumberOfVolumes: int | None = None, extra: dict = <factory>)[source]

Duration-relevant fields from a BIDS JSON sidecar file.

Only the four fields required for scan-duration computation are promoted to typed attributes; every other key present in the JSON sidecar is stored verbatim in extra.

Duration is resolved in priority order (see spec):

  1. FrameAcquisitionDuration (ms) — most reliable when present.

  2. AcquisitionTime array — (last - first + TR) seconds where TR = AcquisitionTime[1] - AcquisitionTime[0].

  3. RepetitionTime (s) × NumberOfVolumes.

AcquisitionTime: List[str] | None
FrameAcquisitionDuration: float | None
NumberOfVolumes: int | None
RepetitionTime: float | None
TaskName: str | None
extra: dict
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class reprostim.qr.bids_inject.ScanRecord(*, filename: str, acq_time: str, extra: dict = <factory>, metadata: ~reprostim.qr.bids_inject.ScanMetadata | None = None, duration_sec: float | None = None, reprostim_path: str | None = None, reprostim_offset: float | None = None, reprostim_buffer_before: float | None = None, reprostim_buffer_after: float | None = None)[source]

A single row from a BIDS _scans.tsv file with optionally expanded metadata.

acq_time: str
duration_sec: float | None
extra: dict
filename: str
metadata: ScanMetadata | None
model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

reprostim_buffer_after: float | None
reprostim_buffer_before: float | None
reprostim_offset: float | None
reprostim_path: str | None
class reprostim.qr.bids_inject.ScansModel(*, path: str, records: ~typing.List[~reprostim.qr.bids_inject.ScanRecord] = <factory>)[source]

Parsed representation of a single BIDS *_scans.tsv file.

model_config: ClassVar[ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

path: str
records: List[ScanRecord]
reprostim.qr.bids_inject.do_main(paths: List[str], videos_tsv: str, recursive: bool, match: str, buffer_before: str, buffer_after: str, buffer_policy: str, time_offset: float, qr: str, layout: str, reprostim_timezone: str, bids_timezone: str, dry_run: bool, overwrite: str, lock: bool, verbose: bool, out_func: Callable) int[source]

Main entry point for the bids-inject command.

Orchestrates loading of videos.tsv, discovery of *_scans.tsv files, per-scan matching, slicing, and injection.

Parameters:
  • paths (List[str]) – List of file or directory paths from the CLI PATHS argument.

  • videos_tsv (str) – Path to videos.tsv produced by video-audit. Video file paths inside the TSV are resolved relative to this file’s location.

  • recursive (bool) – When True, recurse into subdirectories when searching for *_scans.tsv files.

  • match (str) – Regular expression matched against the filename field of each ScanRecord. Only matching records are processed; others are skipped. Default '.*' matches every record.

  • buffer_before (str) – Extra video to include before scan onset. Accepts seconds (e.g. '10') or ISO 8601 duration (e.g. 'PT10S').

  • buffer_after (str) – Extra video to include after scan end. Accepts seconds (e.g. '10') or ISO 8601 duration (e.g. 'PT10S').

  • buffer_policy (str) – 'strict' to error when buffers exceed video boundaries; 'flexible' to trim them instead.

  • time_offset (float) – Clock offset in seconds added to acq_time values after timezone normalisation.

  • qr (str) – QR timing refinement mode: 'none', 'auto', 'embed-existing', or 'parse'.

  • layout (str) – Output placement layout: 'nearby' or 'top-stimuli'.

  • reprostim_timezone (str) – Timezone for naive ReproStim timestamps in videos.tsv. Use 'local' for the OS timezone or an IANA name (e.g. 'America/New_York').

  • bids_timezone (str) – Timezone for naive BIDS acq_time values. Use 'local' or an IANA name.

  • dry_run (bool) – When True, resolve matches and print planned actions but skip split-video and all file writes.

  • lock (bool) – When True (default), acquire the advisory file lock before reading videos.tsv. When False, skip the lock (dirty-read mode) — useful when the lock file is owned by a different OS user.

  • layout – Output file placement layout: 'nearby' places the output next to the NIfTI in the same BIDS datatype folder; 'top-stimuli' places it under a stimuli/ directory at the BIDS root, mirroring the subject/session/datatype hierarchy.

  • overwrite (str) – Policy for existing output files: 'skip' (default) skips the scan, 'force' removes existing file/symlink then re-injects, 'always' runs split-video as-is without an existence check, 'error' treats existing output as an error.

  • verbose (bool) – When True, emit verbose progress output.

  • out_func (Callable) – Callable used for user-facing output (e.g. click.echo).

Returns:

Exit code — 0 on success, non-zero on error.

Return type:

int

reprostim.qr.bids_inject.dt_bids_to_reprostim(dt: datetime, bids_tz: tzinfo, reprostim_tz: tzinfo) datetime[source]

Convert a naive BIDS datetime directly to a naive ReproStim datetime.

Routes through UTC internally: dt_utc_to_reprostim(dt_bids_to_utc(dt, bids_tz), reprostim_tz).

Parameters:
  • dt (datetime.datetime) – Naive BIDS acq_time datetime.

  • bids_tz (datetime.tzinfo) – BIDS dataset timezone.

  • reprostim_tz (datetime.tzinfo) – ReproStim capture machine timezone.

Returns:

Naive datetime in the ReproStim timezone.

Return type:

datetime.datetime

reprostim.qr.bids_inject.dt_bids_to_utc(dt: datetime, tz: tzinfo) datetime[source]

Convert a naive BIDS datetime to a naive UTC datetime.

Parameters:
  • dt (datetime.datetime) – Naive BIDS acq_time datetime.

  • tz (datetime.tzinfo) – Timezone assumed for the BIDS timestamps.

Returns:

Naive UTC datetime.

Return type:

datetime.datetime

reprostim.qr.bids_inject.dt_convert(dt: datetime, tz_from: tzinfo, tz_to: tzinfo) datetime[source]

Convert a naive datetime from one timezone context to another.

Attaches tz_from to dt, converts to tz_to, then strips tzinfo to return a naive datetime. This is the primitive that all higher-level helpers delegate to.

Parameters:
  • dt (datetime.datetime) – Naive source datetime (no tzinfo).

  • tz_from (datetime.tzinfo) – Timezone the naive dt implicitly represents.

  • tz_to (datetime.tzinfo) – Target timezone to convert into.

Returns:

Naive datetime in tz_to.

Return type:

datetime.datetime

reprostim.qr.bids_inject.dt_parse_bids(s: str) datetime[source]

Parse a BIDS acq_time ISO 8601 string to a naive datetime.

Any UTC offset present in s is stripped so the returned object has no tzinfo attached, matching the naive-datetime convention used throughout this module.

Parameters:

s (str) – ISO 8601 datetime string, e.g. '2025-08-14T15:06:09.742500'.

Returns:

Naive datetime (no tzinfo).

Return type:

datetime.datetime

Raises:

ValueError – If s cannot be parsed as an ISO 8601 datetime.

reprostim.qr.bids_inject.dt_parse_dicom_time(value: str) time[source]

Parse a DICOM TM (Time) string into a datetime.time object.

Accepts the DICOM TM format HHMMSS.FFFFFF where:

  • HH — hours, 00–23

  • MM — minutes, 00–59

  • SS — seconds, 00–60 (60 permitted for leap seconds; clamped to 59 for datetime.time compatibility)

  • .FFFFFF — optional fractional seconds (1–6 digits, right-padded with zeros to form microseconds)

Parameters:

value (str) – DICOM TM string, e.g. '151953.397500' or '151953'.

Returns:

Corresponding datetime.time instance.

Return type:

datetime.time

Raises:

ValueError – If value does not match the expected DICOM TM format.

reprostim.qr.bids_inject.dt_reprostim_to_bids(dt: datetime, reprostim_tz: tzinfo, bids_tz: tzinfo) datetime[source]

Convert a naive ReproStim datetime directly to a naive BIDS datetime.

Routes through UTC internally: dt_utc_to_bids(dt_reprostim_to_utc(dt, reprostim_tz), bids_tz).

Parameters:
  • dt (datetime.datetime) – Naive ReproStim datetime.

  • reprostim_tz (datetime.tzinfo) – ReproStim capture machine timezone.

  • bids_tz (datetime.tzinfo) – BIDS dataset timezone.

Returns:

Naive datetime in the BIDS timezone.

Return type:

datetime.datetime

reprostim.qr.bids_inject.dt_reprostim_to_utc(dt: datetime, tz: tzinfo) datetime[source]

Convert a naive ReproStim datetime to a naive UTC datetime.

Parameters:
  • dt (datetime.datetime) – Naive ReproStim datetime from videos.tsv.

  • tz (datetime.tzinfo) – Timezone the ReproStim capture machine was using.

Returns:

Naive UTC datetime.

Return type:

datetime.datetime

reprostim.qr.bids_inject.dt_resolve_tz(name: str) tzinfo[source]

Resolve a timezone name string to a datetime.tzinfo object.

Results are cached via functools.lru_cache(), so repeated calls with the same name are free after the first resolution.

Parameters:

name (str) – 'local' to use the OS system timezone, or any IANA timezone name (e.g. 'America/New_York', 'UTC').

Returns:

Corresponding tzinfo instance.

Return type:

datetime.tzinfo

Raises:

ZoneInfoNotFoundError – If name is not a recognised IANA timezone.

reprostim.qr.bids_inject.dt_time_to_sec(t: time) float[source]

Convert a datetime.time to total seconds since midnight.

Parameters:

t (datetime.time) – Time value to convert.

Returns:

Total seconds since midnight, including microseconds as a fractional part.

Return type:

float

reprostim.qr.bids_inject.dt_tz_label(name: str) str[source]

Return the current UTC offset for a timezone name, e.g. UTC-05:00.

Resolves the timezone name via dt_resolve_tz() and formats the UTC offset as "UTC±HH:MM", e.g. "UTC-05:00" or "UTC+00:00".

Parameters:

name (str) – Timezone name accepted by dt_resolve_tz().

Returns:

UTC offset string.

Return type:

str

reprostim.qr.bids_inject.dt_utc_to_bids(dt: datetime, tz: tzinfo) datetime[source]

Convert a naive UTC datetime to a naive BIDS-domain datetime.

Parameters:
  • dt (datetime.datetime) – Naive UTC datetime.

  • tz (datetime.tzinfo) – Target timezone (BIDS dataset timezone).

Returns:

Naive datetime in tz.

Return type:

datetime.datetime

reprostim.qr.bids_inject.dt_utc_to_reprostim(dt: datetime, tz: tzinfo) datetime[source]

Convert a naive UTC datetime to a naive ReproStim-domain datetime.

Parameters:
  • dt (datetime.datetime) – Naive UTC datetime.

  • tz (datetime.tzinfo) – Target timezone (ReproStim capture machine timezone).

Returns:

Naive datetime in tz.

Return type:

datetime.datetime