spacr 0.4.0__py3-none-any.whl → 0.4.2__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.
spacr/__init__.py CHANGED
@@ -27,7 +27,7 @@ from . import openai
27
27
  from . import ml
28
28
  from . import toxo
29
29
  from . import cellpose
30
- from . import stats
30
+ from . import sp_stats
31
31
  from . import logger
32
32
 
33
33
  __all__ = [
@@ -58,7 +58,7 @@ __all__ = [
58
58
  "ml",
59
59
  "toxo",
60
60
  "cellpose",
61
- "stats",
61
+ "sp_stats",
62
62
  "logger"
63
63
  ]
64
64
 
spacr/core.py CHANGED
@@ -9,12 +9,11 @@ warnings.filterwarnings("ignore", message="3D stack used, but stitch_threshold=0
9
9
 
10
10
  def preprocess_generate_masks(settings):
11
11
 
12
- from .io import preprocess_img_data, _load_and_concatenate_arrays
12
+ from .io import preprocess_img_data, _load_and_concatenate_arrays, convert_to_yokogawa
13
13
  from .plot import plot_image_mask_overlay, plot_arrays
14
- from .utils import _pivot_counts_table, check_mask_folder, adjust_cell_masks, print_progress, save_settings, delete_intermedeate_files
14
+ from .utils import _pivot_counts_table, check_mask_folder, adjust_cell_masks, print_progress, save_settings, delete_intermedeate_files, format_path_for_system, normalize_src_path
15
15
  from .settings import set_default_settings_preprocess_generate_masks
16
16
 
17
-
18
17
  if 'src' in settings:
19
18
  if not isinstance(settings['src'], (str, list)):
20
19
  ValueError(f'src must be a string or a list of strings')
@@ -23,6 +22,8 @@ def preprocess_generate_masks(settings):
23
22
  ValueError(f'src is a required parameter')
24
23
  return
25
24
 
25
+ settings['src'] = normalize_src_path(settings['src'])
26
+
26
27
  if isinstance(settings['src'], str):
27
28
  settings['src'] = [settings['src']]
28
29
 
@@ -30,9 +31,19 @@ def preprocess_generate_masks(settings):
30
31
  source_folders = settings['src']
31
32
  for source_folder in source_folders:
32
33
  print(f'Processing folder: {source_folder}')
34
+
35
+ if settings['metadata_type'] == 'auto':
36
+ convert_to_yokogawa(folder=source_folder)
37
+
38
+ source_folder = format_path_for_system(source_folder)
33
39
  settings['src'] = source_folder
34
40
  src = source_folder
35
41
  settings = set_default_settings_preprocess_generate_masks(settings)
42
+
43
+ if settings['cell_channel'] is None and settings['nucleus_channel'] is None and settings['pathogen_channel'] is None:
44
+ print(f'Error: At least one of cell_channel, nucleus_channel or pathogen_channel must be defined')
45
+ return
46
+
36
47
  save_settings(settings, name='gen_mask_settings')
37
48
 
38
49
  if not settings['pathogen_channel'] is None:
spacr/deep_spacr.py CHANGED
@@ -938,67 +938,8 @@ def deep_spacr(settings={}):
938
938
  if os.path.exists(settings['model_path']):
939
939
  apply_model_to_tar(settings)
940
940
 
941
- def model_knowledge_transfer(
942
- teacher_paths,
943
- student_save_path,
944
- data_loader, # A DataLoader with (images, labels)
945
- device='cpu',
946
- student_model_name='maxvit_t',
947
- pretrained=True,
948
- dropout_rate=None,
949
- use_checkpoint=False,
950
- alpha=0.5,
951
- temperature=2.0,
952
- lr=1e-4,
953
- epochs=10
954
- ):
955
- """
956
- Performs multi-teacher knowledge distillation on a new labeled dataset,
957
- producing a single student TorchModel that combines (distills) the
958
- teachers' knowledge plus the labeled data.
959
-
960
- Usage:
961
- student = model_knowledge_transfer(
962
- teacher_paths=[
963
- 'teacherA.pth',
964
- 'teacherB.pth',
965
- ...
966
- ],
967
- student_save_path='distilled_student.pth',
968
- data_loader=my_data_loader,
969
- device='cuda',
970
- student_model_name='maxvit_t',
971
- alpha=0.5,
972
- temperature=2.0,
973
- lr=1e-4,
974
- epochs=10
975
- )
976
-
977
- Then load it via:
978
- fused_student = torch.load('distilled_student.pth')
979
- # fused_student is a TorchModel instance, ready for inference.
980
-
981
- Args:
982
- teacher_paths (list[str]): List of paths to teacher models (TorchModel
983
- or dict with 'model' in it). They must have the same architecture
984
- or at least produce the same dimension of output.
985
- student_save_path (str): Destination path to save the final student
986
- TorchModel.
987
- data_loader (DataLoader): Yields (images, labels) from the new dataset.
988
- device (str): 'cpu' or 'cuda'.
989
- student_model_name (str): Architecture name for the student TorchModel.
990
- pretrained (bool): If the student should be initialized as pretrained.
991
- dropout_rate (float): If needed by your TorchModel init.
992
- use_checkpoint (bool): If needed by your TorchModel init.
993
- alpha (float): Weight balancing real-label CE vs. distillation loss
994
- (0..1).
995
- temperature (float): Distillation temperature (>1 typically).
996
- lr (float): Learning rate for the student.
997
- epochs (int): Number of training epochs.
941
+ def model_knowledge_transfer(teacher_paths, student_save_path, data_loader, device='cpu', student_model_name='maxvit_t', pretrained=True, dropout_rate=None, use_checkpoint=False, alpha=0.5, temperature=2.0, lr=1e-4, epochs=10):
998
942
 
999
- Returns:
1000
- TorchModel: The final, trained student model.
1001
- """
1002
943
  from spacr.utils import TorchModel # Adapt if needed
1003
944
 
1004
945
  # Adjust filename to reflect knowledge-distillation if desired
@@ -1101,42 +1042,8 @@ def model_knowledge_transfer(
1101
1042
 
1102
1043
  return student_model
1103
1044
 
1104
- def model_fusion(model_paths,
1105
- save_path,
1106
- device='cpu',
1107
- model_name='maxvit_t',
1108
- pretrained=True,
1109
- dropout_rate=None,
1110
- use_checkpoint=False,
1111
- aggregator='mean'):
1112
- """
1113
- Fuses an arbitrary number of TorchModel instances by combining their weights
1114
- (using mean, geomean, median, sum, max, or min) and saves the entire fused
1115
- model object.
1116
-
1117
- You can later load the fused model with:
1118
- model = torch.load('fused_model.pth')
1119
-
1120
- which returns a ready-to-use TorchModel instance.
1121
-
1122
- Parameters:
1123
- model_paths (list of str): Paths to the model checkpoints to fuse.
1124
- Each checkpoint can be:
1125
- - A dict with keys ['model', 'model_name', ...]
1126
- - A TorchModel instance
1127
- save_path (str): Destination path to save the fused model.
1128
- device (str): 'cpu' or 'cuda' for loading weights and final model device.
1129
- model_name (str): Default model name (used if not in checkpoint).
1130
- pretrained (bool): Default if not in checkpoint.
1131
- dropout_rate (float): Default if not in checkpoint.
1132
- use_checkpoint (bool): Default if not in checkpoint.
1133
- aggregator (str): How to combine weights across models:
1134
- 'mean', 'geomean', 'median', 'sum', 'max', or 'min'.
1045
+ def model_fusion(model_paths,save_path,device='cpu',model_name='maxvit_t',pretrained=True,dropout_rate=None,use_checkpoint=False,aggregator='mean'):
1135
1046
 
1136
- Returns:
1137
- fused_model (TorchModel): The final fused TorchModel instance
1138
- with combined weights.
1139
- """
1140
1047
  from spacr.utils import TorchModel
1141
1048
 
1142
1049
  if save_path.endswith('.pth'):
spacr/gui_core.py CHANGED
@@ -170,49 +170,22 @@ def display_figure(fig):
170
170
  #flash_feedback("right")
171
171
  show_next_figure()
172
172
 
173
- def zoom_v1(event):
174
- nonlocal scale_factor
175
-
176
- zoom_speed = 0.1 # Adjust the zoom speed for smoother experience
177
-
178
- # Determine the zoom direction based on the scroll event
179
- if event.num == 4 or event.delta > 0: # Scroll up (zoom in)
180
- scale_factor /= (1 + zoom_speed) # Divide to zoom in
181
- elif event.num == 5 or event.delta < 0: # Scroll down (zoom out)
182
- scale_factor *= (1 + zoom_speed) # Multiply to zoom out
183
-
184
- # Adjust the axes limits based on the new scale factor
185
- for ax in canvas.figure.get_axes():
186
- xlim = ax.get_xlim()
187
- ylim = ax.get_ylim()
188
-
189
- x_center = (xlim[1] + xlim[0]) / 2
190
- y_center = (ylim[1] + ylim[0]) / 2
191
-
192
- x_range = (xlim[1] - xlim[0]) * scale_factor
193
- y_range = (ylim[1] - ylim[0]) * scale_factor
194
-
195
- # Set the new limits
196
- ax.set_xlim([x_center - x_range / 2, x_center + x_range / 2])
197
- ax.set_ylim([y_center - y_range / 2, y_center + y_range / 2])
198
-
199
- # Redraw the figure efficiently
200
- canvas.draw_idle()
201
-
202
173
  def zoom_test(event):
203
174
  if event.num == 4: # Scroll up
204
175
  print("zoom in")
205
176
  elif event.num == 5: # Scroll down
206
177
  print("zoom out")
207
-
208
- def zoom_2(event):
209
- zoom_speed = 0.1 # Change this to control how fast you zoom
178
+
179
+ def zoom_v1(event):
180
+ # Fixed zoom factors (adjust these if you want faster or slower zoom)
181
+ zoom_in_factor = 0.9 # When zooming in, ranges shrink by 10%
182
+ zoom_out_factor = 1.1 # When zooming out, ranges increase by 10%
210
183
 
211
184
  # Determine the zoom direction based on the scroll event
212
185
  if event.num == 4 or (hasattr(event, 'delta') and event.delta > 0): # Scroll up = zoom in
213
- factor = 1 - zoom_speed
186
+ factor = zoom_in_factor
214
187
  elif event.num == 5 or (hasattr(event, 'delta') and event.delta < 0): # Scroll down = zoom out
215
- factor = 1 + zoom_speed
188
+ factor = zoom_out_factor
216
189
  else:
217
190
  return # No recognized scroll direction
218
191
 
@@ -247,23 +220,28 @@ def display_figure(fig):
247
220
  return # No recognized scroll direction
248
221
 
249
222
  for ax in canvas.figure.get_axes():
223
+ # Get the current mouse position in pixel coordinates
224
+ mouse_x, mouse_y = event.x, event.y
225
+
226
+ # Convert pixel coordinates to data coordinates
227
+ inv = ax.transData.inverted()
228
+ data_x, data_y = inv.transform((mouse_x, mouse_y))
229
+
230
+ # Get the current axis limits
250
231
  xlim = ax.get_xlim()
251
232
  ylim = ax.get_ylim()
252
233
 
253
- x_center = (xlim[1] + xlim[0]) / 2
254
- y_center = (ylim[1] + ylim[0]) / 2
255
-
234
+ # Calculate the zooming range around the cursor position
256
235
  x_range = (xlim[1] - xlim[0]) * factor
257
236
  y_range = (ylim[1] - ylim[0]) * factor
258
237
 
259
- # Set the new limits
260
- ax.set_xlim([x_center - x_range / 2, x_center + x_range / 2])
261
- ax.set_ylim([y_center - y_range / 2, y_center + y_range / 2])
238
+ # Adjust the limits while keeping the mouse position fixed
239
+ ax.set_xlim([data_x - (data_x - xlim[0]) * factor, data_x + (xlim[1] - data_x) * factor])
240
+ ax.set_ylim([data_y - (data_y - ylim[0]) * factor, data_y + (ylim[1] - data_y) * factor])
262
241
 
263
242
  # Redraw the figure efficiently
264
243
  canvas.draw_idle()
265
244
 
266
-
267
245
  # Bind events for hover, click interactions, and zoom
268
246
  canvas_widget.bind("<Motion>", on_hover)
269
247
  canvas_widget.bind("<Leave>", on_leave)
@@ -854,7 +832,7 @@ def initiate_abort():
854
832
  global thread_control, q, parent_frame
855
833
  if thread_control.get("run_thread") is not None:
856
834
  try:
857
- q.put("Aborting processes...")
835
+ #q.put("Aborting processes...")
858
836
  thread_control.get("run_thread").terminate()
859
837
  thread_control["run_thread"] = None
860
838
  q.put("Processes aborted.")
@@ -862,22 +840,136 @@ def initiate_abort():
862
840
  q.put(f"Error aborting process: {e}")
863
841
 
864
842
  thread_control = {"run_thread": None, "stop_requested": False}
843
+
844
+ def check_src_folders_files(settings, settings_type, q):
845
+ """
846
+ Checks if 'src' is a key in the settings dictionary and if it exists as a valid path.
847
+ If 'src' is a list, iterates through the list and checks each path.
848
+ If any path is missing, prompts the user to edit or remove invalid paths.
849
+ """
850
+
851
+ request_stop = False
852
+
853
+ def _folder_has_images(folder_path, image_extensions = {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff", ".tif", ".webp", ".npy", ".npz", "nd2", "czi", "lif"}):
854
+ """Check if a folder contains any image files."""
855
+ return any(file.lower().endswith(tuple(image_extensions)) for file in os.listdir(folder_path))
856
+
857
+ def _has_folder(parent_folder, sub_folder="measure"):
858
+ """Check if a specific sub-folder exists inside the given folder."""
859
+ return os.path.isdir(os.path.join(parent_folder, sub_folder))
860
+
861
+ from .utils import normalize_src_path
862
+
863
+ settings['src'] = normalize_src_path(settings['src'])
864
+
865
+ src_value = settings.get("src")
866
+
867
+ # **Skip if 'src' is missing**
868
+ if src_value is None:
869
+ return request_stop
870
+
871
+ # Convert single string src to a list for uniform handling
872
+ if isinstance(src_value, str):
873
+ src_list = [src_value]
874
+ elif isinstance(src_value, list):
875
+ src_list = src_value
876
+ else:
877
+ request_stop = True
878
+ return request_stop # Ensure early exit
879
+
880
+ # Identify missing paths
881
+ missing_paths = {i: path for i, path in enumerate(src_list) if not os.path.exists(path)}
882
+
883
+ if missing_paths:
884
+ q.put(f'Error: The following paths are missing: {missing_paths}')
885
+ request_stop = True
886
+ return request_stop # Ensure early exit
887
+
888
+ conditions = [True] # Initialize conditions list
889
+
890
+ for path in src_list: # Fixed: Use src_list instead of src_value
891
+ if settings_type == 'mask':
892
+ pictures_continue = _folder_has_images(path)
893
+ folder_chan_continue = _has_folder(path, "1")
894
+ folder_stack_continue = _has_folder(path, "stack")
895
+ folder_npz_continue = _has_folder(path, "norm_channel_stack")
896
+
897
+ if not pictures_continue:
898
+ if not any([folder_chan_continue, folder_stack_continue, folder_npz_continue]):
899
+ if not folder_chan_continue:
900
+ q.put(f"Error: Missing channel folder in folder: {path}")
901
+
902
+ if not folder_stack_continue:
903
+ q.put(f"Error: Missing stack folder in folder: {path}")
904
+
905
+ if not folder_npz_continue:
906
+ q.put(f"Error: Missing norm_channel_stack folder in folder: {path}")
907
+ else:
908
+ q.put(f"Error: No images in folder: {path}")
909
+
910
+ #q.put(f"path:{path}")
911
+ #q.put(f"pictures_continue:{pictures_continue}, folder_chan_continue:{folder_chan_continue}, folder_stack_continue:{folder_stack_continue}, folder_npz_continue:{folder_npz_continue}")
912
+
913
+ conditions = [pictures_continue, folder_chan_continue, folder_stack_continue, folder_npz_continue]
914
+
915
+ if settings_type == 'measure':
916
+ npy_continue = _folder_has_images(path, image_extensions={".npy"})
917
+ conditions = [npy_continue]
918
+
919
+ if settings_type == 'recruitment':
920
+ db_continue = _folder_has_images(path, image_extensions={".db"})
921
+ conditions = [db_continue]
922
+
923
+ if settings_type == 'umap':
924
+ db_continue = _folder_has_images(path, image_extensions={".db"})
925
+ conditions = [db_continue]
926
+
927
+ if settings_type == 'analyze_plaques':
928
+ conditions = [True]
929
+
930
+ if settings_type == 'map_barcodes':
931
+ conditions = [True]
932
+
933
+ if settings_type == 'regression':
934
+ db_continue = _folder_has_images(path, image_extensions={".db"})
935
+ conditions = [db_continue]
936
+
937
+ if settings_type == 'classify':
938
+ db_continue = _folder_has_images(path, image_extensions={".db"})
939
+ conditions = [db_continue]
940
+
941
+ if settings_type == 'analyze_plaques':
942
+ db_continue = _folder_has_images(path, image_extensions={".db"})
943
+ conditions = [db_continue]
944
+
945
+ if not any(conditions):
946
+ q.put(f"Error: The following path(s) is missing images or folders: {path}")
947
+ request_stop = True
948
+
949
+ return request_stop
865
950
 
866
951
  def start_process(q=None, fig_queue=None, settings_type='mask'):
867
952
  global thread_control, vars_dict, parent_frame
868
953
  from .settings import check_settings, expected_types
869
954
  from .gui_utils import run_function_gui, set_cpu_affinity, initialize_cuda, display_gif_in_plot_frame, print_widget_structure
870
-
955
+
871
956
  if q is None:
872
957
  q = Queue()
873
958
  if fig_queue is None:
874
959
  fig_queue = Queue()
875
960
  try:
876
- settings = check_settings(vars_dict, expected_types, q)
961
+ settings, errors = check_settings(vars_dict, expected_types, q)
962
+
963
+ if len(errors) > 0:
964
+ return
965
+
966
+ if check_src_folders_files(settings, settings_type, q):
967
+ return
968
+
877
969
  except ValueError as e:
878
970
  q.put(f"Error: {e}")
879
971
  return
880
-
972
+
881
973
  if isinstance(thread_control, dict) and thread_control.get("run_thread") is not None:
882
974
  initiate_abort()
883
975
 
@@ -902,13 +994,176 @@ def start_process(q=None, fig_queue=None, settings_type='mask'):
902
994
 
903
995
  # Store the process in thread_control for future reference
904
996
  thread_control["run_thread"] = process
997
+
905
998
  else:
906
999
  q.put(f"Error: Unknown settings type '{settings_type}'")
907
1000
  return
908
-
1001
+
909
1002
  def process_console_queue():
910
1003
  global q, console_output, parent_frame, progress_bar, process_console_queue
911
1004
 
1005
+ # Initialize function attribute if it doesn't exist
1006
+ if not hasattr(process_console_queue, "completed_tasks"):
1007
+ process_console_queue.completed_tasks = []
1008
+ if not hasattr(process_console_queue, "current_maximum"):
1009
+ process_console_queue.current_maximum = None
1010
+
1011
+ ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
1012
+
1013
+ spacing = 5
1014
+
1015
+ # **Configure styles for different message types**
1016
+ console_output.tag_configure("error", foreground="red", spacing3 = spacing)
1017
+ console_output.tag_configure("warning", foreground="orange", spacing3 = spacing)
1018
+ console_output.tag_configure("normal", foreground="white", spacing3 = spacing)
1019
+
1020
+ while not q.empty():
1021
+ message = q.get_nowait()
1022
+ clean_message = ansi_escape_pattern.sub('', message)
1023
+
1024
+ # **Detect Error Messages (Red)**
1025
+ if clean_message.startswith("Error:"):
1026
+ console_output.insert(tk.END, clean_message + "\n", "error")
1027
+ console_output.see(tk.END)
1028
+ #print("Run aborted due to error:", clean_message) # Debug message
1029
+ #return # **Exit immediately to stop further execution**
1030
+
1031
+ # **Detect Warning Messages (Orange)**
1032
+ elif clean_message.startswith("Warning:"):
1033
+ console_output.insert(tk.END, clean_message + "\n", "warning")
1034
+
1035
+ # **Process Progress Messages Normally**
1036
+ elif clean_message.startswith("Progress:"):
1037
+ try:
1038
+ # Extract the progress information
1039
+ match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
1040
+
1041
+ if match:
1042
+ current_progress = int(match.group(1))
1043
+ total_progress = int(match.group(2))
1044
+ operation_type = match.group(3).strip()
1045
+ additional_info = match.group(4).strip() # Capture everything after operation_type
1046
+
1047
+ # Check if the maximum value has changed
1048
+ if process_console_queue.current_maximum != total_progress:
1049
+ process_console_queue.current_maximum = total_progress
1050
+ process_console_queue.completed_tasks = []
1051
+
1052
+ # Add the task to the completed set
1053
+ process_console_queue.completed_tasks.append(current_progress)
1054
+
1055
+ # Calculate the unique progress count
1056
+ unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
1057
+
1058
+ # Update the progress bar
1059
+ if progress_bar:
1060
+ progress_bar['maximum'] = total_progress
1061
+ progress_bar['value'] = unique_progress_count
1062
+
1063
+ # Store operation type and additional info
1064
+ if operation_type:
1065
+ progress_bar.operation_type = operation_type
1066
+ progress_bar.additional_info = additional_info
1067
+
1068
+ # Update the progress label
1069
+ if progress_bar.progress_label:
1070
+ progress_bar.update_label()
1071
+
1072
+ # Clear completed tasks when progress is complete
1073
+ if unique_progress_count >= total_progress:
1074
+ process_console_queue.completed_tasks.clear()
1075
+
1076
+ except Exception as e:
1077
+ print(f"Error parsing progress message: {e}")
1078
+
1079
+ # **Insert Normal Messages with Extra Line Spacing**
1080
+ else:
1081
+ console_output.insert(tk.END, clean_message + "\n", "normal")
1082
+
1083
+ console_output.see(tk.END)
1084
+
1085
+ # **Continue processing if no error was detected**
1086
+ after_id = console_output.after(uppdate_frequency, process_console_queue)
1087
+ parent_frame.after_tasks.append(after_id)
1088
+
1089
+ def process_console_queue_v2():
1090
+ global q, console_output, parent_frame, progress_bar, process_console_queue
1091
+
1092
+ # Initialize function attribute if it doesn't exist
1093
+ if not hasattr(process_console_queue, "completed_tasks"):
1094
+ process_console_queue.completed_tasks = []
1095
+ if not hasattr(process_console_queue, "current_maximum"):
1096
+ process_console_queue.current_maximum = None
1097
+
1098
+ ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
1099
+
1100
+ while not q.empty():
1101
+ message = q.get_nowait()
1102
+ clean_message = ansi_escape_pattern.sub('', message)
1103
+
1104
+ # **Abort Execution if an Error Message is Detected**
1105
+ if clean_message.startswith("Error:"):
1106
+ console_output.insert(tk.END, clean_message + "\n", "error")
1107
+ console_output.see(tk.END)
1108
+ print("Run aborted due to error:", clean_message) # Debug message
1109
+ return # **Exit immediately to stop further execution**
1110
+
1111
+ # Check if the message contains progress information
1112
+ if clean_message.startswith("Progress:"):
1113
+ try:
1114
+ # Extract the progress information
1115
+ match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
1116
+
1117
+ if match:
1118
+ current_progress = int(match.group(1))
1119
+ total_progress = int(match.group(2))
1120
+ operation_type = match.group(3).strip()
1121
+ additional_info = match.group(4).strip() # Capture everything after operation_type
1122
+
1123
+ # Check if the maximum value has changed
1124
+ if process_console_queue.current_maximum != total_progress:
1125
+ process_console_queue.current_maximum = total_progress
1126
+ process_console_queue.completed_tasks = []
1127
+
1128
+ # Add the task to the completed set
1129
+ process_console_queue.completed_tasks.append(current_progress)
1130
+
1131
+ # Calculate the unique progress count
1132
+ unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
1133
+
1134
+ # Update the progress bar
1135
+ if progress_bar:
1136
+ progress_bar['maximum'] = total_progress
1137
+ progress_bar['value'] = unique_progress_count
1138
+
1139
+ # Store operation type and additional info
1140
+ if operation_type:
1141
+ progress_bar.operation_type = operation_type
1142
+ progress_bar.additional_info = additional_info
1143
+
1144
+ # Update the progress label
1145
+ if progress_bar.progress_label:
1146
+ progress_bar.update_label()
1147
+
1148
+ # Clear completed tasks when progress is complete
1149
+ if unique_progress_count >= total_progress:
1150
+ process_console_queue.completed_tasks.clear()
1151
+
1152
+ except Exception as e:
1153
+ print(f"Error parsing progress message: {e}")
1154
+
1155
+ else:
1156
+ # Insert non-progress messages into the console
1157
+ console_output.insert(tk.END, clean_message + "\n")
1158
+ console_output.see(tk.END)
1159
+
1160
+ # **Continue processing if no error was detected**
1161
+ after_id = console_output.after(uppdate_frequency, process_console_queue)
1162
+ parent_frame.after_tasks.append(after_id)
1163
+
1164
+ def process_console_queue_v1():
1165
+ global q, console_output, parent_frame, progress_bar, process_console_queue
1166
+
912
1167
  # Initialize function attribute if it doesn't exist
913
1168
  if not hasattr(process_console_queue, "completed_tasks"):
914
1169
  process_console_queue.completed_tasks = []