boris-behav-obs 9.7.7__tar.gz → 9.7.12__tar.gz

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.
Files changed (127) hide show
  1. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/PKG-INFO +2 -2
  2. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/README.md +1 -1
  3. boris_behav_obs-9.7.12/boris/analysis_plugins/_export_to_feral.py +225 -0
  4. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/behav_coding_map_creator.py +7 -7
  5. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/coding_pad.py +4 -3
  6. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/core.py +19 -15
  7. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/modifier_coding_map_creator.py +6 -6
  8. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/observation_operations.py +48 -40
  9. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/plot_spectrogram_rt.py +3 -0
  10. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/plot_waveform_rt.py +4 -1
  11. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/plugins.py +60 -23
  12. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/preferences_ui.py +2 -0
  13. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/project_functions.py +2 -1
  14. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/subjects_pad.py +2 -2
  15. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/utilities.py +9 -0
  16. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/version.py +2 -2
  17. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/video_operations.py +3 -2
  18. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris_behav_obs.egg-info/PKG-INFO +2 -2
  19. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris_behav_obs.egg-info/SOURCES.txt +1 -0
  20. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/pyproject.toml +1 -1
  21. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/LICENSE.TXT +0 -0
  22. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/MANIFEST.in +0 -0
  23. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/README.TXT +0 -0
  24. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/__init__.py +0 -0
  25. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/__main__.py +0 -0
  26. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/about.py +0 -0
  27. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/add_modifier.py +0 -0
  28. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/add_modifier_ui.py +0 -0
  29. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/advanced_event_filtering.py +0 -0
  30. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/__init__.py +0 -0
  31. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/_latency.py +0 -0
  32. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/irr_cohen_kappa.py +0 -0
  33. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +0 -0
  34. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/irr_weighted_cohen_kappa.py +0 -0
  35. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +0 -0
  36. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/list_of_dataframe_columns.py +0 -0
  37. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/number_of_occurences.py +0 -0
  38. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
  39. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/analysis_plugins/time_budget.py +0 -0
  40. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/behavior_binary_table.py +0 -0
  41. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/behaviors_coding_map.py +0 -0
  42. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/boris_cli.py +0 -0
  43. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/cmd_arguments.py +0 -0
  44. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/config.py +0 -0
  45. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/config_file.py +0 -0
  46. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/connections.py +0 -0
  47. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/converters.py +0 -0
  48. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/converters_ui.py +0 -0
  49. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/cooccurence.py +0 -0
  50. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/core_qrc.py +0 -0
  51. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/core_ui.py +0 -0
  52. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/db_functions.py +0 -0
  53. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/dev.py +0 -0
  54. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/dialog.py +0 -0
  55. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/duration_widget.py +0 -0
  56. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/edit_event.py +0 -0
  57. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/edit_event_ui.py +0 -0
  58. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/event_operations.py +0 -0
  59. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/events_cursor.py +0 -0
  60. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/events_snapshots.py +0 -0
  61. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/exclusion_matrix.py +0 -0
  62. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/export_events.py +0 -0
  63. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/export_observation.py +0 -0
  64. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/external_processes.py +0 -0
  65. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/geometric_measurement.py +0 -0
  66. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/gui_utilities.py +0 -0
  67. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/image_overlay.py +0 -0
  68. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/import_observations.py +0 -0
  69. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/ipc_mpv.py +0 -0
  70. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/irr.py +0 -0
  71. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/latency.py +0 -0
  72. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/measurement_widget.py +0 -0
  73. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/media_file.py +0 -0
  74. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/menu_options.py +0 -0
  75. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/modifiers_coding_map.py +0 -0
  76. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/mpv.py +0 -0
  77. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/mpv2.py +0 -0
  78. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/observation.py +0 -0
  79. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/observation_ui.py +0 -0
  80. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/observations_list.py +0 -0
  81. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/otx_parser.py +0 -0
  82. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/param_panel.py +0 -0
  83. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/param_panel_ui.py +0 -0
  84. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/player_dock_widget.py +0 -0
  85. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/plot_data_module.py +0 -0
  86. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/plot_events.py +0 -0
  87. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/plot_events_rt.py +0 -0
  88. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/portion/__init__.py +0 -0
  89. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/portion/const.py +0 -0
  90. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/portion/dict.py +0 -0
  91. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/portion/func.py +0 -0
  92. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/portion/interval.py +0 -0
  93. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/portion/io.py +0 -0
  94. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/preferences.py +0 -0
  95. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/project.py +0 -0
  96. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/project_import_export.py +0 -0
  97. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/project_ui.py +0 -0
  98. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/qrc_boris.py +0 -0
  99. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/qrc_boris5.py +0 -0
  100. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/select_modifiers.py +0 -0
  101. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/select_observations.py +0 -0
  102. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/select_subj_behav.py +0 -0
  103. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/state_events.py +0 -0
  104. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/synthetic_time_budget.py +0 -0
  105. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/time_budget_functions.py +0 -0
  106. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/time_budget_widget.py +0 -0
  107. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/transitions.py +0 -0
  108. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/video_equalizer.py +0 -0
  109. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/video_equalizer_ui.py +0 -0
  110. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/view_df.py +0 -0
  111. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/view_df_ui.py +0 -0
  112. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris/write_event.py +0 -0
  113. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  114. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris_behav_obs.egg-info/entry_points.txt +0 -0
  115. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris_behav_obs.egg-info/requires.txt +0 -0
  116. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/boris_behav_obs.egg-info/top_level.txt +0 -0
  117. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/setup.cfg +0 -0
  118. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_db_functions.py +0 -0
  119. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_export_observation.py +0 -0
  120. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_irr.py +0 -0
  121. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_observation_gui.py +0 -0
  122. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_otx_parser.py +0 -0
  123. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_preferences_gui.py +0 -0
  124. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_project_functions.py +0 -0
  125. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_time_budget.py +0 -0
  126. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_utilities.py +0 -0
  127. {boris_behav_obs-9.7.7 → boris_behav_obs-9.7.12}/tests/test_utilities2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.7.7
3
+ Version: 9.7.12
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License-Expression: GPL-3.0-only
@@ -51,7 +51,7 @@ It provides also some analysis tools like time budget and some plotting function
51
51
  <!-- The BO-RIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
52
52
 
53
53
 
54
- The BORIS paper has more than 2423 citations in peer-reviewed scientific publications.
54
+ The BORIS paper has more than 2472 citations in peer-reviewed scientific publications.
55
55
 
56
56
 
57
57
 
@@ -13,7 +13,7 @@ It provides also some analysis tools like time budget and some plotting function
13
13
  <!-- The BO-RIS paper has more than [![BORIS citations counter](https://penelope.unito.it/friard/boris_scopus_citations.png) citations](https://www.boris.unito.it/citations) in peer-reviewed scientific publications. -->
14
14
 
15
15
 
16
- The BORIS paper has more than 2423 citations in peer-reviewed scientific publications.
16
+ The BORIS paper has more than 2472 citations in peer-reviewed scientific publications.
17
17
 
18
18
 
19
19
 
@@ -0,0 +1,225 @@
1
+ """
2
+ BORIS plugin
3
+
4
+ Export to FERAL (getferal.ai)
5
+ """
6
+
7
+ import pandas as pd
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from PySide6.QtWidgets import QFileDialog
12
+
13
+ # dependencies for CategoryDialog
14
+ from PySide6.QtWidgets import QListWidget, QListWidgetItem, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QDialog
15
+ from PySide6.QtCore import Qt
16
+
17
+
18
+ __version__ = "0.1.1"
19
+ __version_date__ = "2025-11-28"
20
+ __plugin_name__ = "Export observations to FERAL"
21
+ __author__ = "Olivier Friard - University of Torino - Italy"
22
+
23
+
24
+ class CategoryDialog(QDialog):
25
+ def __init__(self, items, parent=None):
26
+ super().__init__(parent)
27
+
28
+ self.setWindowTitle("Organize the videos in categories")
29
+
30
+ self.setModal(True)
31
+
32
+ # Main layout
33
+ main_layout = QVBoxLayout(self)
34
+ lists_layout = QHBoxLayout()
35
+
36
+ # All videos
37
+ self.list_unclassified = self._create_list_widget()
38
+ self.label_unclassified = QLabel("All videos")
39
+ col0_layout = QVBoxLayout()
40
+ col0_layout.addWidget(self.label_unclassified)
41
+ col0_layout.addWidget(self.list_unclassified)
42
+
43
+ self.list_cat1 = self._create_list_widget()
44
+ self.label_cat1 = QLabel("train")
45
+ col1_layout = QVBoxLayout()
46
+ col1_layout.addWidget(self.label_cat1)
47
+ col1_layout.addWidget(self.list_cat1)
48
+
49
+ self.list_cat2 = self._create_list_widget()
50
+ self.label_cat2 = QLabel("val")
51
+ col2_layout = QVBoxLayout()
52
+ col2_layout.addWidget(self.label_cat2)
53
+ col2_layout.addWidget(self.list_cat2)
54
+
55
+ self.list_cat3 = self._create_list_widget()
56
+ self.label_cat3 = QLabel("test")
57
+ col3_layout = QVBoxLayout()
58
+ col3_layout.addWidget(self.label_cat3)
59
+ col3_layout.addWidget(self.list_cat3)
60
+
61
+ self.list_cat4 = self._create_list_widget()
62
+ self.label_cat4 = QLabel("inference")
63
+ col4_layout = QVBoxLayout()
64
+ col4_layout.addWidget(self.label_cat4)
65
+ col4_layout.addWidget(self.list_cat4)
66
+
67
+ # Add all columns to the horizontal layout
68
+ lists_layout.addLayout(col0_layout)
69
+ lists_layout.addLayout(col1_layout)
70
+ lists_layout.addLayout(col2_layout)
71
+ lists_layout.addLayout(col3_layout)
72
+ lists_layout.addLayout(col4_layout)
73
+
74
+ main_layout.addLayout(lists_layout)
75
+
76
+ buttons_layout = QHBoxLayout()
77
+ self.btn_ok = QPushButton("OK")
78
+ self.btn_cancel = QPushButton("Cancel")
79
+
80
+ self.btn_ok.clicked.connect(self.accept)
81
+ self.btn_cancel.clicked.connect(self.reject)
82
+
83
+ buttons_layout.addStretch()
84
+ buttons_layout.addWidget(self.btn_ok)
85
+ buttons_layout.addWidget(self.btn_cancel)
86
+
87
+ main_layout.addLayout(buttons_layout)
88
+
89
+ # Populate "Unclassified" with input items
90
+ for text in items:
91
+ QListWidgetItem(text, self.list_unclassified)
92
+
93
+ def _create_list_widget(self):
94
+ """
95
+ Create a QListWidget ready for drag & drop.
96
+ """
97
+ lw = QListWidget()
98
+ lw.setSelectionMode(QListWidget.ExtendedSelection)
99
+ lw.setDragEnabled(True)
100
+ lw.setAcceptDrops(True)
101
+ lw.setDropIndicatorShown(True)
102
+ lw.setDragDropMode(QListWidget.DragDrop)
103
+ lw.setDefaultDropAction(Qt.MoveAction)
104
+ return lw
105
+
106
+ def get_categories(self):
107
+ """
108
+ Return the content of all categories as a dictionary of lists.
109
+ """
110
+
111
+ def collect(widget):
112
+ return [widget.item(i).text().rstrip("*") for i in range(widget.count())]
113
+
114
+ return {
115
+ "unclassified": collect(self.list_unclassified),
116
+ "train": collect(self.list_cat1),
117
+ "val": collect(self.list_cat2),
118
+ "test": collect(self.list_cat3),
119
+ "inference": collect(self.list_cat4),
120
+ }
121
+
122
+
123
+ def run(df: pd.DataFrame, project: dict):
124
+ """
125
+ Export observations to FERAL
126
+ See https://www.getferal.ai/ > Label Preparation
127
+ """
128
+
129
+ out: dict = {
130
+ "is_multilabel": False,
131
+ "splits": {
132
+ "train": [],
133
+ "val": [],
134
+ "test": [],
135
+ "inference": [],
136
+ },
137
+ }
138
+
139
+ log: list = []
140
+
141
+ # class names
142
+ class_names = {x: project["behaviors_conf"][x]["code"] for x in project["behaviors_conf"]}
143
+ out["class_names"] = class_names
144
+ reversed_class_names = {project["behaviors_conf"][x]["code"]: int(x) for x in project["behaviors_conf"]}
145
+ log.append(f"{class_names=}")
146
+
147
+ observations: list = sorted([x for x in project["observations"]])
148
+ log.append(f"Selected observation: {observations}")
149
+
150
+ labels: dict = {}
151
+ video_list: list = []
152
+ for observation_id in observations:
153
+ log.append("---")
154
+ log.append(observation_id)
155
+
156
+ # check number of media file in player #1
157
+ if len(project["observations"][observation_id]["file"]["1"]) != 1:
158
+ log.append(f"The observation {observation_id} contains more than one video")
159
+ continue
160
+
161
+ # check number of coded subjects
162
+ if len(set([x[1] for x in project["observations"][observation_id]["events"]])) > 1:
163
+ log.append(f"The observation {observation_id} contains more than one subject")
164
+ continue
165
+
166
+ media_file_path: str = project["observations"][observation_id]["file"]["1"][0]
167
+ media_file_name = str(Path(media_file_path).name)
168
+
169
+ # skip if no events
170
+ if not project["observations"][observation_id]["events"]:
171
+ video_list.append(media_file_name)
172
+ log.append(f"No events for observation {observation_id}")
173
+ continue
174
+ else:
175
+ video_list.append(media_file_name + "*")
176
+
177
+ # extract FPS
178
+ FPS = project["observations"][observation_id]["media_info"]["fps"][media_file_path]
179
+ log.append(f"{media_file_name} {FPS=}")
180
+ # extract media duration
181
+ duration = project["observations"][observation_id]["media_info"]["length"][media_file_path]
182
+ log.append(f"{media_file_name} {duration=}")
183
+
184
+ number_of_frames = int(duration / (1 / FPS))
185
+ log.append(f"{number_of_frames=}")
186
+
187
+ labels[media_file_name] = [0] * number_of_frames
188
+
189
+ for idx in range(number_of_frames):
190
+ t = idx * (1 / FPS)
191
+ behaviors = (
192
+ df[(df["Observation id"] == observation_id) & (df["Start (s)"] <= t) & (df["Stop (s)"] >= t)]["Behavior"].unique().tolist()
193
+ )
194
+ if len(behaviors) > 1:
195
+ log.append(f"The observation {observation_id} contains more than one behavior for frame {idx}")
196
+ del labels[media_file_name]
197
+ break
198
+ if behaviors:
199
+ behaviors_idx = reversed_class_names[behaviors[0]]
200
+ labels[media_file_name][idx] = behaviors_idx
201
+
202
+ out["labels"] = labels
203
+
204
+ # splits
205
+ dlg = CategoryDialog(video_list)
206
+
207
+ if dlg.exec(): # Dialog accepted
208
+ result = dlg.get_categories()
209
+ del result["unclassified"]
210
+ out["splits"] = result
211
+
212
+ filename, _ = QFileDialog.getSaveFileName(
213
+ None,
214
+ "Choose a file to save",
215
+ "", # start directory
216
+ "JSON files (*.json);;All files (*.*)",
217
+ )
218
+ if filename:
219
+ with open(filename, "w") as f_out:
220
+ f_out.write(json.dumps(out, separators=(",", ": "), indent=1))
221
+
222
+ else:
223
+ log.append("splits section missing")
224
+
225
+ return "\n".join(log)
@@ -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,
@@ -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
 
@@ -630,7 +630,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
630
630
  """
631
631
  handle click received from coding pad
632
632
  """
633
- q = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier, text=behaviorCode)
633
+ q = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Enter, Qt.KeyboardModifier.NoModifier, text=behaviorCode)
634
+
634
635
  self.keyPressEvent(q)
635
636
 
636
637
  def close_signal_from_coding_pad(self, geometry, preferences):
@@ -644,7 +645,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
644
645
  """
645
646
  handle click received from subjects pad
646
647
  """
647
- q = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier, text="#subject#" + subject)
648
+ q = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Enter, Qt.KeyboardModifier.NoModifier, text="#subject#" + subject)
648
649
  self.keyPressEvent(q)
649
650
 
650
651
  def signal_from_subjects_pad(self, event):
@@ -698,7 +699,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
698
699
  self.subjects_pad = subjects_pad.SubjectsPad(self.pj, filtered_subjects)
699
700
  self.subjects_pad.setWindowFlags(Qt.WindowStaysOnTopHint)
700
701
  self.subjects_pad.sendEventSignal.connect(self.signal_from_subjects_pad)
701
- self.subjects_pad.clickSignal.connect(self.click_signal_from_subjects_pad)
702
+ self.subjects_pad.click_signal.connect(self.click_signal_from_subjects_pad)
702
703
  self.subjects_pad.close_signal.connect(self.close_signal_from_subjects_pad)
703
704
  self.subjects_pad.show()
704
705
 
@@ -1500,7 +1501,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1500
1501
  logging.debug("previous media file")
1501
1502
 
1502
1503
  if self.playerType == cfg.MEDIA:
1503
- #if len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1]) == 1:
1504
+ # if len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1]) == 1:
1504
1505
  # self.seek_mediaplayer(dec(0))
1505
1506
  # return
1506
1507
 
@@ -1510,7 +1511,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
1510
1511
 
1511
1512
  elif self.dw_player[0].player.playlist_count == 1:
1512
1513
  self.seek_mediaplayer(dec(0))
1513
- #self.statusbar.showMessage("There is only one media file", 5000)
1514
+ # self.statusbar.showMessage("There is only one media file", 5000)
1514
1515
 
1515
1516
  if hasattr(self, "spectro"):
1516
1517
  self.spectro.memChunk = -1
@@ -4151,8 +4152,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4151
4152
  called only in IPC mode
4152
4153
  """
4153
4154
  # check if eof reached
4154
- #print(f"{self.dw_player[0].player.playlist_pos=}")
4155
- #print(f"{self.dw_player[0].player.playlist_count=}")
4155
+ # print(f"{self.dw_player[0].player.playlist_pos=}")
4156
+ # print(f"{self.dw_player[0].player.playlist_count=}")
4156
4157
  if self.dw_player[0].player.eof_reached and self.dw_player[0].player.core_idle:
4157
4158
  logging.debug("end of playlist reached")
4158
4159
  if self.dw_player[0].player.playlist_pos is not None and self.dw_player[0].player.playlist_count is not None:
@@ -4220,8 +4221,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4220
4221
 
4221
4222
  # sync players 2..8 if time diff >= 1 s
4222
4223
  if not math.isnan(ct) and not math.isnan(ct0):
4223
- if abs(ct0 - (ct + dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OFFSET][str(n_player + 1)]))) >= 1:
4224
- self.sync_time(n_player, ct0) # self.seek_mediaplayer(ct0, n_player)
4224
+ if (
4225
+ abs(ct0 - (ct + dec(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OFFSET][str(n_player + 1)])))
4226
+ >= 1
4227
+ ):
4228
+ self.sync_time(n_player, float(ct0)) # self.seek_mediaplayer(ct0, n_player)
4225
4229
 
4226
4230
  currentTimeOffset = dec(cumulative_time_pos + self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TIME_OFFSET])
4227
4231
 
@@ -4254,7 +4258,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4254
4258
  # current media name
4255
4259
  playlist = self.dw_player[0].player.playlist
4256
4260
  playlist_pos = self.dw_player[0].player.playlist_pos
4257
- if playlist is not None and playlist_pos is not None:
4261
+ if playlist is not None and playlist_pos is not None and playlist:
4258
4262
  current_media_name = Path(playlist[playlist_pos]["filename"]).name
4259
4263
  current_playlist_index = playlist_pos
4260
4264
  else:
@@ -4335,7 +4339,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4335
4339
  # set video scroll bar
4336
4340
 
4337
4341
  if scroll_slider and not self.user_move_slider:
4338
- self.video_slider.setValue(round(current_media_time_pos / current_media_duration * (cfg.SLIDER_MAXIMUM - 1)))
4342
+ if current_media_time_pos is not None and current_media_duration is not None:
4343
+ self.video_slider.setValue(round(current_media_time_pos / current_media_duration * (cfg.SLIDER_MAXIMUM - 1)))
4339
4344
 
4340
4345
  def mpv_eof_reached(self):
4341
4346
  """
@@ -4842,7 +4847,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
4842
4847
 
4843
4848
  # play / pause with space bar
4844
4849
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
4845
- if ek in (Qt.Key_Space, Qt.Key_Enter, Qt.Key_Return):
4850
+ # if ek in (Qt.Key_Space, Qt.Key_Enter, Qt.Key_Return):
4851
+ if ek == Qt.Key_Space:
4846
4852
  if self.liveObservationStarted:
4847
4853
  if (
4848
4854
  dialog.MessageDialog(cfg.programName, "Are you sure to stop the current live observation?", [cfg.YES, cfg.NO])
@@ -5485,7 +5491,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5485
5491
  if self.dw_player[0].player.playlist_pos == self.dw_player[0].player.playlist_count - 1:
5486
5492
  self.seek_mediaplayer(dec(0))
5487
5493
 
5488
-
5489
5494
  # check if player 1 is ended
5490
5495
  for i, dw in enumerate(self.dw_player):
5491
5496
  if (
@@ -5867,7 +5872,7 @@ def main():
5867
5872
  QMessageBox.warning(
5868
5873
  None,
5869
5874
  cfg.programName,
5870
- (f"This version of BORIS for macOS is still EXPERIMENTAL and should be used at your own risk."),
5875
+ ("This version of BORIS for macOS is still EXPERIMENTAL and should be used at your own risk."),
5871
5876
  QMessageBox.Ok | QMessageBox.Default,
5872
5877
  QMessageBox.NoButton,
5873
5878
  )
@@ -5898,4 +5903,3 @@ def main():
5898
5903
  del window
5899
5904
 
5900
5905
  sys.exit(return_code)
5901
-
@@ -134,7 +134,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
134
134
  self.saveMapAction.setShortcut("Ctrl+S")
135
135
  self.saveMapAction.setStatusTip("Save modifiers map")
136
136
  self.saveMapAction.setEnabled(False)
137
- self.saveMapAction.triggered.connect(self.saveMap_clicked)
137
+ self.saveMapAction.triggered.connect(self.save_map_clicked)
138
138
 
139
139
  self.saveAsMapAction = QAction(QIcon(), "Save modifiers map as", self)
140
140
  self.saveAsMapAction.setStatusTip("Save modifiers map as")
@@ -343,7 +343,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
343
343
  )
344
344
 
345
345
  if response == cfg.SAVE:
346
- if not self.saveMap_clicked():
346
+ if not self.save_map_clicked():
347
347
  event.ignore()
348
348
 
349
349
  if response == cfg.CANCEL:
@@ -567,7 +567,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
567
567
  )
568
568
 
569
569
  if response == "Save":
570
- if not self.saveMap_clicked():
570
+ if not self.save_map_clicked():
571
571
  return
572
572
 
573
573
  if response == "Cancel":
@@ -606,7 +606,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
606
606
  )
607
607
 
608
608
  if response == cfg.SAVE:
609
- if not self.saveMap_clicked():
609
+ if not self.save_map_clicked():
610
610
  return
611
611
 
612
612
  if response == cfg.CANCEL:
@@ -733,7 +733,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
733
733
  self.fileName += ".boris_map"
734
734
  self.saveMap()
735
735
 
736
- def saveMap_clicked(self):
736
+ def save_map_clicked(self):
737
737
  if not self.fileName:
738
738
  fn = QFileDialog(self).getSaveFileName(
739
739
  self,
@@ -746,7 +746,7 @@ class ModifiersMapCreatorWindow(QMainWindow):
746
746
  else:
747
747
  self.fileName = fn
748
748
 
749
- if self.fileName and Path(self.fileName).suffix() != ".boris_map":
749
+ if self.fileName and Path(self.fileName).suffix != ".boris_map":
750
750
  self.fileName += ".boris_map"
751
751
 
752
752
  if self.fileName: