boris-behav-obs 9.7.1__py3-none-any.whl → 9.7.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
boris/about.py CHANGED
@@ -40,7 +40,7 @@ def actionAbout_activated(self):
40
40
  About dialog
41
41
  """
42
42
 
43
- programs_versions: list = ["MPV media player"]
43
+ programs_versions: list[str] = ["MPV media player"]
44
44
 
45
45
  mpv_lib_version, mpv_lib_file_path, mpv_api_version = util.mpv_lib_version()
46
46
  programs_versions.append(
@@ -53,13 +53,13 @@ def actionAbout_activated(self):
53
53
 
54
54
  # ffmpeg
55
55
  if self.ffmpeg_bin == "ffmpeg" and sys.platform.startswith("linux"):
56
- ffmpeg_true_path = subprocess.getoutput("which ffmpeg")
56
+ ffmpeg_true_path: str = subprocess.getoutput("which ffmpeg")
57
57
  else:
58
58
  ffmpeg_true_path = self.ffmpeg_bin
59
59
  programs_versions.extend(
60
60
  [
61
61
  "\nFFmpeg",
62
- subprocess.getoutput(f'"{self.ffmpeg_bin}" -version').split("\n")[0],
62
+ subprocess.getoutput(cmd=f'"{self.ffmpeg_bin}" -version').split(sep="\n")[0],
63
63
  f"Path: {ffmpeg_true_path}",
64
64
  "https://www.ffmpeg.org",
65
65
  ]
@@ -75,11 +75,11 @@ def actionAbout_activated(self):
75
75
  programs_versions.extend(["\nPandas", f"version {pd.__version__}", "https://pandas.pydata.org"])
76
76
 
77
77
  # graphviz
78
- gv_result = subprocess.getoutput("dot -V")
78
+ gv_result = subprocess.getoutput(cmd="dot -V")
79
79
 
80
80
  programs_versions.extend(["\nGraphViz", gv_result if "graphviz" in gv_result else "not installed", "https://www.graphviz.org/"])
81
81
 
82
- about_dialog = QMessageBox()
82
+ about_dialog: QMessageBox = QMessageBox()
83
83
  about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
84
84
 
85
85
  about_dialog.setWindowTitle(f"About {cfg.programName}")
boris/add_modifier_ui.py CHANGED
@@ -3,7 +3,7 @@
3
3
  ################################################################################
4
4
  ## Form generated from reading UI file 'add_modifier.ui'
5
5
  ##
6
- ## Created by: Qt User Interface Compiler version 6.8.0
6
+ ## Created by: Qt User Interface Compiler version 6.10.0
7
7
  ##
8
8
  ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
9
  ################################################################################
@@ -24,18 +24,16 @@ class Ui_Dialog(object):
24
24
  def setupUi(self, Dialog):
25
25
  if not Dialog.objectName():
26
26
  Dialog.setObjectName(u"Dialog")
27
- Dialog.resize(1088, 654)
28
- self.verticalLayout_5 = QVBoxLayout(Dialog)
29
- self.verticalLayout_5.setObjectName(u"verticalLayout_5")
27
+ Dialog.resize(1339, 789)
28
+ self.verticalLayout_4 = QVBoxLayout(Dialog)
29
+ self.verticalLayout_4.setObjectName(u"verticalLayout_4")
30
30
  self.cb_ask_at_stop = QCheckBox(Dialog)
31
31
  self.cb_ask_at_stop.setObjectName(u"cb_ask_at_stop")
32
32
 
33
- self.verticalLayout_5.addWidget(self.cb_ask_at_stop)
33
+ self.verticalLayout_4.addWidget(self.cb_ask_at_stop)
34
34
 
35
- self.verticalLayout_4 = QVBoxLayout()
36
- self.verticalLayout_4.setObjectName(u"verticalLayout_4")
37
- self.horizontalLayout_5 = QHBoxLayout()
38
- self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
35
+ self.horizontalLayout_8 = QHBoxLayout()
36
+ self.horizontalLayout_8.setObjectName(u"horizontalLayout_8")
39
37
  self.verticalLayout_2 = QVBoxLayout()
40
38
  self.verticalLayout_2.setObjectName(u"verticalLayout_2")
41
39
  self.lbModifier = QLabel(Dialog)
@@ -69,7 +67,7 @@ class Ui_Dialog(object):
69
67
  self.verticalLayout_2.addItem(self.verticalSpacer)
70
68
 
71
69
 
72
- self.horizontalLayout_5.addLayout(self.verticalLayout_2)
70
+ self.horizontalLayout_8.addLayout(self.verticalLayout_2)
73
71
 
74
72
  self.verticalLayout_3 = QVBoxLayout()
75
73
  self.verticalLayout_3.setObjectName(u"verticalLayout_3")
@@ -88,7 +86,7 @@ class Ui_Dialog(object):
88
86
  self.verticalLayout_3.addItem(self.verticalSpacer_2)
89
87
 
90
88
 
91
- self.horizontalLayout_5.addLayout(self.verticalLayout_3)
89
+ self.horizontalLayout_8.addLayout(self.verticalLayout_3)
92
90
 
93
91
  self.verticalLayout = QVBoxLayout()
94
92
  self.verticalLayout.setObjectName(u"verticalLayout")
@@ -102,30 +100,42 @@ class Ui_Dialog(object):
102
100
 
103
101
  self.verticalLayout.addWidget(self.tabWidgetModifiersSets)
104
102
 
103
+ self.horizontalLayout_5 = QHBoxLayout()
104
+ self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
105
105
  self.lb_name = QLabel(Dialog)
106
106
  self.lb_name.setObjectName(u"lb_name")
107
107
 
108
- self.verticalLayout.addWidget(self.lb_name)
108
+ self.horizontalLayout_5.addWidget(self.lb_name)
109
109
 
110
110
  self.le_name = QLineEdit(Dialog)
111
111
  self.le_name.setObjectName(u"le_name")
112
112
 
113
- self.verticalLayout.addWidget(self.le_name)
113
+ self.horizontalLayout_5.addWidget(self.le_name)
114
114
 
115
+
116
+ self.verticalLayout.addLayout(self.horizontalLayout_5)
117
+
118
+ self.horizontalLayout_6 = QHBoxLayout()
119
+ self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
115
120
  self.lb_description = QLabel(Dialog)
116
121
  self.lb_description.setObjectName(u"lb_description")
117
122
 
118
- self.verticalLayout.addWidget(self.lb_description)
123
+ self.horizontalLayout_6.addWidget(self.lb_description)
119
124
 
120
125
  self.le_description = QLineEdit(Dialog)
121
126
  self.le_description.setObjectName(u"le_description")
122
127
 
123
- self.verticalLayout.addWidget(self.le_description)
128
+ self.horizontalLayout_6.addWidget(self.le_description)
129
+
124
130
 
131
+ self.verticalLayout.addLayout(self.horizontalLayout_6)
132
+
133
+ self.horizontalLayout_7 = QHBoxLayout()
134
+ self.horizontalLayout_7.setObjectName(u"horizontalLayout_7")
125
135
  self.lbType = QLabel(Dialog)
126
136
  self.lbType.setObjectName(u"lbType")
127
137
 
128
- self.verticalLayout.addWidget(self.lbType)
138
+ self.horizontalLayout_7.addWidget(self.lbType)
129
139
 
130
140
  self.cbType = QComboBox(Dialog)
131
141
  self.cbType.addItem("")
@@ -134,7 +144,14 @@ class Ui_Dialog(object):
134
144
  self.cbType.addItem("")
135
145
  self.cbType.setObjectName(u"cbType")
136
146
 
137
- self.verticalLayout.addWidget(self.cbType)
147
+ self.horizontalLayout_7.addWidget(self.cbType)
148
+
149
+ self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
150
+
151
+ self.horizontalLayout_7.addItem(self.horizontalSpacer_3)
152
+
153
+
154
+ self.verticalLayout.addLayout(self.horizontalLayout_7)
138
155
 
139
156
  self.lbValues = QLabel(Dialog)
140
157
  self.lbValues.setObjectName(u"lbValues")
@@ -198,28 +215,32 @@ class Ui_Dialog(object):
198
215
 
199
216
  self.horizontalLayout_4 = QHBoxLayout()
200
217
  self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
201
-
202
- self.verticalLayout.addLayout(self.horizontalLayout_4)
203
-
204
218
  self.pb_add_subjects = QPushButton(Dialog)
205
219
  self.pb_add_subjects.setObjectName(u"pb_add_subjects")
206
220
 
207
- self.verticalLayout.addWidget(self.pb_add_subjects)
221
+ self.horizontalLayout_4.addWidget(self.pb_add_subjects)
208
222
 
209
223
  self.pb_load_file = QPushButton(Dialog)
210
224
  self.pb_load_file.setObjectName(u"pb_load_file")
211
225
 
212
- self.verticalLayout.addWidget(self.pb_load_file)
226
+ self.horizontalLayout_4.addWidget(self.pb_load_file)
227
+
228
+ self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
229
+
230
+ self.horizontalLayout_4.addItem(self.horizontalSpacer_2)
231
+
232
+
233
+ self.verticalLayout.addLayout(self.horizontalLayout_4)
213
234
 
214
235
  self.verticalSpacer_3 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
215
236
 
216
237
  self.verticalLayout.addItem(self.verticalSpacer_3)
217
238
 
218
239
 
219
- self.horizontalLayout_5.addLayout(self.verticalLayout)
240
+ self.horizontalLayout_8.addLayout(self.verticalLayout)
220
241
 
221
242
 
222
- self.verticalLayout_4.addLayout(self.horizontalLayout_5)
243
+ self.verticalLayout_4.addLayout(self.horizontalLayout_8)
223
244
 
224
245
  self.horizontalLayout_2 = QHBoxLayout()
225
246
  self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
@@ -241,9 +262,6 @@ class Ui_Dialog(object):
241
262
  self.verticalLayout_4.addLayout(self.horizontalLayout_2)
242
263
 
243
264
 
244
- self.verticalLayout_5.addLayout(self.verticalLayout_4)
245
-
246
-
247
265
  self.retranslateUi(Dialog)
248
266
 
249
267
  self.tabWidgetModifiersSets.setCurrentIndex(-1)
@@ -256,8 +274,8 @@ class Ui_Dialog(object):
256
274
  Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Set modifiers", None))
257
275
  self.cb_ask_at_stop.setText(QCoreApplication.translate("Dialog", u"Ask for modifier(s) when behavior stops", None))
258
276
  self.lbModifier.setText(QCoreApplication.translate("Dialog", u"Modifier", None))
259
- self.lbCode.setText(QCoreApplication.translate("Dialog", u"Key code", None))
260
- self.lbCodeHelp.setText(QCoreApplication.translate("Dialog", u"Key code is case sensitive. Type one character or a function key (F1, F2... F12)", None))
277
+ self.lbCode.setText(QCoreApplication.translate("Dialog", u"Shortcut", None))
278
+ self.lbCodeHelp.setText(QCoreApplication.translate("Dialog", u"The shortcut is case sensitive. Type one character or a function key (F1, F2... F12)", None))
261
279
  self.pbAddModifier.setText(QCoreApplication.translate("Dialog", u"->", None))
262
280
  self.pbModifyModifier.setText(QCoreApplication.translate("Dialog", u"<-", None))
263
281
  self.lb_name.setText(QCoreApplication.translate("Dialog", u"Set name", None))
@@ -0,0 +1,336 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Export observations to FERAL (getferal.ai)
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import pandas as pd
11
+ from PySide6.QtCore import Qt
12
+ from PySide6.QtWidgets import (
13
+ QDialog,
14
+ QFileDialog,
15
+ QHBoxLayout,
16
+ QLabel,
17
+ QListWidget,
18
+ QListWidgetItem,
19
+ QPushButton,
20
+ QVBoxLayout,
21
+ )
22
+
23
+ __version__ = "0.3.2"
24
+ __version_date__ = "2025-12-19"
25
+ __plugin_name__ = "Export observations to FERAL"
26
+ __author__ = "Jacopo Razzauti - The Rockefeller University; Olivier Friard - University of Torino - Italy"
27
+
28
+
29
+ # ---------------------------
30
+ # Dialog: choose behaviors
31
+ # ---------------------------
32
+ class BehaviorSelectDialog(QDialog):
33
+ """Select which BORIS behavior codes should become FERAL classes.
34
+
35
+ Class 0 is reserved for "other". Any behavior not selected is mapped to 0.
36
+ """
37
+
38
+ def __init__(self, behavior_codes, parent=None):
39
+ super().__init__(parent)
40
+
41
+ self.setWindowTitle("Select behaviors to export (0 is 'other')")
42
+ self.setModal(True)
43
+
44
+ main_layout = QVBoxLayout(self)
45
+
46
+ info = QLabel("Select behaviors to export.\nClass 0 is reserved for 'other'.\nUnselected behaviors are mapped to 0.")
47
+ info.setWordWrap(True)
48
+ main_layout.addWidget(info)
49
+
50
+ self.list_behaviors = QListWidget()
51
+ self.list_behaviors.setSelectionMode(QListWidget.ExtendedSelection)
52
+
53
+ for code in sorted(behavior_codes):
54
+ item = QListWidgetItem(code)
55
+ item.setSelected(True) # default: select all
56
+ self.list_behaviors.addItem(item)
57
+
58
+ main_layout.addWidget(self.list_behaviors)
59
+
60
+ buttons_layout = QHBoxLayout()
61
+ btn_all = QPushButton("Select all")
62
+ btn_none = QPushButton("Select none")
63
+ btn_ok = QPushButton("OK")
64
+ btn_cancel = QPushButton("Cancel")
65
+
66
+ btn_all.clicked.connect(self._select_all)
67
+ btn_none.clicked.connect(self._select_none)
68
+ btn_ok.clicked.connect(self.accept)
69
+ btn_cancel.clicked.connect(self.reject)
70
+
71
+ buttons_layout.addWidget(btn_all)
72
+ buttons_layout.addWidget(btn_none)
73
+ buttons_layout.addStretch()
74
+ buttons_layout.addWidget(btn_ok)
75
+ buttons_layout.addWidget(btn_cancel)
76
+
77
+ main_layout.addLayout(buttons_layout)
78
+
79
+ def _select_all(self):
80
+ for i in range(self.list_behaviors.count()):
81
+ self.list_behaviors.item(i).setSelected(True)
82
+
83
+ def _select_none(self):
84
+ for i in range(self.list_behaviors.count()):
85
+ self.list_behaviors.item(i).setSelected(False)
86
+
87
+ def selected_codes(self):
88
+ return [it.text() for it in self.list_behaviors.selectedItems()]
89
+
90
+
91
+ # ---------------------------
92
+ # Dialog: split videos
93
+ # ---------------------------
94
+ class CategoryDialog(QDialog):
95
+ def __init__(self, items, parent=None):
96
+ super().__init__(parent)
97
+
98
+ self.setWindowTitle("Organize the videos in categories")
99
+ self.setModal(True)
100
+
101
+ main_layout = QVBoxLayout(self)
102
+ lists_layout = QHBoxLayout()
103
+
104
+ self.list_unclassified = self._make_list_widget()
105
+ self.list_train = self._make_list_widget()
106
+ self.list_val = self._make_list_widget()
107
+ self.list_test = self._make_list_widget()
108
+ self.list_inference = self._make_list_widget()
109
+
110
+ lists_layout.addLayout(self._make_column("All videos", self.list_unclassified))
111
+ lists_layout.addLayout(self._make_column("train", self.list_train))
112
+ lists_layout.addLayout(self._make_column("val", self.list_val))
113
+ lists_layout.addLayout(self._make_column("test", self.list_test))
114
+ lists_layout.addLayout(self._make_column("inference", self.list_inference))
115
+
116
+ main_layout.addLayout(lists_layout)
117
+
118
+ buttons_layout = QHBoxLayout()
119
+ btn_ok = QPushButton("OK")
120
+ btn_cancel = QPushButton("Cancel")
121
+ btn_ok.clicked.connect(self.accept)
122
+ btn_cancel.clicked.connect(self.reject)
123
+
124
+ buttons_layout.addStretch()
125
+ buttons_layout.addWidget(btn_ok)
126
+ buttons_layout.addWidget(btn_cancel)
127
+ main_layout.addLayout(buttons_layout)
128
+
129
+ for text in items:
130
+ QListWidgetItem(text, self.list_unclassified)
131
+
132
+ @staticmethod
133
+ def _make_column(title, widget):
134
+ col = QVBoxLayout()
135
+ col.addWidget(QLabel(title))
136
+ col.addWidget(widget)
137
+ return col
138
+
139
+ @staticmethod
140
+ def _make_list_widget():
141
+ lw = QListWidget()
142
+ lw.setSelectionMode(QListWidget.ExtendedSelection)
143
+ lw.setDragEnabled(True)
144
+ lw.setAcceptDrops(True)
145
+ lw.setDropIndicatorShown(True)
146
+ lw.setDragDropMode(QListWidget.DragDrop)
147
+ lw.setDefaultDropAction(Qt.MoveAction)
148
+ return lw
149
+
150
+ @staticmethod
151
+ def _collect(widget):
152
+ # "*" is used to mark videos with at least one event
153
+ return [widget.item(i).text().rstrip("*") for i in range(widget.count())]
154
+
155
+ def categories(self):
156
+ return {
157
+ "unclassified": self._collect(self.list_unclassified),
158
+ "train": self._collect(self.list_train),
159
+ "val": self._collect(self.list_val),
160
+ "test": self._collect(self.list_test),
161
+ "inference": self._collect(self.list_inference),
162
+ }
163
+
164
+
165
+ def run(df: pd.DataFrame, project: dict):
166
+ """Export BORIS observations/events to a FERAL-compatible JSON.
167
+
168
+ See https://www.getferal.ai/ > Label Preparation
169
+ """
170
+
171
+ def log(msg):
172
+ messages.append(str(msg))
173
+
174
+ def safe_float(d, key):
175
+ try:
176
+ return float(d[key])
177
+ except Exception:
178
+ return None
179
+
180
+ messages = []
181
+
182
+ out = {
183
+ "is_multilabel": False,
184
+ "splits": {"train": [], "val": [], "test": [], "inference": []},
185
+ }
186
+
187
+ # ---- Behaviors (FERAL classes) ----
188
+ behavior_conf = project.get("behaviors_conf", {})
189
+ boris_codes = [behavior_conf[k].get("code") for k in behavior_conf]
190
+ boris_codes = [c for c in boris_codes if c] # drop None/empty
191
+
192
+ # Reserve 0 for background. If BORIS has a behavior literally named "other",
193
+ # treat it as background and do not include as a class.
194
+ boris_codes_no_other = [c for c in boris_codes if c != "other"]
195
+
196
+ dlg = BehaviorSelectDialog(boris_codes_no_other)
197
+ if not dlg.exec():
198
+ return "Behavior selection canceled; export aborted."
199
+
200
+ selected = sorted(set(dlg.selected_codes()))
201
+ if not selected:
202
+ log("No behaviors selected: everything will be mapped to class 0 ('other').")
203
+
204
+ class_names = {"0": "other"}
205
+ for i, code in enumerate(selected, start=1):
206
+ class_names[str(i)] = code
207
+
208
+ out["class_names"] = class_names
209
+ behavior_to_idx = {code: i for i, code in enumerate(selected, start=1)}
210
+
211
+ if selected:
212
+ log(f"Selected behaviors: {', '.join(selected)}")
213
+ log(f"Classes: {class_names}")
214
+
215
+ # ---- Iterate observations/videos ----
216
+ labels = {}
217
+ video_list = []
218
+
219
+ # df dataframe cannot have a "Media file" column
220
+ # has_media_file_col = "Media file" in df.columns
221
+ has_subject_col = "Subject" in df.columns
222
+
223
+ observations = sorted(project.get("observations", {}).keys())
224
+ if not observations:
225
+ return "No observations found in project; nothing to export."
226
+
227
+ for obs_id in observations:
228
+ log("---")
229
+ log(obs_id)
230
+
231
+ obs = project["observations"][obs_id]
232
+ media_files = obs.get("file", {}).get("1", [])
233
+ if not media_files:
234
+ log(f"Observation {obs_id} has no video in player 1.")
235
+ continue
236
+
237
+ media_info = obs.get("media_info", {})
238
+ fps_dict = media_info.get("fps", {})
239
+ length_dict = media_info.get("length", {})
240
+ frames_dict = media_info.get("frames", {}) or {}
241
+
242
+ for media_path in media_files:
243
+ video_name = Path(media_path).name
244
+
245
+ if video_name in labels:
246
+ log(f"Duplicate video name '{video_name}' encountered; skipping (obs {obs_id}).")
247
+ continue
248
+
249
+ # df dataframe cannot have a "Media file" column
250
+ # Filter events for this observation + this media file when possible
251
+ # if has_media_file_col:
252
+ # df_video = df[(df["Observation id"] == obs_id) & (df["Media file"] == media_path)]
253
+ # else:
254
+ # df_video = df[df["Observation id"] == obs_id]
255
+ # log("Warning: df has no 'Media file' column; using all events from observation.")
256
+
257
+ df_video = df[df["Observation id"] == obs_id]
258
+
259
+ # Enforce single-subject labeling when Subject column exists
260
+ if has_subject_col and not df_video.empty:
261
+ subjects = df_video["Subject"].dropna().unique().tolist()
262
+ if len(subjects) > 1:
263
+ log(f"More than one subject in {video_name}: {subjects}. Skipping.")
264
+ continue
265
+
266
+ # Mark videos that contain at least one event with "*"
267
+ video_list.append(video_name + ("*" if not df_video.empty else ""))
268
+
269
+ fps = safe_float(fps_dict, media_path)
270
+ duration = safe_float(length_dict, media_path)
271
+ if fps is None:
272
+ log(f"Missing/invalid FPS for {video_name}. Skipping.")
273
+ continue
274
+ if duration is None:
275
+ log(f"Missing/invalid duration for {video_name}. Skipping.")
276
+ continue
277
+
278
+ if media_path in frames_dict:
279
+ n_frames = int(frames_dict[media_path])
280
+ log(f"{video_name}: fps={fps} duration={duration} frames={n_frames} (BORIS)")
281
+ else:
282
+ n_frames = int(round(duration * fps))
283
+ log(f"{video_name}: fps={fps} duration={duration} frames={n_frames} (rounded)")
284
+
285
+ if n_frames <= 0:
286
+ log(f"Non-positive frame count for {video_name}. Skipping.")
287
+ continue
288
+
289
+ frame_dt = 1.0 / fps
290
+ labels[video_name] = [0] * n_frames # default: "other"
291
+
292
+ # Fill per-frame labels
293
+ for frame_idx in range(n_frames):
294
+ t = frame_idx * frame_dt
295
+ behaviors = df_video[(df_video["Start (s)"] <= t) & (df_video["Stop (s)"] >= t)]["Behavior"].unique().tolist()
296
+
297
+ if len(behaviors) > 1:
298
+ log(
299
+ f"{video_name}: overlapping behaviors at frame {frame_idx} (t={t:.6f}s): "
300
+ f"{behaviors}. Removing video (is_multilabel=False)."
301
+ )
302
+ del labels[video_name]
303
+ break
304
+
305
+ if not behaviors:
306
+ continue
307
+
308
+ labels[video_name][frame_idx] = behavior_to_idx.get(behaviors[0], 0)
309
+
310
+ out["labels"] = labels
311
+
312
+ # ---- Splits dialog ----
313
+ split_dlg = CategoryDialog(video_list)
314
+ if not split_dlg.exec():
315
+ log("Export canceled at split assignment stage.")
316
+ return "\n".join(messages)
317
+
318
+ splits = split_dlg.categories()
319
+ splits.pop("unclassified", None)
320
+ out["splits"] = splits
321
+
322
+ filename, _ = QFileDialog.getSaveFileName(
323
+ None,
324
+ "Choose a file to save",
325
+ "",
326
+ "JSON files (*.json);;All files (*.*)",
327
+ )
328
+ if not filename:
329
+ log("No output file selected; nothing written.")
330
+ return "\n".join(messages)
331
+
332
+ with open(filename, "w", encoding="utf-8") as f_out:
333
+ json.dump(out, f_out, indent=2)
334
+
335
+ log(f"Saved: {filename}")
336
+ return "\n".join(messages)
@@ -119,12 +119,12 @@ class BehaviorsMapCreatorWindow(QMainWindow):
119
119
  self.saveMapAction.setShortcut("Ctrl+S")
120
120
  self.saveMapAction.setStatusTip("Save the behavior coding map")
121
121
  self.saveMapAction.setEnabled(False)
122
- self.saveMapAction.triggered.connect(self.saveMap_clicked)
122
+ self.saveMapAction.triggered.connect(self.save_map_clicked)
123
123
 
124
124
  self.saveAsMapAction = QAction(QIcon(), "Save the behavior coding map as ...", self)
125
125
  self.saveAsMapAction.setStatusTip("Save the behavior coding map as ...")
126
126
  self.saveAsMapAction.setEnabled(False)
127
- self.saveAsMapAction.triggered.connect(self.saveAsMap_clicked)
127
+ self.saveAsMapAction.triggered.connect(self.save_as_map_clicked)
128
128
 
129
129
  self.mapNameAction = QAction(QIcon(), "&Edit name of behaviors coding map", self)
130
130
  self.mapNameAction.setShortcut("Ctrl+M")
@@ -389,7 +389,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
389
389
  )
390
390
 
391
391
  if response == cfg.SAVE:
392
- if not self.saveMap_clicked():
392
+ if not self.save_map_clicked():
393
393
  event.ignore()
394
394
 
395
395
  if response == cfg.CANCEL:
@@ -615,7 +615,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
615
615
  )
616
616
 
617
617
  if response == cfg.SAVE:
618
- if not self.saveMap_clicked():
618
+ if not self.save_map_clicked():
619
619
  return
620
620
 
621
621
  if response == cfg.CANCEL:
@@ -659,7 +659,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
659
659
  ["Save", "Discard", "Cancel"],
660
660
  )
661
661
 
662
- if (response == "Save" and not self.saveMap_clicked()) or (response == "Cancel"):
662
+ if (response == "Save" and not self.save_map_clicked()) or (response == "Cancel"):
663
663
  return
664
664
 
665
665
  fileName, _ = QFileDialog(self).getOpenFileName(
@@ -783,7 +783,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
783
783
  else:
784
784
  return False
785
785
 
786
- def saveAsMap_clicked(self):
786
+ def save_as_map_clicked(self):
787
787
  filters = "Behaviors coding map (*.behav_coding_map);;All files (*)"
788
788
 
789
789
  self.fileName, _ = QFileDialog.getSaveFileName(self, "Save behaviors coding map as", "", filters)
@@ -794,7 +794,7 @@ class BehaviorsMapCreatorWindow(QMainWindow):
794
794
  self.fileName += ".behav_coding_map"
795
795
  self.saveMap()
796
796
 
797
- def saveMap_clicked(self):
797
+ def save_map_clicked(self):
798
798
  if not self.fileName:
799
799
  self.fileName, _ = QFileDialog().getSaveFileName(
800
800
  self,
boris/coding_pad.py CHANGED
@@ -38,7 +38,7 @@ class Button(QWidget):
38
38
 
39
39
 
40
40
  class CodingPad(QWidget):
41
- clickSignal = Signal(str)
41
+ click_signal = Signal(str)
42
42
  sendEventSignal = Signal(QEvent)
43
43
  close_signal = Signal(QRect, dict)
44
44
 
@@ -208,7 +208,8 @@ class CodingPad(QWidget):
208
208
  """
209
209
  Button clicked
210
210
  """
211
- self.clickSignal.emit(behavior_code)
211
+ print(f"{behavior_code=}")
212
+ self.click_signal.emit(behavior_code)
212
213
 
213
214
  def eventFilter(self, receiver, event) -> bool:
214
215
  """
@@ -261,7 +262,7 @@ def show_coding_pad(self):
261
262
  self.codingpad.setWindowFlags(Qt.WindowStaysOnTopHint)
262
263
  self.codingpad.sendEventSignal.connect(self.signal_from_widget)
263
264
 
264
- self.codingpad.clickSignal.connect(self.click_signal_from_coding_pad)
265
+ self.codingpad.click_signal.connect(self.click_signal_from_coding_pad)
265
266
  self.codingpad.close_signal.connect(self.close_signal_from_coding_pad)
266
267
  self.codingpad.show()
267
268
 
boris/config.py CHANGED
@@ -132,6 +132,8 @@ POINT_EVENT_PLOT_COLOR = "black"
132
132
 
133
133
  CHAR_FORBIDDEN_IN_MODIFIERS = "(|),`~"
134
134
 
135
+ FAST_FORWARD_DEFAULT_VALUE: float = 10.0
136
+
135
137
  ADAPT_FAST_JUMP = "adapt_fast_jump"
136
138
  ADAPT_FAST_JUMP_DEFAULT = False
137
139
 
@@ -419,6 +421,10 @@ MPV_HWDEC_AUTOSAFE = "auto-safe"
419
421
  MPV_HWDEC_OPTIONS = (MPV_HWDEC_AUTO, MPV_HWDEC_AUTOSAFE, MPV_HWDEC_NO)
420
422
  MPV_HWDEC_DEFAULT_VALUE = MPV_HWDEC_AUTO
421
423
 
424
+ # frame step size (disabled)
425
+ # FRAME_STEP_SIZE: str = "frame_step_size"
426
+ # FRAME_STEP_SIZE_DEFAULT_VALUE: int = 1
427
+
422
428
  ANALYSIS_PLUGINS = "analysis_plugins"
423
429
  EXCLUDED_PLUGINS = "excluded_plugins"
424
430
  PERSONAL_PLUGINS_DIR = "personal_plugins_dir"
@@ -533,10 +539,13 @@ SPECTROGRAM_DEFAULT_TIME_INTERVAL = 10
533
539
  SPECTROGRAM_WINDOW_TYPE = "SPECTROGRAM_WINDOW_TYPE"
534
540
  SPECTROGRAM_DEFAULT_WINDOW_TYPE = "hanning"
535
541
  SPECTROGRAM_NFFT = "SPECTROGRAM_NFFT"
536
- SPECTROGRAM_DEFAULT_NFFT = "1024"
542
+ SPECTROGRAM_DEFAULT_NFFT = "256"
537
543
  SPECTROGRAM_NOVERLAP = "SPECTROGRAM_NOVERLAP"
538
- SPECTROGRAM_DEFAULT_NOVERLAP = 900
544
+ SPECTROGRAM_DEFAULT_NOVERLAP = 128
539
545
  SPECTROGRAM_VMIN = "SPECTROGRAM_VMIN"
546
+
547
+ SPECTROGRAM_USE_VMIN_VMAX = "SPECTROGRAM_USE_VMIN_VMAX"
548
+ SPECTROGRAM_USE_VMIN_VMAX_DEFAULT = False
540
549
  SPECTROGRAM_DEFAULT_VMIN = -100
541
550
  SPECTROGRAM_VMAX = "SPECTROGRAM_VMAX"
542
551
  SPECTROGRAM_DEFAULT_VMAX = -20
@@ -730,6 +739,7 @@ INIT_PARAM = {
730
739
  MPV_HWDEC: MPV_HWDEC_DEFAULT_VALUE,
731
740
  PROJECT_FILE_INDENTATION: PROJECT_FILE_INDENTATION_DEFAULT_VALUE,
732
741
  f"{MEDIA} tw fields": MEDIA_TW_EVENTS_FIELDS_DEFAULT,
742
+ # FRAME_STEP_SIZE: FRAME_STEP_SIZE_DEFAULT_VALUE,
733
743
  }
734
744
 
735
745
  SDIS_EXT = "sds"