boris-behav-obs 9.6.5__py2.py3-none-any.whl → 9.7__py2.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/cmd_arguments.py CHANGED
@@ -30,6 +30,7 @@ def parse_arguments():
30
30
  parser = OptionParser(usage=usage)
31
31
 
32
32
  parser.add_option("-d", "--debug", action="store_true", default=False, dest="debug", help="Use debugging mode")
33
+ parser.add_option("-q", "--quit", action="store_true", default=False, dest="quit", help="Quit after launch")
33
34
  parser.add_option("-v", "--version", action="store_true", default=False, dest="version", help="Print version")
34
35
  parser.add_option("-n", "--nosplashscreen", action="store_true", default=False, help="No splash screen")
35
36
  parser.add_option("-p", "--project", action="store", default="", dest="project", help="Project file")
boris/connections.py CHANGED
@@ -120,6 +120,7 @@ def connections(self):
120
120
  self.actionSelect_observations.triggered.connect(lambda: event_operations.select_events_between_activated(self))
121
121
 
122
122
  self.actionEdit_selected_events.triggered.connect(lambda: event_operations.edit_selected_events(self))
123
+ self.action_add_comment.triggered.connect(lambda: event_operations.add_comment(self))
123
124
  self.actionEdit_event_time.triggered.connect(lambda: event_operations.edit_time_selected_events(self))
124
125
 
125
126
  self.actionCopy_events.triggered.connect(lambda: event_operations.copy_selected_events(self))
@@ -343,6 +344,8 @@ def connections(self):
343
344
 
344
345
  self.tv_events.addAction(self.actionAdd_event)
345
346
  self.tv_events.addAction(self.actionEdit_selected_events)
347
+ self.tv_events.addAction(self.action_add_comment)
348
+
346
349
  self.tv_events.addAction(self.actionEdit_event_time)
347
350
 
348
351
  self.tv_events.addAction(self.actionCopy_events)
boris/core.py CHANGED
@@ -236,8 +236,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
236
236
  processes: list = [] # list of QProcess processes
237
237
  overlays: dict = {} # dict for storing video overlays
238
238
 
239
- undo_queue = deque()
240
- undo_description = deque()
239
+ undo_queue = deque() # queue for undoing event operations
240
+ undo_description = deque() # queue for description of event operations
241
241
 
242
242
  current_player: int = 0 # id of the selected (left click) video player
243
243
 
@@ -3728,77 +3728,78 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3728
3728
  QMessageBox.warning(self, cfg.programName, "Function not yet implemented")
3729
3729
  return
3730
3730
 
3731
- if not self.observationId:
3732
- self.no_observation()
3733
- return
3734
-
3735
- if self.twEvents.selectedItems():
3736
- row_s = self.twEvents.selectedItems()[0].row()
3737
- row_e = self.twEvents.selectedItems()[-1].row()
3738
- eventtime_s = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row_s][0]
3739
- eventtime_e = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row_e][0]
3740
-
3741
- durations = [] # in seconds
3742
-
3743
- # TODO: check for 2nd player
3744
- for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1]:
3745
- durations.append(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]["length"][mediaFile])
3746
-
3747
- mediaFileIdx_s = [idx1 for idx1, x in enumerate(durations) if eventtime_s >= sum(durations[0:idx1])][-1]
3748
- media_path_s = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1][mediaFileIdx_s]
3749
-
3750
- mediaFileIdx_e = [idx1 for idx1, x in enumerate(durations) if eventtime_e >= sum(durations[0:idx1])][-1]
3751
- media_path_e = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1][mediaFileIdx_e]
3752
-
3753
- # calculate time for current media file in case of many queued media files
3754
-
3755
- eventtime_onmedia_s = round(eventtime_s - util.float2decimal(sum(durations[0:mediaFileIdx_s])), 3)
3756
- eventtime_onmedia_e = round(eventtime_e - util.float2decimal(sum(durations[0:mediaFileIdx_e])), 3)
3757
-
3758
- if media_path_s != media_path_e:
3759
- return
3760
-
3761
- media_path = media_path_s
3731
+ # if not self.observationId:
3732
+ # self.no_observation()
3733
+ # return
3762
3734
 
3763
- # example of external command defined in environment:
3764
- # export BORISEXTERNAL="myprog -i {MEDIA_PATH} -s {START_S} -e {END_S} {DURATION_MS} --other"
3765
-
3766
- if "BORISEXTERNAL" in os.environ:
3767
- external_command_template = os.environ["BORISEXTERNAL"]
3768
- else:
3769
- return
3770
-
3771
- external_command = external_command_template.format(
3772
- OBS_ID=self.observationId,
3773
- MEDIA_PATH=f'"{media_path}"',
3774
- MEDIA_BASENAME=f'"{os.path.basename(media_path)}"',
3775
- START_S=eventtime_onmedia_s,
3776
- END_S=eventtime_onmedia_e,
3777
- START_MS=eventtime_onmedia_s * 1000,
3778
- END_MS=eventtime_onmedia_e * 1000,
3779
- DURATION_S=eventtime_onmedia_e - eventtime_onmedia_s,
3780
- DURATION_MS=(eventtime_onmedia_e - eventtime_onmedia_s) * 1000,
3781
- )
3782
-
3783
- print(external_command)
3784
- """
3785
- p = subprocess.Popen(external_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
3786
- """
3787
- """
3788
- if eventtimeS == eventtimeE:
3789
- q = []
3790
- else:
3791
- durationsec = eventtimeE-eventtimeS
3792
- q = ["--durationmsec",str(int(durationsec*1000))]
3793
- args = [ex, "-f",os.path.abspath(fn),"--seekmsec",str(int(eventtimeS*1000)),*q,*("--size 1 --track 1 --redetect 100")
3794
- .split(" ")]
3795
- if os.path.split(fn)[1].split("_")[0] in set(["A1","A2","A3","A4","A5","A6","A7","A8","A9","A10"]):
3796
- args.append("--flip")
3797
- args.append("2")
3798
- print (os.path.split(fn)[1].split("_")[0])
3799
- print ("running",ex,"with",args,"in",os.path.split(ex)[0])
3800
- #pid = subprocess.Popen(args,executable=ex,cwd=os.path.split(ex)[0])
3801
- """
3735
+ #
3736
+ # if self.twEvents.selectedItems():
3737
+ # row_s = self.twEvents.selectedItems()[0].row()
3738
+ # row_e = self.twEvents.selectedItems()[-1].row()
3739
+ # eventtime_s = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row_s][0]
3740
+ # eventtime_e = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row_e][0]
3741
+ #
3742
+ # durations = [] # in seconds
3743
+ #
3744
+ # # TODO: check for 2nd player
3745
+ # for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1]:
3746
+ # durations.append(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]["length"][mediaFile])
3747
+ #
3748
+ # mediaFileIdx_s = [idx1 for idx1, x in enumerate(durations) if eventtime_s >= sum(durations[0:idx1])][-1]
3749
+ # media_path_s = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1][mediaFileIdx_s]
3750
+ #
3751
+ # mediaFileIdx_e = [idx1 for idx1, x in enumerate(durations) if eventtime_e >= sum(durations[0:idx1])][-1]
3752
+ # media_path_e = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][cfg.PLAYER1][mediaFileIdx_e]
3753
+ #
3754
+ # # calculate time for current media file in case of many queued media files
3755
+ #
3756
+ # eventtime_onmedia_s = round(eventtime_s - util.float2decimal(sum(durations[0:mediaFileIdx_s])), 3)
3757
+ # eventtime_onmedia_e = round(eventtime_e - util.float2decimal(sum(durations[0:mediaFileIdx_e])), 3)
3758
+ #
3759
+ # if media_path_s != media_path_e:
3760
+ # return
3761
+ #
3762
+ # media_path = media_path_s
3763
+ #
3764
+ # # example of external command defined in environment:
3765
+ # # export BORISEXTERNAL="myprog -i {MEDIA_PATH} -s {START_S} -e {END_S} {DURATION_MS} --other"
3766
+ #
3767
+ # if "BORISEXTERNAL" in os.environ:
3768
+ # external_command_template = os.environ["BORISEXTERNAL"]
3769
+ # else:
3770
+ # return
3771
+ #
3772
+ # external_command = external_command_template.format(
3773
+ # OBS_ID=self.observationId,
3774
+ # MEDIA_PATH=f'"{media_path}"',
3775
+ # MEDIA_BASENAME=f'"{os.path.basename(media_path)}"',
3776
+ # START_S=eventtime_onmedia_s,
3777
+ # END_S=eventtime_onmedia_e,
3778
+ # START_MS=eventtime_onmedia_s * 1000,
3779
+ # END_MS=eventtime_onmedia_e * 1000,
3780
+ # DURATION_S=eventtime_onmedia_e - eventtime_onmedia_s,
3781
+ # DURATION_MS=(eventtime_onmedia_e - eventtime_onmedia_s) * 1000,
3782
+ # )
3783
+ #
3784
+ # print(external_command)
3785
+ # """
3786
+ # p = subprocess.Popen(external_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
3787
+ # """
3788
+ # """
3789
+ # if eventtimeS == eventtimeE:
3790
+ # q = []
3791
+ # else:
3792
+ # durationsec = eventtimeE-eventtimeS
3793
+ # q = ["--durationmsec",str(int(durationsec*1000))]
3794
+ # args = [ex, "-f",os.path.abspath(fn),"--seekmsec",str(int(eventtimeS*1000)),*q,*("--size 1 --track 1 --redetect 100")
3795
+ # .split(" ")]
3796
+ # if os.path.split(fn)[1].split("_")[0] in set(["A1","A2","A3","A4","A5","A6","A7","A8","A9","A10"]):
3797
+ # args.append("--flip")
3798
+ # args.append("2")
3799
+ # print (os.path.split(fn)[1].split("_")[0])
3800
+ # print ("running",ex,"with",args,"in",os.path.split(ex)[0])
3801
+ # #pid = subprocess.Popen(args,executable=ex,cwd=os.path.split(ex)[0])
3802
+ # """
3802
3803
 
3803
3804
  def no_media(self):
3804
3805
  QMessageBox.warning(self, cfg.programName, "There is no media available")
@@ -5666,16 +5667,31 @@ def main():
5666
5667
  ret, msg = util.check_ffmpeg_path()
5667
5668
  if not ret:
5668
5669
  if sys.platform.startswith("win"):
5669
- QMessageBox.warning(
5670
+ import ctypes
5671
+
5672
+ MessageBoxTimeoutW = ctypes.windll.user32.MessageBoxTimeoutW
5673
+ MessageBoxTimeoutW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint]
5674
+ ctypes.windll.user32.MessageBoxTimeoutW(
5670
5675
  None,
5671
- cfg.programName,
5672
- "FFmpeg is not available.<br>It will be downloaded",
5673
- QMessageBox.Ok | QMessageBox.Default,
5674
- QMessageBox.NoButton,
5675
- )
5676
+ "The FFmpeg framework is not available.\nIt will be downloaded from the BORIS GitHub repository.",
5677
+ "FFmpeg",
5678
+ 0,
5679
+ 0,
5680
+ 10000,
5681
+ ) # time out
5682
+
5683
+ # if (not options.nosplashscreen):
5684
+ # QMessageBox.warning(
5685
+ # None,
5686
+ # cfg.programName,
5687
+ # "FFmpeg is not available.<br>It will be downloaded from the BORIS GitHub repository",
5688
+ # QMessageBox.Ok | QMessageBox.Default,
5689
+ # QMessageBox.NoButton,
5690
+ # )
5691
+ logging.info("FFmpeg is not available. It will be downloaded from the BORIS GitHub repository")
5676
5692
 
5677
5693
  # download ffmpeg and ffprobe from https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/
5678
- url = "https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/"
5694
+ url: str = "https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/"
5679
5695
 
5680
5696
  # search where to download ffmpeg
5681
5697
  ffmpeg_dir = Path(__file__).parent / "misc"
@@ -5807,6 +5823,10 @@ def main():
5807
5823
  if not options.nosplashscreen and (sys.platform != "darwin"):
5808
5824
  splash.finish(window)
5809
5825
 
5826
+ # quit just after launch (used in the deployment procedure)
5827
+ if options.quit:
5828
+ sys.exit()
5829
+
5810
5830
  return_code = app.exec()
5811
5831
 
5812
5832
  del window
boris/core_ui.py CHANGED
@@ -3,7 +3,7 @@
3
3
  ################################################################################
4
4
  ## Form generated from reading UI file 'core.ui'
5
5
  ##
6
- ## Created by: Qt User Interface Compiler version 6.9.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
  ################################################################################
@@ -382,6 +382,9 @@ class Ui_MainWindow(object):
382
382
  self.actionCreate_video_spectrogram.setObjectName(u"actionCreate_video_spectrogram")
383
383
  self.action_change_time_offset_of_players = QAction(MainWindow)
384
384
  self.action_change_time_offset_of_players.setObjectName(u"action_change_time_offset_of_players")
385
+ self.action_add_comment = QAction(MainWindow)
386
+ self.action_add_comment.setObjectName(u"action_add_comment")
387
+ self.action_add_comment.setMenuRole(QAction.MenuRole.NoRole)
385
388
  self.centralwidget = QWidget(MainWindow)
386
389
  self.centralwidget.setObjectName(u"centralwidget")
387
390
  self.horizontalLayout_2 = QHBoxLayout(self.centralwidget)
@@ -488,7 +491,7 @@ class Ui_MainWindow(object):
488
491
  MainWindow.setCentralWidget(self.centralwidget)
489
492
  self.menubar = QMenuBar(MainWindow)
490
493
  self.menubar.setObjectName(u"menubar")
491
- self.menubar.setGeometry(QRect(0, 0, 1509, 25))
494
+ self.menubar.setGeometry(QRect(0, 0, 1509, 20))
492
495
  self.menuHelp = QMenu(self.menubar)
493
496
  self.menuHelp.setObjectName(u"menuHelp")
494
497
  self.menuFile = QMenu(self.menubar)
@@ -1037,6 +1040,7 @@ class Ui_MainWindow(object):
1037
1040
  self.action_load_plugins.setText(QCoreApplication.translate("MainWindow", u"Load plugins", None))
1038
1041
  self.actionCreate_video_spectrogram.setText(QCoreApplication.translate("MainWindow", u"Create video spectrogram", None))
1039
1042
  self.action_change_time_offset_of_players.setText(QCoreApplication.translate("MainWindow", u"Change time offset of players", None))
1043
+ self.action_add_comment.setText(QCoreApplication.translate("MainWindow", u"Add/Edit comment", None))
1040
1044
  self.lbLogoBoris.setText("")
1041
1045
  self.lbLogoUnito.setText("")
1042
1046
  self.lb_player_status.setText(QCoreApplication.translate("MainWindow", u"lb_player_status", None))
boris/event_operations.py CHANGED
@@ -454,6 +454,61 @@ def select_events_between_activated(self):
454
454
  self.tv_events.setSelectionMode(QAbstractItemView.ExtendedSelection)
455
455
 
456
456
 
457
+ def add_comment(self):
458
+ """
459
+ add a comment to the selected events
460
+ operation can be undone with Undo
461
+ """
462
+ tvevents_rows_to_edit = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
463
+ if not len(tvevents_rows_to_edit):
464
+ QMessageBox.warning(self, cfg.programName, "No event selected!")
465
+ return
466
+
467
+ comment_str: str = ""
468
+ if len(tvevents_rows_to_edit) == 1:
469
+ pj_event_idx = self.tv_idx2events_idx[self.tv_events.selectionModel().selectedIndexes()[0].row()]
470
+ comment_str = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
471
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
472
+ ]
473
+ else:
474
+ # check if comment is the same in all selected events
475
+
476
+ if (
477
+ len(
478
+ set(
479
+ [
480
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][self.tv_idx2events_idx[tvevents_row]][
481
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
482
+ ]
483
+ for tvevents_row in tvevents_rows_to_edit
484
+ ]
485
+ )
486
+ )
487
+ == 1
488
+ ):
489
+ pj_event_idx = self.tv_idx2events_idx[self.tv_events.selectionModel().selectedIndexes()[0].row()]
490
+ comment_str = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
491
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
492
+ ]
493
+
494
+ new_comment, ok = QInputDialog.getText(self, "Add/Edit a comment", "Comment:", text=comment_str)
495
+ if not ok:
496
+ return
497
+
498
+ # fill the undo list
499
+ fill_events_undo_list(self, "Undo last comment operation")
500
+
501
+ for tvevents_row in tvevents_rows_to_edit:
502
+ pj_event_idx = self.tv_idx2events_idx[tvevents_row]
503
+
504
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
505
+ cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
506
+ ] = new_comment
507
+
508
+ # reload all events in tw
509
+ self.load_tw_events(self.observationId)
510
+
511
+
457
512
  def edit_selected_events(self):
458
513
  """
459
514
  edit one or more selected events for subject, behavior and/or comment
@@ -19,18 +19,19 @@ Copyright 2012-2025 Olivier Friard
19
19
  MA 02110-1301, USA.
20
20
  """
21
21
 
22
- from math import log2, floor
23
- import os
24
22
  import logging
25
- import time
26
- import tempfile
23
+ from collections import deque
24
+ import datetime as dt
25
+ from decimal import Decimal as dec
27
26
  import json
28
- import subprocess
27
+ from math import log2, floor
28
+ import os
29
+ import pathlib as pl
29
30
  import socket
31
+ import subprocess
30
32
  import sys
31
- from decimal import Decimal as dec
32
- import pathlib as pl
33
- import datetime as dt
33
+ import tempfile
34
+ import time
34
35
  from typing import List, Tuple, Optional
35
36
 
36
37
 
@@ -1183,6 +1184,10 @@ def close_observation(self):
1183
1184
 
1184
1185
  self.observationId = ""
1185
1186
 
1187
+ # delete undo queue
1188
+ self.undo_queue = deque()
1189
+ self.undo_description = deque()
1190
+
1186
1191
  if self.playerType in (cfg.MEDIA, cfg.IMAGES):
1187
1192
  """
1188
1193
  for idx, _ in enumerate(self.dw_player):
boris/plugins.py CHANGED
@@ -310,7 +310,11 @@ def run_plugin(self, plugin_name):
310
310
 
311
311
  logging.info("preparing dataframe for plugin")
312
312
 
313
- df = project_functions.project2dataframe(self.pj, selected_observations)
313
+ message, df = project_functions.project2dataframe(self.pj, selected_observations)
314
+ if message:
315
+ logging.critical(message)
316
+ QMessageBox.critical(self, cfg.programName, message)
317
+ return
314
318
 
315
319
  logging.info("done")
316
320
 
@@ -466,7 +466,7 @@ def check_project_integrity(
466
466
  out += "<br><br>" if out else ""
467
467
  out += f"Observation: <b>{obs_id}</b><br>{msg}"
468
468
 
469
- out_events = ""
469
+ out_events: str = ""
470
470
  for obs_id in pj[cfg.OBSERVATIONS]:
471
471
  # check if timestamp between -2147483647 and 2147483647
472
472
  for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]:
@@ -573,6 +573,41 @@ def check_project_integrity(
573
573
  out += "<br><br>" if out else ""
574
574
  out += tmp_out
575
575
 
576
+ # check if the number of coded modifiers correspond to the number of sets of modifier
577
+ obs_results: dict = {}
578
+ for obs_id in pj[cfg.OBSERVATIONS]:
579
+ for event_idx, event in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
580
+ # event[2]
581
+ for idx in pj[cfg.ETHOGRAM]:
582
+ if pj[cfg.ETHOGRAM][idx]["code"] == event[2]:
583
+ break
584
+ else:
585
+ raise
586
+ if (not event[3]) and not pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]:
587
+ continue
588
+
589
+ if len(event[3].split("|")) != len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS]):
590
+ print("behavior", event[2])
591
+ print(f"modifier(s) #{event[3]}#", len(event[3].split("|")))
592
+ print(pj[cfg.ETHOGRAM][idx]["code"], pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])
593
+ print()
594
+ if obs_id not in obs_results:
595
+ obs_results[obs_id] = []
596
+
597
+ obs_results[obs_id].append(
598
+ (
599
+ f"Event #{event_idx}: the coded modifiers for {event[2]} are {len(event[3].split('|'))} "
600
+ f"but {len(pj[cfg.ETHOGRAM][idx][cfg.MODIFIERS])} sets were defined in ethogram."
601
+ )
602
+ )
603
+
604
+ if obs_results:
605
+ out += "<br><br>" if out else ""
606
+ for o in obs_results:
607
+ out += f"<br>Observation <b>{o}</b>:<br>"
608
+ out += "<br>".join(obs_results[o])
609
+ out += "<br><br>"
610
+
576
611
  return out
577
612
 
578
613
 
@@ -1779,7 +1814,7 @@ def explore_project(self) -> None:
1779
1814
  QMessageBox.information(self, cfg.programName, "No events found")
1780
1815
 
1781
1816
 
1782
- def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1817
+ def project2dataframe(pj: dict, observations_list: list = []) -> Tuple[str, pd.DataFrame]:
1783
1818
  """
1784
1819
  returns a pandas dataframe containing observations data
1785
1820
  """
@@ -1950,7 +1985,10 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1950
1985
  count_set = 0
1951
1986
  for modifier_set in all_modifier_sets:
1952
1987
  if event[2] == modifier_set[0]:
1953
- data[modifier_set].append(event[3].split("|")[count_set])
1988
+ try:
1989
+ data[modifier_set].append(event[3].split("|")[count_set])
1990
+ except Exception:
1991
+ return f"Modifier error for {event[2]} in observation {obs_id}", pd.DataFrame()
1954
1992
  count_set += 1
1955
1993
  else:
1956
1994
  data[modifier_set].append(np.nan)
@@ -1971,7 +2009,7 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1971
2009
  data["Comment stop"].append(event2[4])
1972
2010
  break
1973
2011
  else:
1974
- raise ("not paired")
2012
+ return f"Some events are not paired in {obs_id}", pd.DataFrame()
1975
2013
 
1976
2014
  else: # point
1977
2015
  data["Stop (s)"].append(float(event[0]))
@@ -1985,4 +2023,4 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1985
2023
 
1986
2024
  pd.DataFrame(data).info()
1987
2025
 
1988
- return pd.DataFrame(data)
2026
+ return "", pd.DataFrame(data)
boris/utilities.py CHANGED
@@ -62,11 +62,19 @@ except Exception:
62
62
  if sys.platform.startswith("win"):
63
63
  import ctypes
64
64
 
65
- ctypes.windll.user32.MessageBoxW(0, "The MPV library was not found!\nIt will be downloaded.", "BORIS", 0)
65
+ logger.info("The MPV library was not found!\nIt will be downloaded from the BORIS GitHub repository")
66
+ # ctypes.windll.user32.MessageBoxW(0, "The MPV library was not found!\nIt will be downloaded.", "BORIS", 0)
66
67
 
67
- # download libmpv2.dll and ffprobe from https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/
68
+ # test if following function works on windows
69
+ MessageBoxTimeoutW = ctypes.windll.user32.MessageBoxTimeoutW
70
+ MessageBoxTimeoutW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint]
71
+ ctypes.windll.user32.MessageBoxTimeoutW(
72
+ None, "The MPV library was not found.\nIt will be downloaded from the BORIS GitHub repository.", "MPV library", 0, 0, 10000
73
+ ) # time out
68
74
 
69
- url = "https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/"
75
+ # download libmpv2.dll from https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/
76
+
77
+ url: str = "https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/"
70
78
 
71
79
  external_files_dir = ""
72
80
  # search where to download libmpv-2.dll
@@ -91,7 +99,7 @@ except Exception:
91
99
  try:
92
100
  from . import mpv2 as mpv
93
101
  except Exception:
94
- logger.warning("MPV library not found after dowloading")
102
+ logger.critical("MPV library not found after dowloading")
95
103
  sys.exit(5)
96
104
 
97
105
  elif sys.platform.startswith("linux"):
boris/version.py CHANGED
@@ -20,5 +20,5 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- __version__ = "9.6.5"
24
- __version_date__ = "2025-09-02"
23
+ __version__ = "9.7"
24
+ __version_date__ = "2025-10-21"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.6.5
3
+ Version: 9.7
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
@@ -24,7 +24,7 @@ Requires-Dist: matplotlib==3.10.5
24
24
  Requires-Dist: pandas==2.3.2
25
25
  Requires-Dist: tablib[cli,html,ods,pandas,xls,xlsx]==3.8.0
26
26
  Requires-Dist: pyreadr==0.5.3
27
- Requires-Dist: pyside6==6.9
27
+ Requires-Dist: pyside6==6.10
28
28
  Requires-Dist: hachoir==3.3.0
29
29
  Requires-Dist: scipy==1.16.1
30
30
  Requires-Dist: scikit-learn==1.7.1
@@ -52,13 +52,15 @@ It provides also some analysis tools like time budget and some plotting function
52
52
  <!-- 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. -->
53
53
 
54
54
 
55
- The BORIS paper has more than 2336 citations in peer-reviewed scientific publications.
55
+ The BORIS paper has more than 2337 citations in peer-reviewed scientific publications.
56
56
 
57
57
 
58
58
 
59
59
 
60
60
  See the official [BORIS web site](https://www.boris.unito.it).
61
61
 
62
+ <a href="https://www.boris.unito.it" target="_blank"><img alt="Website" src="https://img.shields.io/website?url=https%3A%2F%2Fwww.boris.unito.it"></a>
63
+ <a href="https://www.boris.unito.it/user_guide/" target="_blank"><img alt="User guide" src="https://img.shields.io/badge/Documentation-orange"></a>
62
64
  [![Python web site](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org)
63
65
  ![Python versions](https://img.shields.io/pypi/pyversions/boris-behav-obs)
64
66
  ![BORIS license](https://img.shields.io/pypi/l/boris-behav-obs)
@@ -136,7 +138,3 @@ GNU General Public License for more details.
136
138
  Distributed with a [GPL v.3 license](LICENSE.TXT).
137
139
 
138
140
  Copyright (C) 2012-2025 Olivier Friard
139
-
140
-
141
-
142
-
@@ -8,24 +8,24 @@ boris/behav_coding_map_creator.py,sha256=5XGY4AZOsGrOj0xHT-dIeiR5ejBhPShsnYNfIQF
8
8
  boris/behavior_binary_table.py,sha256=bpmRDpEjq0rw3YOCoN_He3kfUe8A_R6E48kQR7KnkH8,12453
9
9
  boris/behaviors_coding_map.py,sha256=xIGJxp2eghrpiGDmYH73eJPERuyc4A_54uT-Got3zTs,7302
10
10
  boris/boris_cli.py,sha256=Bc51DEMcD79ZZfM9pCzpaWU6iT6b8gNwR3n8fr42_4E,13193
11
- boris/cmd_arguments.py,sha256=oWb-FvhKLbKJhATlTHy9muWu8XnnUfOZ-3Fmz2M8Yzc,1848
11
+ boris/cmd_arguments.py,sha256=Jn0Byzx0GUq9fsRHEdAPMe1U2JWkPrTTDyQ0EQAzU7A,1961
12
12
  boris/coding_pad.py,sha256=BaDWYIzoRgl0LHymPDmcBMFfwG7Z0XROqqMwkkADtz0,10940
13
13
  boris/config.py,sha256=IbW8PkAFcZIL-8NoSscXSeI82dneLzpywaGXWDcnrWw,17845
14
14
  boris/config_file.py,sha256=5_AB_VE5j3iHkanL5xELN42HJJMLOh40qSgBFs7fCXo,13493
15
- boris/connections.py,sha256=kqc6jaYNzoJe8crPtfwE-fXTW4nTfB8-PojRzbbLEus,19629
15
+ boris/connections.py,sha256=KsC17LnS4tRM6O3Nu3mD1H9kQ7uYhhad9229jXfGF94,19774
16
16
  boris/converters.py,sha256=n6gDM9x2hS-ZOoHLruiifuXxnC7ERsUukiFokhHZPoQ,11678
17
17
  boris/converters_ui.py,sha256=uu7LOBV_fKv2DBdOqsqPwjGsjgONr5ODBoscAA-EP48,9900
18
18
  boris/cooccurence.py,sha256=tVERC-V8MWjWHlGEfDuu08iS94qjt4do-38jwI62QaY,10367
19
- boris/core.py,sha256=R8oQsjmRh0heo7RmFyZVljTdMub4I_kM_7KGxpg9DDI,230461
19
+ boris/core.py,sha256=11y3kUIaqS2bF7DMtPCb186QCIOcliVuy3wtmlq_dJI,231335
20
20
  boris/core_qrc.py,sha256=Hz51Xw70ZIlDbYB281nfGtCm43_ItYhamMu2T5X8Tu8,639882
21
- boris/core_ui.py,sha256=--VDOzUfjsA4TJRw3aFk2CeSL29193vPGLgYJRhQfUY,77143
21
+ boris/core_ui.py,sha256=uDAI9mbadBe2mSsgugzNfRHxASc6Mu5kUf5tB9CZCjY,77445
22
22
  boris/db_functions.py,sha256=TfCJ0Hq0pTFOKrZz3RzdvnR-NKCmrPHU2qL9BSXeeGQ,13379
23
23
  boris/dev.py,sha256=9pUElbjl9g17rFUJXX5aVSu55_iIKIuDxNdrB0DI_d0,3671
24
24
  boris/dialog.py,sha256=LqZ73R9cwiL4qzKyMxeS2G8PKnbVZ0xFvZHw-oSek0M,34039
25
25
  boris/duration_widget.py,sha256=GjZgCAMGOcsNjoPiRImEVe6yMkH2vuNoh44ulpd5nlg,6924
26
26
  boris/edit_event.py,sha256=llfICwuDq4ADKtPlq8dtIN-81jZ92JseUHhUSt5dh6s,7705
27
27
  boris/edit_event_ui.py,sha256=qFgt00cejGB6UGC1mFkyZcsIAdvMeYMK0WYjZtJl1T0,9207
28
- boris/event_operations.py,sha256=bqUZjgJaJ1Z8oTiidG9wfCp2LLUH1Zf4kBDeg_yjC-o,38514
28
+ boris/event_operations.py,sha256=FlL_8FF-BR2OtUCrIplxktybThh7WN2MWkk3L3ZyHyA,40743
29
29
  boris/events_cursor.py,sha256=VPY_ygD0fxE5lp25mcd2l00XQXurCR6hprffF4tKRbU,2078
30
30
  boris/events_snapshots.py,sha256=PjWzQvUGQtIcEc_7FDsRphf7fAhhTccQgYc2eQSA65g,27621
31
31
  boris/exclusion_matrix.py,sha256=K_o8pEMYRQ3curgRQYkn5hPRksLDitICuwjB7mpVRPA,5269
@@ -47,7 +47,7 @@ boris/mpv-1.0.3.py,sha256=EXRtzQqFjOn4wMC6482Ilq3fNQ9N1GRP1VxwLzdeaBY,88077
47
47
  boris/mpv.py,sha256=EfzIHjPbgewG4w3smEtqEUPZoVwYmMQkL4Q8ZyW-a58,76410
48
48
  boris/mpv2.py,sha256=IUI4t4r9GYX7G5OXTjd3RhMMOkDdfal_15buBgksLsk,92152
49
49
  boris/observation.py,sha256=10UkVyY8TDySntIX_-H-IsuFdiF6tEcmC6JQUzD6wYg,57139
50
- boris/observation_operations.py,sha256=GSU0C5A7jn44Jzt0V6-2TzqhFtCHTn8XlgG-EZ2Qn90,107853
50
+ boris/observation_operations.py,sha256=XN_idI_yuBtExCmYmqJGBHF2Dwd-54L9SGekdVxM1ns,107974
51
51
  boris/observation_ui.py,sha256=DAnU94QBNvkLuHT6AxTwqSk_D_n6VUhSl8PexZv_dUk,33309
52
52
  boris/observations_list.py,sha256=NqwECGHtHYmKhSe-qCfqPmJ24SSfzlXvIXS2i3op_zE,10591
53
53
  boris/otx_parser.py,sha256=70QvilzFHXbjAHR88YH0aEXJ3xxheLS5fZGgHFHGpNE,16367
@@ -59,11 +59,11 @@ boris/plot_events.py,sha256=tKiUWH0TNSkK7xz5Vf0tAD3KiuAalv6UZEVtOnoFpWY,24059
59
59
  boris/plot_events_rt.py,sha256=xJmjwqhQxCN4FDBYRQ0O2eHm356Rbexzr3m1qTefMDU,8326
60
60
  boris/plot_spectrogram_rt.py,sha256=wDhnkqwjd2UfCxrfOejOUxoNOqfMNo6vo1JSvYgM-2A,10925
61
61
  boris/plot_waveform_rt.py,sha256=RNXhcBzRKnoGoVjRAHsVvOaj0BJbbI2cpCMjMUiGqX0,7534
62
- boris/plugins.py,sha256=wZ-Azq_4xD_vZf8t-cPm4jT-6kWw_t0lroGYMMZY8bE,15468
62
+ boris/plugins.py,sha256=sn2r8kMxkzaO8kNhem-cTlTBrym9MlFPyRT9Av9rHGg,15603
63
63
  boris/preferences.py,sha256=-WwBlkP3uhSyO3tUhcrctHMzlv46ngW9C4KAq7UJAoI,21563
64
64
  boris/preferences_ui.py,sha256=wbo51aBNdcQTJni1DmUM5ZQPOwAtKSkEQam7rRzRS5g,34166
65
65
  boris/project.py,sha256=nyXfCDY_rLP3jC1QGv-280jUKgbABqESjOm7I19rJ1U,86432
66
- boris/project_functions.py,sha256=o0IOvhGs1cqEjpdeNUeY-qvFfWAQl_7tsUEKxogKRuU,80869
66
+ boris/project_functions.py,sha256=gSG6Pa0BF0wv5AtfvRU8KqOqzgUwbMTmIsYqQ11QwmY,82605
67
67
  boris/project_import_export.py,sha256=oBG1CSXfKISsb3TLNT-8BH8kZPAzxIYSNemlLVH1Lh8,38560
68
68
  boris/project_ui.py,sha256=yB-ewhHt8S8DTTRIk-dNK2tPMNU24lNji9fDW_Xazu8,38805
69
69
  boris/qrc_boris.py,sha256=aH-qUirYY1CGxmTK1SFCPvuZfazIHX4DdUKF1gxZeYM,675008
@@ -77,8 +77,8 @@ boris/synthetic_time_budget.py,sha256=3Eb9onMLmgqCLd50CuxV9L8RV2ESzfaMWvPK_bXUMM
77
77
  boris/time_budget_functions.py,sha256=SbGyTF2xySEqBdRJZeWFTirruvK3r6pwM6e4Gz17W1s,52186
78
78
  boris/time_budget_widget.py,sha256=z-tyITBtIz-KH1H2OdMB5a8x9QQLK7Wu96-zkC6NVDA,43213
79
79
  boris/transitions.py,sha256=okyDCO-Vn4p_Fixd8cGiSIaUhUxG5ePIOqGSuP52g_c,12246
80
- boris/utilities.py,sha256=dD5HpojqlAGLVkr3YnOsaqfbCMHFYroe040ZchB5WnM,56662
81
- boris/version.py,sha256=TvVlNsadnocZHS1Z6ou_pjeTRRwHbRfwriUKmuuyeYA,787
80
+ boris/utilities.py,sha256=25Vge1TXfdH9kEOR95E1iDIdCbuvGShYW9R48uDBD7w,57236
81
+ boris/version.py,sha256=tE2UR0Eck0VGnCgATsJSdV1--vPYDg4FClURpjTphyY,785
82
82
  boris/video_equalizer.py,sha256=cm2JXe1eAPkqDzxYB2OMKyuveMBdq4a9-gD1bBorbn4,5823
83
83
  boris/video_equalizer_ui.py,sha256=1CG3s79eM4JAbaCx3i1ILZXLceb41_gGXlOLNfpBgnw,10142
84
84
  boris/video_operations.py,sha256=rXKWndaALaF-yLEPIY_-Z99XRAncZRzRd1sLzwSpbjs,10768
@@ -101,9 +101,9 @@ boris/portion/dict.py,sha256=uNM-LEY52CZ2VNMMW_C9QukoyTvPlQf8vcbGa1lQBHI,11281
101
101
  boris/portion/func.py,sha256=mSQr20YS1ug7R1fRqBg8LifjtXDRvJ6Kjc3WOeL9P34,2172
102
102
  boris/portion/interval.py,sha256=sOlj3MAGGaB-JxCkigS-n3qw0fY7TANAsXv1pavr8J4,19931
103
103
  boris/portion/io.py,sha256=kpq44pw3xnIyAlPwaR5qRHKRdZ72f8HS9YVIWs5k2pk,6367
104
- boris_behav_obs-9.6.5.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
105
- boris_behav_obs-9.6.5.dist-info/METADATA,sha256=SMRqLS1ScpTMc2n42ZfIiIFsNX6zq5lIOGcyReqTyrs,5089
106
- boris_behav_obs-9.6.5.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
107
- boris_behav_obs-9.6.5.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
108
- boris_behav_obs-9.6.5.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
109
- boris_behav_obs-9.6.5.dist-info/RECORD,,
104
+ boris_behav_obs-9.7.dist-info/licenses/LICENSE.TXT,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
105
+ boris_behav_obs-9.7.dist-info/METADATA,sha256=YkVXQz7WQKk5cDD-ad3kuPXFEhDr0RkNZItQnRAi_FU,5383
106
+ boris_behav_obs-9.7.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
107
+ boris_behav_obs-9.7.dist-info/entry_points.txt,sha256=k__8XvFi4vaA4QFvQehCZjYkKmZH34HSAJI2iYCWrMs,52
108
+ boris_behav_obs-9.7.dist-info/top_level.txt,sha256=fJSgm62S7WesiwTorGbOO4nNN0yzgZ3klgfGi3Px4qI,6
109
+ boris_behav_obs-9.7.dist-info/RECORD,,