setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__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 setiastrosuitepro might be problematic. Click here for more details.

Files changed (115) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/colorwheel.svg +97 -0
  4. setiastro/images/cosmic.svg +40 -0
  5. setiastro/images/cosmicsat.svg +24 -0
  6. setiastro/images/first_quarter.png +0 -0
  7. setiastro/images/full_moon.png +0 -0
  8. setiastro/images/graxpert.svg +19 -0
  9. setiastro/images/last_quarter.png +0 -0
  10. setiastro/images/linearfit.svg +32 -0
  11. setiastro/images/new_moon.png +0 -0
  12. setiastro/images/pixelmath.svg +42 -0
  13. setiastro/images/waning_crescent_1.png +0 -0
  14. setiastro/images/waning_crescent_2.png +0 -0
  15. setiastro/images/waning_crescent_3.png +0 -0
  16. setiastro/images/waning_crescent_4.png +0 -0
  17. setiastro/images/waning_crescent_5.png +0 -0
  18. setiastro/images/waning_gibbous_1.png +0 -0
  19. setiastro/images/waning_gibbous_2.png +0 -0
  20. setiastro/images/waning_gibbous_3.png +0 -0
  21. setiastro/images/waning_gibbous_4.png +0 -0
  22. setiastro/images/waning_gibbous_5.png +0 -0
  23. setiastro/images/waxing_crescent_1.png +0 -0
  24. setiastro/images/waxing_crescent_2.png +0 -0
  25. setiastro/images/waxing_crescent_3.png +0 -0
  26. setiastro/images/waxing_crescent_4.png +0 -0
  27. setiastro/images/waxing_crescent_5.png +0 -0
  28. setiastro/images/waxing_gibbous_1.png +0 -0
  29. setiastro/images/waxing_gibbous_2.png +0 -0
  30. setiastro/images/waxing_gibbous_3.png +0 -0
  31. setiastro/images/waxing_gibbous_4.png +0 -0
  32. setiastro/images/waxing_gibbous_5.png +0 -0
  33. setiastro/qml/ResourceMonitor.qml +84 -82
  34. setiastro/saspro/__main__.py +20 -1
  35. setiastro/saspro/_generated/build_info.py +2 -2
  36. setiastro/saspro/abe.py +37 -4
  37. setiastro/saspro/aberration_ai.py +237 -21
  38. setiastro/saspro/acv_exporter.py +379 -0
  39. setiastro/saspro/add_stars.py +33 -6
  40. setiastro/saspro/backgroundneutral.py +108 -40
  41. setiastro/saspro/blemish_blaster.py +4 -1
  42. setiastro/saspro/blink_comparator_pro.py +74 -24
  43. setiastro/saspro/clahe.py +4 -1
  44. setiastro/saspro/continuum_subtract.py +4 -1
  45. setiastro/saspro/convo.py +13 -7
  46. setiastro/saspro/cosmicclarity.py +129 -18
  47. setiastro/saspro/crop_dialog_pro.py +123 -7
  48. setiastro/saspro/curve_editor_pro.py +109 -42
  49. setiastro/saspro/doc_manager.py +245 -15
  50. setiastro/saspro/exoplanet_detector.py +120 -28
  51. setiastro/saspro/frequency_separation.py +1158 -204
  52. setiastro/saspro/ghs_dialog_pro.py +81 -16
  53. setiastro/saspro/graxpert.py +1 -0
  54. setiastro/saspro/gui/main_window.py +429 -228
  55. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  56. setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
  57. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  58. setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
  59. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  60. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  61. setiastro/saspro/halobgon.py +4 -0
  62. setiastro/saspro/histogram.py +5 -1
  63. setiastro/saspro/image_combine.py +4 -0
  64. setiastro/saspro/image_peeker_pro.py +4 -0
  65. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  66. setiastro/saspro/imageops/stretch.py +582 -62
  67. setiastro/saspro/isophote.py +4 -0
  68. setiastro/saspro/layers.py +13 -9
  69. setiastro/saspro/layers_dock.py +183 -3
  70. setiastro/saspro/legacy/image_manager.py +154 -20
  71. setiastro/saspro/legacy/numba_utils.py +67 -47
  72. setiastro/saspro/legacy/xisf.py +240 -98
  73. setiastro/saspro/live_stacking.py +180 -79
  74. setiastro/saspro/luminancerecombine.py +228 -27
  75. setiastro/saspro/mask_creation.py +174 -15
  76. setiastro/saspro/mfdeconv.py +113 -35
  77. setiastro/saspro/mfdeconvcudnn.py +119 -70
  78. setiastro/saspro/mfdeconvsport.py +112 -35
  79. setiastro/saspro/morphology.py +4 -0
  80. setiastro/saspro/multiscale_decomp.py +51 -12
  81. setiastro/saspro/numba_utils.py +72 -57
  82. setiastro/saspro/ops/commands.py +18 -18
  83. setiastro/saspro/ops/script_editor.py +10 -2
  84. setiastro/saspro/ops/scripts.py +122 -0
  85. setiastro/saspro/perfect_palette_picker.py +37 -3
  86. setiastro/saspro/plate_solver.py +84 -49
  87. setiastro/saspro/psf_viewer.py +119 -37
  88. setiastro/saspro/resources.py +67 -0
  89. setiastro/saspro/rgbalign.py +4 -0
  90. setiastro/saspro/selective_color.py +4 -1
  91. setiastro/saspro/sfcc.py +364 -152
  92. setiastro/saspro/shortcuts.py +160 -29
  93. setiastro/saspro/signature_insert.py +692 -33
  94. setiastro/saspro/stacking_suite.py +1331 -484
  95. setiastro/saspro/star_alignment.py +247 -123
  96. setiastro/saspro/star_spikes.py +4 -0
  97. setiastro/saspro/star_stretch.py +38 -3
  98. setiastro/saspro/stat_stretch.py +743 -128
  99. setiastro/saspro/subwindow.py +786 -360
  100. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  101. setiastro/saspro/wavescale_hdr.py +4 -1
  102. setiastro/saspro/wavescalede.py +4 -1
  103. setiastro/saspro/whitebalance.py +84 -12
  104. setiastro/saspro/widgets/common_utilities.py +28 -21
  105. setiastro/saspro/widgets/resource_monitor.py +109 -59
  106. setiastro/saspro/widgets/spinboxes.py +10 -13
  107. setiastro/saspro/wimi.py +27 -656
  108. setiastro/saspro/wims.py +13 -3
  109. setiastro/saspro/xisf.py +101 -11
  110. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +2 -1
  111. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
  112. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  113. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  114. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  115. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
@@ -13,7 +13,7 @@ IS_APPLE_ARM = (sys.platform == "darwin" and platform.machine() == "arm64")
13
13
  from PyQt6.QtCore import Qt, QThread, pyqtSignal, QStandardPaths, QSettings
14
14
  from PyQt6.QtWidgets import (
15
15
  QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog,
16
- QComboBox, QSpinBox, QProgressBar, QMessageBox, QCheckBox
16
+ QComboBox, QSpinBox, QProgressBar, QMessageBox, QCheckBox, QLineEdit
17
17
  )
18
18
  from PyQt6.QtGui import QIcon
19
19
  from setiastro.saspro.config import Config
@@ -315,11 +315,29 @@ class AberrationAIDialog(QDialog):
315
315
  row.addWidget(QLabel(self.tr("Model:")))
316
316
  self.model_label = QLabel("—")
317
317
  self.model_label.setToolTip("")
318
- btn_browse = QPushButton(self.tr("Browse…")); btn_browse.clicked.connect(self._browse_model)
318
+ btn_browse = QPushButton(self.tr("Browse…")); btn_browse.clicked.connect(self._browse_active_model)
319
319
  row.addWidget(self.model_label, 1)
320
320
  row.addWidget(btn_browse)
321
321
  v.addLayout(row)
322
+ # Custom model row (NEW)
323
+ row_custom = QHBoxLayout()
324
+ self.chk_use_custom = QCheckBox(self.tr("Use custom model file"))
325
+ self.chk_use_custom.setChecked(False)
326
+ self.chk_use_custom.toggled.connect(self._on_use_custom_toggled)
322
327
 
328
+ self.le_custom_model = QLineEdit()
329
+ self.le_custom_model.setReadOnly(True)
330
+ self.le_custom_model.setPlaceholderText(self.tr("No custom model selected"))
331
+ self.le_custom_model.setToolTip("")
332
+
333
+ btn_custom_clear = QPushButton(self.tr("Clear"))
334
+ btn_custom_clear.clicked.connect(self._clear_custom_model)
335
+
336
+ row_custom.addWidget(self.chk_use_custom)
337
+ row_custom.addWidget(self.le_custom_model, 1)
338
+
339
+ row_custom.addWidget(btn_custom_clear)
340
+ v.addLayout(row_custom)
323
341
  # Providers row
324
342
  row2 = QHBoxLayout()
325
343
  self.chk_auto = QCheckBox(self.tr("Auto GPU (if available)"))
@@ -373,7 +391,9 @@ class AberrationAIDialog(QDialog):
373
391
  self._model_path = None
374
392
  self._refresh_providers()
375
393
  self._load_last_model_from_settings()
376
-
394
+ self._load_last_custom_model_from_settings()
395
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
396
+ self.chk_use_custom.setChecked(bool(use_custom))
377
397
  if IS_APPLE_ARM:
378
398
  self.chk_auto.setChecked(False)
379
399
  self.chk_auto.setEnabled(False)
@@ -395,11 +415,73 @@ class AberrationAIDialog(QDialog):
395
415
  if p and os.path.isfile(p):
396
416
  self._set_model_path(p)
397
417
 
398
- def _browse_model(self):
399
- start_dir = _app_model_dir()
418
+ def _browse_active_model(self):
419
+ """
420
+ Single Browse button.
421
+ - If user picks a file inside the app model folder -> treat as "downloaded" selection (use_custom_model=False)
422
+ - If user picks a file outside -> treat as "custom" (use_custom_model=True)
423
+ """
424
+ app_dir = os.path.abspath(_app_model_dir())
425
+
426
+ # Start in last-used folder if possible
427
+ last_custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
428
+ last_downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
429
+ start_dir = None
430
+ for candidate in (last_custom, last_downloaded):
431
+ if candidate and os.path.isfile(candidate):
432
+ d = os.path.dirname(candidate)
433
+ if os.path.isdir(d):
434
+ start_dir = d
435
+ break
436
+ if start_dir is None:
437
+ start_dir = app_dir
438
+
400
439
  p, _ = QFileDialog.getOpenFileName(self, "Select ONNX model", start_dir, "ONNX (*.onnx)")
401
- if p:
402
- self._set_model_path(p)
440
+ if not p:
441
+ return
442
+
443
+ p_abs = os.path.abspath(p)
444
+ # Determine if picked file is inside app model folder
445
+ in_app_dir = False
446
+ try:
447
+ in_app_dir = os.path.commonpath([app_dir, p_abs]) == app_dir
448
+ except Exception:
449
+ in_app_dir = p_abs.startswith(app_dir)
450
+
451
+ if in_app_dir:
452
+ # "Downloaded" selection
453
+ self._set_model_path(p_abs)
454
+ self._set_custom_model_path(None)
455
+ QSettings().setValue("AberrationAI/use_custom_model", False)
456
+ if hasattr(self, "chk_use_custom"):
457
+ self.chk_use_custom.setChecked(False)
458
+ else:
459
+ # "Custom" selection
460
+ self._set_custom_model_path(p_abs)
461
+ QSettings().setValue("AberrationAI/use_custom_model", True)
462
+ if hasattr(self, "chk_use_custom"):
463
+ self.chk_use_custom.setChecked(True)
464
+
465
+ # Keep visuals in sync
466
+ self._refresh_model_label()
467
+ self._refresh_custom_row_visibility()
468
+
469
+
470
+ def _refresh_model_label(self):
471
+ downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
472
+ custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
473
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
474
+
475
+ if use_custom and custom:
476
+ self.model_label.setText(f"Custom: {os.path.basename(custom)}")
477
+ self.model_label.setToolTip(custom)
478
+ elif downloaded:
479
+ self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
480
+ self.model_label.setToolTip(downloaded)
481
+ else:
482
+ self.model_label.setText("—")
483
+ self.model_label.setToolTip("")
484
+
403
485
 
404
486
  def _open_model_folder(self):
405
487
  d = _app_model_dir()
@@ -412,6 +494,108 @@ class AberrationAIDialog(QDialog):
412
494
  import subprocess; subprocess.Popen(["xdg-open", d])
413
495
  except Exception:
414
496
  webbrowser.open(f"file://{d}")
497
+ # ----- custom model helpers (NEW) -----
498
+ def _set_custom_model_path(self, p: str | None):
499
+ if p:
500
+ self.le_custom_model.setText(os.path.basename(p))
501
+ self.le_custom_model.setToolTip(p)
502
+ QSettings().setValue("AberrationAI/custom_model_path", p)
503
+ else:
504
+ self.le_custom_model.clear()
505
+ self.le_custom_model.setToolTip("")
506
+ QSettings().remove("AberrationAI/custom_model_path")
507
+
508
+ def _load_last_custom_model_from_settings(self):
509
+ p = QSettings().value("AberrationAI/custom_model_path", type=str)
510
+ if p:
511
+ if os.path.isfile(p):
512
+ self._set_custom_model_path(p)
513
+ else:
514
+ # Keep the broken path visible in tooltip for debugging
515
+ if hasattr(self, "le_custom_model"):
516
+ self.le_custom_model.setText(os.path.basename(p) + " (missing)")
517
+ self.le_custom_model.setToolTip(p)
518
+
519
+ # After both loads, sync labels/visibility
520
+ self._refresh_model_label()
521
+ self._refresh_custom_row_visibility()
522
+
523
+ def _refresh_custom_row_visibility(self):
524
+ """
525
+ If you keep the custom row in the UI, hide the path field unless custom is enabled.
526
+ """
527
+ if not hasattr(self, "le_custom_model"):
528
+ return
529
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
530
+ self.le_custom_model.setVisible(bool(use_custom))
531
+
532
+
533
+ def _refresh_model_label(self):
534
+ downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
535
+ custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
536
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
537
+
538
+ # Prefer custom only if enabled AND the file exists
539
+ if use_custom and custom:
540
+ if os.path.isfile(custom):
541
+ self.model_label.setText(f"Custom: {os.path.basename(custom)}")
542
+ self.model_label.setToolTip(custom)
543
+ return
544
+ else:
545
+ self.model_label.setText(f"Custom: {os.path.basename(custom)} (missing)")
546
+ self.model_label.setToolTip(custom)
547
+ return
548
+
549
+ # Otherwise show downloaded if valid
550
+ if downloaded and os.path.isfile(downloaded):
551
+ self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
552
+ self.model_label.setToolTip(downloaded)
553
+ else:
554
+ self.model_label.setText("—")
555
+ self.model_label.setToolTip("")
556
+
557
+
558
+ def _browse_custom_model(self):
559
+ # Start at last dir if possible, else app model dir
560
+ last = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
561
+ start_dir = os.path.dirname(last) if last and os.path.isdir(os.path.dirname(last)) else _app_model_dir()
562
+ p, _ = QFileDialog.getOpenFileName(self, "Select custom ONNX model", start_dir, "ONNX (*.onnx)")
563
+ if p:
564
+ self._set_custom_model_path(p)
565
+ QSettings().setValue("AberrationAI/use_custom_model", True)
566
+ if not self.chk_use_custom.isChecked():
567
+ self.chk_use_custom.setChecked(True)
568
+
569
+ def _clear_custom_model(self):
570
+ self._set_custom_model_path(None)
571
+ QSettings().setValue("AberrationAI/use_custom_model", False)
572
+ if hasattr(self, "chk_use_custom"):
573
+ self.chk_use_custom.setChecked(False)
574
+
575
+ self._refresh_model_label()
576
+ self._refresh_custom_row_visibility()
577
+
578
+
579
+ def _on_use_custom_toggled(self, on: bool):
580
+ QSettings().setValue("AberrationAI/use_custom_model", bool(on))
581
+
582
+ if on:
583
+ p = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
584
+ if not (p and os.path.isfile(p)):
585
+ # Don’t spawn another browse button path; use the ONE browse if they want
586
+ QMessageBox.information(
587
+ self,
588
+ self.tr("Custom model"),
589
+ self.tr("Custom model is enabled, but no custom file is selected.\n"
590
+ "Click Browse… to choose a model file.")
591
+ )
592
+ # Optional: auto-open the single browse:
593
+ # self._browse_active_model()
594
+ # return
595
+
596
+ self._refresh_model_label()
597
+ self._refresh_custom_row_visibility()
598
+
415
599
 
416
600
  # ----- provider UI -----
417
601
  def _log(self, msg: str): # NEW
@@ -477,8 +661,16 @@ class AberrationAIDialog(QDialog):
477
661
  def _on_download_ok(self, path: str):
478
662
  self.progress.setValue(100)
479
663
  self._set_model_path(path)
664
+
665
+ # Download becomes the active model unless custom is explicitly enabled
666
+ if not QSettings().value("AberrationAI/use_custom_model", False, type=bool):
667
+ self._set_custom_model_path(None)
668
+
480
669
  QMessageBox.information(self, "Model", f"Downloaded: {os.path.basename(path)}")
481
670
 
671
+ self._refresh_model_label()
672
+ self._refresh_custom_row_visibility()
673
+
482
674
  # ----- run -----
483
675
  def _run(self):
484
676
  if ort is None:
@@ -489,7 +681,22 @@ class AberrationAIDialog(QDialog):
489
681
  "Please try installing an earlier version (for example 1.19.x) and try again."
490
682
  )
491
683
  return
492
- if not self._model_path or not os.path.isfile(self._model_path):
684
+
685
+ # Choose model path (normal vs custom)
686
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
687
+ downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
688
+ custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
689
+
690
+ model_path = custom if use_custom else downloaded
691
+ if self.chk_use_custom.isChecked():
692
+ cp = QSettings().value("AberrationAI/custom_model_path", type=str)
693
+ if cp and os.path.isfile(cp):
694
+ model_path = cp
695
+ else:
696
+ QMessageBox.warning(self, "Model", "Custom model is enabled but the file is missing. Please browse to a valid .onnx.")
697
+ return
698
+
699
+ if not model_path or not os.path.isfile(model_path):
493
700
  QMessageBox.warning(self, "Model", "Please select or download a valid .onnx model first.")
494
701
  return
495
702
 
@@ -516,7 +723,7 @@ class AberrationAIDialog(QDialog):
516
723
  providers = [sel] if sel else ["CPUExecutionProvider"]
517
724
 
518
725
  # --- make patch match the model's requirement (if fixed) ---
519
- req = _model_required_patch(self._model_path)
726
+ req = _model_required_patch(model_path)
520
727
  if req and req > 0:
521
728
  patch = req
522
729
  try:
@@ -537,14 +744,16 @@ class AberrationAIDialog(QDialog):
537
744
 
538
745
  self._t_start = time.perf_counter()
539
746
  prov_txt = ("auto" if self.chk_auto.isChecked() else self.cmb_provider.currentText() or "CPU")
540
- self._log(f"🚀 Aberration AI: model={os.path.basename(self._model_path)}, "
747
+ self._log(f"🚀 Aberration AI: model={os.path.basename(model_path)}, "
541
748
  f"provider={prov_txt}, patch={patch}, overlap={overlap}")
749
+
750
+ self._effective_model_path = model_path
542
751
 
543
752
  # -------- run worker --------
544
753
  self.progress.setValue(0)
545
754
  self.btn_run.setEnabled(False)
546
755
 
547
- self._worker = _ONNXWorker(self._model_path, img, patch, overlap, providers)
756
+ self._worker = _ONNXWorker(model_path, img, patch, overlap, providers)
548
757
  self._worker.progressed.connect(self.progress.setValue)
549
758
  self._worker.failed.connect(self._on_failed)
550
759
  self._worker.finished_ok.connect(self._on_ok)
@@ -553,10 +762,15 @@ class AberrationAIDialog(QDialog):
553
762
 
554
763
 
555
764
  def _on_failed(self, msg: str):
556
- self._log(f"❌ Aberration AI failed: {msg}") # NEW
765
+ model_path = getattr(self, "_effective_model_path", self._model_path)
766
+ self._log(f"❌ Aberration AI failed: {msg}")
557
767
  QMessageBox.critical(self, "ONNX Error", msg)
768
+ self.reject() # closes the dialog
558
769
 
559
770
  def _on_ok(self, out: np.ndarray):
771
+ used = getattr(self._worker, "used_provider", None) or \
772
+ (self.cmb_provider.currentText() if not self.chk_auto.isChecked() else "auto")
773
+ model_path = getattr(self, "_effective_model_path", self._model_path)
560
774
  doc = self.get_active_doc()
561
775
  if doc is None or getattr(doc, "image", None) is None:
562
776
  QMessageBox.warning(self, "Image", "No active image.")
@@ -578,11 +792,10 @@ class AberrationAIDialog(QDialog):
578
792
  "processing_parameters": {
579
793
  **(getattr(doc, "metadata", {}) or {}).get("processing_parameters", {}),
580
794
  "AberrationAI": {
581
- "model_path": self._model_path,
795
+ "model_path": model_path,
582
796
  "patch_size": int(self.spin_patch.value()),
583
797
  "overlap": int(self.spin_overlap.value()),
584
- "provider": (self.cmb_provider.currentText()
585
- if not self.chk_auto.isChecked() else "auto"),
798
+ "provider": used,
586
799
  "border_px": BORDER_PX,
587
800
  }
588
801
  }
@@ -615,7 +828,7 @@ class AberrationAIDialog(QDialog):
615
828
  if main is not None:
616
829
  auto_gpu = bool(self.chk_auto.isChecked())
617
830
  preset = {
618
- "model": self._model_path,
831
+ "model": model_path,
619
832
  "patch": int(self.spin_patch.value()),
620
833
  "overlap": int(self.spin_overlap.value()),
621
834
  "border_px": int(BORDER_PX),
@@ -674,21 +887,24 @@ class AberrationAIDialog(QDialog):
674
887
  BORDER_PX = 10 # same value used above
675
888
  self._log(
676
889
  f"✅ Aberration AI applied "
677
- f"(model={os.path.basename(self._model_path)}, provider={used}, "
890
+ f"(model={os.path.basename(model_path)}, provider={used}, "
678
891
  f"patch={int(self.spin_patch.value())}, overlap={int(self.spin_overlap.value())}, "
679
892
  f"border={BORDER_PX}px, time={dt:.2f}s)"
680
893
  )
681
894
 
682
895
  self.progress.setValue(100)
683
- # Dialog stays open so user can apply to other images
896
+ # NEW: close this UI after a successful run
897
+ self.accept() # or self.close()
898
+ return
684
899
 
685
900
  def _on_worker_finished(self):
686
- # If dialog is already gone, this method is never called because the receiver (self)
687
- # has been destroyed and Qt auto-disconnects the signal.
901
+ # Dialog might have been closed by _on_ok()
902
+ if not self.isVisible():
903
+ return
904
+
688
905
  if hasattr(self, "btn_run"):
689
906
  try:
690
907
  self.btn_run.setEnabled(True)
691
908
  except RuntimeError:
692
- # Button already deleted; ignore
693
909
  pass
694
910
  self._worker = None