Research data infrastructure for a multi-camera operating room: turns raw proprietary camera output into synchronized, redacted, annotated video that behavioural researchers can actually analyse.
Eight cameras record every case in the OR. They start at slightly different moments, write a proprietary NorPix .seq format, and produce footage that cannot be shared until the patient monitors in frame are redacted. Researchers then annotate behaviour in BORIS against those videos — and the annotations are only meaningful if every frame index still points at the same instant it did when the coder made the call.
ScalpelLab is the tooling that holds that chain together: ingestion, a SQLite catalog of what exists, format conversion, timeline reconciliation, privacy redaction, and a synchronized review tool.
| Scale | ~18.9k lines of Python across 56 modules — dashboard, pipeline, and review tool |
| Cameras | 8 per case, each on its own clock, reconciled to one synchronization group |
| Catalog | SQLite: recordings, SEQ/MP4 status, parsed SEQ headers, BORIS events, monitor vitals |
| Privacy | The database is git-crypt encrypted in this public repo; redaction is batch-driven from catalog timing tables |
| Validation | 19 tests pass on a clean clone; CI runs the suite, a compileall sweep, and a config import on Windows |
Recording surgery is the easy part. The problems come after:
- Eight cameras, eight clocks. Each starts when its operator hits record. "Frame 900" means a different moment on every camera unless the whole case is reconciled to one origin.
- The source format is proprietary and fragile. NorPix
.seqfiles need a companion.seq.idxto be seekable. Those companions go missing or get truncated, and a.seqwithout a valid index cannot be converted or trusted. - Redaction is mandatory and mechanical. Patient and ventilator monitors are in frame. Every case needs the same regions blacked out over the same time ranges — hundreds of files, driven from data, not by hand.
- Re-encoding invalidates existing research. Behavioural annotations were coded in BORIS against the original exports. Producing new synchronized MP4s moves every frame index. Without a remap, thousands of hours of coding silently stop pointing at the right moments.
This is the constraint the rest of the system is built around.
flowchart TD
SEQ["NorPix .seq<br/>+ .seq.idx<br/>(per-frame timestamps)"]
OLD["OLD MP4<br/>what BORIS was coded against"]
NEW["NEW MP4<br/>synchronized, 30 fps"]
B1["BORIS events<br/>OLD timeline"]
B2["BORIS events<br/>NEW timeline"]
SEQ <-->|"identity: i_old == K_seq<br/>no resample, no drop"| OLD
SEQ -->|"pre-roll + timestamp scaling"| NEW
B1 -->|"boris_remap_to_new_mp4.py"| B2
OLD -.->|"annotations coded here"| B1
B2 -.->|"analysed against"| NEW
style B2 fill:#1f3a4a,stroke:#38a,color:#fff
style NEW fill:#1f3a4a,stroke:#38a,color:#fff
Two facts make the remap tractable, and both had to be established rather than assumed:
-
OLD MP4 ↔ SEQ is the identity. The original export preserved SEQ frames one-to-one — no resampling, no duplication, no dropped frames, no FPS conversion. So an annotation at OLD frame
irefers to SEQ recordi. -
SEQ → NEW MP4 is a timestamp projection, not a frame copy. Each camera's frames are placed on a common 30 fps grid using the per-frame timestamps in its
.seq.idx, offset by a pre-roll that aligns it to the earliest start in its synchronization group:new_pre_roll_frames = round((first_frame_time - group_t_global_start) * 30) new_frame = new_pre_roll_frames + round((idx_timestamp[i_old] - idx_timestamp[0]) * 30)
The pre-roll belongs only to the SEQ → NEW stage; applying it to the OLD ↔ SEQ mapping would shift every annotation by the camera's start offset. scripts/helpers/boris_remap_to_new_mp4.py writes remapped *_new_mp4.csv files alongside each source export rather than overwriting them, so the original coding stays intact and the remap stays auditable. Cases where the rules do not hold are enumerated as explicit exceptions in docs/new recordings formula.md instead of being quietly mapped anyway.
Deciding what is even convertible. A SEQ file is only safe to convert if its IDX exists, its header parses, its frame timestamps are sane, and it is large enough to be a real recording. Rather than scatter those checks through the converter, they are encoded once as a database view (cur_sync_status.is_syncable) that the conversion planner reads. The gate and the dashboard therefore cannot disagree.
Repairing proprietary index files. repair_seq_idx.py audits .seq.idx companions and rebuilds them from the SEQ body when they are missing or truncated. It checkpoints per file, so an interrupted multi-hour pass over a thousand recordings resumes instead of restarting.
Redaction driven by data, not by hand. batch_black_squere.py reads case timing ranges from mp4_times and applies the same masking across every affected recording, tracking what has been processed so a rerun is incremental.
Reviewing eight angles at once. MPV_Multiviewer/ drives N libmpv players over IPC from a Tkinter UI, scrubs them together, and writes per-camera offset corrections back to the catalog — so a sync correction found during review becomes data the pipeline uses, not a note in someone's file.
Publishing research tooling without publishing research data. The catalog is real: it names cases, dates and cameras. It is committed as a git-crypt encrypted blob, so the schema, the queries and the entire application are public and reviewable while the contents are not. Generated pipeline checkpoints, which embed absolute paths into the data tree, are gitignored for the same reason.
flowchart LR
subgraph ingest["Processing pipeline"]
direction TB
P1["1 · SEQ curation<br/>organize, hash-verify,<br/>flag undersized"]
P2["2 · Update DB + IDX<br/>scan, enrich headers,<br/>ffprobe durations"]
P3["3 · SEQ → MP4<br/>GPU-first, sync group<br/>alignment"]
P1 --> P2 --> P3
end
DB[("SQLite catalog<br/>git-crypt encrypted")]
subgraph review["Review & analysis"]
direction TB
UI["NiceGUI dashboard<br/>12 pages"]
MPV["MPV Multiviewer<br/>N-camera sync"]
RED["Batch redaction"]
end
BORIS["BORIS exports<br/>+ monitor vitals"]
ingest <--> DB
DB <--> review
BORIS -->|"import + remap"| DB
style DB fill:#1f3a4a,stroke:#38a,color:#fff
The catalog is the coordination point: the pipeline writes status into it, the dashboard and review tools read from it, and the redaction and conversion planners take their inputs from views over it rather than from the filesystem.
Python 3.11 · SQLite (+ git-crypt) · NiceGUI + pywebview (desktop dashboard) · Plotly · pandas / NumPy · Tkinter + libmpv (review tool) · FFmpeg / ffprobe with NVENC · NorPix SequenceViewer · PyMuPDF · pytest · GitHub Actions
app/ NiceGUI desktop dashboard (~4.7k lines, 12 pages)
scripts/ the pipeline (~11.2k lines)
1_seq_curation.py organize + verify raw SEQ exports
2_update_db.py scan trees, enrich SEQ metadata, reconcile status
3_seq_to_mp4_convert.py GPU-first conversion with sync-group alignment
helpers/ IDX repair, BORIS import + remap, redaction, comparison
MPV_Multiviewer/ Tkinter + libmpv synchronized review tool (~1.9k lines)
docs/ SEQ/IDX format references, schema notes, frame-mapping spec
tests/ 7 files - unit, smoke, and an opt-in end-to-end pipeline test
config.py all paths in one place; `python config.py` validates them
pip install -r requirements.txt
python -m pytest tests/ -q # 19 passed, 1 skipped on a clean cloneThe end-to-end pipeline test drives the three real scripts as subprocesses and is opt-in: it skips unless SCALPELLAB_TEST_SAMPLE_DIR points at a small SEQ sample and ffmpeg / mkvmerge / NorPix are installed. See tests/README.md for setting one up.
Not covered by tests: anything needing real recordings or the vendor tools — actual SEQ→MP4 conversion, IDX creation via NorPix, redaction output, and the MPV review tool. Those are exercised by operating the system.
- Windows-oriented. Paths, drive letters and the NorPix/mpv integrations assume Windows.
- The database is encrypted and the key is not public. You can read the schema, the queries and every line of application code, but you cannot run the dashboard against real data without the key and the recordings.
- No schema migrations. Changes are applied directly against the live SQLite file; back up first, then re-export the diagram with
scripts/helpers/sqlite_to_dbdiagram.py. - Vendor tools are not bundled. FFmpeg, mpv and NorPix SequenceViewer must be installed separately.
- Single-operator tooling. There is no multi-user access control; it assumes one researcher on one workstation.
- A migration framework, so schema changes stop being a manual
sqlite3session against live research data. - A small synthetic SEQ fixture generator, which would let the end-to-end pipeline test run in CI instead of only on a machine with real recordings.
- Making the sync-group alignment reproducible from the catalog alone, so a conversion can be re-derived years later without the original working directory.
pip install -r requirements.txtEdit config.py and set:
SEQ_ROOTto your organized SEQ rootMP4_ROOTto your MP4 recordings rootANALYSES_ROOTto your finalized per-case analysis root (Analyses\Case_Analyses_synced)NORPIX_SEQUENCE_VIEWER_PATHto the NorPix SequenceViewer executable used to create.seq.idxfiles
Expected layout:
Sequence_Backup/ Recordings/
└── DATA_YY-MM-DD/ └── DATA_YY-MM-DD/
└── CaseN/ └── CaseN/
└── CameraName/ └── CameraName/
├── *.seq └── *.mp4
└── *.seq.idx
Analyses/Case_Analyses_synced/ holds the finalized, per-case analysis
artifacts (behavioral labels and monitor vitals) whose BORIS events have been
remapped onto the NEW MP4 timeline. Older, un-remapped analysis exports live
under Analyses/un_synced/.
Analyses/
├── Case_Analyses_synced/
│ └── DATA_YY-MM-DD/
│ └── CaseN/
│ ├── Boris/
│ │ ├── <date>-caseN_raw.csv # raw BORIS export (OLD MP4 timeline)
│ │ ├── <date>-caseN_standardized.csv # cleaned/standardized events
│ │ └── <date>-caseN_*_new_mp4.csv # events remapped to the NEW MP4 timeline
│ └── Monitor/
│ └── motior_data.csv # per-case monitor vitals time series
└── un_synced/ # older analysis exports (not remapped)
├── Analyses_1/
├── Analyses_2/
└── Analyses_2026-03-18/
File types:
*_raw.csv— direct BORIS observation export;Time/Image indexare on the OLD per-camera MP4 timeline (one frame per SEQ frame).*_standardized.csv— the same events after cleanup/normalization, used for downstream analysis.*_new_mp4.csv— produced byscripts/helpers/boris_remap_to_new_mp4.py; adds NEW-MP4 frame/time columns viadocs/new recordings formula.md.motior_data.csv— exported patient-monitor vital signs for the case (imported byscripts/helpers/import_analysis_finale.py).
python config.pypython run_app.pyrun_app.py starts the NiceGUI app via python -m app.app, which opens a
native pywebview window. The Home page Configuration panel can also edit and
persist the DB, SEQ root, MP4 root, and NorPix SequenceViewer paths.
The dashboard lives under app/ and currently includes:
app/app.py: entry point, Home dashboard, DB selector, and the Configuration panel for editing/persisting paths.app/pages/database.py: browse tables, inspect schema, insert rows, delete rows, and view the ERD fromdocs/ERD.pdf.app/pages/anesthesiology.py: roster and seniority view overcur_seniority+recording_details.app/pages/mp4.py: MP4 coverage and statistics overmp4_statusand thecur_mp4_*views.app/pages/seq.py: SEQ inventory plusseq_enrichedtime-drift analysis.app/pages/boris.py: BORIS event START/STOP pairing and interval analysis.- Launcher pages for the processing pipeline:
nuk_export.py(SEQ Curation),update_db.py(Update DB + IDX), andseq_to_mp4.py(SEQ → MP4).
The sidebar is organized into Dashboards & Monitoring for read/inspection pages and Processing Pipeline for operational actions.
ScalpelDatabase.sqliteis the local working database expected by default in the project root.- There is no migration framework. Schema changes are applied directly with
sqlite3against the live database — back up first, then re-export the schema withscripts/helpers/sqlite_to_dbdiagram.py. docs/scalpel_dbdiagram.txtis a dbdiagram.io schema export.docs/project_context/contains deeper notes on the SQLite schema and NorPix SEQ / IDX formats.
scripts/1_seq_curation.py: curate raw SEQ exports intoDATA_YY-MM-DD/CaseN/CameraName, copy companion files, verify hashes, and flag undersized files as junk.scripts/2_update_db.py: scan SEQ and MP4 trees, updateseq_statusandmp4_status, optionally calculate durations withffprobe, enrich SEQ metadata, and preserve unmanaged DB columns.scripts/3_seq_to_mp4_convert.py: convert missing SEQ recordings to MP4, with GPU-first workflow and fallback behavior.scripts/helpers/import_boris_tags.py: import BORIS TSV exports intoboris_eventsand maintain BORIS-derived views.scripts/helpers/batch_black_squere.py: batch-redact videos from database timing data inmp4_times.
Processing Pipeline order:
- SEQ Curation.
- Update DB + Create IDX files.
- Analyze SEQ Fields.
- SEQ to MP4, followed by a DB refresh so MP4 status reflects the latest files.
IDX creation uses NorPix SequenceViewer, configured by
config.NORPIX_SEQUENCE_VIEWER_PATH and editable from the Home page. The
existing repair_seq_idx.py helper remains an audit/repair path, not the
primary creation flow.
Pipeline logs are written under logs/, grouped by step:
logs/
├── seq_curation/
├── db_update/
├── idx_creation/
├── seq_analysis/
└── seq_to_mp4/
scripts/helpers/analyze_seq_fields.py: optional SEQ field inspection used by the DB updater.scripts/helpers/repair_seq_idx.py: audit and rebuild NorPix.seq.idxfiles from SEQ bodies, with checkpointing indocs/seq_idx_repair_tracking.json(gitignored; created on first run).scripts/helpers/backup_dir.py: copy files while preserving source structure.scripts/helpers/cut_video.py: cut video segments with FFmpeg stream copy.scripts/helpers/boris_remap_to_new_mp4.py: remap BORIS event frame/time from the OLD MP4 to the NEW MP4 timeline, writing*_new_mp4.csvnext to each source CSV (single--csvor--all).scripts/helpers/sqlite_to_dbdiagram.py: export the SQLite schema to dbdiagram.io format.scripts/helpers/compare/compare_databases.py: compare two SQLite databases.scripts/helpers/compare/compare_mp4.py: compare MP4 backups.scripts/helpers/compare/compare_seq.py: compare SEQ backups.
MPV_Multiviewer/ contains a Tkinter + MPV tool for loading
multiple camera angles, synchronizing playback offsets, and saving sync
corrections back to the database.
python MPV_Multiviewer/run_viewer.pySee MPV_Multiviewer/docs/user-guide.md
for usage notes.
python config.py
python run_app.py
python scripts/1_seq_curation.py
python scripts/2_update_db.py --dry-run
python scripts/2_update_db.py --skip-duration
python scripts/3_seq_to_mp4_convert.py
python scripts/helpers/import_boris_tags.py --dry-run
python scripts/helpers/batch_black_squere.py
python scripts/helpers/repair_seq_idx.py --dry-run
python scripts/helpers/cut_video.py
python scripts/helpers/sqlite_to_dbdiagram.py
python MPV_Multiviewer/run_viewer.pyrecording_details: case-level recording metadata.analysis_information: labeling metadata and optional BORIS event linkage.anesthesiology: anesthesiology roster and career dates.seq_status: SEQ presence, size, and path.seq_enriched: parsed SEQ header and IDX metadata cache.mp4_status: MP4 presence, size, duration, redaction metadata, sync offset, and path.mp4_times: case timing ranges used by redaction workflows.
Importer-created optional tables:
boris_events: imported BORIS behavioral event rows.monitor_samples: imported per-sample monitor vitals.monitor_case_summary: per-case monitor import summary.
cur_mp4_missing: cases where SEQ exists but MP4 is missing.cur_sync_status: per-camerais_syncableflag derived fromseq_enriched, encoding the3_seq_to_mp4_convert.pyplanning gates (IDX present, header ok, valid frame timestamps, sane duration, ≥ 50 MB SEQ).cur_seniority: anesthesiology experience / status summary.cur_mp4_status_statistics: aggregated recording statistics used by the MP4 dashboard.
Default camera names from config.py:
Cart_Center_2Cart_LT_4Cart_RT_1General_3MonitorPatient_MonitorVentilator_MonitorInjection_Port
Some functionality depends on tools outside Python:
ffmpegandffprobefor conversion, probing, redaction, and cutting.- NVIDIA NVENC for GPU-accelerated video workflows where available.
- NorPix SequenceViewer at
C:\Program Files\Common Files\NorPix\SequenceViewer.exeby default for opening.seqfiles and creating.seq.idxcompanions. - CLExport as a fallback SEQ export path in some legacy workflows.
mpv.exefor synchronized multi-video playback inMPV_Multiviewer.
- The repo is Windows-oriented; paths and examples assume Windows drive letters.
- The database file defaults to
ScalpelDatabase.sqlitein the project root. scripts/2_update_db.pyis designed to preserve columns it does not manage.- The NiceGUI app can point at different configured paths through the Home page
Configuration panel or the relevant
SCALPEL_*environment variables.