boris-behav-obs 9.7.8__tar.gz → 9.8.1__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.8 → boris_behav_obs-9.8.1}/PKG-INFO +2 -2
  2. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/README.md +1 -1
  3. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/__init__.py +1 -1
  4. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/__main__.py +1 -1
  5. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/about.py +4 -3
  6. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/add_modifier.py +1 -1
  7. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/advanced_event_filtering.py +1 -1
  8. boris_behav_obs-9.8.1/boris/analysis_plugins/export_to_feral.py +336 -0
  9. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_weighted_cohen_kappa.py +2 -2
  10. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/behav_coding_map_creator.py +8 -8
  11. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/behavior_binary_table.py +1 -1
  12. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/behaviors_coding_map.py +1 -1
  13. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/boris_cli.py +1 -1
  14. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/cmd_arguments.py +1 -1
  15. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/coding_pad.py +5 -4
  16. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/config.py +15 -3
  17. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/config_file.py +18 -19
  18. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/connections.py +12 -13
  19. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/converters.py +1 -1
  20. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/converters_ui.py +2 -3
  21. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/cooccurence.py +1 -1
  22. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/core.py +187 -181
  23. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/core_qrc.py +1830 -1967
  24. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/core_ui.py +1 -1
  25. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/db_functions.py +5 -14
  26. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/dialog.py +24 -24
  27. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/edit_event.py +1 -1
  28. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/event_operations.py +1 -1
  29. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/events_cursor.py +1 -1
  30. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/events_snapshots.py +133 -78
  31. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/exclusion_matrix.py +1 -1
  32. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/export_events.py +49 -43
  33. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/export_observation.py +1 -1
  34. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/external_processes.py +1 -1
  35. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/geometric_measurement.py +1 -1
  36. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/gui_utilities.py +1 -1
  37. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/image_overlay.py +1 -1
  38. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/import_observations.py +1 -1
  39. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/ipc_mpv.py +1 -1
  40. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/irr.py +1 -1
  41. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/latency.py +1 -1
  42. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/measurement_widget.py +1 -1
  43. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/media_file.py +1 -1
  44. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/menu_options.py +14 -12
  45. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/modifier_coding_map_creator.py +7 -7
  46. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/modifiers_coding_map.py +1 -1
  47. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observation.py +13 -14
  48. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observation_operations.py +49 -41
  49. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observations_list.py +1 -1
  50. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/otx_parser.py +1 -1
  51. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/param_panel.py +1 -1
  52. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/player_dock_widget.py +1 -1
  53. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_data_module.py +1 -1
  54. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_events.py +1 -1
  55. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_events_rt.py +1 -1
  56. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_spectrogram_rt.py +44 -72
  57. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plot_waveform_rt.py +5 -2
  58. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/plugins.py +61 -24
  59. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/preferences.py +35 -4
  60. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/preferences_ui.py +50 -18
  61. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project.py +1 -1
  62. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project_functions.py +20 -23
  63. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project_import_export.py +1 -1
  64. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/select_modifiers.py +1 -1
  65. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/select_observations.py +22 -23
  66. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/select_subj_behav.py +4 -4
  67. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/state_events.py +1 -1
  68. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/subjects_pad.py +3 -3
  69. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/synthetic_time_budget.py +1 -1
  70. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/time_budget_functions.py +1 -1
  71. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/time_budget_widget.py +1 -1
  72. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/transitions.py +1 -1
  73. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/utilities.py +10 -1
  74. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/version.py +3 -3
  75. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/video_equalizer.py +1 -1
  76. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/video_operations.py +4 -3
  77. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/view_df.py +28 -4
  78. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/write_event.py +1 -1
  79. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/PKG-INFO +2 -2
  80. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/SOURCES.txt +1 -0
  81. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/pyproject.toml +1 -1
  82. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/LICENSE.TXT +0 -0
  83. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/MANIFEST.in +0 -0
  84. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/README.TXT +0 -0
  85. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/add_modifier_ui.py +0 -0
  86. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/__init__.py +0 -0
  87. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/_latency.py +0 -0
  88. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_cohen_kappa.py +0 -0
  89. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +0 -0
  90. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +0 -0
  91. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/list_of_dataframe_columns.py +0 -0
  92. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/number_of_occurences.py +0 -0
  93. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
  94. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/analysis_plugins/time_budget.py +0 -0
  95. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/dev.py +0 -0
  96. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/duration_widget.py +0 -0
  97. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/edit_event_ui.py +0 -0
  98. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/mpv.py +0 -0
  99. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/mpv2.py +0 -0
  100. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/observation_ui.py +0 -0
  101. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/param_panel_ui.py +0 -0
  102. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/__init__.py +0 -0
  103. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/const.py +0 -0
  104. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/dict.py +0 -0
  105. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/func.py +0 -0
  106. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/interval.py +0 -0
  107. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/portion/io.py +0 -0
  108. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/project_ui.py +0 -0
  109. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/qrc_boris.py +0 -0
  110. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/qrc_boris5.py +0 -0
  111. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/video_equalizer_ui.py +0 -0
  112. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris/view_df_ui.py +0 -0
  113. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  114. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/entry_points.txt +0 -0
  115. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/requires.txt +0 -0
  116. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/boris_behav_obs.egg-info/top_level.txt +0 -0
  117. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/setup.cfg +0 -0
  118. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_db_functions.py +0 -0
  119. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_export_observation.py +0 -0
  120. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_irr.py +0 -0
  121. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_observation_gui.py +0 -0
  122. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_otx_parser.py +0 -0
  123. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_preferences_gui.py +0 -0
  124. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_project_functions.py +0 -0
  125. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_time_budget.py +0 -0
  126. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/tests/test_utilities.py +0 -0
  127. {boris_behav_obs-9.7.8 → boris_behav_obs-9.8.1}/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.8
3
+ Version: 9.8.1
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 2442 citations in peer-reviewed scientific publications.
54
+ The BORIS paper has more than 2491 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 2442 citations in peer-reviewed scientific publications.
16
+ The BORIS paper has more than 2491 citations in peer-reviewed scientific publications.
17
17
 
18
18
 
19
19
 
@@ -2,7 +2,7 @@
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
4
 
5
- Copyright 2012-2025 Olivier Friard
5
+ Copyright 2012-2026 Olivier Friard
6
6
 
7
7
  This file is part of BORIS.
8
8
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -80,7 +80,8 @@ def actionAbout_activated(self):
80
80
  programs_versions.extend(["\nGraphViz", gv_result if "graphviz" in gv_result else "not installed", "https://www.graphviz.org/"])
81
81
 
82
82
  about_dialog: QMessageBox = QMessageBox()
83
- about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
83
+ # about_dialog.setIconPixmap(QPixmap(":/boris_unito"))
84
+ about_dialog.setIconPixmap(QPixmap(":/dbios_unito"))
84
85
 
85
86
  about_dialog.setWindowTitle(f"About {cfg.programName}")
86
87
  about_dialog.setStandardButtons(QMessageBox.Ok)
@@ -90,7 +91,7 @@ def actionAbout_activated(self):
90
91
  about_dialog.setInformativeText(
91
92
  (
92
93
  f"<b>{cfg.programName}</b> v. {version.__version__} - {version.__version_date__}"
93
- "<p>Copyright &copy; 2012-2025 Olivier Friard - Marco Gamba<br>"
94
+ "<p>Copyright &copy; 2012-2026 Olivier Friard - Marco Gamba<br>"
94
95
  "Department of Life Sciences and Systems Biology<br>"
95
96
  "University of Torino - Italy<br>"
96
97
  "<br>"
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -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)
@@ -4,9 +4,9 @@ BORIS plugin
4
4
  Inter Rater Reliability (IRR) Weighted Cohen's Kappa
5
5
  """
6
6
 
7
- import pandas as pd
8
- from typing import List, Tuple, Dict, Optional
7
+ from typing import Dict, List, Optional, Tuple
9
8
 
9
+ import pandas as pd
10
10
  from PySide6.QtWidgets import QInputDialog
11
11
 
12
12
  __version__ = "0.0.3"
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -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,
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -3,7 +3,7 @@ BORIS CLI
3
3
 
4
4
  Behavioral Observation Research Interactive Software Command Line Interface
5
5
 
6
- Copyright 2012-2025 Olivier Friard
6
+ Copyright 2012-2026 Olivier Friard
7
7
 
8
8
  This program is free software; you can redistribute it and/or modify
9
9
  it under the terms of the GNU General Public License as published by
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
 
7
7
  This program is free software; you can redistribute it and/or modify
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2025 Olivier Friard
4
+ Copyright 2012-2026 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -165,6 +165,7 @@ REMOVE = "Remove"
165
165
  SAVE = "Save"
166
166
  DISCARD = "Discard"
167
167
  OK = "OK"
168
+ ABORT = "Abort"
168
169
  OVERWRITE = "Overwrite"
169
170
  OVERWRITE_ALL = "Overwrite all"
170
171
  SKIP = "Skip"
@@ -493,6 +494,13 @@ FRAME_DEFAULT_CACHE_SIZE = 1
493
494
 
494
495
  EXCLUDED = "excluded"
495
496
 
497
+ # codes for Input_dialog class
498
+ CHECKBOX = "cb"
499
+ LINE_EDIT = "le"
500
+ SPINBOX = "sb"
501
+ DOUBLE_SPINBOX = "dsb"
502
+ ITEMS_LIST = "il"
503
+
496
504
  # modifiers
497
505
  MODIFIERS = "modifiers"
498
506
  SINGLE_SELECTION = 0
@@ -539,10 +547,13 @@ SPECTROGRAM_DEFAULT_TIME_INTERVAL = 10
539
547
  SPECTROGRAM_WINDOW_TYPE = "SPECTROGRAM_WINDOW_TYPE"
540
548
  SPECTROGRAM_DEFAULT_WINDOW_TYPE = "hanning"
541
549
  SPECTROGRAM_NFFT = "SPECTROGRAM_NFFT"
542
- SPECTROGRAM_DEFAULT_NFFT = "1024"
550
+ SPECTROGRAM_DEFAULT_NFFT = "256"
543
551
  SPECTROGRAM_NOVERLAP = "SPECTROGRAM_NOVERLAP"
544
- SPECTROGRAM_DEFAULT_NOVERLAP = 900
552
+ SPECTROGRAM_DEFAULT_NOVERLAP = 128
545
553
  SPECTROGRAM_VMIN = "SPECTROGRAM_VMIN"
554
+
555
+ SPECTROGRAM_USE_VMIN_VMAX = "SPECTROGRAM_USE_VMIN_VMAX"
556
+ SPECTROGRAM_USE_VMIN_VMAX_DEFAULT = False
546
557
  SPECTROGRAM_DEFAULT_VMIN = -100
547
558
  SPECTROGRAM_VMAX = "SPECTROGRAM_VMAX"
548
559
  SPECTROGRAM_DEFAULT_VMAX = -20
@@ -737,6 +748,7 @@ INIT_PARAM = {
737
748
  PROJECT_FILE_INDENTATION: PROJECT_FILE_INDENTATION_DEFAULT_VALUE,
738
749
  f"{MEDIA} tw fields": MEDIA_TW_EVENTS_FIELDS_DEFAULT,
739
750
  # FRAME_STEP_SIZE: FRAME_STEP_SIZE_DEFAULT_VALUE,
751
+ TOOLBAR_ICON_SIZE: DEFAULT_TOOLBAR_ICON_SIZE_VALUE,
740
752
  }
741
753
 
742
754
  SDIS_EXT = "sds"