boris-behav-obs 9.7.7__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 (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  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 +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/latency.py ADDED
@@ -0,0 +1,244 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program is free software; you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation; either version 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program; if not, write to the Free Software
18
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
+ MA 02110-1301, USA.
20
+
21
+
22
+ Module for analyzing the latency of behaviors after another behavior(s) (marker)
23
+
24
+ """
25
+
26
+ from . import config as cfg
27
+ from . import select_subj_behav
28
+ from . import dialog
29
+ from . import select_observations
30
+ from . import project_functions, observation_operations
31
+
32
+ from PySide6.QtWidgets import QMessageBox
33
+
34
+
35
+ def get_latency(self):
36
+ """
37
+ get latency (time after marker/stimulus)
38
+ """
39
+
40
+ QMessageBox.warning(
41
+ None,
42
+ cfg.programName,
43
+ (
44
+ "This function is experimental. Please test it and report any bug at <br>"
45
+ '<a href="https://github.com/olivierfriard/BORIS/issues">'
46
+ "https://github.com/olivierfriard/BORIS/issues</a><br>"
47
+ "Thank you for your collaboration!"
48
+ ),
49
+ QMessageBox.Ok | QMessageBox.Default,
50
+ QMessageBox.NoButton,
51
+ )
52
+
53
+ SUBJECT, BEHAVIOR, MODIFIERS = 0, 1, 2
54
+
55
+ _, selected_observations = select_observations.select_observations2(
56
+ self, cfg.SELECT1, windows_title="Select one observation for latency analysis"
57
+ )
58
+
59
+ if not selected_observations:
60
+ return
61
+
62
+ # check if coded behaviors are defined in ethogram
63
+ if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
64
+ return
65
+
66
+ # check if state events are paired
67
+ not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
68
+ if not_ok or not selected_observations:
69
+ return
70
+
71
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
72
+ # exit with message if events do not have timestamp
73
+ if start_coding.is_nan():
74
+ QMessageBox.critical(
75
+ None,
76
+ cfg.programName,
77
+ ("This function is not available for observations with events that do not have timestamp"),
78
+ QMessageBox.Ok | QMessageBox.Default,
79
+ QMessageBox.NoButton,
80
+ )
81
+ return
82
+
83
+ parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
84
+ self,
85
+ selected_observations,
86
+ show_exclude_non_coded_behaviors=False,
87
+ window_title="Select the marker behaviors (stimulus)",
88
+ n_observations=len(selected_observations),
89
+ )
90
+
91
+ if parameters == {}:
92
+ return
93
+
94
+ if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
95
+ QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
96
+ return
97
+
98
+ marker_behaviors = parameters[cfg.SELECTED_BEHAVIORS]
99
+ marker_subjects = parameters[cfg.SELECTED_SUBJECTS]
100
+ include_marker_modifiers = parameters[cfg.INCLUDE_MODIFIERS]
101
+
102
+ print(f"{marker_behaviors=} {marker_subjects=} {include_marker_modifiers=}")
103
+
104
+ parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
105
+ self, selected_observations, show_exclude_non_coded_behaviors=False, window_title="Select the latency behaviors"
106
+ )
107
+ if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
108
+ return
109
+ latency_behaviors = parameters[cfg.SELECTED_BEHAVIORS]
110
+ latency_subjects = parameters[cfg.SELECTED_SUBJECTS]
111
+ include_latency_modifiers = parameters[cfg.INCLUDE_MODIFIERS]
112
+
113
+ print(f"{latency_behaviors=} {latency_subjects=} {include_latency_modifiers=}")
114
+
115
+ results: dict = {}
116
+ for obs_id in selected_observations:
117
+ print(f"{obs_id=}")
118
+
119
+ events_with_status = project_functions.events_start_stop(
120
+ self.pj[cfg.ETHOGRAM],
121
+ self.pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS],
122
+ self.pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE],
123
+ )
124
+
125
+ print(f"{events_with_status=}")
126
+
127
+ for idx, event in enumerate(events_with_status):
128
+ print(f"{event=}")
129
+
130
+ print(f"{event[cfg.EVENT_STATUS_FIELD_IDX]=}")
131
+
132
+ print(f"{event[cfg.EVENT_BEHAVIOR_FIELD_IDX]=}")
133
+
134
+ print(f"{event[cfg.EVENT_SUBJECT_FIELD_IDX]=}")
135
+
136
+ if all(
137
+ (
138
+ event[cfg.EVENT_STATUS_FIELD_IDX] in (cfg.START, cfg.POINT),
139
+ event[cfg.EVENT_BEHAVIOR_FIELD_IDX] in marker_behaviors,
140
+ any(
141
+ (
142
+ event[cfg.EVENT_SUBJECT_FIELD_IDX] in marker_subjects,
143
+ all((event[cfg.EVENT_SUBJECT_FIELD_IDX] == "", cfg.NO_FOCAL_SUBJECT in marker_subjects)),
144
+ )
145
+ ),
146
+ )
147
+ ):
148
+ if include_marker_modifiers:
149
+ marker = event[cfg.EVENT_TIME_FIELD_IDX : cfg.EVENT_MODIFIER_FIELD_IDX + 1]
150
+ else:
151
+ marker = event[cfg.EVENT_TIME_FIELD_IDX : cfg.EVENT_BEHAVIOR_FIELD_IDX + 1]
152
+
153
+ print(f"{marker=}")
154
+
155
+ if marker not in results:
156
+ results[marker] = {}
157
+
158
+ for event2 in events_with_status[idx + 1 :]:
159
+ if all(
160
+ (
161
+ event2[cfg.EVENT_STATUS_FIELD_IDX] in (cfg.START, cfg.POINT),
162
+ event2[cfg.EVENT_BEHAVIOR_FIELD_IDX] in latency_behaviors,
163
+ any(
164
+ (
165
+ event2[cfg.EVENT_SUBJECT_FIELD_IDX] in latency_subjects,
166
+ all(
167
+ (
168
+ event2[cfg.EVENT_SUBJECT_FIELD_IDX] == "",
169
+ cfg.NO_FOCAL_SUBJECT in latency_subjects,
170
+ )
171
+ ),
172
+ )
173
+ ),
174
+ )
175
+ ):
176
+ print(event, event2)
177
+ if include_latency_modifiers:
178
+ latency = event2[cfg.EVENT_SUBJECT_FIELD_IDX : cfg.EVENT_MODIFIER_FIELD_IDX + 1]
179
+ else:
180
+ latency = event2[cfg.EVENT_SUBJECT_FIELD_IDX : cfg.EVENT_BEHAVIOR_FIELD_IDX + 1]
181
+
182
+ # print(f"{marker=}")
183
+ print(f"{latency=}")
184
+ if latency not in results[marker]:
185
+ results[marker][latency] = []
186
+ results[marker][latency].append(event2[cfg.EVENT_TIME_FIELD_IDX] - event[cfg.EVENT_TIME_FIELD_IDX])
187
+
188
+ print(f"{results[marker][latency]=}")
189
+
190
+ # check if new marker
191
+ if all(
192
+ (
193
+ event2[cfg.EVENT_STATUS_FIELD_IDX] in (cfg.START, cfg.POINT),
194
+ event2[cfg.EVENT_BEHAVIOR_FIELD_IDX] in marker_behaviors,
195
+ any(
196
+ (
197
+ event2[cfg.EVENT_SUBJECT_FIELD_IDX] in marker_subjects,
198
+ all(
199
+ (
200
+ event2[cfg.EVENT_SUBJECT_FIELD_IDX] == "",
201
+ cfg.NO_FOCAL_SUBJECT in marker_subjects,
202
+ )
203
+ ),
204
+ )
205
+ ),
206
+ )
207
+ ):
208
+ break
209
+
210
+ break
211
+
212
+ # print()
213
+ # import pprint
214
+ # pprint.pprint(results)
215
+
216
+ out = ""
217
+
218
+ for marker in sorted(results.keys()):
219
+ subject = cfg.NO_FOCAL_SUBJECT if marker[cfg.EVENT_SUBJECT_FIELD_IDX] == "" else marker[1]
220
+ if include_marker_modifiers:
221
+ out += f"Marker: <b>{marker[cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> at {marker[cfg.EVENT_TIME_FIELD_IDX]} s (subject: {subject} - modifiers: {marker[cfg.EVENT_MODIFIER_FIELD_IDX]})<br><br>"
222
+ else:
223
+ out += f"Marker: <b>{marker[cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> at {marker[cfg.EVENT_TIME_FIELD_IDX]} s (subject: {subject})<br><br>"
224
+ for behav in results[marker]:
225
+ subject = cfg.NO_FOCAL_SUBJECT if behav[SUBJECT] == "" else behav[SUBJECT]
226
+ if include_latency_modifiers:
227
+ out += f"\nLatency for behavior: <b>{behav[BEHAVIOR]}</b> (subject: {subject} - modifiers: {behav[MODIFIERS]})<br>"
228
+ else:
229
+ out += f"\nLatency for behavior: <b>{behav[BEHAVIOR]}</b> (subject: {subject})<br>"
230
+
231
+ out += "first occurrence: "
232
+ out += f"{sorted(results[marker][behav])[0]} s<br>"
233
+ out += "all occurrences: "
234
+
235
+ out += ", ".join([f"{x} s" for x in sorted(results[marker][behav])])
236
+ out += "<br><br>"
237
+
238
+ out += "<br><br>"
239
+
240
+ self.results = dialog.Results_dialog()
241
+ self.results.setWindowTitle("Latency")
242
+ self.results.ptText.clear()
243
+ self.results.ptText.appendHtml(out)
244
+ self.results.show()
@@ -0,0 +1,161 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ import logging
24
+
25
+ from PySide6.QtCore import Signal
26
+
27
+ # from PySide6.QtGui import *
28
+ from PySide6.QtWidgets import (
29
+ QApplication,
30
+ QWidget,
31
+ QRadioButton,
32
+ QLabel,
33
+ QHBoxLayout,
34
+ QVBoxLayout,
35
+ QLineEdit,
36
+ QPlainTextEdit,
37
+ QCheckBox,
38
+ QPushButton,
39
+ QFileDialog,
40
+ QMessageBox,
41
+ )
42
+ from . import dialog
43
+ from . import config as cfg
44
+
45
+
46
+ class wgMeasurement(QWidget):
47
+ """ """
48
+
49
+ closeSignal, clearSignal = Signal(), Signal()
50
+ flagSaved = True
51
+ draw_mem = []
52
+
53
+ def __init__(self):
54
+ super().__init__()
55
+
56
+ self.setWindowTitle("Geometric measurements")
57
+
58
+ vbox = QVBoxLayout(self)
59
+
60
+ self.rbPoint = QRadioButton("Point (left click)")
61
+ vbox.addWidget(self.rbPoint)
62
+
63
+ self.rbDistance = QRadioButton("Distance (start: left click, end: right click)")
64
+ vbox.addWidget(self.rbDistance)
65
+
66
+ self.rbArea = QRadioButton("Area (left click for area vertices, right click to close area)")
67
+ vbox.addWidget(self.rbArea)
68
+
69
+ self.rbAngle = QRadioButton("Angle (vertex: left click, segments: right click)")
70
+ vbox.addWidget(self.rbAngle)
71
+
72
+ self.cbPersistentMeasurements = QCheckBox("Measurements are persistent")
73
+ self.cbPersistentMeasurements.setChecked(True)
74
+ vbox.addWidget(self.cbPersistentMeasurements)
75
+
76
+ vbox.addWidget(QLabel("<b>Scale</b>"))
77
+
78
+ hbox1 = QHBoxLayout()
79
+
80
+ self.lbRef = QLabel("Reference")
81
+ hbox1.addWidget(self.lbRef)
82
+
83
+ self.lbPx = QLabel("Pixels")
84
+ hbox1.addWidget(self.lbPx)
85
+
86
+ vbox.addLayout(hbox1)
87
+
88
+ hbox2 = QHBoxLayout()
89
+
90
+ self.leRef = QLineEdit()
91
+ self.leRef.setText("1")
92
+ hbox2.addWidget(self.leRef)
93
+
94
+ self.lePx = QLineEdit()
95
+ self.lePx.setText("1")
96
+ hbox2.addWidget(self.lePx)
97
+
98
+ vbox.addLayout(hbox2)
99
+
100
+ self.pte = QPlainTextEdit()
101
+ vbox.addWidget(self.pte)
102
+
103
+ self.status_lb = QLabel()
104
+ vbox.addWidget(self.status_lb)
105
+
106
+ hbox3 = QHBoxLayout()
107
+
108
+ self.pbClear = QPushButton("Clear measurements", clicked=self.pbClear_clicked)
109
+ hbox3.addWidget(self.pbClear)
110
+
111
+ self.pbSave = QPushButton("Save results", clicked=self.pbSave_clicked)
112
+ hbox3.addWidget(self.pbSave)
113
+
114
+ self.pbClose = QPushButton(cfg.CLOSE, clicked=self.pbClose_clicked)
115
+ hbox3.addWidget(self.pbClose)
116
+
117
+ vbox.addLayout(hbox3)
118
+
119
+ def pbClear_clicked(self):
120
+ """
121
+ clear measurements draw and results
122
+ """
123
+ self.draw_mem = {}
124
+ self.pte.clear()
125
+ self.clearSignal.emit()
126
+
127
+ def pbClose_clicked(self):
128
+ if not self.flagSaved:
129
+ response = dialog.MessageDialog(
130
+ cfg.programName,
131
+ "The current results are not saved. Do you want to save results before closing?",
132
+ [cfg.YES, cfg.NO, cfg.CANCEL],
133
+ )
134
+ if response == cfg.YES:
135
+ self.pbSave_clicked()
136
+ if response == cfg.CANCEL:
137
+ return
138
+ self.closeSignal.emit()
139
+
140
+ def pbSave_clicked(self):
141
+ """
142
+ save results
143
+ """
144
+ if self.pte.toPlainText():
145
+ fileName, _ = QFileDialog().getSaveFileName(self, "Save measurement results", "", "Text files (*.txt);;All files (*)")
146
+ if fileName:
147
+ with open(fileName, "w") as f:
148
+ f.write(self.pte.toPlainText())
149
+ self.flagSaved = True
150
+ else:
151
+ QMessageBox.information(self, cfg.programName, "There are no results to save")
152
+
153
+
154
+ if __name__ == "__main__":
155
+ import sys
156
+
157
+ app = QApplication(sys.argv)
158
+ w = wgMeasurement(logging.getLogger().getEffectiveLevel())
159
+ w.show()
160
+
161
+ sys.exit(app.exec_())
boris/media_file.py ADDED
@@ -0,0 +1,115 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ from PySide6.QtWidgets import QFileDialog
24
+
25
+ from . import config as cfg
26
+ from . import utilities as util
27
+ from . import dialog
28
+ from . import project_functions
29
+ from . import utilities as util
30
+
31
+
32
+ def get_info(self) -> None:
33
+ """
34
+ show info about media file (current media file if an observation is opened)
35
+ """
36
+
37
+ def media_analysis_str(ffmpeg_bin: str, media_full_path: str) -> str:
38
+ r = util.accurate_media_analysis(ffmpeg_bin, media_full_path)
39
+
40
+ if "error" in r:
41
+ ffmpeg_output = f"File path: {media_full_path}<br><br>{r['error']}<br><br>"
42
+ else:
43
+ ffmpeg_output = f"<br><b>{r['analysis_program']} analysis</b><br>"
44
+
45
+ ffmpeg_output += (
46
+ f"File path: <b>{media_full_path}</b><br><br>"
47
+ f"Duration: {r['duration']} seconds ({util.convertTime(self.timeFormat, r['duration'])})<br>"
48
+ f"FPS: {r['fps']}<br>"
49
+ f"Resolution: {r['resolution']} pixels<br>"
50
+ f"Format long name: {r.get('format_long_name', cfg.NA)}<br>"
51
+ f"Creation time: {r.get('creation_time', cfg.NA)}<br>"
52
+ f"Number of frames: {r['frames_number']}<br>"
53
+ f"Bitrate: {util.smart_size_format(r['bitrate'])} <br>"
54
+ f"Has video: {r['has_video']}<br>"
55
+ f"Has audio: {r['has_audio']}<br>"
56
+ f"File size: {util.smart_size_format(r.get('file size', cfg.NA))}<br>"
57
+ f"Video codec: {r.get('video_codec', cfg.NA)}<br>"
58
+ f"Audio codec: {r.get('audio_codec', cfg.NA)}<br>"
59
+ )
60
+
61
+ return ffmpeg_output
62
+
63
+ if self.observationId and self.playerType == cfg.MEDIA:
64
+ tot_output: str = ""
65
+
66
+ for i, dw in enumerate(self.dw_player):
67
+ if not (
68
+ str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
69
+ and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][str(i + 1)]
70
+ ):
71
+ continue
72
+
73
+ mpv_output = (
74
+ "<b>MPV information</b><br>"
75
+ f"Duration: {dw.player.duration} seconds ({util.seconds2time(dw.player.duration)})<br>"
76
+ # "Position: {} %<br>"
77
+ f"FPS: {dw.player.container_fps}<br>"
78
+ # "Rate: {}<br>"
79
+ f"Resolution: {dw.player.width}x{dw.player.height} pixels<br>"
80
+ # "Scale: {}<br>"
81
+ f"Video format: {dw.player.video_format}<br>"
82
+ # "State: {}<br>"
83
+ # "Media Resource Location: {}<br>"
84
+ # "File name: {}<br>"
85
+ # "Track: {}/{}<br>"
86
+ f"Number of media in media list: {dw.player.playlist_count}<br>"
87
+ f"Current time position: {dw.player.time_pos}<br>"
88
+ f"Aspect ratio: {round(dw.player.width / dw.player.height, 3)}<br>"
89
+ # "is seekable? {}<br>"
90
+ # "has_vout? {}<br>"
91
+ )
92
+
93
+ # FFmpeg/FFprobe analysis
94
+ ffmpeg_output: str = ""
95
+ for file_path in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][str(i + 1)]:
96
+ media_full_path = project_functions.full_path(file_path, self.projectFileName)
97
+ ffmpeg_output += media_analysis_str(self.ffmpeg_bin, media_full_path)
98
+
99
+ ffmpeg_output += f"<br>Total duration: {sum(self.dw_player[i].media_durations) / 1000} ({util.convertTime(self.timeFormat, sum(self.dw_player[i].media_durations) / 1000)})"
100
+
101
+ tot_output += mpv_output + ffmpeg_output + "<br><hr>"
102
+
103
+ else: # no open observation
104
+ file_paths, _ = QFileDialog().getOpenFileNames(self, "Select a media file", "", "Media files (*)")
105
+ if not file_paths:
106
+ return
107
+
108
+ tot_output: str = ""
109
+ for file_path in file_paths:
110
+ tot_output += media_analysis_str(self.ffmpeg_bin, file_path)
111
+
112
+ self.results = dialog.Results_dialog()
113
+ self.results.setWindowTitle(f"{cfg.programName} - Media file information")
114
+ self.results.ptText.appendHtml(tot_output)
115
+ self.results.show()