samwell/galamsey-v9-e3 (which itself is a perception fine-tune of LiquidAI/LFM2.5-VL-450M). One weight set produces three outputs from three different prompts:{"action": ...} for the on-orbit policy decisionsamwell/galamsey-unified-v3: same base, same LoRA hyperparams, but a multitask training mixture that lets a single 450 M model serve all three jobs without losing v9-e3's perception ability or v3's action accuracy.| Prompt | Output |
|---|---|
| Action / policy prompt | One of discard, flag_for_review, request_higher_resolution, request_neighbor_tile, downlink_now |
| Grounding prompt | JSON list of bounding boxes: [{"label": ..., "bbox": [x1, y1, x2, y2]}, ...] (normalised 0-1) |
| Description prompt | Free-form scene description |
galamsey-v9-e3) to emit boxes + a scene description, then a 2.6 B text-only LFM2 policy read those plus the scalar context and picked the action. The text description between the two layers is a real bottleneck - visual cues that the perception VLM doesn't surface in prose can't reach the policy.galamsey-unified-v3 collapsed both jobs into a single 450 M model with one LoRA on top of v9-e3, with action-only target. That gave +11.1 pp over the strongest baseline at 6.8 x fewer parameters, but it had a wart: training on action-only target partially overwrote v9-e3's perception ability, so the on-ground analyst-review step still needed a separate v9-e3 forward pass.galamsey-unified-v4-1 is the same recipe with a multitask data mixture that re-introduces a small, regulariser-sized dose of perception examples during the LoRA fine-tune. Result: perception is preserved at v9-e3 quality, action accuracy slightly exceeds v3, one weight set covers everything.| System | Total params | 99-tile accuracy |
|---|---|---|
| Always-discard floor | - | 59.6 % |
| Two-layer (bare; perception + budget context only) | 3.05 B | 65.7 % |
| Two-layer (rich-context; + mission_priors + neighbor_summary) | 3.05 B | 63.6 % |
| Unified v2 (LoRA on bare base) | 450 M | 70.7 % |
| Unified v3 (action-only LoRA on v9-e3) | 450 M | 76.8 % |
| galamsey-unified-v4-1 (multitask LoRA on v9-e3) | 450 M | 77.8 % |
| Action | Bare two-layer | Rich-context two-layer | galamsey-unified-v4-1 |
|---|---|---|---|
discard (n = 59) | 1.00 | 0.78 | 0.86 |
flag_for_review (n = 18) | 0.11 | 0.56 | 0.89 |
downlink_now (n = 21) | 0.19 | 0.33 | 0.48 |
request_higher_resolution (n = 1) | 0.00 | 0.00 | 0.00 |
flag_for_review recall gap (0.89 vs 0.11) is the cleanest single-class evidence of the architectural advantage.galamsey-v9-e3)| Metric | galamsey-v9-e3 (specialist) | galamsey-unified-v4-1 (multitask) |
|---|---|---|
| Grounding mean IoU | 0.337 | 0.334 |
| Box-count match rate | 25 % | 30 % |
| Description BLEU | 34.13 | 33.18 |
request_higher_resolution and request_neighbor_tile sit at 0 % recall. Only 2 hires examples in the train set (oversampled to 80 by repetition, which the model memorised rather than generalised); zero neighbor examples. These two actions need deliberate hand-construction.downlink_now recall is 0.48 (down from v3's 0.62). The multitask mixture makes the model slightly more cautious about emitting downlink_now because it now also sees perception examples that disambiguate "bright soil != necessarily mining". On a high-bandwidth pass this trades favourably (fewer wasted downlinks); on a low-bandwidth pass it could miss real signals. For deployments that prioritise downlink recall over per-pass economy, prefer v3.trl integration gaps; not in this checkpoint.samwell/galamsey-v9-e3 (full fine-tune of LFM2.5-VL-450M on SmallMinesDS for galamsey perception, 4 x D4 augmentation).q_proj, k_proj, v_proj, out_proj, in_proj, plus vision-tower fc1/fc2 and multimodal projector.2e-5, warmup 5 %, 15 epochs, batch 4 x grad-accum 2, bf16. Training ran ~13 minutes on Modal H100.training/configs/galamsey_unified_v4_1_multitask_modal.yaml and training/scripts/build_unified_v4_1_multitask_dataset_modal.py.1from transformers import AutoModelForImageTextToText, AutoProcessor
2from PIL import Image
3import json
4
5model = AutoModelForImageTextToText.from_pretrained(
6 "samwell/galamsey-unified-v4-1",
7 torch_dtype="bfloat16",
8 trust_remote_code=True,
9).cuda().eval()
10processor = AutoProcessor.from_pretrained(
11 "samwell/galamsey-unified-v4-1", trust_remote_code=True,
12)
13
14rgb = Image.open("rgb.png").convert("RGB")
15swir = Image.open("swir.png").convert("RGB")
16
17def run(messages, max_new_tokens=128):
18 inputs = processor.apply_chat_template(
19 [messages], tokenize=True, return_dict=True, return_tensors="pt",
20 add_generation_prompt=True,
21 )
22 inputs = {k: v.cuda() for k, v in inputs.items() if v is not None}
23 out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
24 return processor.tokenizer.decode(
25 out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True,
26 ).strip()
27
28# 1) Action prompt (the on-orbit decision)
29system_prompt = "You are an on-orbit Earth-observation policy adjudicator. ..." # see repo
30user_text = "Tile u0001 at lon=-2.75, lat=5.64. Cloud cover: 0.001. Pass budget: 320 of 512 KB remaining. ..."
31action_msgs = [
32 {"role": "system", "content": [{"type": "text", "text": system_prompt}]},
33 {"role": "user", "content": [
34 {"type": "image", "image": rgb},
35 {"type": "image", "image": swir},
36 {"type": "text", "text": user_text},
37 ]},
38]
39action_text = run(action_msgs, max_new_tokens=32)
40action = json.loads(action_text[action_text.find("{"):action_text.rfind("}")+1])["action"]
41print("ACTION:", action)
42
43# 2) Grounding prompt (boxes for analyst review)
44grounding_prompt = (
45 "You are viewing two images of the same Sentinel-2 patch: a natural-color RGB "
46 "composite and a SWIR false-color composite. Detect any illegal small-scale gold "
47 'mining pits. Provide result as a valid JSON: [{"label": str, "bbox": [x1,y1,x2,y2]}, ...]. '
48 "Coordinates must be normalized to 0-1. If no pits are visible, return []."
49)
50grounding_msgs = [{"role": "user", "content": [
51 {"type": "image", "image": rgb},
52 {"type": "image", "image": swir},
53 {"type": "text", "text": grounding_prompt},
54]}]
55print("BOXES:", run(grounding_msgs, max_new_tokens=512))
56
57# 3) Description prompt (free-form scene summary)
58description_prompt = (
59 "You are analyzing two views of the same Sentinel-2 patch of southwestern Ghana. "
60 "Describe any signs of illegal small-scale gold mining (galamsey) activity: "
61 "exposed soil, excavation pits, sediment plumes, vegetation loss, proximity to water. "
62 "If no mining is visible, say so."
63)
64desc_msgs = [{"role": "user", "content": [
65 {"type": "image", "image": rgb},
66 {"type": "image", "image": swir},
67 {"type": "text", "text": description_prompt},
68]}]
69print("DESCRIPTION:", run(desc_msgs, max_new_tokens=128))samwell/galamsey-unified-v3 - same base, same LoRA, action-only target. Slightly higher downlink_now recall, no perception preserved.samwell/galamsey-v9-e3 - the perception fine-tune this model stacks on.samwell/galamsey-v9-e3-onnxsamwell/galamsey-unified-decisions - 250 hand-labeled Sentinel-2 tiles + scalar context + 5-action targetssamadon1/GalamseyWatch1@misc{galamseywatch2026,
2 author = {Donkor, Samuel},
3 title = {GalamseyWatch: agentic Earth observation for galamsey detection},
4 year = {2026},
5 publisher = {GitHub},
6 url = {https://github.com/samadon1/GalamseyWatch}
7}