seriffic Claude Opus 4.7 (1M context) commited on
Commit
7ad8df4
·
1 Parent(s): 94e4c67

fix: synthesis DEM shape matches local 3-D contract

Browse files

The 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 uses (1, 1, H, W); send the same shape.
294
- dem_remote = dem[None, None, :, :].astype("float32")
 
 
 
 
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
- # Accept (H, W), (1, H, W), or (1, 1, H, W) — the local code builds
385
- # (1, 1, H, W) so that's the most common.
386
- while dem_t.ndim < 4:
387
- dem_t = dem_t.unsqueeze(0)
 
 
 
 
 
 
 
 
 
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 (NaN-safe; 0 when not the buildings adapter).
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