boris-behav-obs 8.16.6__py3-none-any.whl → 9.7.1__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.
Files changed (125) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +24 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +235 -131
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +16 -34
  23. boris/config.py +101 -49
  24. boris/config_file.py +55 -64
  25. boris/connections.py +105 -58
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2108 -1275
  30. boris/core_qrc.py +15892 -10829
  31. boris/core_ui.py +941 -806
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +134 -0
  34. boris/dialog.py +461 -242
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +405 -281
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +180 -203
  43. boris/export_observation.py +60 -73
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +427 -218
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +304 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +16 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv2.py +127 -36
  58. boris/observation.py +493 -210
  59. boris/observation_operations.py +1010 -391
  60. boris/observation_ui.py +573 -363
  61. boris/observations_list.py +51 -58
  62. boris/otx_parser.py +74 -68
  63. boris/param_panel.py +45 -59
  64. boris/param_panel_ui.py +254 -138
  65. boris/player_dock_widget.py +91 -56
  66. boris/plot_data_module.py +18 -53
  67. boris/plot_events.py +56 -153
  68. boris/plot_events_rt.py +16 -30
  69. boris/plot_spectrogram_rt.py +80 -56
  70. boris/plot_waveform_rt.py +23 -48
  71. boris/plugins.py +431 -0
  72. boris/portion/__init__.py +18 -8
  73. boris/portion/const.py +35 -18
  74. boris/portion/dict.py +5 -5
  75. boris/portion/func.py +2 -2
  76. boris/portion/interval.py +21 -41
  77. boris/portion/io.py +41 -32
  78. boris/preferences.py +298 -123
  79. boris/preferences_ui.py +664 -225
  80. boris/project.py +293 -270
  81. boris/project_functions.py +610 -537
  82. boris/project_import_export.py +204 -213
  83. boris/project_ui.py +673 -441
  84. boris/qrc_boris.py +6 -3
  85. boris/qrc_boris5.py +6 -3
  86. boris/select_modifiers.py +62 -90
  87. boris/select_observations.py +19 -197
  88. boris/select_subj_behav.py +67 -39
  89. boris/state_events.py +51 -33
  90. boris/subjects_pad.py +6 -8
  91. boris/synthetic_time_budget.py +25 -17
  92. boris/time_budget_functions.py +169 -169
  93. boris/time_budget_widget.py +71 -86
  94. boris/transitions.py +41 -41
  95. boris/utilities.py +562 -222
  96. boris/version.py +3 -3
  97. boris/video_equalizer.py +16 -14
  98. boris/video_equalizer_ui.py +199 -130
  99. boris/video_operations.py +78 -28
  100. boris/view_df.py +104 -0
  101. boris/view_df_ui.py +75 -0
  102. boris/write_event.py +240 -136
  103. boris_behav_obs-9.7.1.dist-info/METADATA +140 -0
  104. boris_behav_obs-9.7.1.dist-info/RECORD +109 -0
  105. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.dist-info}/WHEEL +1 -1
  106. boris_behav_obs-9.7.1.dist-info/entry_points.txt +2 -0
  107. boris/README.TXT +0 -22
  108. boris/add_modifier.ui +0 -323
  109. boris/converters.ui +0 -289
  110. boris/core.qrc +0 -37
  111. boris/core.ui +0 -1571
  112. boris/edit_event.ui +0 -233
  113. boris/icons/logo_eye.ico +0 -0
  114. boris/map_creator.py +0 -982
  115. boris/observation.ui +0 -814
  116. boris/param_panel.ui +0 -379
  117. boris/preferences.ui +0 -537
  118. boris/project.ui +0 -1074
  119. boris/vlc_local.py +0 -90
  120. boris_behav_obs-8.16.6.dist-info/LICENSE.TXT +0 -674
  121. boris_behav_obs-8.16.6.dist-info/METADATA +0 -134
  122. boris_behav_obs-8.16.6.dist-info/RECORD +0 -106
  123. boris_behav_obs-8.16.6.dist-info/entry_points.txt +0 -2
  124. {boris → boris_behav_obs-9.7.1.dist-info/licenses}/LICENSE.TXT +0 -0
  125. {boris_behav_obs-8.16.6.dist-info → boris_behav_obs-9.7.1.dist-info}/top_level.txt +0 -0
boris/utilities.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 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
@@ -19,31 +19,243 @@ Copyright 2012-2023 Olivier Friard
19
19
  MA 02110-1301, USA.
20
20
  """
21
21
 
22
+ from decimal import Decimal as dec
23
+ from decimal import getcontext, ROUND_DOWN
24
+ from hachoir.metadata import extractMetadata
25
+ from hachoir.parser import createParser
26
+ from shutil import copyfile, which
27
+ from typing import Union, Tuple
22
28
  import csv
23
29
  import datetime
24
- import hashlib
30
+ import datetime as dt
31
+ import exifread
25
32
  import json
26
33
  import logging
27
34
  import math
35
+ import numpy as np
28
36
  import os
29
- import pathlib as pl
37
+ from pathlib import Path
38
+ from PIL.ImageQt import Image
39
+ import platform
30
40
  import re
31
- import socket
41
+ import shutil
32
42
  import subprocess
33
43
  import sys
34
44
  import urllib.parse
45
+ import urllib.request
35
46
  import wave
36
- from decimal import Decimal as dec
37
- from decimal import getcontext, ROUND_DOWN
38
- from shutil import copyfile
39
- from typing import Union, Tuple
40
-
41
- import numpy as np
42
- from PyQt5.QtGui import QPixmap, QImage
47
+ import socket
43
48
 
44
- from PIL.ImageQt import Image
49
+ from PySide6 import __version__ as pyside6_version
50
+ from PySide6.QtGui import QPixmap, QImage
51
+ from PySide6.QtCore import qVersion
45
52
 
46
53
  from . import config as cfg
54
+ from . import version
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ if (sys.platform.startswith("win") or sys.platform.startswith("linux")) and ("-i" not in sys.argv) and ("--ipc" not in sys.argv):
59
+ try:
60
+ from . import mpv2 as mpv
61
+ except Exception:
62
+ logger.warning("MPV library not found")
63
+
64
+ if sys.platform.startswith("win"):
65
+ import ctypes
66
+
67
+ logger.info("The MPV library was not found!\nIt will be downloaded from the BORIS GitHub repository")
68
+ # ctypes.windll.user32.MessageBoxW(0, "The MPV library was not found!\nIt will be downloaded.", "BORIS", 0)
69
+
70
+ # test if following function works on windows
71
+ MessageBoxTimeoutW = ctypes.windll.user32.MessageBoxTimeoutW
72
+ MessageBoxTimeoutW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint]
73
+ ctypes.windll.user32.MessageBoxTimeoutW(
74
+ None, "The MPV library was not found.\nIt will be downloaded from the BORIS GitHub repository.", "MPV library", 0, 0, 10000
75
+ ) # time out
76
+
77
+ # download libmpv2.dll from https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/
78
+
79
+ url: str = "https://github.com/boris-behav-obs/boris-behav-obs.github.io/releases/download/files/"
80
+
81
+ external_files_dir = ""
82
+ # search where to download libmpv-2.dll
83
+
84
+ external_files_dir = Path(__file__).parent / "misc"
85
+ if not external_files_dir.is_dir():
86
+ logger.info(f"Creating {external_files_dir} directory")
87
+ external_files_dir.mkdir(parents=True, exist_ok=True)
88
+
89
+ logger.info(f"MPV library directory: {external_files_dir}")
90
+
91
+ local_filename = external_files_dir / "libmpv-2.dll"
92
+ logger.info("Downloading libmpv-2.dll...")
93
+ try:
94
+ urllib.request.urlretrieve(url + "libmpv-2.dll", local_filename)
95
+ logger.info(f"File downloaded as {local_filename}")
96
+ except Exception:
97
+ logger.critical("The MPV library can not be downloaded! Check your connection.")
98
+ ctypes.windll.user32.MessageBoxW(0, "The MPV library can not be downloaded!\nCheck your connection.", "BORIS", 0)
99
+ sys.exit(5)
100
+ # reload package
101
+ try:
102
+ from . import mpv2 as mpv
103
+ except Exception:
104
+ logger.critical("MPV library not found after dowloading")
105
+ sys.exit(5)
106
+
107
+ elif sys.platform.startswith("linux"):
108
+ text = (
109
+ "The MPV library was not found!\nInstall it\n\n"
110
+ "With Debian/Ubuntu/Mint:\nsudo apt install libmpv2\n\n"
111
+ "With Fedora:\nsudo dnf install mpv-libs\n\n"
112
+ "With OpenSUSE:\nsudo zypper install mpv\n\n"
113
+ "Arch Linux / Manjaro:\nsudo pacman -S mpv\n\n"
114
+ )
115
+ if shutil.which("zenity") is not None:
116
+ subprocess.run(["zenity", "--error", f"--text={text}"])
117
+ elif shutil.which("kdialog"):
118
+ subprocess.run(["kdialog", "--msgbox", text])
119
+ elif shutil.which("gxmessage"):
120
+ subprocess.run(["gxmessage", text])
121
+ elif shutil.which("xmessage"):
122
+ subprocess.run(["xmessage", text])
123
+
124
+ sys.exit(5)
125
+ else:
126
+ sys.exit(5)
127
+
128
+
129
+ def test_mpv_ipc(socket_path: str = cfg.MPV_SOCKET) -> bool:
130
+ """
131
+ test if socket available
132
+ """
133
+ if not os.path.exists(socket_path):
134
+ return False
135
+
136
+ try:
137
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
138
+ client.connect(socket_path)
139
+ client.close()
140
+ return True
141
+ except Exception:
142
+ return False
143
+
144
+
145
+ def extract_exif_DateTimeOriginal(file_path: str) -> int:
146
+ """
147
+ extract the EXIF DateTimeOriginal tag
148
+ return epoch time
149
+ if the tag is not available return -1
150
+
151
+ Args:
152
+ file_path (str): path of the media file
153
+
154
+ Returns:
155
+ int: timestamp
156
+
157
+ """
158
+ try:
159
+ with open(file_path, "rb") as f_in:
160
+ tags = exifread.process_file(f_in, details=False, stop_tag="EXIF DateTimeOriginal")
161
+ if "EXIF DateTimeOriginal" in tags:
162
+ date_time_original = (
163
+ f"{tags['EXIF DateTimeOriginal'].values[:4]}-"
164
+ f"{tags['EXIF DateTimeOriginal'].values[5:7]}-"
165
+ f"{tags['EXIF DateTimeOriginal'].values[8:10]} "
166
+ f"{tags['EXIF DateTimeOriginal'].values.split(' ')[-1]}"
167
+ )
168
+ return int(datetime.datetime.strptime(date_time_original, "%Y-%m-%d %H:%M:%S").timestamp())
169
+ else:
170
+ try:
171
+ # read from file name (YYYY-MM-DD_HHMMSS)
172
+ return int(datetime.datetime.strptime(Path(file_path).stem, "%Y-%m-%d_%H%M%S").timestamp())
173
+ except Exception:
174
+ # read from file name (YYYY-MM-DD_HH:MM:SS)
175
+ return int(datetime.datetime.strptime(Path(file_path).stem, "%Y-%m-%d_%H:%M:%S").timestamp())
176
+
177
+ except Exception:
178
+ return -1
179
+
180
+
181
+ def extract_video_creation_date(file_path: str) -> int | None:
182
+ """
183
+ returns the timestamp of the media creation date time with Hachoir
184
+ """
185
+
186
+ logger.debug(f"extract_video_creation_date for {file_path}")
187
+
188
+ if not Path(file_path).is_file():
189
+ logger.debug(f"{file_path} not found")
190
+ return None
191
+ try:
192
+ parser = createParser(file_path)
193
+ metadata = extractMetadata(parser)
194
+ except Exception:
195
+ return None
196
+
197
+ if metadata.has("creation_date"):
198
+ if metadata.get("creation_date") == datetime.datetime(1904, 1, 1, 0, 0):
199
+ return None
200
+ return metadata.get("creation_date").timestamp()
201
+ else:
202
+ return None
203
+
204
+
205
+ def extract_date_time_from_file_name(file_path: str) -> int | None:
206
+ """
207
+ extract YYYY-MM-DD_HHMMSS or YYYY-MM-DD_HH:MM:SS from file name
208
+ """
209
+
210
+ patterns = (r"\d{4}-\d{2}-\d{2}_\d{6}", r"\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}")
211
+ for pattern in patterns:
212
+ matches = re.findall(pattern, file_path)
213
+
214
+ if matches:
215
+ if pattern == r"\d{4}-\d{2}-\d{2}_\d{6}":
216
+ logger.debug(
217
+ f"extract_date_time_from_file_name timestamp from {file_path}: {int(datetime.datetime.strptime(matches[0], '%Y-%m-%d_%H%M%S').timestamp())}"
218
+ )
219
+
220
+ return int(datetime.datetime.strptime(matches[0], "%Y-%m-%d_%H%M%S").timestamp())
221
+
222
+ if pattern == r"\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}":
223
+ logger.debug(
224
+ f"extract_date_time_from_file_name timestamp from {file_path}: {int(datetime.datetime.strptime(matches[0], '%Y-%m-%d_%H:%M:%S').timestamp())}"
225
+ )
226
+
227
+ return int(datetime.datetime.strptime(matches[0], "%Y-%m-%d_%H:%M:%S").timestamp())
228
+
229
+ return None
230
+
231
+
232
+ def mpv_lib_version() -> Tuple[str, str, str]:
233
+ """
234
+ Version of MPV library
235
+
236
+ Returns:
237
+ str: MPV library version
238
+ """
239
+ if ("-i" in sys.argv) or ("--ipc" in sys.argv) or sys.platform.startswith("darwin"):
240
+ return "MPV IPC mode", "", ""
241
+
242
+ mpv_lib_file = None
243
+ if sys.platform.startswith("linux"):
244
+ mpv_lib_file = mpv.sofile
245
+ if sys.platform.startswith("win"):
246
+ mpv_lib_file = mpv.dll
247
+
248
+ return (".".join([str(x) for x in mpv._mpv_client_api_version()]), mpv_lib_file, mpv.MPV_VERSION)
249
+
250
+
251
+ def python_mpv_script_version() -> str:
252
+ """
253
+ version of python-mpv script
254
+ """
255
+ try:
256
+ return mpv.__version__
257
+ except Exception:
258
+ return "Not found"
47
259
 
48
260
 
49
261
  def error_info(exc_info: tuple) -> tuple:
@@ -154,28 +366,7 @@ def return_file_header_footer(file_name: str, file_row_number: int = 0, row_numb
154
366
  return header, footer
155
367
 
156
368
 
157
- def bytes_to_str(b: bytes) -> str:
158
- """
159
- Translate bytes to string.
160
-
161
- Args:
162
- b (bytes): byte to convert
163
-
164
- Returns:
165
- str: converted byte
166
- """
167
-
168
- if isinstance(b, bytes):
169
- fileSystemEncoding = sys.getfilesystemencoding()
170
- # hack for PyInstaller
171
- if fileSystemEncoding is None:
172
- fileSystemEncoding = "UTF-8"
173
- return b.decode(fileSystemEncoding)
174
- else:
175
- return b
176
-
177
-
178
- def convertTime(time_format: str, sec: Union[float, dec]) -> str:
369
+ def convertTime(time_format: str, sec: Union[float, dec]) -> Union[str, None]:
179
370
  """
180
371
  convert time in base at the current format (S or HHMMSS)
181
372
 
@@ -198,10 +389,12 @@ def convertTime(time_format: str, sec: Union[float, dec]) -> str:
198
389
  return None
199
390
 
200
391
 
201
- def dynamic_time_format(sec: Union[float, dec], cutoff: dec, time_format: str) -> str:
392
+ def smart_time_format(sec: Union[float, dec], time_format: str = cfg.S, cutoff: dec = cfg.SMART_TIME_CUTOFF_DEFAULT) -> str:
202
393
  """
394
+ Smart time format
203
395
  returns time in seconds if <= cutoff else in HH:MM:SS.ZZZ format
204
396
  """
397
+ # cutoff = 0 follows the time format selectd by user
205
398
  if cutoff == 0:
206
399
  return convertTime(time_format, sec)
207
400
  if sec <= cutoff:
@@ -222,7 +415,10 @@ def convert_time_to_decimal(pj: dict) -> dict:
222
415
  """
223
416
  for obs_id in pj[cfg.OBSERVATIONS]:
224
417
  if cfg.TIME_OFFSET in pj[cfg.OBSERVATIONS][obs_id]:
225
- pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec(str(pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]))
418
+ if pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] is not None:
419
+ pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec(str(pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]))
420
+ else:
421
+ pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET] = dec("0.000")
226
422
  for idx, _ in enumerate(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]):
227
423
  pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX] = dec(
228
424
  pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX]
@@ -238,20 +434,6 @@ def count_media_file(media_files: dict) -> int:
238
434
  return sum([len(media_files[idx]) for idx in media_files])
239
435
 
240
436
 
241
- def file_content_md5(file_name: str) -> str:
242
- """
243
- returns the MD5 sum of file content
244
- """
245
- hash_md5 = hashlib.md5()
246
- try:
247
- with open(file_name, "rb") as f:
248
- for chunk in iter(lambda: f.read(4096), b""):
249
- hash_md5.update(chunk)
250
- return hash_md5.hexdigest()
251
- except FileNotFoundError:
252
- return ""
253
-
254
-
255
437
  def txt2np_array(
256
438
  file_name: str, columns_str: str, substract_first_value: str, converters=None, column_converter=None
257
439
  ) -> Tuple[bool, str, np.array]:
@@ -324,9 +506,7 @@ def txt2np_array(
324
506
  return False, f"{sys.exc_info()[1]}", np.array([])
325
507
 
326
508
  try:
327
- data = np.loadtxt(
328
- file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters
329
- )
509
+ data = np.loadtxt(file_name, delimiter=dialect.delimiter, usecols=columns, skiprows=header_rows_nb, converters=np_converters)
330
510
 
331
511
  except Exception:
332
512
  return False, f"{sys.exc_info()[1]}", np.array([])
@@ -338,7 +518,7 @@ def txt2np_array(
338
518
  return True, "", data
339
519
 
340
520
 
341
- def versiontuple(version_str: str):
521
+ def versiontuple(version_str: str) -> tuple:
342
522
  """
343
523
  Convert version from str to tuple of str
344
524
 
@@ -381,7 +561,7 @@ def behav_category_user_color(behavioral_categories: dict, name: str) -> Union[s
381
561
 
382
562
  def state_behavior_codes(ethogram: dict) -> list:
383
563
  """
384
- behavior codes defined as STATE event
564
+ returns a list of behavior codes defined as STATE event
385
565
 
386
566
  Args:
387
567
  ethogram (dict): ethogram dictionary
@@ -390,12 +570,12 @@ def state_behavior_codes(ethogram: dict) -> list:
390
570
  list: list of behavior codes defined as STATE event
391
571
 
392
572
  """
393
- return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if cfg.STATE in ethogram[x][cfg.TYPE].upper()]
573
+ return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if ethogram[x][cfg.TYPE] in cfg.STATE_EVENT_TYPES]
394
574
 
395
575
 
396
576
  def point_behavior_codes(ethogram: dict) -> list:
397
577
  """
398
- behavior codes defined as POINT event
578
+ returns a list of behavior codes defined as POINT event
399
579
 
400
580
  Args:
401
581
  ethogram (dict): ethogram dictionary
@@ -404,7 +584,7 @@ def point_behavior_codes(ethogram: dict) -> list:
404
584
  list: list of behavior codes defined as POINT event
405
585
 
406
586
  """
407
- return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if cfg.POINT in ethogram[x][cfg.TYPE].upper()]
587
+ return [ethogram[x][cfg.BEHAVIOR_CODE] for x in ethogram if ethogram[x][cfg.TYPE] in (cfg.POINT_EVENT, cfg.POINT_EVENT_WITH_CODING_MAP)]
408
588
 
409
589
 
410
590
  def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict:
@@ -457,8 +637,21 @@ def group_events(pj: dict, obs_id: str, include_modifiers: bool = False) -> dict
457
637
  return {"error": ""}
458
638
 
459
639
 
640
+ def flatten_list(nested_list) -> list:
641
+ """
642
+ Flatten a list of lists.
643
+ """
644
+ flattened: list = []
645
+ for item in nested_list:
646
+ if isinstance(item, list):
647
+ flattened.extend(flatten_list(item))
648
+ else:
649
+ flattened.append(item)
650
+ return flattened
651
+
652
+
460
653
  def get_current_states_modifiers_by_subject(
461
- state_behaviors_codes: list, events: list, subjects: dict, time: dec, include_modifiers: bool = False
654
+ state_behaviors_codes: list, events: list, subjects: dict, time_: dec, include_modifiers: bool = False
462
655
  ) -> dict:
463
656
  """
464
657
  get current states and modifiers (if requested) for subjects at given time
@@ -474,7 +667,7 @@ def get_current_states_modifiers_by_subject(
474
667
  dict: current states by subject. dict of list
475
668
  """
476
669
  current_states: dict = {}
477
- if time.is_nan():
670
+ if time_.is_nan():
478
671
  for idx in subjects:
479
672
  current_states[idx] = []
480
673
  return current_states
@@ -487,43 +680,45 @@ def get_current_states_modifiers_by_subject(
487
680
 
488
681
  if include_modifiers:
489
682
  for idx in subjects:
490
- current_states[idx] = []
491
- for sbc in state_behaviors_codes:
492
- bl = [
683
+ current_states[subjects[idx]["name"]] = {}
684
+ for x in events:
685
+ if x[check_index] > time_:
686
+ break
687
+ if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
688
+ if (x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX]) not in current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]]:
689
+ current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])] = (
690
+ False
691
+ )
692
+
693
+ current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][
493
694
  (x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
494
- for x in events
495
- if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx][cfg.SUBJECT_NAME]
496
- and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
497
- and x[check_index] <= time
498
- ]
695
+ ] = not current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][(x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])]
499
696
 
500
- if len(bl) % 2: # test if odd
501
- current_states[idx].append(bl[-1][0] + f" ({bl[-1][1]})" * (bl[-1][1] != ""))
697
+ r: dict = {}
698
+ for idx in subjects:
699
+ r[idx] = [f"{bm[0]} ({bm[1]})" for bm in current_states[subjects[idx]["name"]] if current_states[subjects[idx]["name"]][bm]]
502
700
 
503
701
  else:
504
702
  for idx in subjects:
505
- current_states[idx] = []
506
- for sbc in state_behaviors_codes:
507
- if (
508
- len(
509
- [
510
- x[cfg.EVENT_BEHAVIOR_FIELD_IDX]
511
- for x in events
512
- if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx][cfg.SUBJECT_NAME]
513
- and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
514
- and x[check_index] <= time
515
- ]
516
- )
517
- % 2
518
- ): # test if odd
519
- current_states[idx].append(sbc)
703
+ current_states[subjects[idx]["name"]] = {}
704
+ for b in state_behaviors_codes:
705
+ current_states[subjects[idx]["name"]][b] = False
706
+ for x in events:
707
+ if x[check_index] > time_:
708
+ break
709
+ if x[cfg.EVENT_BEHAVIOR_FIELD_IDX] in state_behaviors_codes:
710
+ current_states[x[cfg.EVENT_SUBJECT_FIELD_IDX]][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]] = not current_states[
711
+ x[cfg.EVENT_SUBJECT_FIELD_IDX]
712
+ ][x[cfg.EVENT_BEHAVIOR_FIELD_IDX]]
713
+
714
+ r: dict = {}
715
+ for idx in subjects:
716
+ r[idx] = [b for b in state_behaviors_codes if current_states[subjects[idx]["name"]][b]]
520
717
 
521
- return current_states
718
+ return r
522
719
 
523
720
 
524
- def get_current_states_modifiers_by_subject_2(
525
- state_behaviors_codes: list, events: list, subjects: dict, time: dec
526
- ) -> dict:
721
+ def get_current_states_modifiers_by_subject_2(state_behaviors_codes: list, events: list, subjects: dict, time: dec) -> dict:
527
722
  """
528
723
  get current states and modifiers for subjects at given time
529
724
  differs from get_current_states_modifiers_by_subject in the output format: [behavior, modifiers]
@@ -533,7 +728,6 @@ def get_current_states_modifiers_by_subject_2(
533
728
  events (list): list of events
534
729
  subjects (dict): dictionary of subjects
535
730
  time (Decimal): time
536
- include_modifiers (bool): include modifier if True (default: False)
537
731
 
538
732
  Returns:
539
733
  dict: current states by subject. dict of list
@@ -587,7 +781,8 @@ def get_current_points_by_subject(
587
781
  point_events = [
588
782
  (x[cfg.EVENT_BEHAVIOR_FIELD_IDX], x[cfg.EVENT_MODIFIER_FIELD_IDX])
589
783
  for x in events
590
- if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"] and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
784
+ if x[cfg.EVENT_SUBJECT_FIELD_IDX] == subjects[idx]["name"]
785
+ and x[cfg.EVENT_BEHAVIOR_FIELD_IDX] == sbc
591
786
  # and abs(x[EVENT_TIME_FIELD_IDX] - time) <= tolerance
592
787
  and time <= x[cfg.EVENT_TIME_FIELD_IDX] < (time + tolerance)
593
788
  ]
@@ -604,19 +799,6 @@ def get_current_points_by_subject(
604
799
  return current_points
605
800
 
606
801
 
607
- def get_ip_address():
608
- """Get current IP address
609
-
610
- Args:
611
-
612
- Returns:
613
- str: IP address
614
- """
615
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
616
- s.connect(("8.8.8.8", 80))
617
- return s.getsockname()[0]
618
-
619
-
620
802
  def check_txt_file(file_name: str) -> dict:
621
803
  """
622
804
  Extract parameters of txt file (test for tsv csv)
@@ -686,15 +868,15 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
686
868
  str: wav file path or "" if error
687
869
  """
688
870
 
689
- wav_file_path = pl.Path(tmp_dir) / pl.Path(media_file_path + ".wav").name
871
+ wav_file_path = Path(tmp_dir) / Path(media_file_path + ".wav").name
690
872
 
691
873
  # check if media file is a wav file
692
874
  try:
693
875
  wav = wave.open(media_file_path, "r")
694
876
  wav.close()
695
- logging.debug(f"{media_file_path} is a WAV file. Copying in the temp directory...")
877
+ logger.debug(f"{media_file_path} is a WAV file. Copying in the temp directory...")
696
878
  copyfile(media_file_path, wav_file_path)
697
- logging.debug(f"{media_file_path} copied in {wav_file_path}")
879
+ logger.debug(f"{media_file_path} copied in {wav_file_path}")
698
880
  return str(wav_file_path)
699
881
  except Exception:
700
882
  if wav_file_path.is_file():
@@ -709,7 +891,7 @@ def extract_wav(ffmpeg_bin: str, media_file_path: str, tmp_dir: str) -> str:
709
891
  )
710
892
  out, error = p.communicate()
711
893
  out, error = out.decode("utf-8"), error.decode("utf-8")
712
- logging.debug(f"{out}, {error}")
894
+ logger.debug(f"{out}, {error}")
713
895
 
714
896
  if "does not contain any stream" not in error:
715
897
  if wav_file_path.is_file():
@@ -743,21 +925,28 @@ def complete(lst: list, max_: int) -> list:
743
925
 
744
926
  def datetime_iso8601(dt) -> str:
745
927
  """
746
- current date time in ISO8601 format without milliseconds
928
+ current date time in ISO8601 format without microseconds
747
929
  example: 2019-06-13 10:01:02
748
930
 
749
931
  Returns:
750
- str: date time in ISO8601 format
932
+ str: date time in ISO8601 format without microseconds
751
933
  """
752
- return dt.isoformat(" ").split(".")[0]
934
+ return dt.isoformat(sep=" ", timespec="seconds")
753
935
 
754
936
 
755
- def seconds_of_day(dt) -> dec:
937
+ def seconds_of_day(timestamp: dt.datetime) -> dec:
756
938
  """
757
939
  return the number of seconds since start of the day
940
+
941
+ Returns:
942
+ dev: number of seconds since the start of the day
758
943
  """
759
944
 
760
- return dec((dt - datetime.datetime.combine(dt.date(), datetime.time(0))).total_seconds()).quantize(dec("0.001"))
945
+ # logger.debug("function: seconds_of_day")
946
+ # logger.debug(f"{timestamp = }")
947
+
948
+ t = timestamp.time()
949
+ return dec(t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1000000).quantize(dec("0.001"))
761
950
 
762
951
 
763
952
  def sorted_keys(d: dict) -> list:
@@ -787,7 +976,7 @@ def intfloatstr(s: str) -> int:
787
976
  return s
788
977
 
789
978
 
790
- def distance(p1, p2):
979
+ def distance(p1: tuple, p2: tuple) -> float:
791
980
  """
792
981
  euclidean distance between 2 points
793
982
  """
@@ -819,6 +1008,62 @@ def angle(vertex: tuple, side1: tuple, side2: tuple) -> float:
819
1008
  )
820
1009
 
821
1010
 
1011
+ def oriented_angle(P1: tuple, P2: tuple, P3: tuple) -> float:
1012
+ """
1013
+ Calculate the oriented angle between two segments.
1014
+
1015
+ Args:
1016
+ P1 (tuple): Coordinates of the vertex
1017
+ P2 (tuple): Coordinates of the first point
1018
+ P3 (tuple): Coordinates of the second point
1019
+
1020
+ Returns:
1021
+ float: The oriented angle between the two segments in degrees.
1022
+ """
1023
+
1024
+ x1, y1 = P1
1025
+ x2, y2 = P2
1026
+ x3, y3 = P1
1027
+ x4, y4 = P3
1028
+
1029
+ angle_AB = math.atan2(y2 - y1, x2 - x1)
1030
+ angle_CD = math.atan2(y4 - y3, x4 - x3)
1031
+
1032
+ oriented_angle = math.degrees(angle_AB - angle_CD)
1033
+
1034
+ return oriented_angle
1035
+
1036
+
1037
+ def oriented_angle_trigo(B: Tuple[float, float], A: Tuple[float, float], C: Tuple[float, float]) -> float:
1038
+ """
1039
+ Calculates the oriented angle between vectors BA and BC, in degrees.
1040
+ The angle is positive in the counter-clockwise (trigonometric) direction.
1041
+
1042
+ Parameters:
1043
+ B: The pivot point (the origin of the vectors BA and BC).
1044
+ A, C: Points that define the vectors.
1045
+
1046
+ Returns:
1047
+ Angle in degrees, between 0 and 360.
1048
+ """
1049
+ # Vectors BA and BC
1050
+ v1 = (A[0] - B[0], A[1] - B[1])
1051
+ v2 = (C[0] - B[0], C[1] - B[1])
1052
+
1053
+ # Dot product and 2D cross product (determinant)
1054
+ dot = v1[0] * v2[0] + v1[1] * v2[1]
1055
+ det = v1[0] * v2[1] - v1[1] * v2[0]
1056
+
1057
+ # Signed angle in radians, then converted to degrees
1058
+ angle_rad = math.atan2(det, dot)
1059
+ angle_deg = math.degrees(angle_rad)
1060
+
1061
+ if angle_deg < 0:
1062
+ angle_deg += 360
1063
+
1064
+ return angle_deg
1065
+
1066
+
822
1067
  def mem_info():
823
1068
  """
824
1069
  get info about total mem, used mem and available mem using:
@@ -836,9 +1081,7 @@ def mem_info():
836
1081
  process = subprocess.run(["free", "-m"], stdout=subprocess.PIPE)
837
1082
  # out, err = process.communicate()
838
1083
  out = process.stdout
839
- _, tot_mem, used_mem, _, _, _, available_mem = [
840
- x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""
841
- ]
1084
+ _, tot_mem, used_mem, _, _, _, available_mem = [x.decode("utf-8") for x in out.split(b"\n")[1].split(b" ") if x != b""]
842
1085
  return False, {
843
1086
  "total_memory": int(tot_mem),
844
1087
  "used_memory": int(used_mem),
@@ -858,9 +1101,7 @@ def mem_info():
858
1101
 
859
1102
  if sys.platform.startswith("win"):
860
1103
  try:
861
- output = subprocess.run(
862
- ["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE
863
- )
1104
+ output = subprocess.run(["wmic", "computersystem", "get", "TotalPhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
864
1105
  tot_mem = int(output.stdout.strip().split(b"=")[-1].decode("utf-8")) / 1024 / 1024
865
1106
 
866
1107
  output = subprocess.run(["wmic", "OS", "get", "FreePhysicalMemory", "/", "Value"], stdout=subprocess.PIPE)
@@ -873,7 +1114,7 @@ def mem_info():
873
1114
  return True, {"msg": "Unknown operating system"}
874
1115
 
875
1116
 
876
- def polygon_area(poly):
1117
+ def polygon_area(poly: list) -> float:
877
1118
  """
878
1119
  area of polygon
879
1120
  from http://www.mathopenref.com/coordpolygonarea.html
@@ -888,7 +1129,20 @@ def polygon_area(poly):
888
1129
  return abs(tot / 2)
889
1130
 
890
1131
 
891
- def url2path(url):
1132
+ def polyline_length(poly: list) -> float:
1133
+ """
1134
+ length of polyline
1135
+ """
1136
+ tot = 0
1137
+ for p in range(1, len(poly)):
1138
+ x1, y1 = poly[p - 1]
1139
+ x2, y2 = poly[p]
1140
+ tot += ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
1141
+
1142
+ return tot
1143
+
1144
+
1145
+ def url2path(url: str) -> str:
892
1146
  """
893
1147
  convert URL in local path name
894
1148
  under windows, check if path name begin with /
@@ -919,14 +1173,20 @@ def time2seconds(time_: str) -> dec:
919
1173
  Decimal: time in seconds
920
1174
  """
921
1175
 
922
- try:
923
- flag_neg = "-" in time_
924
- time_ = time_.replace("-", "")
925
- tsplit = time_.split(":")
926
- h, m, s = int(tsplit[0]), int(tsplit[1]), dec(tsplit[2])
927
- return dec(-(h * 3600 + m * 60 + s)) if flag_neg else dec(h * 3600 + m * 60 + s)
928
- except Exception:
929
- return dec("0.000")
1176
+ if " " in time_:
1177
+ try:
1178
+ return dec(str(dt.datetime.strptime(time_, "%Y-%m-%d %H:%M:%S.%f").timestamp()))
1179
+ except Exception:
1180
+ return dec("0.000")
1181
+ else:
1182
+ try:
1183
+ flag_neg = "-" in time_
1184
+ time_ = time_.replace("-", "")
1185
+ tsplit = time_.split(":")
1186
+ h, m, s = int(tsplit[0]), int(tsplit[1]), dec(tsplit[2])
1187
+ return dec(-(h * 3600 + m * 60 + s)) if flag_neg else dec(h * 3600 + m * 60 + s)
1188
+ except Exception:
1189
+ return dec("0.000")
930
1190
 
931
1191
 
932
1192
  def seconds2time(sec: dec) -> str:
@@ -942,8 +1202,9 @@ def seconds2time(sec: dec) -> str:
942
1202
  if math.isnan(sec):
943
1203
  return cfg.NA
944
1204
 
945
- if sec > 1_600_000_000: # epoch time
946
- t = datetime.datetime.fromtimestamp(float(sec))
1205
+ # if sec > one day treat as date
1206
+ if sec > cfg.DATE_CUTOFF:
1207
+ t = dt.datetime.fromtimestamp(float(sec))
947
1208
  return f"{t:%Y-%m-%d %H:%M:%S}.{t.microsecond / 1000:03.0f}"
948
1209
 
949
1210
  neg_sign = "-" * (sec < 0)
@@ -1002,7 +1263,7 @@ def eol2space(s: str) -> str:
1002
1263
  return s.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
1003
1264
 
1004
1265
 
1005
- def test_ffmpeg_path(FFmpegPath):
1266
+ def test_ffmpeg_path(FFmpegPath: str) -> Tuple[bool, str]:
1006
1267
  """
1007
1268
  test if ffmpeg has valid path
1008
1269
 
@@ -1014,22 +1275,20 @@ def test_ffmpeg_path(FFmpegPath):
1014
1275
  str: message
1015
1276
  """
1016
1277
 
1017
- out, error = subprocess.Popen(
1018
- f'"{FFmpegPath}" -version', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
1019
- ).communicate()
1020
- logging.debug(f"test ffmpeg path output: {out}")
1021
- logging.debug(f"test ffmpeg path error: {error}")
1278
+ out, error = subprocess.Popen(f'"{FFmpegPath}" -version', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
1279
+ logger.debug(f"test ffmpeg path output: {out}")
1280
+ logger.debug(f"test ffmpeg path error: {error}")
1022
1281
 
1023
1282
  if (b"avconv" in out) or (b"the Libav developers" in error):
1024
1283
  return False, "Please use FFmpeg from https://www.ffmpeg.org in place of FFmpeg from Libav project."
1025
1284
 
1026
1285
  if (b"ffmpeg version" not in out) and (b"ffmpeg version" not in error):
1027
- return False, "FFmpeg is required but it was not found...<br>See https://www.ffmpeg.org"
1286
+ return False, "FFmpeg is required but it was not found.<br>See https://www.ffmpeg.org"
1028
1287
 
1029
1288
  return True, ""
1030
1289
 
1031
1290
 
1032
- def check_ffmpeg_path():
1291
+ def check_ffmpeg_path() -> Tuple[bool, str]:
1033
1292
  """
1034
1293
  check for ffmpeg path
1035
1294
  firstly search for embedded version
@@ -1040,43 +1299,24 @@ def check_ffmpeg_path():
1040
1299
  str: if bool True returns ffmpegpath else returns error message
1041
1300
  """
1042
1301
 
1302
+ # search embedded ffmpeg
1043
1303
  if sys.platform.startswith("linux") or sys.platform.startswith("darwin"):
1044
- ffmpeg_path = pl.Path("")
1045
- # search embedded ffmpeg
1046
- if sys.argv[0].endswith("start_boris.py"):
1047
- ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "boris" / "misc" / "ffmpeg"
1048
- if sys.argv[0].endswith("__main__.py"):
1049
- ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "misc" / "ffmpeg"
1050
-
1051
- if not ffmpeg_path.is_file():
1052
- # search global ffmpeg
1053
- ffmpeg_path = "ffmpeg"
1054
-
1055
- # test ffmpeg
1056
- r, msg = test_ffmpeg_path(str(ffmpeg_path))
1057
- if r:
1058
- return True, str(ffmpeg_path)
1059
- else:
1060
- return False, "FFmpeg is not available"
1304
+ ffmpeg_executable = Path("ffmpeg")
1305
+ elif sys.platform.startswith("win"):
1306
+ ffmpeg_executable = Path("ffmpeg.exe")
1061
1307
 
1062
- if sys.platform.startswith("win"):
1063
- ffmpeg_path = pl.Path("")
1064
- # search embedded ffmpeg
1065
- if sys.argv[0].endswith("start_boris.py"):
1066
- ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "boris" / "misc" / "ffmpeg.exe"
1067
- if sys.argv[0].endswith("__main__.py"):
1068
- ffmpeg_path = pl.Path(sys.argv[0]).resolve().parent / "misc" / "ffmpeg.exe"
1069
-
1070
- if not ffmpeg_path.is_file():
1071
- # search global ffmpeg
1072
- ffmpeg_path = "ffmpeg"
1073
-
1074
- # test ffmpeg
1075
- r, msg = test_ffmpeg_path(str(ffmpeg_path))
1076
- if r:
1077
- return True, str(ffmpeg_path)
1078
- else:
1079
- return False, "FFmpeg is not available"
1308
+ ffmpeg_path = Path(__file__).parent / "misc" / ffmpeg_executable
1309
+
1310
+ if not ffmpeg_path.is_file():
1311
+ # search global ffmpeg
1312
+ ffmpeg_path = ffmpeg_executable
1313
+
1314
+ # test ffmpeg
1315
+ r, msg = test_ffmpeg_path(str(ffmpeg_path))
1316
+ if r:
1317
+ return True, str(ffmpeg_path)
1318
+ else:
1319
+ return False, "FFmpeg is not available"
1080
1320
 
1081
1321
 
1082
1322
  def smart_size_format(n: Union[float, int, str, None]) -> str:
@@ -1098,6 +1338,73 @@ def smart_size_format(n: Union[float, int, str, None]) -> str:
1098
1338
  return f"{n / 1_000_000_000:,.1f} Gb"
1099
1339
 
1100
1340
 
1341
+ def get_systeminfo() -> str:
1342
+ """
1343
+ returns info about the system
1344
+ """
1345
+
1346
+ mpv_lib_version_, mpv_lib_file_path, mpv_api_version = mpv_lib_version()
1347
+
1348
+ system_info = (
1349
+ f"BORIS version: {version.__version__}\n"
1350
+ f"OS: {platform.uname().system} {platform.uname().release} {platform.uname().version}\n"
1351
+ f"CPU: {platform.uname().machine} {platform.uname().processor}\n"
1352
+ f"Python {platform.python_version()} ({'64-bit' if sys.maxsize > 2**32 else '32-bit'})\n"
1353
+ f"Qt {qVersion()} - PySide {pyside6_version}\n"
1354
+ f"MPV library version: {mpv_lib_version_}\n"
1355
+ f"MPV API version: {mpv_api_version}\n"
1356
+ f"MPV library file path: {mpv_lib_file_path}\n\n"
1357
+ )
1358
+
1359
+ r, memory = mem_info()
1360
+ if not r:
1361
+ system_info += (
1362
+ f"Memory (RAM) Total: {memory.get('total_memory', 'Not available'):.2f} Mb "
1363
+ f"Free: {memory.get('free_memory', 'Not available'):.2f} Mb\n\n"
1364
+ )
1365
+
1366
+ return system_info
1367
+
1368
+ """
1369
+ # system info
1370
+ systeminfo = ""
1371
+ if sys.platform.startswith("win"):
1372
+ # systeminfo = subprocess.getoutput("systeminfo")
1373
+ systeminfo = subprocess.run("systeminfo /FO csv /NH", capture_output=True, text=True, encoding="mbcs", shell=True).stdout
1374
+
1375
+ import csv
1376
+ from io import StringIO
1377
+
1378
+ # Parse it as CSV
1379
+ f = StringIO(systeminfo)
1380
+ reader = csv.reader(f)
1381
+ parsed_data = list(reader)[0]
1382
+ # Print specific fields by index
1383
+ info_to_show = ""
1384
+ info_to_show += f"Computer Name: {parsed_data[0]}\n"
1385
+ info_to_show += f"OS Name: {parsed_data[1]}\n"
1386
+ info_to_show += f"OS Version: {parsed_data[2]}\n"
1387
+ info_to_show += f"System Manufacturer: {parsed_data[11]}\n"
1388
+ info_to_show += f"System Model: {parsed_data[12]}\n"
1389
+ info_to_show += f"Processor: {parsed_data[14]}\n"
1390
+ info_to_show += f"Locale: {parsed_data[19]}\n"
1391
+ info_to_show += f"Installed Memory: {parsed_data[22]}\n"
1392
+
1393
+ # info about graphic card
1394
+ graphic_info = subprocess.run(
1395
+ "wmic path win32_videocontroller get name", capture_output=True, text=True, encoding="mbcs", shell=True
1396
+ ).stdout
1397
+ info_to_show += graphic_info.replace("\n", "").replace("Name", "Graphic card model")
1398
+
1399
+ systeminfo = info_to_show
1400
+
1401
+ if sys.platform.startswith("linux"):
1402
+ systeminfo = subprocess.getoutput("cat /etc/*rel*; uname -a")
1403
+
1404
+ return systeminfo
1405
+ """
1406
+
1407
+
1101
1408
  def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1102
1409
  """
1103
1410
  analyse video parameters with ffprobe (if available)
@@ -1109,28 +1416,32 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1109
1416
  Returns:
1110
1417
  dict
1111
1418
  """
1112
- # ffprobe -v quiet -print_format json -show_format -show_streams /tmp/ramdisk/video1.mp4
1419
+
1420
+ # check ffprobe executable in same place than ffmpeg
1113
1421
  ffprobe_bin = ffmpeg_bin.replace("ffmpeg", "ffprobe")
1422
+ if not Path(ffprobe_bin).is_file():
1423
+ if which(ffprobe_bin) is None:
1424
+ return {"error": "ffprobe not found"}
1114
1425
 
1115
1426
  command = f'"{ffprobe_bin}" -hide_banner -v error -print_format json -show_format -show_streams "{file_name}"'
1116
1427
 
1117
- # print(command)
1118
-
1119
1428
  p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
1120
1429
  out, error = p.communicate()
1121
1430
  if error:
1122
- return {"error": f"{error}"}
1431
+ if b"invalid data" in error:
1432
+ return {"error": f"{error}"}
1123
1433
 
1124
1434
  try:
1125
1435
  hasVideo = False
1126
1436
  hasAudio = False
1127
- '''bitrate = None'''
1437
+ """bitrate = None"""
1128
1438
  video_bitrate = None
1129
1439
  audio_bitrate = []
1130
1440
  resolution = None
1131
- fps = 0
1441
+ fps: float = 0
1132
1442
  sample_rate = None
1133
- duration = 0
1443
+ duration = None
1444
+ audio_duration = cfg.NA
1134
1445
  frames_number = None
1135
1446
  size = None
1136
1447
  audio_codec = None
@@ -1164,9 +1475,16 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1164
1475
  fps = eval(stream["r_frame_rate"])
1165
1476
  except Exception:
1166
1477
  fps = 0
1478
+ if fps >= 1000 and "avg_frame_rate" in stream: # case for some h265 video ("r_frame_rate": "1200000/1")
1479
+ try:
1480
+ fps = eval(stream["avg_frame_rate"])
1481
+ except Exception:
1482
+ pass
1167
1483
 
1168
- duration = float(stream["duration"])
1169
- # frames_number = int(stream["nb_frames"]) if "nb_frames" in stream else None
1484
+ fps = round(fps, 3)
1485
+
1486
+ if "duration" in stream:
1487
+ duration = float(stream["duration"])
1170
1488
  if "duration_ts" in stream:
1171
1489
  frames_number = int(stream["duration_ts"])
1172
1490
  elif "nb_frames" in stream:
@@ -1178,11 +1496,19 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1178
1496
 
1179
1497
  if stream["codec_type"] == "audio":
1180
1498
  hasAudio = True
1181
- sample_rate = float(stream["sample_rate"])
1182
- duration = float(stream["duration"])
1499
+ sample_rate = float(stream["sample_rate"]) if "sample_rate" in stream else cfg.NA
1500
+ # TODO manage audio_duration parameter
1501
+ audio_duration = float(stream["duration"]) if "duration" in stream else cfg.NA
1183
1502
  audio_codec = stream["codec_long_name"]
1184
1503
  audio_bitrate.append(int(stream.get("bit_rate", 0)))
1185
1504
 
1505
+ # check duration
1506
+ if duration is None:
1507
+ if "duration" in video_param["format"]:
1508
+ duration = float(video_param["format"]["duration"])
1509
+ else:
1510
+ duration = 0
1511
+
1186
1512
  # check bit rate
1187
1513
  if "bit_rate" in video_param["format"]:
1188
1514
  all_bitrate = int(video_param["format"]["bit_rate"])
@@ -1192,11 +1518,22 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1192
1518
  if video_bitrate is None and all_bitrate is not None:
1193
1519
  video_bitrate = all_bitrate - sum(audio_bitrate)
1194
1520
 
1521
+ # extract format long name
1522
+ format_long_name = video_param["format"]["format_long_name"] if "format_long_name" in video_param["format"] else cfg.NA
1523
+
1524
+ # extract creation time ("creation_time": "2023-03-22T16:50:32.000000Z")
1525
+ creation_time = cfg.NA
1526
+ if "tags" in video_param["format"] and "creation_time" in video_param["format"]["tags"]:
1527
+ creation_time = video_param["format"]["tags"]["creation_time"].replace("T", " ")
1528
+ if "." in creation_time:
1529
+ creation_time = creation_time.split(".")[0]
1530
+
1195
1531
  return {
1196
1532
  "analysis_program": "ffprobe",
1197
1533
  "frames_number": frames_number,
1198
1534
  "duration_ms": duration * 1000,
1199
1535
  "duration": duration,
1536
+ "audio_duration": audio_duration,
1200
1537
  "fps": fps,
1201
1538
  "has_video": hasVideo,
1202
1539
  "has_audio": hasAudio,
@@ -1206,6 +1543,8 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1206
1543
  "file size": size,
1207
1544
  "audio_codec": audio_codec,
1208
1545
  "video_codec": video_codec,
1546
+ "creation_time": creation_time,
1547
+ "format_long_name": format_long_name,
1209
1548
  }
1210
1549
 
1211
1550
  except Exception as e:
@@ -1214,7 +1553,7 @@ def ffprobe_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1214
1553
 
1215
1554
  def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1216
1555
  """
1217
- analyse frame rate and video duration with ffmpeg
1556
+ analyse frame rate and video duration with ffprobe or ffmpeg if ffprobe not available
1218
1557
  Returns parameters: duration, duration_ms, bitrate, frames_number, fps, has_video (True/False), has_audio (True/False)
1219
1558
 
1220
1559
  Args:
@@ -1228,13 +1567,14 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1228
1567
 
1229
1568
  ffprobe_results = ffprobe_media_analysis(ffmpeg_bin, file_name)
1230
1569
 
1570
+ logger.debug(f"file_name: {file_name}")
1571
+ logger.debug(f"ffprobe_results: {ffprobe_results}")
1572
+
1231
1573
  if ("error" not in ffprobe_results) and (ffprobe_results["bitrate"] is not None):
1232
1574
  return ffprobe_results
1233
1575
  else:
1234
1576
  # use ffmpeg
1235
- command = (
1236
- f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
1237
- )
1577
+ command = f'"{ffmpeg_bin}" -hide_banner -i "{file_name}" > {"NUL" if sys.platform.startswith("win") else "/dev/null"}'
1238
1578
 
1239
1579
  p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
1240
1580
 
@@ -1292,7 +1632,7 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1292
1632
  if b"Stream #" in row and b"Video:" in row:
1293
1633
  hasVideo = True
1294
1634
  # get resolution \d{3,5}x\d{3,5}
1295
- re_results = re.search(b"\d{3,5}x\d{3,5}", row, re.IGNORECASE)
1635
+ re_results = re.search(r"\d{3,5}x\d{3,5}", row, re.IGNORECASE)
1296
1636
  if re_results:
1297
1637
  resolution = re_results.group(0).decode("utf-8")
1298
1638
  break
@@ -1317,11 +1657,13 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1317
1657
  "frames_number": int(fps * duration),
1318
1658
  "duration_ms": duration * 1000,
1319
1659
  "duration": duration,
1660
+ "audio_duration": cfg.NA,
1320
1661
  "fps": fps,
1321
1662
  "has_video": hasVideo,
1322
1663
  "has_audio": hasAudio,
1323
1664
  "bitrate": bitrate,
1324
1665
  "resolution": resolution,
1666
+ "format_long_name": "",
1325
1667
  }
1326
1668
 
1327
1669
 
@@ -1362,12 +1704,37 @@ def all_behaviors(ethogram: dict) -> list:
1362
1704
  return [ethogram[x][cfg.BEHAVIOR_CODE] for x in sorted_keys(ethogram)]
1363
1705
 
1364
1706
 
1707
+ def all_subjects(subjects: dict) -> list:
1708
+ """
1709
+ extract all subjects from the subject configuration dictionary
1710
+
1711
+ Args:
1712
+ subject configuration (dict)
1713
+
1714
+ Returns:
1715
+ list: subjects name
1716
+ """
1717
+
1718
+ return [subjects[x][cfg.SUBJECT_NAME] for x in sorted_keys(subjects)]
1719
+
1720
+
1721
+ def has_coding_map(ethogram: dict, behavior_idx: str) -> bool:
1722
+ """
1723
+ check if behavior index has a coding map
1724
+ """
1725
+ if not ethogram.get(behavior_idx, False):
1726
+ return False
1727
+ if not ethogram[behavior_idx].get("coding map", False):
1728
+ return False
1729
+ return True
1730
+
1731
+
1365
1732
  def dir_images_number(dir_path_str: str) -> dict:
1366
1733
  """
1367
1734
  return number of images in dir_path (see cfg.IMAGE_EXTENSIONS)
1368
1735
  """
1369
1736
 
1370
- dir_path = pl.Path(dir_path_str)
1737
+ dir_path = Path(dir_path_str)
1371
1738
  if not dir_path.is_dir():
1372
1739
  return {"error": f"The directory {dir_path_str} does not exists"}
1373
1740
  img_count = 0
@@ -1405,29 +1772,11 @@ def intersection(A, B, C, D):
1405
1772
  ym = slope * xm + intersept
1406
1773
  else:
1407
1774
  xm = (
1408
- (
1409
- xd * xa * yc
1410
- - xd * xb * yc
1411
- - xd * xa * yb
1412
- - xc * xa * yd
1413
- + xc * xa * yb
1414
- + xd * ya * xb
1415
- + xc * xb * yd
1416
- - xc * ya * xb
1417
- )
1775
+ (xd * xa * yc - xd * xb * yc - xd * xa * yb - xc * xa * yd + xc * xa * yb + xd * ya * xb + xc * xb * yd - xc * ya * xb)
1418
1776
  / (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
1419
1777
  ).quantize(dec(".001"), rounding=ROUND_DOWN)
1420
1778
  ym = (
1421
- (
1422
- yb * xc * yd
1423
- - yb * yc * xd
1424
- - ya * xc * yd
1425
- + ya * yc * xd
1426
- - xa * yb * yd
1427
- + xa * yb * yc
1428
- + ya * xb * yd
1429
- - ya * xb * yc
1430
- )
1779
+ (yb * xc * yd - yb * yc * xd - ya * xc * yd + ya * yc * xd - xa * yb * yd + xa * yb * yc + ya * xb * yd - ya * xb * yc)
1431
1780
  / (-yb * xd + yb * xc + ya * xd - ya * xc + xb * yd - xb * yc - xa * yd + xa * yc)
1432
1781
  ).quantize(dec(".001"), rounding=ROUND_DOWN)
1433
1782
 
@@ -1436,16 +1785,7 @@ def intersection(A, B, C, D):
1436
1785
  ymin1, ymax1 = min(ya, yb), max(ya, yb)
1437
1786
  ymin2, ymax2 = min(yc, yd), max(yc, yd)
1438
1787
 
1439
- return (
1440
- xm >= xmin1
1441
- and xm <= xmax1
1442
- and xm >= xmin2
1443
- and xm <= xmax2
1444
- and ym >= ymin1
1445
- and ym <= ymax1
1446
- and ym >= ymin2
1447
- and ym <= ymax2
1448
- )
1788
+ return xm >= xmin1 and xm <= xmax1 and xm >= xmin2 and xm <= xmax2 and ym >= ymin1 and ym <= ymax1 and ym >= ymin2 and ym <= ymax2
1449
1789
 
1450
1790
  except Exception: # for cases xa=xb=xc=xd
1451
1791
  return True