setiastrosuitepro 1.8.0.post3__py3-none-any.whl → 1.8.1.post2__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.

@@ -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, expected_sha256: str | None = None, should_cancel=None):
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": "google_drive",
95
- "file_id": fid,
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
+
@@ -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("Open the models folder in your browser (Primary/Backup)"))
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
- fid = m.get("file_id", "")
461
- self.lbl_models_status.setText(
462
- self.tr("Status: installed\nLocation: {0}").format(models_root())
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):
@@ -2175,6 +2175,52 @@ class PlanetProjectionDialog(QDialog):
2175
2175
  self._show_stereo_pair(cross_eye=cross_eye)
2176
2176
  return
2177
2177
 
2178
+ # ---- GALAXY TOP-DOWN (early exit) ----
2179
+ # IMPORTANT: do this BEFORE any ROI crop, otherwise huge galaxies get clipped by ROI sizing.
2180
+ is_galaxy = (ptype == 3) or (mode == 5) # planet_type==Galaxy OR output==Galaxy Polar View
2181
+ if is_galaxy:
2182
+ def to01(x):
2183
+ if x.dtype == np.uint8:
2184
+ return x.astype(np.float32) / 255.0
2185
+ if x.dtype == np.uint16:
2186
+ return x.astype(np.float32) / 65535.0
2187
+ return x.astype(np.float32, copy=False)
2188
+
2189
+ roi = img[..., :3] # FULL IMAGE
2190
+ cx0 = float(cx) # FULL-IMAGE coords
2191
+ cy0 = float(cy)
2192
+ roi01 = to01(roi)
2193
+
2194
+ pa = float(self.spin_ring_pa.value()) # reuse ring PA widget as galaxy PA
2195
+ tilt = float(self.spin_ring_tilt.value()) # reuse ring tilt widget as galaxy b/a
2196
+
2197
+ # output size: tied to full image (clamped)
2198
+ out_size = int(max(256, min(2400, max(roi.shape[0], roi.shape[1]))))
2199
+
2200
+ try:
2201
+ top8 = deproject_galaxy_topdown_u8(
2202
+ roi01,
2203
+ cx0=cx0, cy0=cy0,
2204
+ rpx=float(r),
2205
+ pa_deg=pa,
2206
+ tilt=tilt,
2207
+ out_size=out_size,
2208
+ )
2209
+ except Exception as e:
2210
+ QMessageBox.warning(self, "Galaxy Polar View", f"Failed to deproject galaxy:\n{e}")
2211
+ return
2212
+
2213
+ # push single-frame output
2214
+ self._left = None
2215
+ self._right = None
2216
+ self._wiggle_frames = None
2217
+ self._wiggle_state = False
2218
+
2219
+ self._last_preview_u8 = top8
2220
+ self._push_preview_u8(top8)
2221
+ return
2222
+
2223
+
2178
2224
  # ---- Saturn rings ROI expansion (only increases s) ----
2179
2225
  is_saturn = (self.cmb_planet_type.currentIndex() == 1)
2180
2226
  rings_on = bool(is_saturn and getattr(self, "chk_rings", None) is not None and self.chk_rings.isChecked())
@@ -2209,6 +2255,28 @@ class PlanetProjectionDialog(QDialog):
2209
2255
  y1 = min(Hfull, y0 + s)
2210
2256
 
2211
2257
  roi = img[y0:y1, x0:x1, :3]
2258
+ # ---- GALAXY ROI expansion (ensure full projected ellipse fits) ----
2259
+ if ptype == 3:
2260
+ tilt = float(self.spin_ring_tilt.value())
2261
+ pa = float(self.spin_ring_pa.value())
2262
+
2263
+ tilt = float(np.clip(tilt, 0.02, 1.0))
2264
+
2265
+ # ellipse in SOURCE pixels
2266
+ a = float(r) # semi-major
2267
+ b = max(1.0, a * tilt) # semi-minor
2268
+
2269
+ th = np.deg2rad(pa)
2270
+ cth, sth = np.cos(th), np.sin(th)
2271
+
2272
+ # bounding half-extents of rotated ellipse
2273
+ dx = np.sqrt((a * cth) ** 2 + (b * sth) ** 2)
2274
+ dy = np.sqrt((a * sth) ** 2 + (b * cth) ** 2)
2275
+ need_half = float(max(dx, dy))
2276
+
2277
+ margin = 12.0
2278
+ s_need = int(np.ceil(2.0 * (need_half + margin)))
2279
+ s = max(s, s_need)
2212
2280
 
2213
2281
  # ---- disk mask (ROI coords) ----
2214
2282
  H0, W0 = roi.shape[:2]
@@ -2226,42 +2294,6 @@ class PlanetProjectionDialog(QDialog):
2226
2294
 
2227
2295
  theta = float(self.spin_theta.value())
2228
2296
 
2229
- # ---- GALAXY TOP-DOWN (early exit) ----
2230
- is_galaxy = (ptype == 3) or (mode == 5) # planet_type==Galaxy OR output==Galaxy Polar View
2231
-
2232
- if is_galaxy:
2233
- # Galaxy wants the ROI disk params (cx0, cy0, r) + PA/tilt
2234
- roi01 = to01(roi)
2235
-
2236
- pa = float(self.spin_ring_pa.value()) # reuse ring PA widget as galaxy PA
2237
- tilt = float(self.spin_ring_tilt.value()) # reuse ring tilt widget as galaxy b/a
2238
-
2239
- # choose output size: use ROI size or clamp to something reasonable
2240
- out_size = int(max(256, min(2000, max(roi.shape[0], roi.shape[1]))))
2241
-
2242
- try:
2243
- top8 = deproject_galaxy_topdown_u8(
2244
- roi01,
2245
- cx0=float(cx0), cy0=float(cy0),
2246
- rpx=float(r),
2247
- pa_deg=pa,
2248
- tilt=tilt,
2249
- out_size=out_size,
2250
- )
2251
- except Exception as e:
2252
- QMessageBox.warning(self, "Galaxy Polar View", f"Failed to deproject galaxy:\n{e}")
2253
- return
2254
-
2255
- # push single-frame output
2256
- self._left = None
2257
- self._right = None
2258
- self._wiggle_frames = None
2259
- self._wiggle_state = False
2260
-
2261
- self._last_preview_u8 = top8
2262
- self._push_preview_u8(top8)
2263
- return
2264
-
2265
2297
  # ---- BODY (sphere reprojection) ----
2266
2298
  interp = cv2.INTER_LANCZOS4
2267
2299
  left_w, right_w, maskL, maskR = make_stereo_pair(
@@ -603,23 +603,27 @@ class Resources:
603
603
 
604
604
  @lru_cache(maxsize=8)
605
605
  def get_models_dir() -> str:
606
- # Prefer user-installed models
607
- try:
608
- from setiastro.saspro.model_manager import models_root
609
- p = Path(models_root())
610
- if p.is_dir():
611
- if any(p.rglob("*.pth")) or any(p.rglob("*.onnx")):
612
- return str(p)
613
- except Exception:
614
- pass
606
+ """
607
+ Models are NOT packaged resources. They must be installed via the model manager.
608
+ This returns the user models root and never falls back to _internal/data/models.
609
+ """
610
+ from setiastro.saspro.model_manager import models_root
611
+ p = Path(models_root())
612
+ # Ensure dir exists (models_root should already do this, but harmless)
613
+ p.mkdir(parents=True, exist_ok=True)
614
+ return str(p)
615
+
615
616
 
616
- # Fallback to packaged (dev/frozen) path
617
- return _resource_path('data/models')
617
+ def _assert_not_internal_models_path(p: str):
618
+ s = str(p).lower().replace("/", "\\")
619
+ if "\\_internal\\" in s and "\\data\\models\\" in s:
620
+ raise RuntimeError(f"Legacy internal model path detected: {p}")
618
621
 
619
622
  def model_path(filename: str) -> str:
620
- base = Path(get_models_dir())
621
- path = base / filename
622
- return str(path)
623
+ from setiastro.saspro.model_manager import require_model
624
+ p = str(require_model(filename))
625
+ _assert_not_internal_models_path(p)
626
+ return p
623
627
 
624
628
  # Export all legacy paths as module-level variables
625
629
  _legacy = _init_legacy_paths()