fix: synthesis DEM shape matches local 3-D contract
Browse filesThe terramind v1 base generative encoder adds its batch dim internally;
sending an extra leading dim makes the embedding layer try to unpack
5-D into B, C, H, W and raise ValueError. Match the local code: HF
sends (1, H, W); droplet treats incoming (1,1,H,W) as a legacy shape
and squeezes; and rejects anything else with a clean 400 instead of
a 500 with a noisy traceback.
Also add n_building_components (scipy.ndimage.label connected-component
count) to the droplet's buildings response so the TerraMind Buildings
card renders the real number instead of '0 distinct components'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
app/context/terramind_synthesis.py
CHANGED
|
@@ -290,8 +290,12 @@ def fetch(lat: float, lon: float, timeout_s: float = 60.0) -> dict[str, Any]:
|
|
| 290 |
try:
|
| 291 |
from app import inference as _inf
|
| 292 |
if _inf.remote_enabled():
|
| 293 |
-
# Local code
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
remote = _inf.terramind("synthesis", None, None, dem_remote,
|
| 296 |
timeout=timeout_s)
|
| 297 |
if remote.get("ok"):
|
|
|
|
| 290 |
try:
|
| 291 |
from app import inference as _inf
|
| 292 |
if _inf.remote_enabled():
|
| 293 |
+
# Local code does `torch.from_numpy(dem).unsqueeze(0)` —
|
| 294 |
+
# i.e. 2-D (H, W) → 3-D (1, H, W). The terramind v1 base
|
| 295 |
+
# generative encoder adds the batch dim internally; sending
|
| 296 |
+
# an extra leading dim makes its embedding layer trip on
|
| 297 |
+
# `B, C, H, W = x.shape` (5-D in, expects 4). Match local.
|
| 298 |
+
dem_remote = dem[None, :, :].astype("float32")
|
| 299 |
remote = _inf.terramind("synthesis", None, None, dem_remote,
|
| 300 |
timeout=timeout_s)
|
| 301 |
if remote.get("ok"):
|
services/riprap-models/main.py
CHANGED
|
@@ -381,10 +381,19 @@ def _terramind_synthesis_inference(payload: TerramindIn) -> dict[str, Any]:
|
|
| 381 |
import numpy as np
|
| 382 |
import torch
|
| 383 |
dem_t = torch.from_numpy(dem_np).float()
|
| 384 |
-
#
|
| 385 |
-
#
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
dem_t = _to_device(dem_t)
|
| 389 |
|
| 390 |
spec = _TERRAMIND_SPECS["synthesis"]
|
|
@@ -473,6 +482,19 @@ def _terramind_inference(payload: TerramindIn) -> dict[str, Any]:
|
|
| 473 |
fractions = {k: v for k, v in fractions.items() if v > 0}
|
| 474 |
dom_idx = int(max(range(spec["num_classes"]),
|
| 475 |
key=lambda i: int((pred == i).sum())))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
return {
|
| 477 |
"ok": True,
|
| 478 |
"adapter": payload.adapter,
|
|
@@ -483,9 +505,10 @@ def _terramind_inference(payload: TerramindIn) -> dict[str, Any]:
|
|
| 483 |
"class_fractions": fractions,
|
| 484 |
"dominant_class": spec["labels"][dom_idx],
|
| 485 |
"dominant_pct": fractions.get(spec["labels"][dom_idx], 0.0),
|
| 486 |
-
# Buildings-specific stat (
|
| 487 |
"pct_buildings": round(100.0 * float((pred == 1).sum()) / n, 2)
|
| 488 |
if payload.adapter == "buildings" else None,
|
|
|
|
| 489 |
}
|
| 490 |
|
| 491 |
|
|
|
|
| 381 |
import numpy as np
|
| 382 |
import torch
|
| 383 |
dem_t = torch.from_numpy(dem_np).float()
|
| 384 |
+
# Match the local-inference shape contract from
|
| 385 |
+
# app/context/terramind_synthesis.py:_ensure_model — the v1 base
|
| 386 |
+
# generative encoder wants 3-D (1, H, W) and adds the batch dim
|
| 387 |
+
# internally. Anything more triggers `B, C, H, W = x.shape` to
|
| 388 |
+
# unpack 5-D and fail in the embedding layer.
|
| 389 |
+
if dem_t.ndim == 2:
|
| 390 |
+
dem_t = dem_t.unsqueeze(0) # (H, W) -> (1, H, W)
|
| 391 |
+
elif dem_t.ndim == 4 and dem_t.shape[0] == 1 and dem_t.shape[1] == 1:
|
| 392 |
+
dem_t = dem_t.squeeze(0) # (1, 1, H, W) -> (1, H, W)
|
| 393 |
+
elif dem_t.ndim != 3:
|
| 394 |
+
raise HTTPException(status_code=400,
|
| 395 |
+
detail=f"unexpected DEM shape {tuple(dem_t.shape)}; "
|
| 396 |
+
f"expected (H, W) or (1, H, W)")
|
| 397 |
dem_t = _to_device(dem_t)
|
| 398 |
|
| 399 |
spec = _TERRAMIND_SPECS["synthesis"]
|
|
|
|
| 482 |
fractions = {k: v for k, v in fractions.items() if v > 0}
|
| 483 |
dom_idx = int(max(range(spec["num_classes"]),
|
| 484 |
key=lambda i: int((pred == i).sum())))
|
| 485 |
+
|
| 486 |
+
# Buildings: connected-component count (parity with local
|
| 487 |
+
# _summarize_buildings). The card subhead reads this — without it,
|
| 488 |
+
# the UI shows "0 distinct components".
|
| 489 |
+
n_components = None
|
| 490 |
+
if payload.adapter == "buildings":
|
| 491 |
+
try:
|
| 492 |
+
from scipy.ndimage import label
|
| 493 |
+
_, n_components = label((pred == 1).astype("uint8"))
|
| 494 |
+
n_components = int(n_components)
|
| 495 |
+
except Exception:
|
| 496 |
+
log.debug("terramind/buildings: scipy.ndimage unavailable")
|
| 497 |
+
|
| 498 |
return {
|
| 499 |
"ok": True,
|
| 500 |
"adapter": payload.adapter,
|
|
|
|
| 505 |
"class_fractions": fractions,
|
| 506 |
"dominant_class": spec["labels"][dom_idx],
|
| 507 |
"dominant_pct": fractions.get(spec["labels"][dom_idx], 0.0),
|
| 508 |
+
# Buildings-specific stat (None when not the buildings adapter).
|
| 509 |
"pct_buildings": round(100.0 * float((pred == 1).sum()) / n, 2)
|
| 510 |
if payload.adapter == "buildings" else None,
|
| 511 |
+
"n_building_components": n_components,
|
| 512 |
}
|
| 513 |
|
| 514 |
|