setiastrosuitepro 1.8.0.post3__py3-none-any.whl → 1.8.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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/finderchart.png +0 -0
- setiastro/saspro/__main__.py +41 -39
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +1 -1
- setiastro/saspro/blink_comparator_pro.py +3 -1
- setiastro/saspro/bright_stars.py +305 -0
- setiastro/saspro/continuum_subtract.py +2 -1
- setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +22 -2
- setiastro/saspro/cosmicclarity_engines/denoise_engine.py +68 -15
- setiastro/saspro/cosmicclarity_engines/satellite_engine.py +7 -3
- setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +371 -98
- setiastro/saspro/cosmicclarity_engines/superres_engine.py +1 -0
- setiastro/saspro/cosmicclarity_preset.py +2 -1
- setiastro/saspro/doc_manager.py +8 -0
- setiastro/saspro/exoplanet_detector.py +22 -17
- setiastro/saspro/finder_chart.py +1639 -0
- setiastro/saspro/gui/main_window.py +36 -14
- setiastro/saspro/gui/mixins/menu_mixin.py +2 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +9 -1
- setiastro/saspro/legacy/image_manager.py +18 -4
- setiastro/saspro/legacy/xisf.py +3 -3
- setiastro/saspro/main_helpers.py +18 -0
- setiastro/saspro/memory_utils.py +18 -14
- setiastro/saspro/model_manager.py +65 -0
- setiastro/saspro/model_workers.py +58 -24
- setiastro/saspro/ops/settings.py +45 -8
- setiastro/saspro/planetprojection.py +68 -36
- setiastro/saspro/resources.py +193 -175
- setiastro/saspro/runtime_torch.py +622 -137
- setiastro/saspro/sfcc.py +5 -3
- setiastro/saspro/stacking_suite.py +4 -3
- setiastro/saspro/star_alignment.py +266 -212
- setiastro/saspro/texture_clarity.py +1 -1
- setiastro/saspro/widgets/image_utils.py +12 -4
- setiastro/saspro/widgets/spinboxes.py +5 -7
- setiastro/saspro/wimi.py +2 -1
- setiastro/saspro/xisf.py +3 -3
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.2.dist-info}/METADATA +4 -4
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.2.dist-info}/RECORD +43 -40
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.2.dist-info}/licenses/license.txt +0 -0
|
@@ -33,8 +33,8 @@ from urllib.parse import quote, quote_plus
|
|
|
33
33
|
# ============================================================================
|
|
34
34
|
import numpy as np
|
|
35
35
|
import matplotlib
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# tifffile and XISF imports removed (unused in this file)
|
|
37
|
+
|
|
38
38
|
|
|
39
39
|
# ============================================================================
|
|
40
40
|
# Bootstrap Configuration (must run early)
|
|
@@ -93,18 +93,8 @@ from setiastro.saspro.widgets.common_utilities import (
|
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
# Reproject
|
|
97
|
-
try:
|
|
98
|
-
from reproject import reproject_interp
|
|
99
|
-
except ImportError:
|
|
100
|
-
reproject_interp = None # fallback if not installed
|
|
96
|
+
# Reproject and OpenCV imports removed (unused or available via lazy_imports)
|
|
101
97
|
|
|
102
|
-
# OpenCV for transform estimation & warping
|
|
103
|
-
try:
|
|
104
|
-
import cv2
|
|
105
|
-
OPENCV_AVAILABLE = True
|
|
106
|
-
except ImportError:
|
|
107
|
-
OPENCV_AVAILABLE = False
|
|
108
98
|
|
|
109
99
|
|
|
110
100
|
|
|
@@ -196,7 +186,7 @@ from setiastro.saspro.resources import (
|
|
|
196
186
|
colorwheel_path, font_path, csv_icon_path, spinner_path, wims_path, narrowbandnormalization_path,
|
|
197
187
|
wimi_path, linearfit_path, debayer_path, aberration_path, acv_icon_path,
|
|
198
188
|
functionbundles_path, viewbundles_path, selectivecolor_path, rgbalign_path, planetarystacker_path,
|
|
199
|
-
background_path, script_icon_path, planetprojection_path,clonestampicon_path,
|
|
189
|
+
background_path, script_icon_path, planetprojection_path,clonestampicon_path, finderchart_path,
|
|
200
190
|
)
|
|
201
191
|
|
|
202
192
|
import faulthandler
|
|
@@ -4238,6 +4228,38 @@ class AstroSuiteProMainWindow(
|
|
|
4238
4228
|
except Exception:
|
|
4239
4229
|
pass
|
|
4240
4230
|
|
|
4231
|
+
def _doc_has_wcs(self, doc) -> bool:
|
|
4232
|
+
if doc is None:
|
|
4233
|
+
return False
|
|
4234
|
+
meta = getattr(doc, "metadata", None) or {}
|
|
4235
|
+
if meta.get("wcs") is not None:
|
|
4236
|
+
return True
|
|
4237
|
+
|
|
4238
|
+
hdr = meta.get("original_header") or meta.get("fits_header") or meta.get("header")
|
|
4239
|
+
if hdr is None:
|
|
4240
|
+
return False
|
|
4241
|
+
|
|
4242
|
+
try:
|
|
4243
|
+
keys = {str(k).upper() for k in hdr.keys()}
|
|
4244
|
+
except Exception:
|
|
4245
|
+
try:
|
|
4246
|
+
keys = {str(k).upper() for k in dict(hdr).keys()}
|
|
4247
|
+
except Exception:
|
|
4248
|
+
return False
|
|
4249
|
+
|
|
4250
|
+
return {"CTYPE1","CTYPE2","CRVAL1","CRVAL2"}.issubset(keys)
|
|
4251
|
+
|
|
4252
|
+
|
|
4253
|
+
def _open_finder_chart(self):
|
|
4254
|
+
doc = self._active_doc()
|
|
4255
|
+
if not self._doc_has_wcs(doc):
|
|
4256
|
+
QMessageBox.information(self, self.tr("Finder Chart"), self.tr("Active image has no astrometric solution (WCS). Plate solve first."))
|
|
4257
|
+
return
|
|
4258
|
+
|
|
4259
|
+
from setiastro.saspro.finder_chart import FinderChartDialog
|
|
4260
|
+
dlg = FinderChartDialog(doc=doc, settings=self.settings, parent=self)
|
|
4261
|
+
dlg.setWindowIcon(QIcon(finderchart_path))
|
|
4262
|
+
dlg.show()
|
|
4241
4263
|
|
|
4242
4264
|
def _open_stellar_alignment(self):
|
|
4243
4265
|
from setiastro.saspro.star_alignment import StellarAlignmentDialog
|
|
@@ -204,6 +204,7 @@ class MenuMixin:
|
|
|
204
204
|
|
|
205
205
|
m_star.addAction(self.act_astrospike)
|
|
206
206
|
m_star.addAction(self.act_exo_detector)
|
|
207
|
+
|
|
207
208
|
m_star.addAction(self.act_image_peeker)
|
|
208
209
|
m_star.addAction(self.act_isophote)
|
|
209
210
|
m_star.addAction(self.act_live_stacking)
|
|
@@ -233,6 +234,7 @@ class MenuMixin:
|
|
|
233
234
|
m_wim = mb.addMenu(self.tr("&What's In My..."))
|
|
234
235
|
m_wim.addAction(self.act_whats_in_my_sky)
|
|
235
236
|
m_wim.addAction(self.act_wimi)
|
|
237
|
+
m_wim.addAction(self.act_finder_chart)
|
|
236
238
|
|
|
237
239
|
m_scripts = mb.addMenu(self.tr("&Scripts"))
|
|
238
240
|
self.menu_scripts = m_scripts
|
|
@@ -36,7 +36,7 @@ from setiastro.saspro.resources import (
|
|
|
36
36
|
nbtorgb_path, freqsep_path, multiscale_decomp_path, contsub_path, halo_path, cosmic_path,
|
|
37
37
|
satellite_path, imagecombine_path, wims_path, wimi_path, linearfit_path,
|
|
38
38
|
debayer_path, aberration_path, functionbundles_path, viewbundles_path, planetarystacker_path,
|
|
39
|
-
selectivecolor_path, rgbalign_path, planetprojection_path, clonestampicon_path,
|
|
39
|
+
selectivecolor_path, rgbalign_path, planetprojection_path, clonestampicon_path, finderchart_path,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
# Import shortcuts module
|
|
@@ -298,6 +298,7 @@ class ToolbarMixin:
|
|
|
298
298
|
tb_star.addAction(self.act_planetary_stacker)
|
|
299
299
|
tb_star.addAction(self.act_planet_projection)
|
|
300
300
|
tb_star.addAction(self.act_plate_solve)
|
|
301
|
+
|
|
301
302
|
tb_star.addAction(self.act_star_align)
|
|
302
303
|
tb_star.addAction(self.act_star_register)
|
|
303
304
|
tb_star.addAction(self.act_rgb_align)
|
|
@@ -332,6 +333,7 @@ class ToolbarMixin:
|
|
|
332
333
|
tb_wim = DraggableToolBar(self.tr("What's In My..."), self)
|
|
333
334
|
tb_wim.setObjectName("What's In My...")
|
|
334
335
|
tb_wim.setSettingsKey("Toolbar/WhatsInMy")
|
|
336
|
+
tb_wim.addAction(self.act_finder_chart)
|
|
335
337
|
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, tb_wim)
|
|
336
338
|
|
|
337
339
|
tb_wim.addAction(self.act_whats_in_my_sky)
|
|
@@ -1245,6 +1247,11 @@ class ToolbarMixin:
|
|
|
1245
1247
|
self.act_rgb_align.setStatusTip(self.tr("Align R and B channels to G using astroalign (affine/homography/poly)"))
|
|
1246
1248
|
self.act_rgb_align.triggered.connect(self._open_rgb_align)
|
|
1247
1249
|
|
|
1250
|
+
self.act_finder_chart = QAction(QIcon(finderchart_path), self.tr("Finder Chart..."), self)
|
|
1251
|
+
self.act_finder_chart.setIconVisibleInMenu(True)
|
|
1252
|
+
self.act_finder_chart.setStatusTip(self.tr("Show a finder chart for the active plate-solved image"))
|
|
1253
|
+
self.act_finder_chart.triggered.connect(self._open_finder_chart)
|
|
1254
|
+
|
|
1248
1255
|
self.act_whats_in_my_sky = QAction(QIcon(wims_path), self.tr("What's In My Sky..."), self)
|
|
1249
1256
|
self.act_whats_in_my_sky.setIconVisibleInMenu(True)
|
|
1250
1257
|
self.act_whats_in_my_sky.setStatusTip(self.tr("Plan targets by altitude, transit time, and lunar separation"))
|
|
@@ -1446,6 +1453,7 @@ class ToolbarMixin:
|
|
|
1446
1453
|
reg("astrospike", self.act_astrospike)
|
|
1447
1454
|
reg("exo_detector", self.act_exo_detector)
|
|
1448
1455
|
reg("isophote", self.act_isophote)
|
|
1456
|
+
reg("finder_chart", self.act_finder_chart)
|
|
1449
1457
|
reg("rgb_align", self.act_rgb_align)
|
|
1450
1458
|
reg("whats_in_my_sky", self.act_whats_in_my_sky)
|
|
1451
1459
|
reg("whats_in_my_image", self.act_wimi)
|
|
@@ -242,8 +242,16 @@ class ImageManager(QObject):
|
|
|
242
242
|
def set_image(self, new_image, metadata, step_name=None):
|
|
243
243
|
slot = self.current_slot
|
|
244
244
|
if self._images[slot] is not None:
|
|
245
|
+
# OPTIMIZATION: If we are setting the EXACT SAME image object (e.g. metadata update),
|
|
246
|
+
# do not deep-copy the image data to undo stack.
|
|
247
|
+
# This saves massive memory when just renaming a slot or changing WCS.
|
|
248
|
+
if new_image is self._images[slot]:
|
|
249
|
+
stored_img = self._images[slot] # Shallow copy / reference
|
|
250
|
+
else:
|
|
251
|
+
stored_img = self._images[slot].copy() # Deep copy for safety
|
|
252
|
+
|
|
245
253
|
self._undo_stacks[slot].append(
|
|
246
|
-
(
|
|
254
|
+
(stored_img, self._metadata[slot].copy(), step_name or "Unnamed Step")
|
|
247
255
|
)
|
|
248
256
|
self._redo_stacks[slot].clear()
|
|
249
257
|
print(f"ImageManager: Previous image in slot {slot} pushed to undo stack.")
|
|
@@ -1526,8 +1534,12 @@ def load_image(filename, max_retries=3, wait_seconds=3, return_metadata: bool =
|
|
|
1526
1534
|
# 1) PixInsight astrometric solution (fallback only)
|
|
1527
1535
|
try:
|
|
1528
1536
|
if not all(k in hdr for k in ("CRPIX1","CRPIX2","CRVAL1","CRVAL2")):
|
|
1529
|
-
|
|
1530
|
-
|
|
1537
|
+
p_img = props['PCL:AstrometricSolution:ReferenceImageCoordinates']
|
|
1538
|
+
p_sky = props['PCL:AstrometricSolution:ReferenceCelestialCoordinates']
|
|
1539
|
+
|
|
1540
|
+
# Resolve lazy properties (decode base64/binary)
|
|
1541
|
+
ref_img = xisf.resolve_property(p_img)
|
|
1542
|
+
ref_sky = xisf.resolve_property(p_sky)
|
|
1531
1543
|
|
|
1532
1544
|
# Some files store extra values; only first two are CRPIX/CRVAL
|
|
1533
1545
|
im0, im1 = float(ref_img[0]), float(ref_img[1])
|
|
@@ -1547,7 +1559,9 @@ def load_image(filename, max_retries=3, wait_seconds=3, return_metadata: bool =
|
|
|
1547
1559
|
# 2) CD matrix (fallback only)
|
|
1548
1560
|
try:
|
|
1549
1561
|
if not all(k in hdr for k in ("CD1_1","CD1_2","CD2_1","CD2_2")):
|
|
1550
|
-
|
|
1562
|
+
p_mat = props['PCL:AstrometricSolution:LinearTransformationMatrix']
|
|
1563
|
+
lin = np.asarray(xisf.resolve_property(p_mat), float)
|
|
1564
|
+
|
|
1551
1565
|
hdr['CD1_1'], hdr['CD1_2'] = float(lin[0,0]), float(lin[0,1])
|
|
1552
1566
|
hdr['CD2_1'], hdr['CD2_2'] = float(lin[1,0]), float(lin[1,1])
|
|
1553
1567
|
_filled |= {'CD1_1','CD1_2','CD2_1','CD2_2'}
|
setiastro/saspro/legacy/xisf.py
CHANGED
|
@@ -1072,7 +1072,7 @@ class XISF:
|
|
|
1072
1072
|
}
|
|
1073
1073
|
try:
|
|
1074
1074
|
return _dtypes[s]
|
|
1075
|
-
except:
|
|
1075
|
+
except KeyError:
|
|
1076
1076
|
raise NotImplementedError(f"sampleFormat {s} not implemented")
|
|
1077
1077
|
|
|
1078
1078
|
# Return XISF data type from numpy dtype
|
|
@@ -1087,7 +1087,7 @@ class XISF:
|
|
|
1087
1087
|
}
|
|
1088
1088
|
try:
|
|
1089
1089
|
return _sampleFormats[str(dtype)]
|
|
1090
|
-
except:
|
|
1090
|
+
except KeyError:
|
|
1091
1091
|
raise NotImplementedError(f"sampleFormat for {dtype} not implemented")
|
|
1092
1092
|
|
|
1093
1093
|
@staticmethod
|
|
@@ -1121,7 +1121,7 @@ class XISF:
|
|
|
1121
1121
|
}
|
|
1122
1122
|
try:
|
|
1123
1123
|
return _dtypes[type_prefix]
|
|
1124
|
-
except:
|
|
1124
|
+
except KeyError:
|
|
1125
1125
|
raise NotImplementedError(f"data type {type_name} not implemented")
|
|
1126
1126
|
|
|
1127
1127
|
# __/ Auxiliary functions for compression/shuffling \________
|
setiastro/saspro/main_helpers.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
# pro/main_helpers.py
|
|
2
3
|
"""
|
|
3
4
|
Helper functions extracted from the main module.
|
|
@@ -7,12 +8,15 @@ Contains utility functions used throughout the main window:
|
|
|
7
8
|
- Document name/type detection
|
|
8
9
|
- Widget safety checks
|
|
9
10
|
- WCS/FITS header utilities
|
|
11
|
+
- UI responsiveness helpers
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
14
|
import os
|
|
15
|
+
import time
|
|
13
16
|
from typing import Optional, Tuple
|
|
14
17
|
|
|
15
18
|
from PyQt6 import sip
|
|
19
|
+
from PyQt6.QtWidgets import QApplication
|
|
16
20
|
|
|
17
21
|
from setiastro.saspro.file_utils import (
|
|
18
22
|
_normalize_ext,
|
|
@@ -23,6 +27,20 @@ from setiastro.saspro.file_utils import (
|
|
|
23
27
|
)
|
|
24
28
|
|
|
25
29
|
|
|
30
|
+
def non_blocking_sleep(duration_sec: float):
|
|
31
|
+
"""
|
|
32
|
+
Sleep for duration_sec seconds while keeping the UI responsive.
|
|
33
|
+
Uses QApplication.processEvents() to process pending events.
|
|
34
|
+
"""
|
|
35
|
+
end_time = time.time() + duration_sec
|
|
36
|
+
while time.time() < end_time:
|
|
37
|
+
QApplication.processEvents()
|
|
38
|
+
# Sleep a tiny bit to avoid 100% CPU usage
|
|
39
|
+
sleep_time = min(0.05, end_time - time.time())
|
|
40
|
+
if sleep_time > 0:
|
|
41
|
+
time.sleep(sleep_time)
|
|
42
|
+
|
|
43
|
+
|
|
26
44
|
def safe_join_dir_and_name(directory: str, basename: str) -> str:
|
|
27
45
|
"""
|
|
28
46
|
Join directory + sanitized basename.
|
setiastro/saspro/memory_utils.py
CHANGED
|
@@ -128,30 +128,34 @@ class LRUDict(OrderedDict):
|
|
|
128
128
|
When maxsize is exceeded, oldest items are evicted.
|
|
129
129
|
Thread-safe for basic operations.
|
|
130
130
|
"""
|
|
131
|
-
__slots__ = ('maxsize',)
|
|
131
|
+
__slots__ = ('maxsize', '_lock')
|
|
132
132
|
|
|
133
133
|
def __init__(self, maxsize: int = 500):
|
|
134
134
|
super().__init__()
|
|
135
135
|
self.maxsize = maxsize
|
|
136
|
+
self._lock = threading.RLock()
|
|
136
137
|
|
|
137
138
|
def __getitem__(self, key):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return super().__getitem__(key)
|
|
141
|
-
|
|
142
|
-
def get(self, key, default=None):
|
|
143
|
-
if key in self:
|
|
139
|
+
with self._lock:
|
|
140
|
+
# Move to end on access (most recently used)
|
|
144
141
|
self.move_to_end(key)
|
|
145
142
|
return super().__getitem__(key)
|
|
146
|
-
|
|
143
|
+
|
|
144
|
+
def get(self, key, default=None):
|
|
145
|
+
with self._lock:
|
|
146
|
+
if key in self:
|
|
147
|
+
self.move_to_end(key)
|
|
148
|
+
return super().__getitem__(key)
|
|
149
|
+
return default
|
|
147
150
|
|
|
148
151
|
def __setitem__(self, key, value):
|
|
149
|
-
|
|
150
|
-
self
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
self
|
|
152
|
+
with self._lock:
|
|
153
|
+
if key in self:
|
|
154
|
+
self.move_to_end(key)
|
|
155
|
+
super().__setitem__(key, value)
|
|
156
|
+
# Evict oldest if over limit
|
|
157
|
+
while len(self) > self.maxsize:
|
|
158
|
+
self.popitem(last=False) # Remove oldest
|
|
155
159
|
|
|
156
160
|
|
|
157
161
|
# ============================================================================
|
|
@@ -131,6 +131,71 @@ def _parse_gdrive_download_form(html: str) -> tuple[Optional[str], Optional[dict
|
|
|
131
131
|
|
|
132
132
|
return action, params
|
|
133
133
|
|
|
134
|
+
def download_http_file(
|
|
135
|
+
url: str,
|
|
136
|
+
dst_path: str | os.PathLike,
|
|
137
|
+
*,
|
|
138
|
+
progress_cb: ProgressCB = None,
|
|
139
|
+
should_cancel=None,
|
|
140
|
+
timeout: int = 60,
|
|
141
|
+
chunk_size: int = 1024 * 1024,
|
|
142
|
+
) -> Path:
|
|
143
|
+
import requests
|
|
144
|
+
|
|
145
|
+
dst = Path(dst_path)
|
|
146
|
+
tmp = dst.with_suffix(dst.suffix + ".part")
|
|
147
|
+
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
def log(msg: str):
|
|
150
|
+
if progress_cb:
|
|
151
|
+
progress_cb(msg)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
tmp.unlink(missing_ok=True)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
with requests.Session() as s:
|
|
159
|
+
log(f"Connecting… {url}")
|
|
160
|
+
r = s.get(url, stream=True, timeout=timeout, allow_redirects=True)
|
|
161
|
+
r.raise_for_status()
|
|
162
|
+
|
|
163
|
+
total = int(r.headers.get("Content-Length") or 0)
|
|
164
|
+
done = 0
|
|
165
|
+
t_last = time.time()
|
|
166
|
+
done_last = 0
|
|
167
|
+
|
|
168
|
+
with open(tmp, "wb") as f:
|
|
169
|
+
for chunk in r.iter_content(chunk_size=chunk_size):
|
|
170
|
+
if should_cancel and should_cancel():
|
|
171
|
+
try:
|
|
172
|
+
f.close()
|
|
173
|
+
tmp.unlink(missing_ok=True)
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
raise RuntimeError("Download canceled.")
|
|
177
|
+
|
|
178
|
+
if not chunk:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
f.write(chunk)
|
|
182
|
+
done += len(chunk)
|
|
183
|
+
|
|
184
|
+
now = time.time()
|
|
185
|
+
if now - t_last >= 0.5:
|
|
186
|
+
if total > 0:
|
|
187
|
+
pct = (done * 100.0) / total
|
|
188
|
+
log(f"Downloading… {pct:5.1f}% ({done}/{total} bytes)")
|
|
189
|
+
else:
|
|
190
|
+
bps = (done - done_last) / max(now - t_last, 1e-9)
|
|
191
|
+
log(f"Downloading… {done} bytes ({bps/1024/1024:.1f} MB/s)")
|
|
192
|
+
t_last = now
|
|
193
|
+
done_last = done
|
|
194
|
+
|
|
195
|
+
os.replace(str(tmp), str(dst))
|
|
196
|
+
log(f"Download complete: {dst}")
|
|
197
|
+
return dst
|
|
198
|
+
|
|
134
199
|
|
|
135
200
|
def download_google_drive_file(
|
|
136
201
|
file_id: str,
|
|
@@ -9,6 +9,7 @@ import zipfile
|
|
|
9
9
|
from setiastro.saspro.model_manager import (
|
|
10
10
|
extract_drive_file_id,
|
|
11
11
|
download_google_drive_file,
|
|
12
|
+
download_http_file,
|
|
12
13
|
install_models_zip,
|
|
13
14
|
sha256_file,
|
|
14
15
|
)
|
|
@@ -54,49 +55,82 @@ class ModelsDownloadWorker(QObject):
|
|
|
54
55
|
progress = pyqtSignal(str)
|
|
55
56
|
finished = pyqtSignal(bool, str)
|
|
56
57
|
|
|
57
|
-
def __init__(self, primary: str, backup: str,
|
|
58
|
+
def __init__(self, primary: str, backup: str, tertiary: str | None = None,
|
|
59
|
+
expected_sha256: str | None = None, should_cancel=None):
|
|
58
60
|
super().__init__()
|
|
59
61
|
self.primary = primary
|
|
60
62
|
self.backup = backup
|
|
63
|
+
self.tertiary = (tertiary or "").strip() or None
|
|
61
64
|
self.expected_sha256 = (expected_sha256 or "").strip() or None
|
|
62
65
|
self.should_cancel = should_cancel # callable -> bool
|
|
63
66
|
|
|
64
67
|
def run(self):
|
|
65
68
|
try:
|
|
66
|
-
# The inputs should be FILE links (or IDs), not folder links.
|
|
67
|
-
fid = extract_drive_file_id(self.primary) or extract_drive_file_id(self.backup)
|
|
68
|
-
if not fid:
|
|
69
|
-
raise RuntimeError(
|
|
70
|
-
"Models URL is not a Google Drive *file* link or id.\n"
|
|
71
|
-
"Please provide a shared file link (…/file/d/<ID>/view) to the models zip."
|
|
72
|
-
)
|
|
73
|
-
|
|
74
69
|
tmp = os.path.join(tempfile.gettempdir(), "saspro_models_latest.zip")
|
|
75
|
-
try:
|
|
76
|
-
self.progress.emit("Downloading from primary…")
|
|
77
|
-
download_google_drive_file(fid, tmp, progress_cb=lambda s: self.progress.emit(s), should_cancel=self.should_cancel)
|
|
78
|
-
except Exception as e:
|
|
79
|
-
# Try backup if primary fails AND backup has a different file id
|
|
80
|
-
fid2 = extract_drive_file_id(self.backup)
|
|
81
|
-
if fid2 and fid2 != fid:
|
|
82
|
-
self.progress.emit("Primary failed. Trying backup…")
|
|
83
|
-
download_google_drive_file(fid2, tmp, progress_cb=lambda s: self.progress.emit(s), should_cancel=self.should_cancel)
|
|
84
|
-
else:
|
|
85
|
-
raise
|
|
86
70
|
|
|
71
|
+
# 1) Try Google Drive primary/backup (by file id)
|
|
72
|
+
fid_primary = extract_drive_file_id(self.primary)
|
|
73
|
+
fid_backup = extract_drive_file_id(self.backup)
|
|
74
|
+
|
|
75
|
+
drive_ok = False
|
|
76
|
+
used_source = None
|
|
77
|
+
|
|
78
|
+
if fid_primary or fid_backup:
|
|
79
|
+
try:
|
|
80
|
+
if fid_primary:
|
|
81
|
+
self.progress.emit("Downloading from primary (Google Drive)…")
|
|
82
|
+
download_google_drive_file(
|
|
83
|
+
fid_primary, tmp,
|
|
84
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
85
|
+
should_cancel=self.should_cancel
|
|
86
|
+
)
|
|
87
|
+
drive_ok = True
|
|
88
|
+
used_source = ("google_drive", fid_primary)
|
|
89
|
+
else:
|
|
90
|
+
raise RuntimeError("Primary is not a valid Drive file link/id.")
|
|
91
|
+
except Exception:
|
|
92
|
+
# Try backup if different id
|
|
93
|
+
if fid_backup and fid_backup != fid_primary:
|
|
94
|
+
self.progress.emit("Primary failed. Trying backup (Google Drive)…")
|
|
95
|
+
download_google_drive_file(
|
|
96
|
+
fid_backup, tmp,
|
|
97
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
98
|
+
should_cancel=self.should_cancel
|
|
99
|
+
)
|
|
100
|
+
drive_ok = True
|
|
101
|
+
used_source = ("google_drive", fid_backup)
|
|
102
|
+
|
|
103
|
+
# 2) If Drive failed (or links weren’t Drive), try GitHub / HTTP tertiary
|
|
104
|
+
if not drive_ok:
|
|
105
|
+
if not self.tertiary:
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
"Google Drive download failed and no tertiary mirror URL was provided."
|
|
108
|
+
)
|
|
109
|
+
self.progress.emit("Google Drive failed. Trying GitHub mirror…")
|
|
110
|
+
download_http_file(
|
|
111
|
+
self.tertiary, tmp,
|
|
112
|
+
progress_cb=lambda s: self.progress.emit(s),
|
|
113
|
+
should_cancel=self.should_cancel
|
|
114
|
+
)
|
|
115
|
+
used_source = ("http", self.tertiary)
|
|
116
|
+
|
|
117
|
+
# 3) Optional checksum
|
|
87
118
|
if self.expected_sha256:
|
|
88
119
|
self.progress.emit("Verifying checksum…")
|
|
89
120
|
got = sha256_file(tmp)
|
|
90
121
|
if got.lower() != self.expected_sha256.lower():
|
|
91
122
|
raise RuntimeError(f"SHA256 mismatch.\nExpected: {self.expected_sha256}\nGot: {got}")
|
|
92
123
|
|
|
124
|
+
# 4) Install + manifest
|
|
125
|
+
src_kind, src_val = used_source if used_source else ("unknown", "")
|
|
93
126
|
manifest = {
|
|
94
|
-
"source":
|
|
95
|
-
"
|
|
96
|
-
"sha256": self.expected_sha256,
|
|
127
|
+
"source": src_kind,
|
|
128
|
+
"source_ref": src_val,
|
|
129
|
+
"sha256": self.expected_sha256 or "",
|
|
97
130
|
}
|
|
98
|
-
install_models_zip(tmp, progress_cb=lambda s: self.progress.emit(s), manifest=manifest)
|
|
99
131
|
|
|
132
|
+
install_models_zip(tmp, progress_cb=lambda s: self.progress.emit(s), manifest=manifest)
|
|
100
133
|
self.finished.emit(True, "Models updated successfully.")
|
|
101
134
|
except Exception as e:
|
|
102
135
|
self.finished.emit(False, str(e))
|
|
136
|
+
|
setiastro/saspro/ops/settings.py
CHANGED
|
@@ -248,7 +248,8 @@ class SettingsDialog(QDialog):
|
|
|
248
248
|
self.btn_models_install_zip.clicked.connect(self._models_install_from_zip_clicked)
|
|
249
249
|
|
|
250
250
|
self.btn_models_open_drive = QPushButton(self.tr("Open Drive…"))
|
|
251
|
-
self.btn_models_open_drive.setToolTip(self.tr("
|
|
251
|
+
self.btn_models_open_drive.setToolTip(self.tr("Download models (Primary/Backup/GitHub mirror)"))
|
|
252
|
+
|
|
252
253
|
self.btn_models_open_drive.clicked.connect(self._models_open_drive_clicked)
|
|
253
254
|
|
|
254
255
|
row_models = QHBoxLayout()
|
|
@@ -319,16 +320,22 @@ class SettingsDialog(QDialog):
|
|
|
319
320
|
def _models_open_drive_clicked(self):
|
|
320
321
|
PRIMARY_FOLDER = "https://drive.google.com/drive/folders/1-fktZb3I9l-mQimJX2fZAmJCBj_t0yAF?usp=drive_link"
|
|
321
322
|
BACKUP_FOLDER = "https://drive.google.com/drive/folders/1j46RV6touQtOmtxkhdFWGm_LQKwEpTl9?usp=drive_link"
|
|
323
|
+
GITHUB_ZIP = "https://github.com/setiastro/setiastrosuitepro/releases/download/benchmarkFIT/SASPro_Models_Latest.zip"
|
|
322
324
|
|
|
323
325
|
menu = QMenu(self)
|
|
324
|
-
act_primary = menu.addAction(self.tr("Primary"))
|
|
325
|
-
act_backup = menu.addAction(self.tr("Backup"))
|
|
326
|
+
act_primary = menu.addAction(self.tr("Primary (Google Drive)"))
|
|
327
|
+
act_backup = menu.addAction(self.tr("Backup (Google Drive)"))
|
|
328
|
+
menu.addSeparator()
|
|
329
|
+
act_gh = menu.addAction(self.tr("GitHub (no quota limit)"))
|
|
326
330
|
|
|
327
331
|
chosen = menu.exec(self.btn_models_open_drive.mapToGlobal(self.btn_models_open_drive.rect().bottomLeft()))
|
|
328
332
|
if chosen == act_primary:
|
|
329
333
|
webbrowser.open(PRIMARY_FOLDER)
|
|
330
334
|
elif chosen == act_backup:
|
|
331
335
|
webbrowser.open(BACKUP_FOLDER)
|
|
336
|
+
elif chosen == act_gh:
|
|
337
|
+
webbrowser.open(GITHUB_ZIP)
|
|
338
|
+
|
|
332
339
|
|
|
333
340
|
|
|
334
341
|
def _models_install_from_zip_clicked(self):
|
|
@@ -404,6 +411,7 @@ class SettingsDialog(QDialog):
|
|
|
404
411
|
# Put your actual *zip file* share links here once you create them.
|
|
405
412
|
PRIMARY = "https://drive.google.com/file/d/1n4p0grtNpfllalMqtgaEmsTYaFhT5u7Y/view?usp=drive_link"
|
|
406
413
|
BACKUP = "https://drive.google.com/file/d/1uRGJCITlfMMN89ZkOO5ICWEKMH24KGit/view?usp=drive_link"
|
|
414
|
+
TERTIARY = "https://github.com/setiastro/setiastrosuitepro/releases/download/benchmarkFIT/SASPro_Models_Latest.zip"
|
|
407
415
|
|
|
408
416
|
|
|
409
417
|
self.btn_models_update.setEnabled(False)
|
|
@@ -418,7 +426,7 @@ class SettingsDialog(QDialog):
|
|
|
418
426
|
from setiastro.saspro.model_workers import ModelsDownloadWorker
|
|
419
427
|
|
|
420
428
|
self._models_thread = QThread(self)
|
|
421
|
-
self._models_worker = ModelsDownloadWorker(PRIMARY, BACKUP, expected_sha256=None)
|
|
429
|
+
self._models_worker = ModelsDownloadWorker(PRIMARY, BACKUP, TERTIARY, expected_sha256=None)
|
|
422
430
|
self._models_worker.moveToThread(self._models_thread)
|
|
423
431
|
|
|
424
432
|
self._models_thread.started.connect(self._models_worker.run, Qt.ConnectionType.QueuedConnection)
|
|
@@ -457,10 +465,39 @@ class SettingsDialog(QDialog):
|
|
|
457
465
|
if not m:
|
|
458
466
|
self.lbl_models_status.setText(self.tr("Status: not installed"))
|
|
459
467
|
return
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
)
|
|
468
|
+
|
|
469
|
+
# New fields (preferred)
|
|
470
|
+
src = (m.get("source") or "").strip()
|
|
471
|
+
ref = (m.get("source_ref") or "").strip()
|
|
472
|
+
sha = (m.get("sha256") or "").strip()
|
|
473
|
+
|
|
474
|
+
# Back-compat with older manifests
|
|
475
|
+
if not src:
|
|
476
|
+
# old versions used google_drive + file_id
|
|
477
|
+
src = "google_drive" if m.get("file_id") else (m.get("source") or "unknown")
|
|
478
|
+
if not ref:
|
|
479
|
+
ref = (m.get("file_id") or m.get("file") or "").strip()
|
|
480
|
+
|
|
481
|
+
# Keep it short + readable
|
|
482
|
+
src_label = {
|
|
483
|
+
"google_drive": "Google Drive",
|
|
484
|
+
"http": "GitHub/HTTP",
|
|
485
|
+
"manual_zip": "Manual ZIP",
|
|
486
|
+
}.get(src, src or "Unknown")
|
|
487
|
+
|
|
488
|
+
lines = [self.tr("Status: installed"),
|
|
489
|
+
self.tr("Location: {0}").format(models_root())]
|
|
490
|
+
|
|
491
|
+
if ref:
|
|
492
|
+
# show just a compact hint (id or url or filename)
|
|
493
|
+
lines.append(self.tr("Source: {0}").format(src_label))
|
|
494
|
+
lines.append(self.tr("Ref: {0}").format(ref))
|
|
495
|
+
|
|
496
|
+
if sha:
|
|
497
|
+
lines.append(self.tr("SHA256: {0}").format(sha[:12] + "…"))
|
|
498
|
+
|
|
499
|
+
self.lbl_models_status.setText("\n".join(lines))
|
|
500
|
+
self.lbl_models_status.setStyleSheet("color:#888;")
|
|
464
501
|
|
|
465
502
|
|
|
466
503
|
def _accel_pref_changed(self, idx: int):
|