cleanup what I just did

This commit is contained in:
tyler committed 2026-06-28 09:09:06 -07:00
1 parent 617daa0e9b
commit f10eb64332
16 files changed
+77 -64

No files matched your search

+849
View File
@@ -0,0 +1,849 @@
import os
import json
from enum import Enum, auto
from datetime import datetime
import numpy as np
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, QHBoxLayout, QMessageBox, QFileDialog
from PySide6.QtCore import Qt
from mne import Annotations
from mne.io import read_raw_snirf
from mne_nirs.io import write_raw_snirf
from src.shared.shareddata import APP_NAME
class EventUpdateMode(Enum):
WRITE_SNIRF = auto() # destructive
WRITE_JSON = auto() # non-destructive
class UpdateEventsWindow(QWidget):
def __init__(self, parent=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
super().__init__(parent, Qt.WindowType.Window)
self.mode = mode
self.caller = caller or self.__class__.__name__
self.setWindowTitle("Update event markers")
self.resize(760, 200)
print("INIT MODE:", mode)
self.label_file_a = QLabel("SNIRF file:")
self.line_edit_file_a = QLineEdit()
self.line_edit_file_a.setReadOnly(True)
self.btn_browse_a = QPushButton("Browse .snirf")
self.btn_browse_a.clicked.connect(self.browse_file_a)
self.label_file_b = QLabel("BORIS file:")
self.line_edit_file_b = QLineEdit()
self.line_edit_file_b.setReadOnly(True)
self.btn_browse_b = QPushButton("Browse .boris")
self.btn_browse_b.clicked.connect(self.browse_file_b)
self.label_suffix = QLabel("Filename in BORIS project file:")
self.combo_suffix = QComboBox()
self.combo_suffix.setEditable(False)
self.combo_suffix.currentIndexChanged.connect(self.on_observation_selected)
self.label_events = QLabel("Events in selected observation:")
self.combo_events = QComboBox()
self.combo_events.setEnabled(False)
self.label_snirf_events = QLabel("Events in SNIRF file:")
self.combo_snirf_events = QComboBox()
self.combo_snirf_events.setEnabled(False)
self.btn_clear = QPushButton("Clear")
self.btn_go = QPushButton("Go")
self.btn_clear.clicked.connect(self.clear_files)
self.btn_go.clicked.connect(self.go_action)
# ---
layout = QVBoxLayout()
self.description = QLabel()
self.description.setTextFormat(Qt.TextFormat.RichText)
self.description.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.description.setOpenExternalLinks(True)
self.description.setText("The events that are present in a snirf file may not be the events that are to be studied and examined.<br>"
"Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.<br>"
"The software <a href='https://www.boris.unito.it/'>BORIS</a> is used to add these events to video files, and these events can be applied to the snirf file <br>"
"selected below by selecting the correct BORIS observation and time syncing it to an event that it shares with the snirf file.")
layout.addWidget(self.description)
help_text_a = "Select the SNIRF (.snirf) file to update with new event markers."
file_a_layout = QHBoxLayout()
# Help button on the left
help_btn_a = QPushButton("?")
help_btn_a.setFixedWidth(25)
help_btn_a.setToolTip(help_text_a)
help_btn_a.clicked.connect(lambda _, text=help_text_a: self.show_help_popup(text))
file_a_layout.addWidget(help_btn_a)
# Container for label + line_edit + browse button with tooltip
file_a_container = QWidget()
file_a_container_layout = QHBoxLayout()
file_a_container_layout.setContentsMargins(0, 0, 0, 0)
file_a_container_layout.addWidget(self.label_file_a)
file_a_container_layout.addWidget(self.line_edit_file_a)
file_a_container_layout.addWidget(self.btn_browse_a)
file_a_container.setLayout(file_a_container_layout)
file_a_container.setToolTip(help_text_a)
file_a_layout.addWidget(file_a_container)
layout.addLayout(file_a_layout)
help_text_b = "Provide a .boris project file that contains events for this participant."
file_b_layout = QHBoxLayout()
help_btn_b = QPushButton("?")
help_btn_b.setFixedWidth(25)
help_btn_b.setToolTip(help_text_b)
help_btn_b.clicked.connect(lambda _, text=help_text_b: self.show_help_popup(text))
file_b_layout.addWidget(help_btn_b)
file_b_container = QWidget()
file_b_container_layout = QHBoxLayout()
file_b_container_layout.setContentsMargins(0, 0, 0, 0)
file_b_container_layout.addWidget(self.label_file_b)
file_b_container_layout.addWidget(self.line_edit_file_b)
file_b_container_layout.addWidget(self.btn_browse_b)
file_b_container.setLayout(file_b_container_layout)
file_b_container.setToolTip(help_text_b)
file_b_layout.addWidget(file_b_container)
layout.addLayout(file_b_layout)
help_text_suffix = "This participant from the .boris project file matches the .snirf file."
suffix_layout = QHBoxLayout()
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
suffix_layout.addWidget(help_btn_suffix)
suffix_container = QWidget()
suffix_container_layout = QHBoxLayout()
suffix_container_layout.setContentsMargins(0, 0, 0, 0)
suffix_container_layout.addWidget(self.label_suffix)
suffix_container_layout.addWidget(self.combo_suffix)
suffix_container.setLayout(suffix_container_layout)
suffix_container.setToolTip(help_text_suffix)
suffix_layout.addWidget(suffix_container)
layout.addLayout(suffix_layout)
help_text_suffix = "The events extracted from the BORIS project file for the selected observation."
suffix2_layout = QHBoxLayout()
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
suffix2_layout.addWidget(help_btn_suffix)
suffix2_container = QWidget()
suffix2_container_layout = QHBoxLayout()
suffix2_container_layout.setContentsMargins(0, 0, 0, 0)
suffix2_container_layout.addWidget(self.label_events)
suffix2_container_layout.addWidget(self.combo_events)
suffix2_container.setLayout(suffix2_container_layout)
suffix2_container.setToolTip(help_text_suffix)
suffix2_layout.addWidget(suffix2_container)
layout.addLayout(suffix2_layout)
snirf_events_layout = QHBoxLayout()
help_text_snirf_events = "The event markers extracted from the SNIRF file."
help_btn_snirf_events = QPushButton("?")
help_btn_snirf_events.setFixedWidth(25)
help_btn_snirf_events.setToolTip(help_text_snirf_events)
help_btn_snirf_events.clicked.connect(lambda _, text=help_text_snirf_events: self.show_help_popup(text))
snirf_events_layout.addWidget(help_btn_snirf_events)
snirf_events_container = QWidget()
snirf_events_container_layout = QHBoxLayout()
snirf_events_container_layout.setContentsMargins(0, 0, 0, 0)
snirf_events_container_layout.addWidget(self.label_snirf_events)
snirf_events_container_layout.addWidget(self.combo_snirf_events)
snirf_events_container.setLayout(snirf_events_container_layout)
snirf_events_container.setToolTip(help_text_snirf_events)
snirf_events_layout.addWidget(snirf_events_container)
layout.addLayout(snirf_events_layout)
buttons_layout = QHBoxLayout()
buttons_layout.addStretch()
buttons_layout.addWidget(self.btn_clear)
buttons_layout.addWidget(self.btn_go)
layout.addLayout(buttons_layout)
self.setLayout(layout)
def show_help_popup(self, text):
msg = QMessageBox(self)
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
msg.setText(text)
msg.exec()
def browse_file_a(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select SNIRF File", "", "SNIRF Files (*.snirf)")
if file_path:
self.line_edit_file_a.setText(file_path)
try:
# TODO: Bad! read_raw_snirf doesnt release memory properly! Should be spawned in a seperate process and killed once completed
raw = read_raw_snirf(file_path, preload=False)
annotations = raw.annotations
# Build individual event entries
event_entries = []
for onset, description in zip(annotations.onset, annotations.description):
event_str = f"{description} @ {onset:.3f}s"
event_entries.append(event_str)
if not event_entries:
QMessageBox.information(self, "No Events", "No events found in SNIRF file.")
self.combo_snirf_events.clear()
self.combo_snirf_events.setEnabled(False)
return
self.combo_snirf_events.clear()
self.combo_snirf_events.addItems(event_entries)
self.combo_snirf_events.setEnabled(True)
except Exception as e:
QMessageBox.warning(self, "Error", f"Could not read SNIRF file with MNE:\n{str(e)}")
self.combo_snirf_events.clear()
self.combo_snirf_events.setEnabled(False)
def browse_file_b(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select BORIS File", "", "BORIS project Files (*.boris)")
if file_path:
self.line_edit_file_b.setText(file_path)
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
self.boris_data = data
observation_keys = self.extract_boris_observation_keys(data)
self.combo_suffix.clear()
self.combo_suffix.addItems(observation_keys)
except (json.JSONDecodeError, FileNotFoundError, KeyError) as e:
QMessageBox.warning(self, "Error", f"Failed to parse BORIS file:\n{e}")
def extract_boris_observation_keys(self, data):
if "observations" not in data:
raise KeyError("Missing 'observations' key in BORIS file.")
observations = data["observations"]
if not isinstance(observations, dict):
raise TypeError("'observations' must be a dictionary.")
return list(observations.keys())
def on_observation_selected(self):
selected_obs = self.combo_suffix.currentText()
if not selected_obs or not hasattr(self, 'boris_data'):
self.combo_events.clear()
self.combo_events.setEnabled(False)
return
try:
events = self.boris_data["observations"][selected_obs]["events"]
except (KeyError, TypeError):
self.combo_events.clear()
self.combo_events.setEnabled(False)
return
event_entries = []
for event in events:
if isinstance(event, list) and len(event) >= 3:
timestamp = event[0]
label = event[2]
display = f"{label} @ {timestamp:.3f}"
event_entries.append(display)
self.combo_events.clear()
self.combo_events.addItems(event_entries)
self.combo_events.setEnabled(bool(event_entries))
def clear_files(self):
self.line_edit_file_a.clear()
self.line_edit_file_b.clear()
def go_action(self):
file_a = self.line_edit_file_a.text()
suffix = "flare"
if not hasattr(self, "boris_data") or self.combo_events.count() == 0 or self.combo_snirf_events.count() == 0:
QMessageBox.warning(self, "Missing data", "Please make sure a BORIS and SNIRF event are selected.")
return
# Extract BORIS anchor
try:
boris_label, boris_time_str = self.combo_events.currentText().split(" @ ")
boris_anchor_time = float(boris_time_str.replace("s", "").strip())
except Exception as e:
QMessageBox.critical(self, "BORIS Event Error", f"Could not parse BORIS anchor event:\n{e}")
return
# Extract SNIRF anchor
try:
snirf_label, snirf_time_str = self.combo_snirf_events.currentText().split(" @ ")
snirf_anchor_time = float(snirf_time_str.replace("s", "").strip())
except Exception as e:
QMessageBox.critical(self, "SNIRF Event Error", f"Could not parse SNIRF anchor event:\n{e}")
return
time_shift = snirf_anchor_time - boris_anchor_time
selected_obs = self.combo_suffix.currentText()
if not selected_obs or selected_obs not in self.boris_data["observations"]:
QMessageBox.warning(self, "Invalid selection", "Selected observation not found in BORIS file.")
return
boris_events = self.boris_data["observations"][selected_obs].get("events", [])
if not boris_events:
QMessageBox.warning(self, "No BORIS events", "No events found in selected BORIS observation.")
return
snirf_path = self.line_edit_file_a.text()
if not snirf_path:
QMessageBox.warning(self, "No SNIRF file", "Please select a SNIRF file.")
return
boris_obs = self.boris_data["observations"][selected_obs]
# --- Extract videos + delays ---
files = boris_obs.get("file", {})
offsets = boris_obs.get("media_info", {}).get("offset", {})
videos = {}
for key, path in files.items():
if path: # only include videos that exist
delay = offsets.get(key, 0.0) # default 0 if missing
videos[key] = {"file": path, "delay": delay}
base_name = os.path.splitext(os.path.basename(file_a))[0]
if self.mode == EventUpdateMode.WRITE_SNIRF:
# Open save dialog for SNIRF
base_name = os.path.splitext(os.path.basename(file_a))[0]
suggested_name = f"{base_name}_{suffix}.snirf"
save_path, _ = QFileDialog.getSaveFileName(
self,
"Save SNIRF File As",
suggested_name,
"SNIRF Files (*.snirf)"
)
if not save_path:
print("SNIRF save cancelled.")
return
if not save_path.lower().endswith(".snirf"):
save_path += ".snirf"
try:
raw = read_raw_snirf(file_a, preload=True)
# --- Align BORIS events to SNIRF ---
boris_events = boris_obs.get("events", [])
onsets, durations, descriptions = [], [], []
open_events = {} # label -> list of start times
label_counts = {}
used_times = set()
sfreq = raw.info['sfreq']
min_shift = 1.0 / sfreq
max_attempts = 10
for event in boris_events:
if not isinstance(event, list) or len(event) < 3:
continue
event_time = event[0]
label = event[2]
count = label_counts.get(label, 0) + 1
label_counts[label] = count
if label not in open_events:
open_events[label] = []
if count % 2 == 1:
open_events[label].append(event_time)
else:
if open_events[label]:
start_time = open_events[label].pop(0)
duration = event_time - start_time
if duration <= 0:
continue
adjusted_time = start_time + time_shift
attempts = 0
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
adjusted_time += min_shift
attempts += 1
if attempts == max_attempts:
continue
adjusted_time = round(adjusted_time, 6)
used_times.add(adjusted_time)
onsets.append(adjusted_time)
durations.append(duration)
descriptions.append(label)
# Handle unmatched starts
for label, starts in open_events.items():
for start_time in starts:
adjusted_time = start_time + time_shift
attempts = 0
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
adjusted_time += min_shift
attempts += 1
if attempts == max_attempts:
continue
adjusted_time = round(adjusted_time, 6)
used_times.add(adjusted_time)
onsets.append(adjusted_time)
durations.append(0.0)
descriptions.append(label)
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
raw.set_annotations(new_annotations)
write_raw_snirf(raw, save_path)
QMessageBox.information(self, "Success", "SNIRF file updated with aligned BORIS events.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
elif self.mode == EventUpdateMode.WRITE_JSON:
# Open save dialog for JSON
base_name = os.path.splitext(os.path.basename(file_a))[0]
suggested_name = f"{base_name}_{suffix}_alignment.json"
save_path, _ = QFileDialog.getSaveFileName(
self,
"Save Event Alignment JSON As",
suggested_name,
"JSON Files (*.json)"
)
if not save_path:
print("JSON save cancelled.")
return
if not save_path.lower().endswith(".json"):
save_path += ".json"
# Build JSON dict
json_data = {
"observation": selected_obs,
"snirf_anchor": {"label": snirf_label, "time": snirf_anchor_time},
"boris_anchor": {"label": boris_label, "time": boris_anchor_time},
"time_shift": time_shift,
"videos": videos
}
# Write JSON
try:
with open(save_path, "w", encoding="utf-8") as f:
json.dump(json_data, f, indent=4)
QMessageBox.information(self, "Success", f"Event alignment saved to:\n{save_path}")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to write JSON:\n{e}")
def update_optode_positions(self, file_a, file_b, save_path):
fiducials = {}
ch_positions = {}
# Read the lines from the optode file
with open(file_b, 'r') as f:
for line in f:
if line.strip():
# Split by the semicolon and convert to meters
ch_name, coords_str = line.split(":")
coords = np.array(list(map(float, coords_str.strip().split()))) * 0.001
# The key we have is a fiducial
if ch_name.lower() in ['lpa', 'nz', 'rpa']:
fiducials[ch_name.lower()] = coords
# The key we have is a source or detector
else:
ch_positions[ch_name.upper()] = coords
# Create montage with updated coords in head space
initial_montage = make_dig_montage(ch_pos=ch_positions, nasion=fiducials.get('nz'), lpa=fiducials.get('lpa'), rpa=fiducials.get('rpa'), coord_frame='head') # type: ignore
# Read the SNIRF file, set the montage, and write it back
# TODO: Bad! read_raw_snirf doesnt release memory properly! Should be spawned in a seperate process and killed once completed
raw = read_raw_snirf(file_a, preload=True)
raw.set_montage(initial_montage)
write_raw_snirf(raw, save_path)
def _apply_events_to_snirf(self, raw, new_annotations, save_path):
raw.set_annotations(new_annotations)
write_raw_snirf(raw, save_path)
def _write_event_mapping_json(
self,
file_a,
file_b,
selected_obs,
snirf_anchor,
boris_anchor,
time_shift,
mapped_events,
save_path
):
payload = {
"source": {
"called_from": self.caller,
"snirf_file": os.path.basename(file_a),
"boris_file": os.path.basename(file_b),
"observation": selected_obs
},
"alignment": {
"snirf_anchor": snirf_anchor,
"boris_anchor": boris_anchor,
"time_shift_seconds": time_shift
},
"events": mapped_events,
"created_at": datetime.utcnow().isoformat() + "Z"
}
with open(save_path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
return save_path
class UpdateEventsBlazesWindow(QWidget):
def __init__(self, parent=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
super().__init__(parent, Qt.WindowType.Window)
self.mode = mode
self.caller = caller or self.__class__.__name__
self.setWindowTitle("Update event markers (BLAZES)")
self.resize(760, 200)
self.label_file_a = QLabel("SNIRF file:")
self.line_edit_file_a = QLineEdit()
self.line_edit_file_a.setReadOnly(True)
self.btn_browse_a = QPushButton("Browse .snirf")
self.btn_browse_a.clicked.connect(self.browse_file_a)
self.label_file_b = QLabel("BLAZES file:")
self.line_edit_file_b = QLineEdit()
self.line_edit_file_b.setReadOnly(True)
self.btn_browse_b = QPushButton("Browse .blaze")
self.btn_browse_b.clicked.connect(self.browse_file_b)
self.label_events = QLabel("Events in selected blazes file:")
self.combo_events = QComboBox()
self.combo_events.setEnabled(False)
self.label_snirf_events = QLabel("Events in SNIRF file:")
self.combo_snirf_events = QComboBox()
self.combo_snirf_events.setEnabled(False)
self.btn_clear = QPushButton("Clear")
self.btn_go = QPushButton("Go")
self.btn_clear.clicked.connect(self.clear_files)
self.btn_go.clicked.connect(self.go_action)
# ---
layout = QVBoxLayout()
self.description = QLabel()
self.description.setTextFormat(Qt.TextFormat.RichText)
self.description.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.description.setOpenExternalLinks(True)
self.description.setText("The events that are present in a snirf file may not be the events that are to be studied and examined.<br>"
"Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.<br>"
"The software <a href='https://git.research.dezeeuw.ca/tyler/blazes/'>BLAZES</a> is used to create these events in video files, and these events can be applied to the snirf file <br>"
"selected below by time syncing it to an event that it shares with the snirf file.")
layout.addWidget(self.description)
help_text_a = "Select the SNIRF (.snirf) file to update with new event markers."
file_a_layout = QHBoxLayout()
# Help button on the left
help_btn_a = QPushButton("?")
help_btn_a.setFixedWidth(25)
help_btn_a.setToolTip(help_text_a)
help_btn_a.clicked.connect(lambda _, text=help_text_a: self.show_help_popup(text))
file_a_layout.addWidget(help_btn_a)
# Container for label + line_edit + browse button with tooltip
file_a_container = QWidget()
file_a_container_layout = QHBoxLayout()
file_a_container_layout.setContentsMargins(0, 0, 0, 0)
file_a_container_layout.addWidget(self.label_file_a)
file_a_container_layout.addWidget(self.line_edit_file_a)
file_a_container_layout.addWidget(self.btn_browse_a)
file_a_container.setLayout(file_a_container_layout)
file_a_container.setToolTip(help_text_a)
file_a_layout.addWidget(file_a_container)
layout.addLayout(file_a_layout)
help_text_b = "Provide a .blaze output file that contains events for this participant."
file_b_layout = QHBoxLayout()
help_btn_b = QPushButton("?")
help_btn_b.setFixedWidth(25)
help_btn_b.setToolTip(help_text_b)
help_btn_b.clicked.connect(lambda _, text=help_text_b: self.show_help_popup(text))
file_b_layout.addWidget(help_btn_b)
file_b_container = QWidget()
file_b_container_layout = QHBoxLayout()
file_b_container_layout.setContentsMargins(0, 0, 0, 0)
file_b_container_layout.addWidget(self.label_file_b)
file_b_container_layout.addWidget(self.line_edit_file_b)
file_b_container_layout.addWidget(self.btn_browse_b)
file_b_container.setLayout(file_b_container_layout)
file_b_container.setToolTip(help_text_b)
file_b_layout.addWidget(file_b_container)
layout.addLayout(file_b_layout)
help_text_suffix = "The events extracted from the blaze file."
suffix2_layout = QHBoxLayout()
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
suffix2_layout.addWidget(help_btn_suffix)
suffix2_container = QWidget()
suffix2_container_layout = QHBoxLayout()
suffix2_container_layout.setContentsMargins(0, 0, 0, 0)
suffix2_container_layout.addWidget(self.label_events)
suffix2_container_layout.addWidget(self.combo_events)
suffix2_container.setLayout(suffix2_container_layout)
suffix2_container.setToolTip(help_text_suffix)
suffix2_layout.addWidget(suffix2_container)
layout.addLayout(suffix2_layout)
snirf_events_layout = QHBoxLayout()
help_text_snirf_events = "The event markers extracted from the SNIRF file."
help_btn_snirf_events = QPushButton("?")
help_btn_snirf_events.setFixedWidth(25)
help_btn_snirf_events.setToolTip(help_text_snirf_events)
help_btn_snirf_events.clicked.connect(lambda _, text=help_text_snirf_events: self.show_help_popup(text))
snirf_events_layout.addWidget(help_btn_snirf_events)
snirf_events_container = QWidget()
snirf_events_container_layout = QHBoxLayout()
snirf_events_container_layout.setContentsMargins(0, 0, 0, 0)
snirf_events_container_layout.addWidget(self.label_snirf_events)
snirf_events_container_layout.addWidget(self.combo_snirf_events)
snirf_events_container.setLayout(snirf_events_container_layout)
snirf_events_container.setToolTip(help_text_snirf_events)
snirf_events_layout.addWidget(snirf_events_container)
layout.addLayout(snirf_events_layout)
buttons_layout = QHBoxLayout()
buttons_layout.addStretch()
buttons_layout.addWidget(self.btn_clear)
buttons_layout.addWidget(self.btn_go)
layout.addLayout(buttons_layout)
self.setLayout(layout)
def show_help_popup(self, text):
msg = QMessageBox(self)
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
msg.setText(text)
msg.exec()
def browse_file_a(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select SNIRF File", "", "SNIRF Files (*.snirf)")
if file_path:
self.line_edit_file_a.setText(file_path)
try:
# TODO: Bad! read_raw_snirf doesnt release memory properly! Should be spawned in a seperate process and killed once completed
raw = read_raw_snirf(file_path, preload=False)
annotations = raw.annotations
# Build individual event entries
event_entries = []
for onset, description in zip(annotations.onset, annotations.description):
event_str = f"{description} @ {onset:.3f}s"
event_entries.append(event_str)
if not event_entries:
QMessageBox.information(self, "No Events", "No events found in SNIRF file.")
self.combo_snirf_events.clear()
self.combo_snirf_events.setEnabled(False)
return
self.combo_snirf_events.clear()
self.combo_snirf_events.addItems(event_entries)
self.combo_snirf_events.setEnabled(True)
except Exception as e:
QMessageBox.warning(self, "Error", f"Could not read SNIRF file with MNE:\n{str(e)}")
self.combo_snirf_events.clear()
self.combo_snirf_events.setEnabled(False)
def browse_file_b(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select JSON Timeline File", "", "JSON Files (*.json)")
if file_path:
self.line_edit_file_b.setText(file_path)
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
self.json_data = data
obs_keys = self.extract_json_observation_strings(data)
self.combo_events.clear()
if obs_keys:
self.combo_events.addItems(obs_keys)
self.combo_events.setEnabled(True)
else:
QMessageBox.information(self, "No Events", "No events found in JSON file.")
self.combo_events.setEnabled(False)
except (json.JSONDecodeError, FileNotFoundError, KeyError, TypeError) as e:
QMessageBox.warning(self, "Error", f"Failed to parse JSON file:\n{e}")
self.combo_events.clear()
self.combo_events.setEnabled(False)
def extract_json_observation_strings(self, data):
if "events" not in data:
raise KeyError("Missing 'events' key in JSON file.")
event_strings = []
# The new format is a flat list chronologically ordered
for event in data["events"]:
track_name = event.get("track_name", "Unknown")
onset = event.get("start_sec", 0.0)
# Formatting to match your SNIRF style: "Event Name @ 0.000s"
display_str = f"{track_name} @ {onset:.3f}s"
event_strings.append(display_str)
return event_strings
def clear_files(self):
self.line_edit_file_a.clear()
self.line_edit_file_b.clear()
def go_action(self):
file_a = self.line_edit_file_a.text()
file_b = self.line_edit_file_b.text()
suffix = APP_NAME
if not hasattr(self, "json_data") or self.combo_events.count() == 0 or self.combo_snirf_events.count() == 0:
QMessageBox.warning(self, "Missing data", "Please make sure a JSON and SNIRF event are selected.")
return
try:
json_text = self.combo_events.currentText()
_, json_time_str = json_text.split(" @ ")
json_anchor_time = float(json_time_str.replace("s", "").strip())
except Exception as e:
QMessageBox.critical(self, "JSON Event Error", f"Could not parse JSON anchor:\n{e}")
return
try:
snirf_text = self.combo_snirf_events.currentText()
_, snirf_time_str = snirf_text.split(" @ ")
snirf_anchor_time = float(snirf_time_str.replace("s", "").strip())
except Exception as e:
QMessageBox.critical(self, "SNIRF Event Error", f"Could not parse SNIRF anchor:\n{e}")
return
time_shift = snirf_anchor_time - json_anchor_time
onsets, durations, descriptions = [], [], []
skipped_count = 0
try:
events_list = self.json_data.get("events", [])
for event in events_list:
track_name = event.get("track_name", "Unknown")
clean_name = track_name.replace("AI: ", "").strip()
original_start = event.get("start_sec", 0.0)
original_end = event.get("end_sec", original_start)
duration = original_end - original_start
# FILTER: Minimum 0.1s duration
if duration < 0.1:
skipped_count += 1
continue
# Apply shift
adjusted_onset = original_start + time_shift
onsets.append(round(adjusted_onset, 6))
durations.append(round(duration, 6))
descriptions.append(clean_name)
except Exception as e:
QMessageBox.critical(self, "Track Error", f"Failed to process tracks: {e}")
return
if not onsets:
QMessageBox.warning(self, "No Data", f"No events met the 0.1s threshold. (Skipped {skipped_count})")
return
if self.mode == EventUpdateMode.WRITE_SNIRF:
suggested_name = f"{os.path.splitext(os.path.basename(file_a))[0]}_{suffix}.snirf"
save_path, _ = QFileDialog.getSaveFileName(self, "Save SNIRF", suggested_name, "SNIRF Files (*.snirf)")
if not save_path: return
if not save_path.lower().endswith(".snirf"): save_path += ".snirf"
try:
raw = read_raw_snirf(file_a, preload=True)
# Create annotations
new_annotations = Annotations(
onset=onsets,
duration=durations,
description=descriptions
)
# Replace existing annotations with the new aligned JSON tracks
raw.set_annotations(new_annotations)
write_raw_snirf(raw, save_path)
QMessageBox.information(self, "Success",
f"Aligned {len(onsets)} events.\n(Filtered out {skipped_count} short events)")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")