boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__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.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  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 +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  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 +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  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.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
@@ -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 file is part of BORIS.
7
7
 
@@ -20,13 +20,13 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
-
24
23
  import os
25
24
  import tempfile
26
- import pathlib as pl
25
+ from pathlib import Path
26
+ import logging
27
27
 
28
- from PyQt5.QtWidgets import QFileDialog, QMessageBox, QInputDialog
29
- from PyQt5.QtCore import (
28
+ from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
29
+ from PySide6.QtCore import (
30
30
  Qt,
31
31
  QProcess,
32
32
  )
@@ -41,26 +41,36 @@ def ffmpeg_process(self, action: str):
41
41
  launch ffmpeg process with QProcess
42
42
 
43
43
  Args:
44
- action (str): "reencode_resize, rotate, merge
44
+ action (str): "reencode_resize, rotate, merge, video_spectrogram
45
45
  """
46
- if action not in ("reencode_resize", "rotate", "merge"):
46
+ if action not in ("reencode_resize", "rotate", "merge", "video_spectrogram"):
47
47
  return
48
48
 
49
49
  def readStdOutput(idx):
50
-
50
+ """
51
+ read stdout and stderr form qprocess and display them
52
+ """
51
53
  self.processes_widget.label.setText(
52
54
  (
53
- "This operation can be long. Be patient...\n\n"
55
+ "This operation can be long. Be patient...\n"
56
+ "In the meanwhile you can continue to use BORIS\n\n"
54
57
  f"Done: {self.processes_widget.number_of_files - len(self.processes)} of {self.processes_widget.number_of_files}"
55
58
  )
56
59
  )
57
- self.processes_widget.lwi.clear()
58
- self.processes_widget.lwi.addItems(
59
- [
60
- self.processes[idx - 1][1][2],
61
- self.processes[idx - 1][0].readAllStandardOutput().data().decode("utf-8"),
62
- ]
63
- )
60
+
61
+ # self.processes_widget.lwi.clear()
62
+ std_out = self.processes[idx - 1][0].readAllStandardOutput().data().decode("utf-8")
63
+ if std_out:
64
+ self.processes_widget.lwi.addItems((f"{Path(self.processes[idx - 1][1][2]).name}: {std_out}",))
65
+
66
+ """
67
+ std_err = self.processes[idx - 1][0].readAllStandardError().data().decode("utf-8")
68
+ if std_err:
69
+ self.processes_widget.lwi.addItems((f"{pl.Path(self.processes[idx - 1][1][2]).name}: ERROR: {std_err}",))
70
+ self.flag_ffmpeg_error = True
71
+ """
72
+
73
+ self.processes_widget.lwi.scrollToBottom()
64
74
 
65
75
  def qprocess_finished(idx):
66
76
  """
@@ -69,13 +79,19 @@ def ffmpeg_process(self, action: str):
69
79
  if self.processes:
70
80
  del self.processes[idx - 1]
71
81
  if self.processes:
82
+ # start new process
72
83
  self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
73
84
  else:
85
+ self.processes_widget.label.setText(
86
+ (f"Done: {self.processes_widget.number_of_files - len(self.processes)} of {self.processes_widget.number_of_files}")
87
+ )
88
+ """
74
89
  self.processes_widget.hide()
75
90
  del self.processes_widget
91
+ """
76
92
 
77
93
  if self.processes:
78
- QMessageBox.warning(self, cfg.programName, "BORIS is already doing some job.")
94
+ QMessageBox.warning(self, cfg.programName, "BORIS is already running some job.")
79
95
  return
80
96
 
81
97
  if action == "merge":
@@ -84,34 +100,37 @@ def ffmpeg_process(self, action: str):
84
100
  else:
85
101
  msg = f"Select one or more video files to {action.replace('_', ' and ')}"
86
102
  file_type = "Video files (*)"
87
- fn = QFileDialog().getOpenFileNames(self, msg, "", file_type)
88
- fileNames = fn[0] if type(fn) is tuple else fn
103
+ file_names, _ = QFileDialog().getOpenFileNames(self, msg, "", file_type)
89
104
 
90
- if not fileNames:
105
+ if not file_names:
91
106
  return
92
107
 
93
108
  if action == "reencode_resize":
94
- current_bitrate = 2000
109
+ current_bitrate = 10_000_000 # default 10 Mb/s
95
110
  current_resolution = 1024
96
111
 
97
- r = util.accurate_media_analysis(self.ffmpeg_bin, fileNames[0])
112
+ r = util.accurate_media_analysis(self.ffmpeg_bin, file_names[0])
98
113
  if "error" in r:
99
- QMessageBox.warning(self, cfg.programName, f"{fileNames[0]}. {r['error']}")
114
+ QMessageBox.warning(self, cfg.programName, f"{file_names[0]}. {r['error']}")
100
115
  elif r["has_video"]:
101
- current_bitrate = r.get("bitrate", -1)
116
+ current_bitrate = r.get("bitrate", None)
117
+ if current_bitrate is None:
118
+ current_bitrate = -1
119
+ else:
120
+ current_bitrate = round(current_bitrate / 1024 / 1024) # Convert to Mb/s
102
121
  current_resolution = int(r["resolution"].split("x")[0]) if r["resolution"] is not None else None
103
122
 
104
123
  ib = dialog.Input_dialog(
105
124
  "Set the parameters for re-encoding / resizing",
106
125
  [
107
126
  ("sb", "Horizontal resolution (in pixel)", 352, 3840, 100, current_resolution),
108
- ("sb", "Video quality (bitrate)", 100, 1000000, 500, current_bitrate),
127
+ ("sb", "Video quality (bitrate Mb/s)", 1, 1000, 1, current_bitrate),
109
128
  ],
110
129
  )
111
130
  if not ib.exec_():
112
131
  return
113
132
 
114
- if len(fileNames) > 1:
133
+ if len(file_names) > 1:
115
134
  if (
116
135
  dialog.MessageDialog(
117
136
  cfg.programName,
@@ -123,18 +142,18 @@ def ffmpeg_process(self, action: str):
123
142
  return
124
143
 
125
144
  horiz_resol = ib.elements["Horizontal resolution (in pixel)"].value()
126
- video_quality = ib.elements["Video quality (bitrate)"].value()
145
+ video_quality = ib.elements["Video quality (bitrate Mb/s)"].value()
127
146
 
128
147
  if action == "merge":
129
- if len(fileNames) == 1:
148
+ if len(file_names) == 1:
130
149
  QMessageBox.critical(self, cfg.programName, "Select more than one file")
131
150
  return
132
151
 
133
152
  file_extensions = [] # check extension of 1st media file
134
153
  file_list_lst = []
135
- for file_name in fileNames:
154
+ for file_name in file_names:
136
155
  file_list_lst.append(f"file '{file_name}'")
137
- file_extensions.append(pl.Path(file_name).suffix)
156
+ file_extensions.append(Path(file_name).suffix)
138
157
  if len(set(file_extensions)) > 1:
139
158
  QMessageBox.critical(self, cfg.programName, "All media files must have the same format")
140
159
  return
@@ -143,11 +162,14 @@ def ffmpeg_process(self, action: str):
143
162
  output_file_name, _ = QFileDialog().getSaveFileName(self, "Output file name", "", "*")
144
163
  if output_file_name == "":
145
164
  return
146
- if pl.Path(output_file_name).suffix != file_extensions[0]:
165
+ if Path(output_file_name).suffix != file_extensions[0]:
147
166
  QMessageBox.warning(
148
167
  self,
149
168
  cfg.programName,
150
- f"The extension of output file must be the same than the extension of input files (<b>{file_extensions[0]}</b>).<br>You selected a {pl.Path(output_file_name).suffix} file.",
169
+ (
170
+ "The extension of output file must be the same than the extension of input files "
171
+ f"(<b>{file_extensions[0]}</b>).<br>You selected a {Path(output_file_name).suffix} file."
172
+ ),
151
173
  )
152
174
  else:
153
175
  break
@@ -170,11 +192,13 @@ def ffmpeg_process(self, action: str):
170
192
  # check if processed files already exist
171
193
  if action in ("reencode_resize", "rotate"):
172
194
  files_list = []
173
- for file_name in fileNames:
195
+ for file_name in file_names:
174
196
  if action == "reencode_resize":
175
- fn = f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}k.avi"
197
+ fn = f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi"
198
+
176
199
  if action == "rotate":
177
200
  fn = f"{file_name}.rotated{['', '90', '-90', '180'][rotation_idx]}.avi"
201
+
178
202
  if os.path.isfile(fn):
179
203
  files_list.append(fn)
180
204
 
@@ -188,86 +212,121 @@ def ffmpeg_process(self, action: str):
188
212
  return
189
213
 
190
214
  self.processes_widget = dialog.Info_widget()
191
- self.processes_widget.resize(350, 100)
192
- self.processes_widget.setWindowFlags(Qt.WindowStaysOnTopHint)
193
- if action == "reencode_resize":
194
- self.processes_widget.setWindowTitle("Re-encoding and resizing with FFmpeg")
195
- if action == "rotate":
196
- self.processes_widget.setWindowTitle("Rotating the video with FFmpeg")
197
- if action == "merge":
198
- self.processes_widget.setWindowTitle("Merging media files")
215
+ self.processes_widget.resize(700, 300)
199
216
 
200
- self.processes_widget.label.setText("This operation can be long. Be patient...\n\n")
201
- self.processes_widget.number_of_files = len(fileNames)
217
+ self.processes_widget.setWindowFlags(Qt.WindowStaysOnTopHint)
218
+ match action:
219
+ case "reencode_resize":
220
+ self.processes_widget.setWindowTitle("Re-encoding and resizing with FFmpeg")
221
+ case "rotate":
222
+ self.processes_widget.setWindowTitle("Rotating the video with FFmpeg")
223
+ case "merge":
224
+ self.processes_widget.setWindowTitle("Merging media files")
225
+ case "video_spectrogram":
226
+ self.processes_widget.setWindowTitle("Creating a video spectrogram")
227
+
228
+ self.processes_widget.label.setText("This operation can be long. Be patient...\nIn the meanwhile you can continue to use BORIS\n\n")
229
+ self.processes_widget.number_of_files = len(file_names)
202
230
  self.processes_widget.show()
203
231
 
204
- if action == "merge":
205
- # ffmpeg -f concat -safe 0 -i join_video.txt -c copy output_demuxer.mp4
206
- args = ["-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output_file_name]
207
- self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
208
- self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
209
- self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
210
- self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
211
- self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
212
-
213
- self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
232
+ match action:
233
+ case "merge":
234
+ # ffmpeg -f concat -safe 0 -i join_video.txt -c copy output.mp4
235
+ args = ["-hide_banner", "-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output_file_name]
236
+ self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
237
+ self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
238
+ self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
239
+ self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
240
+ self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
214
241
 
215
- if action in ("reencode_resize", "rotate"):
216
- for file_name in fileNames:
242
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
217
243
 
218
- if action == "reencode_resize":
244
+ case "video_spectrogram":
245
+ # ffmpeg -i video.mp4 -filter_complex showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt -y -acodec copy output.mp4
246
+ for file_name in sorted(file_names, reverse=True):
247
+ output_file_name = str(Path(file_name).with_suffix(f".spectrogram{Path(file_name).suffix}"))
219
248
  args = [
249
+ "-hide_banner",
220
250
  "-y",
221
251
  "-i",
222
- f"{file_name}",
223
- "-vf",
224
- f"scale={horiz_resol}:-1",
225
- "-b:v",
226
- f"{video_quality}k",
227
- f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}k.avi",
252
+ file_name,
253
+ "-filter_complex",
254
+ "showspectrum=mode=combined:color=intensity:slide=1:scale=cbrt",
255
+ "-acodec",
256
+ "copy",
257
+ output_file_name,
228
258
  ]
259
+ self.processes.append([QProcess(self), [self.ffmpeg_bin, args, output_file_name]])
260
+ self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
261
+ self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
262
+ self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
263
+ self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
229
264
 
230
- if action == "rotate":
231
-
232
- # check bitrate
233
- r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
234
- if "error" not in r and r["bitrate"] != -1:
235
- video_quality = r["bitrate"]
236
- else:
237
- video_quality = 2000
238
-
239
- if rotation_idx in [1, 2]:
240
- args = [
241
- "-y",
242
- "-i",
243
- f"{file_name}",
244
- "-vf",
245
- f"transpose={rotation_idx}",
246
- "-codec:a",
247
- "copy",
248
- "-b:v",
249
- f"{video_quality}k",
250
- f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
251
- ]
265
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
252
266
 
253
- if rotation_idx == 3: # 180
267
+ case "reencode_resize" | "rotate":
268
+ for file_name in sorted(file_names, reverse=True):
269
+ if action == "reencode_resize":
254
270
  args = [
271
+ "-hide_banner",
255
272
  "-y",
256
273
  "-i",
257
274
  f"{file_name}",
258
275
  "-vf",
259
- "transpose=2,transpose=2",
260
- "-codec:a",
261
- "copy",
276
+ f"scale={horiz_resol}:-1",
262
277
  "-b:v",
263
- f"{video_quality}k",
264
- f"{file_name}.rotated180.avi",
278
+ f"{video_quality * 1024 * 1024}",
279
+ f"{file_name}.re-encoded.{horiz_resol}px.{video_quality}Mb.avi",
265
280
  ]
266
281
 
267
- self.processes.append([QProcess(self), [self.ffmpeg_bin, args, file_name]])
268
- self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
269
- self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
270
- self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
271
- self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
282
+ if action == "rotate":
283
+ # check bitrate
284
+ r = util.accurate_media_analysis(self.ffmpeg_bin, file_name)
285
+ if "error" not in r and r["bitrate"] is not None:
286
+ current_bitrate = r["bitrate"]
287
+ else:
288
+ current_bitrate = 10_000_000
289
+
290
+ if rotation_idx in (1, 2):
291
+ args = [
292
+ "-hide_banner",
293
+ "-y",
294
+ "-i",
295
+ f"{file_name}",
296
+ "-vf",
297
+ f"transpose={rotation_idx}",
298
+ "-codec:a",
299
+ "copy",
300
+ "-b:v",
301
+ f"{current_bitrate}",
302
+ f"{file_name}.rotated{['', '90', '-90'][rotation_idx]}.avi",
303
+ ]
304
+
305
+ if rotation_idx == 3: # 180
306
+ args = [
307
+ "-hide_banner",
308
+ "-y",
309
+ "-i",
310
+ f"{file_name}",
311
+ "-vf",
312
+ "transpose=2,transpose=2",
313
+ "-codec:a",
314
+ "copy",
315
+ "-b:v",
316
+ f"{current_bitrate}",
317
+ f"{file_name}.rotated180.avi",
318
+ ]
319
+
320
+ logging.debug("Launch process")
321
+ logging.debug(f"{self.ffmpeg_bin} {' '.join(args)}")
322
+
323
+ self.processes.append([QProcess(self), [self.ffmpeg_bin, args, file_name]])
324
+
325
+ ## FFmpeg output the work in progress on stderr
326
+ self.processes[-1][0].setProcessChannelMode(QProcess.MergedChannels)
327
+ self.processes[-1][0].readyReadStandardOutput.connect(lambda: readStdOutput(len(self.processes)))
328
+ # self.processes[-1][0].readyReadStandardError.connect(lambda: readStdOutput(len(self.processes)))
329
+
330
+ self.processes[-1][0].finished.connect(lambda: qprocess_finished(len(self.processes)))
272
331
 
273
- self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])
332
+ self.processes[-1][0].start(self.processes[-1][1][0], self.processes[-1][1][1])