Image-to-Text
PyTorch
Safetensors
PEFT
English
remote-sensing
satellite-imagery
earth-observation
change-detection
visual-grounding
image-captioning
visual-question-answering
optical-sar-fusion
sar
multimodal
lora
Instructions to use thundercode/SatQuery with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thundercode/SatQuery with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
release: add docs/architecture/01-system-overview.md
Browse files
docs/architecture/01-system-overview.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 01 — System Overview
|
| 2 |
+
|
| 3 |
+
**Parent:** [Architecture hub](../ARCHITECTURE.md) · **Status tags:** `IMPLEMENTED` · `VERIFIED` ·
|
| 4 |
+
`MEASURED` · `NOT RUN`
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 1. What the system is
|
| 9 |
+
|
| 10 |
+
SatQuery AI takes a natural-language question and one or two image assets, and returns a typed
|
| 11 |
+
`ResultEnvelope` containing an answer, a list of evidence items, an execution trace, and a
|
| 12 |
+
confidence breakdown.
|
| 13 |
+
|
| 14 |
+
It is a **router + specialists** architecture. There is no single end-to-end model that "does
|
| 15 |
+
satellite QA". Instead:
|
| 16 |
+
|
| 17 |
+
1. a **router** reads the question and predicts *which capability* is being requested;
|
| 18 |
+
2. a **controller** validates the request, dispatches exactly one specialist, and assembles the
|
| 19 |
+
result;
|
| 20 |
+
3. the **specialist** computes the actual answer using a frozen backbone plus a small trained module;
|
| 21 |
+
4. an **evidence engine** aggregates, orders, deduplicates and bounds what the specialist produced;
|
| 22 |
+
5. a **confidence** stage attaches a calibrated number, or honestly declines to.
|
| 23 |
+
|
| 24 |
+
### 1.1 Why not one end-to-end model
|
| 25 |
+
|
| 26 |
+
| Constraint | Consequence |
|
| 27 |
+
|---|---|
|
| 28 |
+
| The system must run on **CPU** | End-to-end VLM inference at usable quality needs a GPU; small per-task modules do not. |
|
| 29 |
+
| Different tasks have **incompatible outputs** | `change` returns a spatial change map; `caption` returns prose; `optical_sar` returns a class distribution. One head cannot emit all three. |
|
| 30 |
+
| Tasks have **different data and metrics** | Each specialist is trained and evaluated on its own split with its own protocol. |
|
| 31 |
+
| **Truthfulness** | Per-task metrics are auditable. A single end-to-end number would hide which component failed. |
|
| 32 |
+
|
| 33 |
+
The cost of this design is that there is **no system-level accuracy number** — because there is no
|
| 34 |
+
single model to measure. That absence is stated rather than papered over.
|
| 35 |
+
|
| 36 |
+
## 2. The six capabilities
|
| 37 |
+
|
| 38 |
+
Declared in `configs/base.yaml` (`router.tasks`) and mirrored in the live
|
| 39 |
+
`GET /api/capabilities` response. All six report `available: true` in the deployed system.
|
| 40 |
+
|
| 41 |
+
| Task | Assets | Output | Backbone (frozen) | Trained module |
|
| 42 |
+
|---|---|---|---|---|
|
| 43 |
+
| `vqa` | 1 | short answer | SmolVLM-500M-Instruct | (unadapted; LoRA exists but is rejected) |
|
| 44 |
+
| `caption` | 1 | prose caption | SmolVLM-500M-Instruct | (unadapted) |
|
| 45 |
+
| `grounding` | 1 | boxes / regions | RemoteCLIP ViT-B/32 | grounding head |
|
| 46 |
+
| `change` | 2 (equal shape) | change map + regions | STANet-style (ResNet-18 + PAM) | change head |
|
| 47 |
+
| `change_vqa` | 2 | short answer | — (cached change features) | change-VQA head |
|
| 48 |
+
| `optical_sar` | 2 (GeoTIFF pair) | class distribution | CROMA-base | fusion head |
|
| 49 |
+
|
| 50 |
+
`Task` in `core/schemas.py` also carries `unsupported` — the router's explicit "this is not a
|
| 51 |
+
satellite-imagery question" class. The router's label space is therefore **6 classes**
|
| 52 |
+
(`router/label_space.py`: `vqa, caption, grounding, change, optical_sar, unsupported`), and
|
| 53 |
+
`change_vqa` is reached through the change family rather than being a separate router class.
|
| 54 |
+
|
| 55 |
+
> **Note the asymmetry.** `router/label_space.py` lists **six** task classes; the capabilities
|
| 56 |
+
> endpoint lists **six** tasks but a *different* six — `change_vqa` appears in capabilities and
|
| 57 |
+
> `unsupported` does not. This is intentional: `unsupported` is a routing outcome, not a servable
|
| 58 |
+
> capability. `core/schemas.py::Task` carries all seven values.
|
| 59 |
+
|
| 60 |
+
## 3. Component inventory
|
| 61 |
+
|
| 62 |
+
Every path below is real and is the authoritative location.
|
| 63 |
+
|
| 64 |
+
| Layer | Module | Responsibility |
|
| 65 |
+
|---|---|---|
|
| 66 |
+
| **Contracts** | `core/schemas.py` | the binding typed contract: `Task`, `Intent`, `Evidence`, `SpecialistResult`, `ResultEnvelope`, `ExecutionTrace`, … |
|
| 67 |
+
| **Config** | `core/config.py` | load, deep-merge, validate, hash the registry; `get_config()` singleton |
|
| 68 |
+
| **Errors** | `core/errors.py` | the error taxonomy (`ConfigError`, `ModelLoadError`, `WorkflowPlanError`, …) |
|
| 69 |
+
| **Planning** | `core/planner.py` | turn an `Intent` into a concrete workflow |
|
| 70 |
+
| **Registry** | `core/registry.py` | specialist registration / lookup |
|
| 71 |
+
| **Controller** | `core/controller.py` | the nine-state FSM; the only thing that dispatches |
|
| 72 |
+
| **Router** | `router/encoder.py` | frozen MiniLM embedding, cached |
|
| 73 |
+
| | `router/adapter.py` | the 5-head `IntentAdapter` (the only trainable router part) |
|
| 74 |
+
| | `router/classifier.py` | learned classification + confidence threshold |
|
| 75 |
+
| | `router/fallback.py` | deterministic lexical fallback (`lexical_route`) |
|
| 76 |
+
| | `router/label_space.py` | the ontology, single source of truth |
|
| 77 |
+
| | `router/dataset.py`, `router/train.py` | dataset generation and training |
|
| 78 |
+
| **Specialists** | `specialists/base.py` | the specialist interface |
|
| 79 |
+
| | `specialists/vqa/{model,inference,prompts}.py` | SmolVLM VQA |
|
| 80 |
+
| | `specialists/grounding/{remoteclip,head,inference,specialist}.py` | RemoteCLIP + head |
|
| 81 |
+
| | `specialists/change/{stanet,specialist,postprocess,vqa_specialist}.py` | change detection + change-VQA |
|
| 82 |
+
| | `specialists/optical_sar/{croma,fusion_head,inference,specialist,sensor_adapter,radiometry,prompts}.py` | CROMA fusion |
|
| 83 |
+
| **Evidence** | `evidence/engine.py` | aggregation: dedup → sort → renumber → cap |
|
| 84 |
+
| | `evidence/confidence.py` | temperature scaling, honest pass-through |
|
| 85 |
+
| **Inference app** | `app/space_app.py` | `build_space_app()`; the four-endpoint JSON contract |
|
| 86 |
+
| | `app/serving.py` | the composition root (`build_serving_controller()`) |
|
| 87 |
+
| | `app/deployment.py` | deployment helpers |
|
| 88 |
+
| **Gateway** | `gateway/` | the Render orchestrator |
|
| 89 |
+
| **Frontend** | `frontend/` | the static site |
|
| 90 |
+
|
| 91 |
+
## 4. The frozen-backbone strategy
|
| 92 |
+
|
| 93 |
+
Four backbones, all pinned by revision in `configs/base.yaml`, all fetched from the Hub on first use:
|
| 94 |
+
|
| 95 |
+
| Role | Repository | Revision | Why frozen |
|
| 96 |
+
|---|---|---|---|
|
| 97 |
+
| Router encoder | `sentence-transformers/all-MiniLM-L6-v2` | `1110a243fdf4` | embeddings are cached; the adapter trains on cached vectors in 0.28 s on CPU |
|
| 98 |
+
| VLM | `HuggingFaceTB/SmolVLM-500M-Instruct` | `a7da5b986cb5` | a 500M VLM cannot be fine-tuned end-to-end on CPU |
|
| 99 |
+
| Grounding | `chendelong/RemoteCLIP` | `bf1d8a3ccf2d` | provides the visual-language embedding space; only the head is trained |
|
| 100 |
+
| Optical-SAR | `antofuller/CROMA` | `0dd28e3d633b` | provides optical/SAR/joint embeddings; only the fusion head is trained |
|
| 101 |
+
|
| 102 |
+
Two consequences:
|
| 103 |
+
|
| 104 |
+
1. **The system is small.** The six trained artifacts total ~125 MiB. Everything else is public
|
| 105 |
+
weights.
|
| 106 |
+
2. **Backbones are not redistributed.** The release publishes only the six trained modules, each
|
| 107 |
+
with its backbone dependency documented.
|
| 108 |
+
|
| 109 |
+
## 5. The design vocabulary
|
| 110 |
+
|
| 111 |
+
`docs/ARCHITECTURE_FREEZE.md` section 5 assigns one verb per layer. This is not decoration — it
|
| 112 |
+
resolves real ambiguities about *where* a decision belongs:
|
| 113 |
+
|
| 114 |
+
| Layer | Verb | Consequence |
|
| 115 |
+
|---|---|---|
|
| 116 |
+
| Router | *understands* | its output (`Intent`) is **advisory only** — the controller decides |
|
| 117 |
+
| Policy engine / planner | *decides* | picks the workflow; may override the router |
|
| 118 |
+
| Specialists | *compute* | produce evidence; never decide routing |
|
| 119 |
+
| VLM | *explains* | produces prose; never produces a confidence number |
|
| 120 |
+
| Evidence engine | *proves* | aggregates; never re-derives a specialist's claim |
|
| 121 |
+
|
| 122 |
+
The `Intent` schema makes the first row explicit in code:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
class Intent(BaseModel):
|
| 126 |
+
"""Output of the learned router. Advisory only — the controller decides."""
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## 6. What is deliberately absent
|
| 130 |
+
|
| 131 |
+
| Absent | Status |
|
| 132 |
+
|---|---|
|
| 133 |
+
| Database / persistence | by design — the gateway is stateless |
|
| 134 |
+
| Authentication / users | by design |
|
| 135 |
+
| Job queue | by design — inference is synchronous |
|
| 136 |
+
| GPU requirement | by design — CPU-first, `.to(device)` everywhere |
|
| 137 |
+
| Gradio GUI | `app/space_app.py` serves JSON only |
|
| 138 |
+
| Chain-of-thought in traces | by design — observable facts only |
|
| 139 |
+
| System-level end-to-end benchmark | **NOT RUN — none exists** |
|
| 140 |
+
| Router test-split evaluation | **NOT RUN** |
|
| 141 |
+
|
| 142 |
+
## 7. Evidence for this document
|
| 143 |
+
|
| 144 |
+
| Claim | Source |
|
| 145 |
+
|---|---|
|
| 146 |
+
| six tasks, config-declared | `configs/base.yaml` §`router.tasks` |
|
| 147 |
+
| router label space is 6 classes | `router/label_space.py` §`TASK_CLASSES` |
|
| 148 |
+
| `Intent` is advisory | `core/schemas.py` §`Intent` docstring |
|
| 149 |
+
| layer verbs | `docs/ARCHITECTURE_FREEZE.md` §5 (quoted in `evidence/engine.py`) |
|
| 150 |
+
| frozen backbones + revisions | `configs/base.yaml` |
|
| 151 |
+
| six trained artifacts | `release/repo/models/manifest.json` (generated) |
|