Developing Modules for VentOCR
VentOCR supports a secondary analysis layer called Modules. A Module is a Python script that reads archived waveform data (Frames.csv / Breaths.csv) for a completed analysis and writes additional computed fields back to those files. Any developer familiar with pandas can create a Module without touching the core platform.
1. What a Module Does
After VentOCR finishes primary OCR analysis, the results are stored in the Archive as CSV files. A Module is called with a single analyze_id and user_id, reads those CSVs, computes new values, and writes them back.
Archive/
users/
{user_id}/
Archive/
AID_{analyze_id}/
Frames.csv ← frame-level raw data
Breaths.csv ← breath-level aggregated data ← Module writes here
extra_fields.txt ← column registry ← Module appends here
2. Execution Flow
User selects Analyze targets + Module(s) on the Modules page
↓
POST /run_modules_batch → tasks inserted into Cue table (task_type = "module")
↓
queue_worker picks up the task
↓
Validates Engine–Module mapping (Module_Engine_Map)
↓
Loads Module_Path / Entry_Point from Modules table
↓
Spawns a subprocess running the entry point
↓
main(analyze_id, user_id) is called
↓
Return value (string) logged to Analysis_Module_Run
↓
Result shown on the Modules page
Each module is executed in its own subprocess with the current working directory set to the /app root (same as the Flask process).
3. Directory Structure
Place your module under the Module/ directory, which is volume-mounted into the container at /app/Module/.
Module/
<Module_Key>/
core.py ← required: entry point containing main()
README.md ← recommended: shown in the Module Selection UI
Example:
Module/
MyAnalysis_VentOCR/
core.py
README.md
The directory name does not need to match Module_Key, but using the same name is strongly recommended for clarity.
4. Required Interface — main()
The entry point file must define a top-level function named main with the following signature:
def main(analyze_id: int, user_id: str) -> str:
...
return "Success message or error description"
| Parameter | Type | Description |
|---|---|---|
| analyze_id | int | The Analyze_ID of the target record. Use this to locate the archive directory. |
| user_id | str | The user ID of the owner. Use this together with Global_Setting to resolve paths. |
| return value | str | A plain text result message. Stored in Analysis_Module_Run.Result_Summary. Any exception will be caught and logged as an error. |
5. Locating Archive Files
Use Global_Setting to resolve archive paths consistently across environments (Docker / local):
import os
import Global_Setting as GS
def main(analyze_id: int, user_id: str) -> str:
aid_dir = os.path.join(GS.Dir_Archive(user_id), f"AID_{analyze_id}")
frames_path = os.path.join(aid_dir, "Frames.csv")
breaths_path = os.path.join(aid_dir, "Breaths.csv")
extra_fields_path = os.path.join(aid_dir, "extra_fields.txt")
Key Files
| File | Description | Typical Columns |
|---|---|---|
| Frames.csv | Frame-level OCR results. One row per frame. | Frame, BreathNumber, Pressure, Flow, Volume, EdiPeak, ... |
| Breaths.csv | Breath-level aggregated results. One row per breath. Write your output here. | BreathNumber, Start_Frame, End_Frame, ... |
| extra_fields.txt | JSON registry of additional columns. Append your column here so the UI can display it. | — (JSON) |
6. Registering Output Columns via extra_fields.txt
After writing new columns to Breaths.csv, register them in extra_fields.txt so the VentOCR UI can detect and display them.
File Structure
{
"secondary_fields": [
["ModuleDisplayName", "ColumnName"]
],
"custom_fields": []
}
Example Code
import json, os
def _register_extra_field(extra_fields_path, module_label, column_name):
extra_fields = {"secondary_fields": [], "custom_fields": []}
if os.path.exists(extra_fields_path):
try:
with open(extra_fields_path, "r", encoding="utf-8") as f:
extra_fields = json.load(f)
except Exception:
pass
pair = [module_label, column_name]
if pair not in extra_fields.get("secondary_fields", []):
extra_fields.setdefault("secondary_fields", []).append(pair)
with open(extra_fields_path, "w", encoding="utf-8") as f:
json.dump(extra_fields, f, indent=2, ensure_ascii=False)
7. Database Registration
After placing the files, register the module in two database tables.
Modules table
INSERT OR IGNORE INTO Modules (
Module_Key,
Display_Name,
Description,
Entry_Point,
Module_Path,
Version,
Developer_Name,
Is_Active,
Sort_Order
) VALUES (
'MyAnalysis_VentOCR', -- unique key (used in URLs and mapping)
'My Analysis', -- short display name shown in the UI
'One-line description.', -- shown as fallback if README has no Overview
'core.py', -- entry point filename within Module_Path
'Module/MyAnalysis_VentOCR', -- path relative to /app (the working directory)
'1.0.0',
'Your Name',
1, -- 1 = active
100 -- display order (lower = shown first)
);
Module_Engine_Map table
A module must be explicitly mapped to one or more engines. Only mapped modules appear in the Module Selection UI for a given study.
INSERT OR IGNORE INTO Module_Engine_Map (Module_Key, Engine_Key, Is_Active)
VALUES ('MyAnalysis_VentOCR', 'nava_ventocr', 1);
| Column | Required | Notes |
|---|---|---|
| Module_Key | required | Must be unique. Alphanumeric and underscores recommended. |
| Display_Name | required | Short label shown in the Module card. |
| Description | optional | Fallback text if README.md has no ## Overview section. |
| Entry_Point | optional | Defaults to core.py. |
| Module_Path | required | Relative path from /app to the module directory. |
| Is_Active | optional | Set to 0 to hide the module without deleting the record. |
8. README.md Recommendations
A README is not required for execution, but it is strongly recommended. The ## Overview section is extracted and shown as a one-line description in the Module Selection UI. The full README is accessible via the README button on each module card.
# MyAnalysis_VentOCR Module ## Overview One-sentence summary of what this module computes. (Keep this to a single line.) --- ## Purpose Explain the clinical or analytical motivation. --- ## Input Files - Frames.csv — columns you read - Breaths.csv — columns you read --- ## Output ### Breaths.csv Adds: - MyColumn ### extra_fields.txt Adds: ["MyAnalysis", "MyColumn"] under "secondary_fields" --- ## Dependencies - pandas - Global_Setting --- ## Author Your Name Your Institution
9. Minimal Module Template
import os
import json
import pandas as pd
import Global_Setting as GS
def main(analyze_id: int, user_id: str) -> str:
aid_dir = os.path.join(GS.Dir_Archive(user_id), f"AID_{analyze_id}")
breaths_path = os.path.join(aid_dir, "Breaths.csv")
extra_fields_path = os.path.join(aid_dir, "extra_fields.txt")
if not os.path.exists(breaths_path):
return f"Breaths.csv not found for AID_{analyze_id}"
df = pd.read_csv(breaths_path)
# --- Compute your new column here ---
df["MyColumn"] = 0.0
df.to_csv(breaths_path, index=False)
# --- Register the column in extra_fields.txt ---
extra_fields = {"secondary_fields": [], "custom_fields": []}
if os.path.exists(extra_fields_path):
try:
with open(extra_fields_path, "r", encoding="utf-8") as f:
extra_fields = json.load(f)
except Exception:
pass
pair = ["MyAnalysis", "MyColumn"]
if pair not in extra_fields.get("secondary_fields", []):
extra_fields.setdefault("secondary_fields", []).append(pair)
with open(extra_fields_path, "w", encoding="utf-8") as f:
json.dump(extra_fields, f, indent=2, ensure_ascii=False)
return f"MyColumn AID_{analyze_id} Success"
10. Tips for Module Developers
- Use Global_Setting.Dir_Archive(user_id) for all path resolution — never hard-code absolute paths.
- Always check that input files exist before reading them, and return an informative string if they are missing.
- Do not raise unhandled exceptions; return an error string instead so the failure is logged properly.
- Use df.to_csv(path, index=False) to overwrite the CSV and preserve all existing columns.
- Register every new column in extra_fields.txt even if the column already exists in the CSV — idempotent registration is safe.
- If your module depends on another module's output (e.g. a column written by EdiCorrection), document that dependency in your README.
- Set Is_Active = 0 in the Modules table to disable a module without deleting its records.