Spaces:
Sleeping
Sleeping
Jovan Bjegovic commited on
Commit Β·
6e32234
0
Parent(s):
Move region indexes to HF dataset (geobot-indexes); load on demand via hf_hub_download
Browse files- .gitattributes +35 -0
- Dockerfile +20 -0
- README.md +26 -0
- app.py +229 -0
- atlas.py +127 -0
- data/atlas_head.npz +3 -0
- data/centroids.json +818 -0
- data/country_scripts.json +1 -0
- data/index_fast.npy +3 -0
- data/index_geo.npy +3 -0
- data/index_pro.npy +3 -0
- data/meta_fast.json +0 -0
- data/meta_geo.json +0 -0
- data/meta_pro.json +0 -0
- data/priors.json +1 -0
- data/script_names.json +1 -0
- data/script_text.npy +3 -0
- data/text_geo.npy +3 -0
- data/text_geo_countries.json +1 -0
- guess.py +207 -0
- region.py +56 -0
- requirements.txt +8 -0
- shared/__init__.py +0 -0
- shared/embedder.py +33 -0
- shared/version.py +160 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
ENV HF_HOME=/app/hf-cache \
|
| 4 |
+
PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu \
|
| 8 |
+
&& pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
|
| 10 |
+
# Bake BOTH switchable CLIP models into the image (Space disk is non-persistent at runtime).
|
| 11 |
+
RUN python - <<'PY'
|
| 12 |
+
from transformers import CLIPVisionModelWithProjection, CLIPImageProcessor
|
| 13 |
+
for mid in ("openai/clip-vit-base-patch32", "openai/clip-vit-large-patch14", "geolocal/StreetCLIP"):
|
| 14 |
+
CLIPVisionModelWithProjection.from_pretrained(mid)
|
| 15 |
+
CLIPImageProcessor.from_pretrained(mid)
|
| 16 |
+
PY
|
| 17 |
+
|
| 18 |
+
COPY . .
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: GeoBot
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# GeoBot
|
| 12 |
+
|
| 13 |
+
A tiny, fully self-hosted AI opponent for a GeoGuessr-style game. It receives two
|
| 14 |
+
Street View images (headings ~0Β° and ~180Β°) and returns a country guess with a
|
| 15 |
+
coordinate and human-readable reasoning.
|
| 16 |
+
|
| 17 |
+
It does **no training and no external AI calls**. It embeds the two images with
|
| 18 |
+
CLIP (`openai/clip-vit-base-patch32`, vision tower only, CPU) and retrieves the
|
| 19 |
+
nearest examples from a precomputed index of visual geography clues, votes on a
|
| 20 |
+
country, and fills reasoning templates from the matched clue labels.
|
| 21 |
+
|
| 22 |
+
- `POST /guess` β multipart `image0`, `image180`; header `X-API-Key`.
|
| 23 |
+
- `GET /health` β liveness + index size (also the wake-up ping target).
|
| 24 |
+
|
| 25 |
+
Geographic clue knowledge derived from the community guides at
|
| 26 |
+
[plonkit.net](https://plonkit.net) (embeddings only; no images redistributed).
|
app.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 2 β FastAPI inference service for the HF Docker Space (port 7860).
|
| 2 |
+
|
| 3 |
+
Loads ALL switchable CLIP models + their indexes once at startup, asserts each
|
| 4 |
+
index matches its model, then serves POST /guess (multipart, X-API-Key) and
|
| 5 |
+
GET /health (no auth). Pick a model per request with ?model=fast|pro.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import io
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import time
|
| 13 |
+
from collections import Counter
|
| 14 |
+
from contextlib import asynccontextmanager
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
from fastapi import FastAPI, File, Header, HTTPException, Query, UploadFile
|
| 19 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 20 |
+
from PIL import Image
|
| 21 |
+
|
| 22 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent / "shared"))
|
| 23 |
+
from version import MODELS, DEFAULT_MODEL # noqa: E402
|
| 24 |
+
from embedder import Embedder # noqa: E402
|
| 25 |
+
import guess as guesslib # noqa: E402
|
| 26 |
+
import atlas as atlaslib # noqa: E402
|
| 27 |
+
import region as regionlib # noqa: E402
|
| 28 |
+
|
| 29 |
+
DATA = Path(__file__).resolve().parent / "data"
|
| 30 |
+
API_KEY = os.environ.get("API_KEY", "")
|
| 31 |
+
ALLOWED_ORIGIN = os.environ.get("ALLOWED_ORIGIN", "*")
|
| 32 |
+
MAX_BYTES = 4 * 1024 * 1024
|
| 33 |
+
RATE_LIMIT_PER_MIN = 30
|
| 34 |
+
|
| 35 |
+
# Region kNN indexes live in a free HF Dataset repo (the 1GB Space can't hold them all).
|
| 36 |
+
# Resolve a region file from local data/ if present, else download from the dataset (cached).
|
| 37 |
+
INDEX_DATASET = os.environ.get("INDEX_DATASET", "GeoguessrAngular/geobot-indexes")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def region_file_path(fname):
|
| 41 |
+
local = DATA / fname
|
| 42 |
+
if local.exists():
|
| 43 |
+
return local
|
| 44 |
+
from huggingface_hub import hf_hub_download
|
| 45 |
+
return Path(hf_hub_download(repo_id=INDEX_DATASET, filename=fname, repo_type="dataset"))
|
| 46 |
+
|
| 47 |
+
Image.MAX_IMAGE_PIXELS = 50_000_000
|
| 48 |
+
|
| 49 |
+
STATE = {}
|
| 50 |
+
_req_times = [] # global in-memory rate limiter timestamps
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@asynccontextmanager
|
| 54 |
+
async def lifespan(app: FastAPI):
|
| 55 |
+
centroids = json.loads((DATA / "centroids.json").read_text(encoding="utf-8"))
|
| 56 |
+
priors_path = DATA / "priors.json"
|
| 57 |
+
priors = json.loads(priors_path.read_text(encoding="utf-8")) if priors_path.exists() else None
|
| 58 |
+
|
| 59 |
+
models = {}
|
| 60 |
+
embedder_cache = {} # model_id -> Embedder (heavy weights loaded once)
|
| 61 |
+
index_cache = {} # index_file -> (index, rows, count, model_id, index_version)
|
| 62 |
+
for key, m in MODELS.items():
|
| 63 |
+
if m["model_id"] not in embedder_cache:
|
| 64 |
+
embedder_cache[m["model_id"]] = Embedder(m["model_id"])
|
| 65 |
+
|
| 66 |
+
# --- region kNN locator (e.g. Serbia) ---
|
| 67 |
+
if m.get("region_file"):
|
| 68 |
+
ref_emb, ref_lat, ref_lng = regionlib.load_region(region_file_path(m["region_file"]))
|
| 69 |
+
models[key] = {
|
| 70 |
+
"type": "region", "embedder": embedder_cache[m["model_id"]],
|
| 71 |
+
"ref_emb": ref_emb, "ref_lat": ref_lat, "ref_lng": ref_lng,
|
| 72 |
+
"country_slug": m.get("country_slug", key), "country_name": m.get("country_name", key),
|
| 73 |
+
"model_id": m["model_id"], "label": m.get("label", key),
|
| 74 |
+
}
|
| 75 |
+
print(f"Loaded model '{key}': region kNN {ref_emb.shape}, {m['model_id']}")
|
| 76 |
+
continue
|
| 77 |
+
|
| 78 |
+
# --- learned classifier head (Atlas) ---
|
| 79 |
+
if m.get("head_file"):
|
| 80 |
+
W, b, classes = atlaslib.load_head(DATA / m["head_file"])
|
| 81 |
+
models[key] = {
|
| 82 |
+
"type": "head", "embedder": embedder_cache[m["model_id"]],
|
| 83 |
+
"W": W, "b": b, "classes": classes,
|
| 84 |
+
"model_id": m["model_id"], "label": m.get("label", key),
|
| 85 |
+
}
|
| 86 |
+
print(f"Loaded model '{key}': head W{W.shape} {len(classes)} classes, {m['model_id']}")
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
# --- retrieval (index) model ---
|
| 90 |
+
if m["index_file"] not in index_cache:
|
| 91 |
+
meta = json.loads((DATA / m["meta_file"]).read_text(encoding="utf-8"))
|
| 92 |
+
index = np.load(DATA / m["index_file"]).astype(np.float32)
|
| 93 |
+
rows = meta["rows"]
|
| 94 |
+
assert index.shape[0] == len(rows), f"[{key}] index/meta row mismatch"
|
| 95 |
+
index_cache[m["index_file"]] = (
|
| 96 |
+
index, rows, Counter(r["country"] for r in rows),
|
| 97 |
+
meta["model_id"], meta["index_version"])
|
| 98 |
+
index, rows, count, meta_mid, meta_iv = index_cache[m["index_file"]]
|
| 99 |
+
if meta_mid != m["model_id"] or meta_iv != m["index_version"]:
|
| 100 |
+
raise RuntimeError(
|
| 101 |
+
f"[{key}] index/version mismatch: meta has {meta_mid}/{meta_iv}, "
|
| 102 |
+
f"expected {m['model_id']}/{m['index_version']}")
|
| 103 |
+
|
| 104 |
+
text_vecs, text_countries = None, None
|
| 105 |
+
if m.get("text_file"):
|
| 106 |
+
text_vecs = np.load(DATA / m["text_file"]).astype(np.float32)
|
| 107 |
+
text_countries = json.loads((DATA / m["text_countries_file"]).read_text(encoding="utf-8"))
|
| 108 |
+
|
| 109 |
+
models[key] = {
|
| 110 |
+
"type": "retrieval", "embedder": embedder_cache[m["model_id"]],
|
| 111 |
+
"index": index, "rows": rows, "count": count,
|
| 112 |
+
"model_id": m["model_id"], "index_version": m["index_version"],
|
| 113 |
+
"label": m.get("label", key),
|
| 114 |
+
"text_vecs": text_vecs, "text_countries": text_countries,
|
| 115 |
+
}
|
| 116 |
+
print(f"Loaded model '{key}': index {index.shape}, {m['model_id']} {m['index_version']}"
|
| 117 |
+
f"{', +zeroshot-text' if text_vecs is not None else ''}")
|
| 118 |
+
|
| 119 |
+
# Optional script (writing-system) branch for the Atlas head.
|
| 120 |
+
script_vecs = script_names = country_scripts = None
|
| 121 |
+
if (DATA / "script_text.npy").exists() and (DATA / "country_scripts.json").exists():
|
| 122 |
+
script_vecs = np.load(DATA / "script_text.npy").astype(np.float32)
|
| 123 |
+
script_names = json.loads((DATA / "script_names.json").read_text(encoding="utf-8"))
|
| 124 |
+
country_scripts = json.loads((DATA / "country_scripts.json").read_text(encoding="utf-8"))
|
| 125 |
+
print(f"Loaded script branch: {len(script_names)} scripts, {len(country_scripts)} country maps")
|
| 126 |
+
|
| 127 |
+
STATE["models"] = models
|
| 128 |
+
STATE["centroids"] = centroids
|
| 129 |
+
STATE["priors"] = priors
|
| 130 |
+
STATE["script_vecs"] = script_vecs
|
| 131 |
+
STATE["script_names"] = script_names
|
| 132 |
+
STATE["country_scripts"] = country_scripts
|
| 133 |
+
STATE["cfg"] = guesslib.Config()
|
| 134 |
+
print(f"Ready. models={list(models)} default={DEFAULT_MODEL} "
|
| 135 |
+
f"centroids={len(centroids)} priors={len(priors) if priors else 0}")
|
| 136 |
+
yield
|
| 137 |
+
STATE.clear()
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
app = FastAPI(title="GeoBot", lifespan=lifespan)
|
| 141 |
+
app.add_middleware(CORSMiddleware, allow_origins=[ALLOWED_ORIGIN] if ALLOWED_ORIGIN != "*" else ["*"],
|
| 142 |
+
allow_methods=["*"], allow_headers=["*"])
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _check_key(x_api_key):
|
| 146 |
+
if not API_KEY:
|
| 147 |
+
return # unset key disables auth (local dev)
|
| 148 |
+
if x_api_key != API_KEY:
|
| 149 |
+
raise HTTPException(status_code=401, detail="bad or missing API key")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _rate_limit():
|
| 153 |
+
now = time.time()
|
| 154 |
+
cutoff = now - 60
|
| 155 |
+
while _req_times and _req_times[0] < cutoff:
|
| 156 |
+
_req_times.pop(0)
|
| 157 |
+
if len(_req_times) >= RATE_LIMIT_PER_MIN:
|
| 158 |
+
raise HTTPException(status_code=429, detail="rate limited")
|
| 159 |
+
_req_times.append(now)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
async def _read_image(upload: UploadFile):
|
| 163 |
+
raw = await upload.read()
|
| 164 |
+
if len(raw) > MAX_BYTES:
|
| 165 |
+
raise HTTPException(status_code=413, detail="image too large (>4 MB)")
|
| 166 |
+
try:
|
| 167 |
+
return Image.open(io.BytesIO(raw)).convert("RGB")
|
| 168 |
+
except Exception:
|
| 169 |
+
raise HTTPException(status_code=400, detail="undecodable image")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@app.get("/health")
|
| 173 |
+
def health():
|
| 174 |
+
models = STATE.get("models", {})
|
| 175 |
+
return {
|
| 176 |
+
"status": "ok",
|
| 177 |
+
"default_model": DEFAULT_MODEL,
|
| 178 |
+
"models": {
|
| 179 |
+
k: {"type": v.get("type"), "model_id": v["model_id"], "label": v["label"],
|
| 180 |
+
**({"index_size": int(v["index"].shape[0]), "index_version": v["index_version"]}
|
| 181 |
+
if v.get("type") == "retrieval" else
|
| 182 |
+
{"refs": int(v["ref_emb"].shape[0])} if v.get("type") == "region" else
|
| 183 |
+
{"classes": len(v["classes"])})}
|
| 184 |
+
for k, v in models.items()
|
| 185 |
+
},
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@app.post("/guess")
|
| 190 |
+
async def do_guess(images: list[UploadFile] = File(None),
|
| 191 |
+
image0: UploadFile = File(None), image180: UploadFile = File(None),
|
| 192 |
+
skill: float = Query(1.0, ge=0.0, le=1.0),
|
| 193 |
+
model: str = Query(DEFAULT_MODEL),
|
| 194 |
+
x_api_key: str = Header(None, alias="X-API-Key")):
|
| 195 |
+
_check_key(x_api_key)
|
| 196 |
+
_rate_limit()
|
| 197 |
+
# Accept either N frames under repeated field "images", or legacy image0/image180.
|
| 198 |
+
uploads = [u for u in (images or []) if u is not None]
|
| 199 |
+
if not uploads:
|
| 200 |
+
uploads = [u for u in (image0, image180) if u is not None]
|
| 201 |
+
if not uploads:
|
| 202 |
+
raise HTTPException(status_code=400, detail="no images")
|
| 203 |
+
key = model if model in STATE["models"] else DEFAULT_MODEL
|
| 204 |
+
M = STATE["models"][key]
|
| 205 |
+
t0 = time.time()
|
| 206 |
+
pil = [await _read_image(u) for u in uploads]
|
| 207 |
+
emb = M["embedder"].embed(pil)
|
| 208 |
+
try:
|
| 209 |
+
if M.get("type") == "region":
|
| 210 |
+
result = regionlib.predict(list(emb), M["ref_emb"], M["ref_lat"], M["ref_lng"],
|
| 211 |
+
M["country_slug"], M["country_name"])
|
| 212 |
+
elif M.get("type") == "head":
|
| 213 |
+
result = atlaslib.predict(list(emb), M["W"], M["b"], M["classes"],
|
| 214 |
+
STATE["centroids"], STATE["cfg"], STATE["priors"],
|
| 215 |
+
STATE["script_vecs"], STATE["script_names"],
|
| 216 |
+
STATE["country_scripts"])
|
| 217 |
+
else:
|
| 218 |
+
result = guesslib.guess(list(emb), M["index"], M["rows"],
|
| 219 |
+
STATE["centroids"], M["count"], STATE["cfg"],
|
| 220 |
+
STATE["priors"], M["text_vecs"], M["text_countries"])
|
| 221 |
+
except Exception as e:
|
| 222 |
+
raise HTTPException(status_code=500, detail=f"guess failed: {type(e).__name__}")
|
| 223 |
+
result["timing_ms"] = int((time.time() - t0) * 1000)
|
| 224 |
+
result["model"] = key
|
| 225 |
+
result["model_id"] = M["model_id"]
|
| 226 |
+
result["index_version"] = M.get("index_version")
|
| 227 |
+
print(f"[{key}] guess winner={result['country']} conf={result['confidence']} "
|
| 228 |
+
f"ms={result['timing_ms']}")
|
| 229 |
+
return result
|
atlas.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Atlas β the LEARNED model's inference. A linear classifier head on top of the
|
| 2 |
+
frozen StreetCLIP embedding: probs = softmax(emb @ W + b) over playable countries,
|
| 3 |
+
averaged across the round's frames, then weighted by the game-location prior.
|
| 4 |
+
|
| 5 |
+
Unlike guess.py (nearest-neighbour retrieval), this is a trained model: it learned
|
| 6 |
+
what distinguishes each country from all PlonkIt + geohints images.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
ASSERTIVE = [
|
| 12 |
+
"I've learned to read scenes like this as {country} β the overall mix of road, signage and surroundings fits.",
|
| 13 |
+
"This reads clearly as {country} to me, weighing everything in view.",
|
| 14 |
+
"Confident on {country}: the combination of cues I trained on lines up.",
|
| 15 |
+
"My read is {country} β the whole scene matches what I know for it.",
|
| 16 |
+
]
|
| 17 |
+
HEDGED = [
|
| 18 |
+
"Looks most like {country}, though {runner} crossed my mind. Going with {country}.",
|
| 19 |
+
"Leaning {country} here; {runner} was the next best. Committing to {country}.",
|
| 20 |
+
"Probably {country} β {runner} was close, but I'll take {country}.",
|
| 21 |
+
"My best read is {country}, with {runner} as a maybe.",
|
| 22 |
+
]
|
| 23 |
+
UNCERTAIN = [
|
| 24 |
+
"Tough scene β nothing jumps out, but it leans {country}, so that's my guess.",
|
| 25 |
+
"Low confidence, but the overall feel points to {country}.",
|
| 26 |
+
"Hard to read; I'll take {country} as the most likely.",
|
| 27 |
+
"Not sure, but {country} fits best, so guessing there.",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_head(path):
|
| 32 |
+
z = np.load(path, allow_pickle=True)
|
| 33 |
+
return (z["W"].astype(np.float32), z["b"].astype(np.float32),
|
| 34 |
+
[str(c) for c in z["classes"]])
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _pick(variants, seed_text):
|
| 38 |
+
return variants[abs(hash(seed_text)) % len(variants)]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
SCRIPT_PENALTY = 0.15 # multiply prob of countries that don't use the detected (non-Latin) script
|
| 42 |
+
# Only these distinctive scripts are reliable zero-shot; sinhala/lao/bengali/etc. are
|
| 43 |
+
# noisy attractors that misfire on text-less scenes, so they're excluded from detection.
|
| 44 |
+
RELIABLE_SCRIPTS = {"thai", "cyrillic", "greek", "cjk", "devanagari", "arabic", "hebrew"}
|
| 45 |
+
SCRIPT_MARGIN = 0.015 # the detected script must beat Latin by at least this
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _detect_script(embs, script_vecs, script_names):
|
| 49 |
+
"""Return the detected writing system, or None (Latin / text-less / uncertain).
|
| 50 |
+
Fires only when a RELIABLE non-Latin script clearly beats Latin."""
|
| 51 |
+
if "latin" not in script_names:
|
| 52 |
+
return None
|
| 53 |
+
sims = np.zeros(len(script_names))
|
| 54 |
+
for e in embs:
|
| 55 |
+
sims += script_vecs @ e
|
| 56 |
+
sims /= len(embs)
|
| 57 |
+
latin = sims[script_names.index("latin")]
|
| 58 |
+
best, best_s = None, -1e9
|
| 59 |
+
for i, name in enumerate(script_names):
|
| 60 |
+
if name in RELIABLE_SCRIPTS and sims[i] > best_s:
|
| 61 |
+
best, best_s = name, sims[i]
|
| 62 |
+
return best if best is not None and (best_s - latin) >= SCRIPT_MARGIN else None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def predict(embs, W, b, classes, centroids, cfg, priors=None,
|
| 66 |
+
script_vecs=None, script_names=None, country_scripts=None):
|
| 67 |
+
"""embs: list of (768,) StreetCLIP embeddings. Returns the response dict."""
|
| 68 |
+
if not isinstance(embs, (list, tuple)):
|
| 69 |
+
embs = [embs]
|
| 70 |
+
probs = np.zeros(len(classes), dtype=np.float64)
|
| 71 |
+
for e in embs:
|
| 72 |
+
logits = e @ W + b
|
| 73 |
+
logits = logits - logits.max()
|
| 74 |
+
ex = np.exp(logits)
|
| 75 |
+
probs += ex / ex.sum()
|
| 76 |
+
probs /= len(embs)
|
| 77 |
+
|
| 78 |
+
# weight by the game-location prior (and hard-filter 0-location countries)
|
| 79 |
+
if priors is not None:
|
| 80 |
+
for i, c in enumerate(classes):
|
| 81 |
+
av = priors.get(c, 0)
|
| 82 |
+
probs[i] = 0.0 if av <= 0 else probs[i] * (av ** cfg.PRIOR_BETA)
|
| 83 |
+
s = probs.sum()
|
| 84 |
+
if s > 0:
|
| 85 |
+
probs /= s
|
| 86 |
+
|
| 87 |
+
# script branch: if a distinctive (non-Latin) writing system is detected,
|
| 88 |
+
# down-weight countries that don't use it (Thai β only Thailand, etc.)
|
| 89 |
+
detected = None
|
| 90 |
+
if script_vecs is not None and country_scripts is not None:
|
| 91 |
+
detected = _detect_script(embs, script_vecs, script_names)
|
| 92 |
+
if detected:
|
| 93 |
+
for i, c in enumerate(classes):
|
| 94 |
+
if detected not in country_scripts.get(c, ["latin"]):
|
| 95 |
+
probs[i] *= SCRIPT_PENALTY
|
| 96 |
+
s = probs.sum()
|
| 97 |
+
if s > 0:
|
| 98 |
+
probs /= s
|
| 99 |
+
|
| 100 |
+
order = np.argsort(-probs)
|
| 101 |
+
winner = classes[order[0]]
|
| 102 |
+
confidence = float(probs[order[0]])
|
| 103 |
+
runner = classes[order[1]] if len(order) > 1 else None
|
| 104 |
+
cen = centroids[winner]
|
| 105 |
+
runner_name = centroids.get(runner, {}).get("name", runner) if runner else None
|
| 106 |
+
|
| 107 |
+
seed = winner + f"{confidence:.2f}"
|
| 108 |
+
if confidence >= 0.55:
|
| 109 |
+
reasoning = _pick(ASSERTIVE, seed).format(country=cen["name"])
|
| 110 |
+
elif confidence >= 0.30:
|
| 111 |
+
reasoning = _pick(HEDGED, seed).format(country=cen["name"], runner=runner_name or "a neighbour")
|
| 112 |
+
else:
|
| 113 |
+
reasoning = _pick(UNCERTAIN, seed).format(country=cen["name"])
|
| 114 |
+
if detected:
|
| 115 |
+
reasoning += f" (I can see {detected} script, which points here.)"
|
| 116 |
+
|
| 117 |
+
return {
|
| 118 |
+
"lat": cen["lat"], "lon": cen["lon"],
|
| 119 |
+
"country": winner, "country_name": cen["name"],
|
| 120 |
+
"confidence": round(confidence, 3),
|
| 121 |
+
"runner_up": runner,
|
| 122 |
+
"runner_up_name": runner_name,
|
| 123 |
+
"runner_up_conf": round(float(probs[order[1]]), 3) if len(order) > 1 else None,
|
| 124 |
+
"reasoning": reasoning,
|
| 125 |
+
"clues": [],
|
| 126 |
+
"script": detected,
|
| 127 |
+
}
|
data/atlas_head.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8bf5bc3985fee140b7a5b323434e5d27f66c325619004b89afdd85a87bdd5ede
|
| 3 |
+
size 352815
|
data/centroids.json
ADDED
|
@@ -0,0 +1,818 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"alaska": {
|
| 3 |
+
"lat": 64.2,
|
| 4 |
+
"lon": -149.49,
|
| 5 |
+
"name": "Alaska",
|
| 6 |
+
"src": "hardcoded"
|
| 7 |
+
},
|
| 8 |
+
"albania": {
|
| 9 |
+
"lat": 41.14564,
|
| 10 |
+
"lon": 20.00649,
|
| 11 |
+
"name": "Albania",
|
| 12 |
+
"src": "Albania"
|
| 13 |
+
},
|
| 14 |
+
"american-samoa": {
|
| 15 |
+
"lat": -14.31113,
|
| 16 |
+
"lon": -170.75345,
|
| 17 |
+
"name": "American Samoa",
|
| 18 |
+
"src": "American Samoa"
|
| 19 |
+
},
|
| 20 |
+
"andorra": {
|
| 21 |
+
"lat": 42.53604,
|
| 22 |
+
"lon": 1.56174,
|
| 23 |
+
"name": "Andorra",
|
| 24 |
+
"src": "Andorra"
|
| 25 |
+
},
|
| 26 |
+
"antarctica": {
|
| 27 |
+
"lat": -76.60565,
|
| 28 |
+
"lon": 66.27535,
|
| 29 |
+
"name": "Antarctica",
|
| 30 |
+
"src": "Antarctica"
|
| 31 |
+
},
|
| 32 |
+
"argentina": {
|
| 33 |
+
"lat": -37.0905,
|
| 34 |
+
"lon": -63.96988,
|
| 35 |
+
"name": "Argentina",
|
| 36 |
+
"src": "Argentina"
|
| 37 |
+
},
|
| 38 |
+
"australia": {
|
| 39 |
+
"lat": -24.92291,
|
| 40 |
+
"lon": 133.08113,
|
| 41 |
+
"name": "Australia",
|
| 42 |
+
"src": "Australia"
|
| 43 |
+
},
|
| 44 |
+
"austria": {
|
| 45 |
+
"lat": 47.69458,
|
| 46 |
+
"lon": 14.7636,
|
| 47 |
+
"name": "Austria",
|
| 48 |
+
"src": "Austria"
|
| 49 |
+
},
|
| 50 |
+
"azores": {
|
| 51 |
+
"lat": 37.80081,
|
| 52 |
+
"lon": -25.4669,
|
| 53 |
+
"name": "Azores",
|
| 54 |
+
"src": "Azores"
|
| 55 |
+
},
|
| 56 |
+
"bangladesh": {
|
| 57 |
+
"lat": 23.67772,
|
| 58 |
+
"lon": 89.85934,
|
| 59 |
+
"name": "Bangladesh",
|
| 60 |
+
"src": "Bangladesh"
|
| 61 |
+
},
|
| 62 |
+
"belarus": {
|
| 63 |
+
"lat": 53.69944,
|
| 64 |
+
"lon": 28.01873,
|
| 65 |
+
"name": "Belarus",
|
| 66 |
+
"src": "Belarus"
|
| 67 |
+
},
|
| 68 |
+
"belgium": {
|
| 69 |
+
"lat": 51.09263,
|
| 70 |
+
"lon": 4.16957,
|
| 71 |
+
"name": "Belgium",
|
| 72 |
+
"src": "Flemish"
|
| 73 |
+
},
|
| 74 |
+
"bermuda": {
|
| 75 |
+
"lat": 32.3192,
|
| 76 |
+
"lon": -64.72802,
|
| 77 |
+
"name": "Bermuda",
|
| 78 |
+
"src": "Bermuda"
|
| 79 |
+
},
|
| 80 |
+
"bhutan": {
|
| 81 |
+
"lat": 27.52397,
|
| 82 |
+
"lon": 90.29573,
|
| 83 |
+
"name": "Bhutan",
|
| 84 |
+
"src": "Bhutan"
|
| 85 |
+
},
|
| 86 |
+
"bolivia": {
|
| 87 |
+
"lat": -16.28784,
|
| 88 |
+
"lon": -64.28579,
|
| 89 |
+
"name": "Bolivia",
|
| 90 |
+
"src": "Bolivia"
|
| 91 |
+
},
|
| 92 |
+
"botswana": {
|
| 93 |
+
"lat": -22.3453,
|
| 94 |
+
"lon": 24.47144,
|
| 95 |
+
"name": "Botswana",
|
| 96 |
+
"src": "Botswana"
|
| 97 |
+
},
|
| 98 |
+
"brazil": {
|
| 99 |
+
"lat": -14.23886,
|
| 100 |
+
"lon": -49.72801,
|
| 101 |
+
"name": "Brazil",
|
| 102 |
+
"src": "Brazil"
|
| 103 |
+
},
|
| 104 |
+
"british-indian-ocean-territory": {
|
| 105 |
+
"lat": -6.19232,
|
| 106 |
+
"lon": 71.34757,
|
| 107 |
+
"name": "British Indian Ocean Territory",
|
| 108 |
+
"src": "Br. Indian Ocean Ter."
|
| 109 |
+
},
|
| 110 |
+
"bulgaria": {
|
| 111 |
+
"lat": 42.73222,
|
| 112 |
+
"lon": 25.18968,
|
| 113 |
+
"name": "Bulgaria",
|
| 114 |
+
"src": "Bulgaria"
|
| 115 |
+
},
|
| 116 |
+
"cambodia": {
|
| 117 |
+
"lat": 12.55877,
|
| 118 |
+
"lon": 105.10263,
|
| 119 |
+
"name": "Cambodia",
|
| 120 |
+
"src": "Cambodia"
|
| 121 |
+
},
|
| 122 |
+
"canada": {
|
| 123 |
+
"lat": 56.83692,
|
| 124 |
+
"lon": -110.43087,
|
| 125 |
+
"name": "Canada",
|
| 126 |
+
"src": "Canada"
|
| 127 |
+
},
|
| 128 |
+
"chile": {
|
| 129 |
+
"lat": -35.71034,
|
| 130 |
+
"lon": -71.4964,
|
| 131 |
+
"name": "Chile",
|
| 132 |
+
"src": "Chile"
|
| 133 |
+
},
|
| 134 |
+
"china": {
|
| 135 |
+
"lat": 36.90367,
|
| 136 |
+
"lon": 98.60392,
|
| 137 |
+
"name": "China",
|
| 138 |
+
"src": "China"
|
| 139 |
+
},
|
| 140 |
+
"christmas-island": {
|
| 141 |
+
"lat": -10.49635,
|
| 142 |
+
"lon": 105.64974,
|
| 143 |
+
"name": "Christmas Island",
|
| 144 |
+
"src": "Christmas I."
|
| 145 |
+
},
|
| 146 |
+
"cocos-islands": {
|
| 147 |
+
"lat": -12.17779,
|
| 148 |
+
"lon": 96.91627,
|
| 149 |
+
"name": "Cocos Islands",
|
| 150 |
+
"src": "Cocos Is."
|
| 151 |
+
},
|
| 152 |
+
"colombia": {
|
| 153 |
+
"lat": 4.11347,
|
| 154 |
+
"lon": -72.58619,
|
| 155 |
+
"name": "Colombia",
|
| 156 |
+
"src": "Colombia"
|
| 157 |
+
},
|
| 158 |
+
"costa-rica": {
|
| 159 |
+
"lat": 9.62102,
|
| 160 |
+
"lon": -83.6334,
|
| 161 |
+
"name": "Costa Rica",
|
| 162 |
+
"src": "Costa Rica"
|
| 163 |
+
},
|
| 164 |
+
"croatia": {
|
| 165 |
+
"lat": 44.74512,
|
| 166 |
+
"lon": 15.32158,
|
| 167 |
+
"name": "Croatia",
|
| 168 |
+
"src": "Croatia"
|
| 169 |
+
},
|
| 170 |
+
"curacao": {
|
| 171 |
+
"lat": 12.21206,
|
| 172 |
+
"lon": -69.03453,
|
| 173 |
+
"name": "CuraΓ§ao",
|
| 174 |
+
"src": "CuraΓ§ao"
|
| 175 |
+
},
|
| 176 |
+
"cyprus": {
|
| 177 |
+
"lat": 34.90489,
|
| 178 |
+
"lon": 32.97995,
|
| 179 |
+
"name": "Cyprus",
|
| 180 |
+
"src": "Cyprus"
|
| 181 |
+
},
|
| 182 |
+
"czechia": {
|
| 183 |
+
"lat": 49.80045,
|
| 184 |
+
"lon": 15.51259,
|
| 185 |
+
"name": "Czechia",
|
| 186 |
+
"src": "Czechia"
|
| 187 |
+
},
|
| 188 |
+
"denmark": {
|
| 189 |
+
"lat": 56.27434,
|
| 190 |
+
"lon": 9.26047,
|
| 191 |
+
"name": "Denmark",
|
| 192 |
+
"src": "Denmark"
|
| 193 |
+
},
|
| 194 |
+
"dominican-republic": {
|
| 195 |
+
"lat": 18.77655,
|
| 196 |
+
"lon": -70.12528,
|
| 197 |
+
"name": "Dominican Republic",
|
| 198 |
+
"src": "Dominican Rep."
|
| 199 |
+
},
|
| 200 |
+
"ecuador": {
|
| 201 |
+
"lat": -1.78872,
|
| 202 |
+
"lon": -78.28143,
|
| 203 |
+
"name": "Ecuador",
|
| 204 |
+
"src": "Ecuador"
|
| 205 |
+
},
|
| 206 |
+
"egypt": {
|
| 207 |
+
"lat": 26.82439,
|
| 208 |
+
"lon": 29.46414,
|
| 209 |
+
"name": "Egypt",
|
| 210 |
+
"src": "Egypt"
|
| 211 |
+
},
|
| 212 |
+
"estonia": {
|
| 213 |
+
"lat": 58.58849,
|
| 214 |
+
"lon": 25.49317,
|
| 215 |
+
"name": "Estonia",
|
| 216 |
+
"src": "Estonia"
|
| 217 |
+
},
|
| 218 |
+
"eswatini": {
|
| 219 |
+
"lat": -26.53957,
|
| 220 |
+
"lon": 31.44785,
|
| 221 |
+
"name": "Eswatini",
|
| 222 |
+
"src": "eSwatini"
|
| 223 |
+
},
|
| 224 |
+
"falkland-islands": {
|
| 225 |
+
"lat": -51.79424,
|
| 226 |
+
"lon": -58.61389,
|
| 227 |
+
"name": "Falkland Islands",
|
| 228 |
+
"src": "Falkland Is."
|
| 229 |
+
},
|
| 230 |
+
"faroe-islands": {
|
| 231 |
+
"lat": 62.19918,
|
| 232 |
+
"lon": -6.78831,
|
| 233 |
+
"name": "Faroe Islands",
|
| 234 |
+
"src": "Faeroe Is."
|
| 235 |
+
},
|
| 236 |
+
"finland": {
|
| 237 |
+
"lat": 64.94389,
|
| 238 |
+
"lon": 27.41573,
|
| 239 |
+
"name": "Finland",
|
| 240 |
+
"src": "Finland"
|
| 241 |
+
},
|
| 242 |
+
"france": {
|
| 243 |
+
"lat": 46.6,
|
| 244 |
+
"lon": 2.5,
|
| 245 |
+
"name": "France",
|
| 246 |
+
"src": "manual (metropolitan France β NE polygon repr. point fell in French Guiana)"
|
| 247 |
+
},
|
| 248 |
+
"germany": {
|
| 249 |
+
"lat": 51.08513,
|
| 250 |
+
"lon": 10.48123,
|
| 251 |
+
"name": "Germany",
|
| 252 |
+
"src": "Germany"
|
| 253 |
+
},
|
| 254 |
+
"ghana": {
|
| 255 |
+
"lat": 7.95184,
|
| 256 |
+
"lon": -1.07721,
|
| 257 |
+
"name": "Ghana",
|
| 258 |
+
"src": "Ghana"
|
| 259 |
+
},
|
| 260 |
+
"gibraltar": {
|
| 261 |
+
"lat": 36.12684,
|
| 262 |
+
"lon": -5.34628,
|
| 263 |
+
"name": "Gibraltar",
|
| 264 |
+
"src": "Gibraltar"
|
| 265 |
+
},
|
| 266 |
+
"greece": {
|
| 267 |
+
"lat": 39.07032,
|
| 268 |
+
"lon": 21.95268,
|
| 269 |
+
"name": "Greece",
|
| 270 |
+
"src": "Greece"
|
| 271 |
+
},
|
| 272 |
+
"greenland": {
|
| 273 |
+
"lat": 71.811,
|
| 274 |
+
"lon": -40.33342,
|
| 275 |
+
"name": "Greenland",
|
| 276 |
+
"src": "Greenland"
|
| 277 |
+
},
|
| 278 |
+
"guam": {
|
| 279 |
+
"lat": 13.45962,
|
| 280 |
+
"lon": 144.76055,
|
| 281 |
+
"name": "Guam",
|
| 282 |
+
"src": "Guam"
|
| 283 |
+
},
|
| 284 |
+
"guatemala": {
|
| 285 |
+
"lat": 15.77389,
|
| 286 |
+
"lon": -90.28683,
|
| 287 |
+
"name": "Guatemala",
|
| 288 |
+
"src": "Guatemala"
|
| 289 |
+
},
|
| 290 |
+
"hawaii": {
|
| 291 |
+
"lat": 19.9,
|
| 292 |
+
"lon": -155.58,
|
| 293 |
+
"name": "Hawaii",
|
| 294 |
+
"src": "hardcoded"
|
| 295 |
+
},
|
| 296 |
+
"hong-kong": {
|
| 297 |
+
"lat": 22.4112,
|
| 298 |
+
"lon": 114.056,
|
| 299 |
+
"name": "Hong Kong",
|
| 300 |
+
"src": "Hong Kong"
|
| 301 |
+
},
|
| 302 |
+
"hungary": {
|
| 303 |
+
"lat": 47.15799,
|
| 304 |
+
"lon": 19.11919,
|
| 305 |
+
"name": "Hungary",
|
| 306 |
+
"src": "Hungary"
|
| 307 |
+
},
|
| 308 |
+
"iceland": {
|
| 309 |
+
"lat": 64.96471,
|
| 310 |
+
"lon": -18.46765,
|
| 311 |
+
"name": "Iceland",
|
| 312 |
+
"src": "Iceland"
|
| 313 |
+
},
|
| 314 |
+
"india": {
|
| 315 |
+
"lat": 21.78675,
|
| 316 |
+
"lon": 80.22651,
|
| 317 |
+
"name": "India",
|
| 318 |
+
"src": "India"
|
| 319 |
+
},
|
| 320 |
+
"indonesia": {
|
| 321 |
+
"lat": 0.10491,
|
| 322 |
+
"lon": 113.32523,
|
| 323 |
+
"name": "Indonesia",
|
| 324 |
+
"src": "Indonesia"
|
| 325 |
+
},
|
| 326 |
+
"iraq": {
|
| 327 |
+
"lat": 35.93951,
|
| 328 |
+
"lon": 44.51636,
|
| 329 |
+
"name": "Iraq",
|
| 330 |
+
"src": "Iraqi Kurdistan"
|
| 331 |
+
},
|
| 332 |
+
"ireland": {
|
| 333 |
+
"lat": 53.41606,
|
| 334 |
+
"lon": -7.95824,
|
| 335 |
+
"name": "Ireland",
|
| 336 |
+
"src": "Ireland"
|
| 337 |
+
},
|
| 338 |
+
"isle-of-man": {
|
| 339 |
+
"lat": 54.23865,
|
| 340 |
+
"lon": -4.50997,
|
| 341 |
+
"name": "Isle of Man",
|
| 342 |
+
"src": "Isle of Man"
|
| 343 |
+
},
|
| 344 |
+
"israel-west-bank": {
|
| 345 |
+
"lat": 31.44616,
|
| 346 |
+
"lon": 34.66272,
|
| 347 |
+
"name": "Israel & the West Bank",
|
| 348 |
+
"src": "Israel"
|
| 349 |
+
},
|
| 350 |
+
"italy": {
|
| 351 |
+
"lat": 42.49249,
|
| 352 |
+
"lon": 12.69203,
|
| 353 |
+
"name": "Italy",
|
| 354 |
+
"src": "Italy"
|
| 355 |
+
},
|
| 356 |
+
"japan": {
|
| 357 |
+
"lat": 43.46212,
|
| 358 |
+
"lon": 143.33765,
|
| 359 |
+
"name": "Japan",
|
| 360 |
+
"src": "Japan"
|
| 361 |
+
},
|
| 362 |
+
"jersey": {
|
| 363 |
+
"lat": 49.21833,
|
| 364 |
+
"lon": -2.12238,
|
| 365 |
+
"name": "Jersey",
|
| 366 |
+
"src": "Jersey"
|
| 367 |
+
},
|
| 368 |
+
"jordan": {
|
| 369 |
+
"lat": 31.26985,
|
| 370 |
+
"lon": 36.2997,
|
| 371 |
+
"name": "Jordan",
|
| 372 |
+
"src": "Jordan"
|
| 373 |
+
},
|
| 374 |
+
"kazakhstan": {
|
| 375 |
+
"lat": 48.01087,
|
| 376 |
+
"lon": 66.3259,
|
| 377 |
+
"name": "Kazakhstan",
|
| 378 |
+
"src": "Kazakhstan"
|
| 379 |
+
},
|
| 380 |
+
"kenya": {
|
| 381 |
+
"lat": 0.15482,
|
| 382 |
+
"lon": 37.44792,
|
| 383 |
+
"name": "Kenya",
|
| 384 |
+
"src": "Kenya"
|
| 385 |
+
},
|
| 386 |
+
"kyrgyzstan": {
|
| 387 |
+
"lat": 41.22467,
|
| 388 |
+
"lon": 75.06975,
|
| 389 |
+
"name": "Kyrgyzstan",
|
| 390 |
+
"src": "Kyrgyzstan"
|
| 391 |
+
},
|
| 392 |
+
"laos": {
|
| 393 |
+
"lat": 18.20551,
|
| 394 |
+
"lon": 104.68352,
|
| 395 |
+
"name": "Laos",
|
| 396 |
+
"src": "Laos"
|
| 397 |
+
},
|
| 398 |
+
"latvia": {
|
| 399 |
+
"lat": 56.86803,
|
| 400 |
+
"lon": 24.38329,
|
| 401 |
+
"name": "Latvia",
|
| 402 |
+
"src": "Latvia"
|
| 403 |
+
},
|
| 404 |
+
"lebanon": {
|
| 405 |
+
"lat": 33.8692,
|
| 406 |
+
"lon": 35.90693,
|
| 407 |
+
"name": "Lebanon",
|
| 408 |
+
"src": "Lebanon"
|
| 409 |
+
},
|
| 410 |
+
"lesotho": {
|
| 411 |
+
"lat": -29.6153,
|
| 412 |
+
"lon": 28.16436,
|
| 413 |
+
"name": "Lesotho",
|
| 414 |
+
"src": "Lesotho"
|
| 415 |
+
},
|
| 416 |
+
"liechtenstein": {
|
| 417 |
+
"lat": 47.15736,
|
| 418 |
+
"lon": 9.5351,
|
| 419 |
+
"name": "Liechtenstein",
|
| 420 |
+
"src": "Liechtenstein"
|
| 421 |
+
},
|
| 422 |
+
"lithuania": {
|
| 423 |
+
"lat": 55.17273,
|
| 424 |
+
"lon": 24.14687,
|
| 425 |
+
"name": "Lithuania",
|
| 426 |
+
"src": "Lithuania"
|
| 427 |
+
},
|
| 428 |
+
"luxembourg": {
|
| 429 |
+
"lat": 49.80704,
|
| 430 |
+
"lon": 6.06511,
|
| 431 |
+
"name": "Luxembourg",
|
| 432 |
+
"src": "Luxembourg"
|
| 433 |
+
},
|
| 434 |
+
"macau": {
|
| 435 |
+
"lat": 22.13618,
|
| 436 |
+
"lon": 113.55943,
|
| 437 |
+
"name": "Macau",
|
| 438 |
+
"src": "Macao"
|
| 439 |
+
},
|
| 440 |
+
"madagascar": {
|
| 441 |
+
"lat": -18.76902,
|
| 442 |
+
"lon": 46.72857,
|
| 443 |
+
"name": "Madagascar",
|
| 444 |
+
"src": "Madagascar"
|
| 445 |
+
},
|
| 446 |
+
"madeira": {
|
| 447 |
+
"lat": 32.75674,
|
| 448 |
+
"lon": -16.95071,
|
| 449 |
+
"name": "Madeira",
|
| 450 |
+
"src": "Madeira"
|
| 451 |
+
},
|
| 452 |
+
"malaysia": {
|
| 453 |
+
"lat": 3.98945,
|
| 454 |
+
"lon": 102.11153,
|
| 455 |
+
"name": "Malaysia",
|
| 456 |
+
"src": "Malaysia"
|
| 457 |
+
},
|
| 458 |
+
"mali": {
|
| 459 |
+
"lat": 17.60567,
|
| 460 |
+
"lon": -0.75406,
|
| 461 |
+
"name": "Mali",
|
| 462 |
+
"src": "Mali"
|
| 463 |
+
},
|
| 464 |
+
"malta": {
|
| 465 |
+
"lat": 35.89501,
|
| 466 |
+
"lon": 14.43814,
|
| 467 |
+
"name": "Malta",
|
| 468 |
+
"src": "Malta"
|
| 469 |
+
},
|
| 470 |
+
"martinique": {
|
| 471 |
+
"lat": 14.64433,
|
| 472 |
+
"lon": -61.01815,
|
| 473 |
+
"name": "Martinique",
|
| 474 |
+
"src": "Martinique"
|
| 475 |
+
},
|
| 476 |
+
"mexico": {
|
| 477 |
+
"lat": 23.62927,
|
| 478 |
+
"lon": -102.25815,
|
| 479 |
+
"name": "Mexico",
|
| 480 |
+
"src": "Mexico"
|
| 481 |
+
},
|
| 482 |
+
"monaco": {
|
| 483 |
+
"lat": 43.74161,
|
| 484 |
+
"lon": 7.40293,
|
| 485 |
+
"name": "Monaco",
|
| 486 |
+
"src": "Monaco"
|
| 487 |
+
},
|
| 488 |
+
"mongolia": {
|
| 489 |
+
"lat": 46.85869,
|
| 490 |
+
"lon": 105.40843,
|
| 491 |
+
"name": "Mongolia",
|
| 492 |
+
"src": "Mongolia"
|
| 493 |
+
},
|
| 494 |
+
"montenegro": {
|
| 495 |
+
"lat": 42.70141,
|
| 496 |
+
"lon": 19.28945,
|
| 497 |
+
"name": "Montenegro",
|
| 498 |
+
"src": "Montenegro"
|
| 499 |
+
},
|
| 500 |
+
"namibia": {
|
| 501 |
+
"lat": -22.95749,
|
| 502 |
+
"lon": 17.23377,
|
| 503 |
+
"name": "Namibia",
|
| 504 |
+
"src": "Namibia"
|
| 505 |
+
},
|
| 506 |
+
"nepal": {
|
| 507 |
+
"lat": 28.37663,
|
| 508 |
+
"lon": 83.10311,
|
| 509 |
+
"name": "Nepal",
|
| 510 |
+
"src": "Nepal"
|
| 511 |
+
},
|
| 512 |
+
"netherlands": {
|
| 513 |
+
"lat": 52.10563,
|
| 514 |
+
"lon": 5.51641,
|
| 515 |
+
"name": "Netherlands",
|
| 516 |
+
"src": "Netherlands"
|
| 517 |
+
},
|
| 518 |
+
"new-zealand": {
|
| 519 |
+
"lat": -43.59547,
|
| 520 |
+
"lon": 171.23435,
|
| 521 |
+
"name": "New Zealand",
|
| 522 |
+
"src": "New Zealand"
|
| 523 |
+
},
|
| 524 |
+
"nigeria": {
|
| 525 |
+
"lat": 9.07622,
|
| 526 |
+
"lon": 7.93263,
|
| 527 |
+
"name": "Nigeria",
|
| 528 |
+
"src": "Nigeria"
|
| 529 |
+
},
|
| 530 |
+
"north-macedonia": {
|
| 531 |
+
"lat": 41.60515,
|
| 532 |
+
"lon": 21.73102,
|
| 533 |
+
"name": "North Macedonia",
|
| 534 |
+
"src": "North Macedonia"
|
| 535 |
+
},
|
| 536 |
+
"northern-mariana-islands": {
|
| 537 |
+
"lat": 14.15843,
|
| 538 |
+
"lon": 145.21334,
|
| 539 |
+
"name": "Northern Mariana Islands",
|
| 540 |
+
"src": "N. Mariana Is."
|
| 541 |
+
},
|
| 542 |
+
"norway": {
|
| 543 |
+
"lat": 64.56244,
|
| 544 |
+
"lon": 12.67465,
|
| 545 |
+
"name": "Norway",
|
| 546 |
+
"src": "Norway"
|
| 547 |
+
},
|
| 548 |
+
"oman": {
|
| 549 |
+
"lat": 20.81733,
|
| 550 |
+
"lon": 56.96465,
|
| 551 |
+
"name": "Oman",
|
| 552 |
+
"src": "Oman"
|
| 553 |
+
},
|
| 554 |
+
"pakistan": {
|
| 555 |
+
"lat": 30.38006,
|
| 556 |
+
"lon": 70.08668,
|
| 557 |
+
"name": "Pakistan",
|
| 558 |
+
"src": "Pakistan"
|
| 559 |
+
},
|
| 560 |
+
"panama": {
|
| 561 |
+
"lat": 8.41721,
|
| 562 |
+
"lon": -81.46731,
|
| 563 |
+
"name": "Panama",
|
| 564 |
+
"src": "Panama"
|
| 565 |
+
},
|
| 566 |
+
"peru": {
|
| 567 |
+
"lat": -9.18342,
|
| 568 |
+
"lon": -75.76765,
|
| 569 |
+
"name": "Peru",
|
| 570 |
+
"src": "Peru"
|
| 571 |
+
},
|
| 572 |
+
"philippines": {
|
| 573 |
+
"lat": 7.698,
|
| 574 |
+
"lon": 125.24433,
|
| 575 |
+
"name": "Philippines",
|
| 576 |
+
"src": "Philippines"
|
| 577 |
+
},
|
| 578 |
+
"pitcairn-islands": {
|
| 579 |
+
"lat": -24.36871,
|
| 580 |
+
"lon": -128.31689,
|
| 581 |
+
"name": "Pitcairn Islands",
|
| 582 |
+
"src": "Pitcairn Is."
|
| 583 |
+
},
|
| 584 |
+
"poland": {
|
| 585 |
+
"lat": 51.91992,
|
| 586 |
+
"lon": 19.15627,
|
| 587 |
+
"name": "Poland",
|
| 588 |
+
"src": "Poland"
|
| 589 |
+
},
|
| 590 |
+
"portugal": {
|
| 591 |
+
"lat": 39.56508,
|
| 592 |
+
"lon": -8.29157,
|
| 593 |
+
"name": "Portugal",
|
| 594 |
+
"src": "Portugal"
|
| 595 |
+
},
|
| 596 |
+
"puerto-rico": {
|
| 597 |
+
"lat": 18.22116,
|
| 598 |
+
"lon": -66.40149,
|
| 599 |
+
"name": "Puerto Rico",
|
| 600 |
+
"src": "Puerto Rico"
|
| 601 |
+
},
|
| 602 |
+
"qatar": {
|
| 603 |
+
"lat": 25.36365,
|
| 604 |
+
"lon": 51.13794,
|
| 605 |
+
"name": "Qatar",
|
| 606 |
+
"src": "Qatar"
|
| 607 |
+
},
|
| 608 |
+
"reunion": {
|
| 609 |
+
"lat": -21.11907,
|
| 610 |
+
"lon": 55.54424,
|
| 611 |
+
"name": "Reunion",
|
| 612 |
+
"src": "RΓ©union"
|
| 613 |
+
},
|
| 614 |
+
"romania": {
|
| 615 |
+
"lat": 45.96169,
|
| 616 |
+
"lon": 24.24783,
|
| 617 |
+
"name": "Romania",
|
| 618 |
+
"src": "Romania"
|
| 619 |
+
},
|
| 620 |
+
"russia": {
|
| 621 |
+
"lat": 59.46461,
|
| 622 |
+
"lon": 88.38747,
|
| 623 |
+
"name": "Russia",
|
| 624 |
+
"src": "Russia"
|
| 625 |
+
},
|
| 626 |
+
"rwanda": {
|
| 627 |
+
"lat": -1.94732,
|
| 628 |
+
"lon": 29.98075,
|
| 629 |
+
"name": "Rwanda",
|
| 630 |
+
"src": "Rwanda"
|
| 631 |
+
},
|
| 632 |
+
"saint-pierre-and-miquelon": {
|
| 633 |
+
"lat": 46.78162,
|
| 634 |
+
"lon": -56.19375,
|
| 635 |
+
"name": "Saint Pierre and Miquelon",
|
| 636 |
+
"src": "St. Pierre and Miquelon"
|
| 637 |
+
},
|
| 638 |
+
"san-marino": {
|
| 639 |
+
"lat": 43.93418,
|
| 640 |
+
"lon": 12.43819,
|
| 641 |
+
"name": "San Marino",
|
| 642 |
+
"src": "San Marino"
|
| 643 |
+
},
|
| 644 |
+
"sao-tome-and-principe": {
|
| 645 |
+
"lat": 0.21552,
|
| 646 |
+
"lon": 6.60119,
|
| 647 |
+
"name": "SΓ£o TomΓ© and PrΓncipe",
|
| 648 |
+
"src": "SΓ£o TomΓ© and Principe"
|
| 649 |
+
},
|
| 650 |
+
"senegal": {
|
| 651 |
+
"lat": 14.48858,
|
| 652 |
+
"lon": -14.65724,
|
| 653 |
+
"name": "Senegal",
|
| 654 |
+
"src": "Senegal"
|
| 655 |
+
},
|
| 656 |
+
"serbia": {
|
| 657 |
+
"lat": 43.66231,
|
| 658 |
+
"lon": 20.97381,
|
| 659 |
+
"name": "Serbia",
|
| 660 |
+
"src": "Serbia"
|
| 661 |
+
},
|
| 662 |
+
"singapore": {
|
| 663 |
+
"lat": 1.36455,
|
| 664 |
+
"lon": 103.83054,
|
| 665 |
+
"name": "Singapore",
|
| 666 |
+
"src": "Singapore"
|
| 667 |
+
},
|
| 668 |
+
"slovakia": {
|
| 669 |
+
"lat": 48.67465,
|
| 670 |
+
"lon": 19.64504,
|
| 671 |
+
"name": "Slovakia",
|
| 672 |
+
"src": "Slovakia"
|
| 673 |
+
},
|
| 674 |
+
"slovenia": {
|
| 675 |
+
"lat": 46.15021,
|
| 676 |
+
"lon": 14.61594,
|
| 677 |
+
"name": "Slovenia",
|
| 678 |
+
"src": "Slovenia"
|
| 679 |
+
},
|
| 680 |
+
"south-africa": {
|
| 681 |
+
"lat": -28.47459,
|
| 682 |
+
"lon": 26.12089,
|
| 683 |
+
"name": "South Africa",
|
| 684 |
+
"src": "South Africa"
|
| 685 |
+
},
|
| 686 |
+
"south-georgia-sandwich-islands": {
|
| 687 |
+
"lat": -54.42978,
|
| 688 |
+
"lon": -36.49215,
|
| 689 |
+
"name": "South Georgia & Sandwich Islands",
|
| 690 |
+
"src": "S. Geo. and the Is."
|
| 691 |
+
},
|
| 692 |
+
"south-korea": {
|
| 693 |
+
"lat": 38.20457,
|
| 694 |
+
"lon": 127.00228,
|
| 695 |
+
"name": "South Korea",
|
| 696 |
+
"src": "Korean DMZ (south)"
|
| 697 |
+
},
|
| 698 |
+
"spain": {
|
| 699 |
+
"lat": 39.89948,
|
| 700 |
+
"lon": -3.47653,
|
| 701 |
+
"name": "Spain",
|
| 702 |
+
"src": "Spain"
|
| 703 |
+
},
|
| 704 |
+
"sri-lanka": {
|
| 705 |
+
"lat": 7.87889,
|
| 706 |
+
"lon": 80.66733,
|
| 707 |
+
"name": "Sri Lanka",
|
| 708 |
+
"src": "Sri Lanka"
|
| 709 |
+
},
|
| 710 |
+
"svalbard": {
|
| 711 |
+
"lat": 79.85177,
|
| 712 |
+
"lon": 22.69697,
|
| 713 |
+
"name": "Svalbard",
|
| 714 |
+
"src": "Svalbard Is."
|
| 715 |
+
},
|
| 716 |
+
"sweden": {
|
| 717 |
+
"lat": 62.19453,
|
| 718 |
+
"lon": 14.90512,
|
| 719 |
+
"name": "Sweden",
|
| 720 |
+
"src": "Sweden"
|
| 721 |
+
},
|
| 722 |
+
"switzerland": {
|
| 723 |
+
"lat": 46.81195,
|
| 724 |
+
"lon": 8.427,
|
| 725 |
+
"name": "Switzerland",
|
| 726 |
+
"src": "Switzerland"
|
| 727 |
+
},
|
| 728 |
+
"taiwan": {
|
| 729 |
+
"lat": 23.61665,
|
| 730 |
+
"lon": 120.82792,
|
| 731 |
+
"name": "Taiwan",
|
| 732 |
+
"src": "Taiwan"
|
| 733 |
+
},
|
| 734 |
+
"tanzania": {
|
| 735 |
+
"lat": -6.35412,
|
| 736 |
+
"lon": 34.20886,
|
| 737 |
+
"name": "Tanzania",
|
| 738 |
+
"src": "Tanzania"
|
| 739 |
+
},
|
| 740 |
+
"thailand": {
|
| 741 |
+
"lat": 13.03238,
|
| 742 |
+
"lon": 101.69302,
|
| 743 |
+
"name": "Thailand",
|
| 744 |
+
"src": "Thailand"
|
| 745 |
+
},
|
| 746 |
+
"tunisia": {
|
| 747 |
+
"lat": 33.79141,
|
| 748 |
+
"lon": 8.86273,
|
| 749 |
+
"name": "Tunisia",
|
| 750 |
+
"src": "Tunisia"
|
| 751 |
+
},
|
| 752 |
+
"turkey": {
|
| 753 |
+
"lat": 38.96105,
|
| 754 |
+
"lon": 35.47854,
|
| 755 |
+
"name": "Turkey",
|
| 756 |
+
"src": "Turkey"
|
| 757 |
+
},
|
| 758 |
+
"uganda": {
|
| 759 |
+
"lat": 1.38017,
|
| 760 |
+
"lon": 32.6841,
|
| 761 |
+
"name": "Uganda",
|
| 762 |
+
"src": "Uganda"
|
| 763 |
+
},
|
| 764 |
+
"ukraine": {
|
| 765 |
+
"lat": 48.79126,
|
| 766 |
+
"lon": 31.06142,
|
| 767 |
+
"name": "Ukraine",
|
| 768 |
+
"src": "Ukraine"
|
| 769 |
+
},
|
| 770 |
+
"united-arab-emirates": {
|
| 771 |
+
"lat": 24.35262,
|
| 772 |
+
"lon": 55.13711,
|
| 773 |
+
"name": "United Arab Emirates",
|
| 774 |
+
"src": "United Arab Emirates"
|
| 775 |
+
},
|
| 776 |
+
"united-kingdom": {
|
| 777 |
+
"lat": 54.64134,
|
| 778 |
+
"lon": -6.88882,
|
| 779 |
+
"name": "United Kingdom",
|
| 780 |
+
"src": "N. Ireland"
|
| 781 |
+
},
|
| 782 |
+
"united-states": {
|
| 783 |
+
"lat": 37.24636,
|
| 784 |
+
"lon": -99.69843,
|
| 785 |
+
"name": "United States of America",
|
| 786 |
+
"src": "United States of America"
|
| 787 |
+
},
|
| 788 |
+
"uruguay": {
|
| 789 |
+
"lat": -32.52663,
|
| 790 |
+
"lon": -55.81866,
|
| 791 |
+
"name": "Uruguay",
|
| 792 |
+
"src": "Uruguay"
|
| 793 |
+
},
|
| 794 |
+
"us-virgin-islands": {
|
| 795 |
+
"lat": 17.73298,
|
| 796 |
+
"lon": -64.75742,
|
| 797 |
+
"name": "US Virgin Islands",
|
| 798 |
+
"src": "U.S. Virgin Is."
|
| 799 |
+
},
|
| 800 |
+
"vanuatu": {
|
| 801 |
+
"lat": -17.67514,
|
| 802 |
+
"lon": 168.35423,
|
| 803 |
+
"name": "Vanuatu",
|
| 804 |
+
"src": "Vanuatu"
|
| 805 |
+
},
|
| 806 |
+
"vietnam": {
|
| 807 |
+
"lat": 15.96243,
|
| 808 |
+
"lon": 107.85285,
|
| 809 |
+
"name": "Vietnam",
|
| 810 |
+
"src": "Vietnam"
|
| 811 |
+
},
|
| 812 |
+
"us-minor-outlying-islands": {
|
| 813 |
+
"lat": -0.02,
|
| 814 |
+
"lon": -176.3,
|
| 815 |
+
"name": "US Minor Outlying Islands",
|
| 816 |
+
"src": "hardcoded"
|
| 817 |
+
}
|
| 818 |
+
}
|
data/country_scripts.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"alaska":["latin"],"albania":["latin"],"american-samoa":["latin"],"india":["latin","devanagari"],"andorra":["latin"],"argentina":["latin"],"russia":["latin","cyrillic"],"australia":["latin"],"austria":["latin"],"azores":["latin"],"spain":["latin"],"bangladesh":["latin","bengali"],"belarus":["latin","cyrillic"],"belgium":["latin"],"bermuda":["latin"],"bhutan":["latin"],"bolivia":["latin"],"botswana":["latin"],"brazil":["latin"],"bulgaria":["latin","cyrillic"],"cambodia":["latin","khmer"],"canada":["latin"],"chile":["latin"],"colombia":["latin"],"france":["latin"],"costa-rica":["latin"],"croatia":["latin"],"curacao":["latin"],"czechia":["latin"],"denmark":["latin"],"dominican-republic":["latin"],"ecuador":["latin"],"united-kingdom":["latin"],"estonia":["latin"],"eswatini":["latin"],"faroe-islands":["latin"],"finland":["latin"],"germany":["latin"],"ghana":["latin"],"gibraltar":["latin"],"greece":["latin","greek"],"greenland":["latin"],"guatemala":["latin"],"hawaii":["latin"],"hong-kong":["latin","cjk"],"hungary":["latin"],"iceland":["latin"],"ireland":["latin"],"isle-of-man":["latin"],"israel-west-bank":["latin","hebrew"],"italy":["latin"],"japan":["latin","cjk"],"jersey":["latin"],"jordan":["latin","arabic"],"indonesia":["latin"],"kazakhstan":["latin","cyrillic"],"kyrgyzstan":["latin","cyrillic"],"kenya":["latin"],"laos":["latin","lao"],"latvia":["latin"],"lebanon":["latin","arabic"],"lesotho":["latin"],"liechtenstein":["latin"],"lithuania":["latin"],"luxembourg":["latin"],"madeira":["latin"],"malaysia":["latin"],"malta":["latin"],"mexico":["latin"],"monaco":["latin"],"mongolia":["latin","cyrillic"],"montenegro":["latin","cyrillic"],"namibia":["latin"],"nepal":["latin","devanagari"],"netherlands":["latin"],"new-zealand":["latin"],"nigeria":["latin"],"north-macedonia":["latin","cyrillic"],"norway":["latin"],"oman":["latin","arabic"],"pakistan":["latin"],"panama":["latin"],"peru":["latin"],"philippines":["latin"],"poland":["latin"],"portugal":["latin"],"puerto-rico":["latin"],"qatar":["latin","arabic"],"cyprus":["latin","greek"],"romania":["latin"],"rwanda":["latin"],"reunion":["latin"],"san-marino":["latin"],"senegal":["latin"],"serbia":["latin","cyrillic"],"singapore":["latin"],"slovakia":["latin"],"slovenia":["latin"],"south-africa":["latin"],"sri-lanka":["latin","sinhala"],"sweden":["latin"],"switzerland":["latin"],"sao-tome-and-principe":["latin"],"taiwan":["latin","cjk"],"thailand":["latin","thai"],"tunisia":["latin","arabic"],"turkey":["latin"],"us-virgin-islands":["latin"],"uganda":["latin"],"ukraine":["latin","cyrillic"],"united-arab-emirates":["latin","arabic"],"united-states":["latin"],"uruguay":["latin"],"vietnam":["latin"]}
|
data/index_fast.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e85b6c2e9cf17d7b0d087ee52766153bcb7424c4a940ce7d5a72592c62421935
|
| 3 |
+
size 4879488
|
data/index_geo.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3d36c01d8fa7969e27d31b14cf7a3a6729cbf8ef95e7611476523538d206d831
|
| 3 |
+
size 7319168
|
data/index_pro.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f7c329f262461377f4ed439f8a71d2d34b0cb61046b03d59b24e8f29b6360f66
|
| 3 |
+
size 7319168
|
data/meta_fast.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/meta_geo.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/meta_pro.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/priors.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"alaska":90,"albania":210,"american-samoa":30,"india":1215,"andorra":30,"argentina":1200,"russia":1650,"australia":1410,"austria":450,"azores":60,"spain":1362,"bangladesh":600,"belarus":6,"belgium":400,"bermuda":30,"bhutan":300,"bolivia":510,"botswana":420,"brazil":1350,"bulgaria":600,"cambodia":450,"canada":1200,"chile":840,"colombia":900,"france":1300,"costa-rica":300,"croatia":540,"curacao":60,"czechia":600,"denmark":450,"dominican-republic":150,"ecuador":600,"united-kingdom":1350,"estonia":300,"eswatini":150,"faroe-islands":90,"finland":600,"germany":1200,"ghana":450,"gibraltar":30,"greece":700,"greenland":3,"guatemala":300,"hawaii":150,"hong-kong":60,"hungary":510,"iceland":450,"ireland":600,"isle-of-man":45,"israel-west-bank":600,"italy":1150,"japan":1350,"jersey":15,"jordan":300,"indonesia":1790,"kazakhstan":300,"kyrgyzstan":720,"kenya":650,"laos":90,"latvia":300,"lebanon":45,"lesotho":150,"liechtenstein":45,"lithuania":300,"luxembourg":135,"madeira":45,"malaysia":900,"malta":60,"mexico":1200,"monaco":60,"mongolia":300,"montenegro":240,"namibia":360,"nepal":240,"netherlands":360,"new-zealand":750,"nigeria":750,"north-macedonia":240,"norway":600,"oman":600,"pakistan":30,"panama":300,"peru":1140,"philippines":1200,"poland":600,"portugal":540,"puerto-rico":120,"qatar":90,"cyprus":240,"romania":600,"rwanda":300,"reunion":60,"san-marino":60,"senegal":450,"serbia":450,"singapore":90,"slovakia":450,"slovenia":450,"south-africa":1200,"sri-lanka":300,"sweden":600,"switzerland":300,"sao-tome-and-principe":15,"taiwan":300,"thailand":1200,"tunisia":300,"turkey":750,"us-virgin-islands":90,"uganda":150,"ukraine":450,"united-arab-emirates":450,"united-states":1200,"uruguay":300,"vietnam":540}
|
data/script_names.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["latin", "cyrillic", "greek", "thai", "cjk", "arabic", "hebrew", "devanagari", "bengali", "sinhala", "lao", "khmer"]
|
data/script_text.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8a98b41c66636024e464c8795557d7c42559faf5f2b5526d996d876264573ef
|
| 3 |
+
size 18560
|
data/text_geo.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:12ca4f31d1a267ff6c2fd51cc86f3d90b5a56f2bafc283af30977b288807fc27
|
| 3 |
+
size 175232
|
data/text_geo_countries.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["alaska", "albania", "american-samoa", "andorra", "argentina", "australia", "austria", "azores", "bangladesh", "belarus", "belgium", "bermuda", "bhutan", "bolivia", "botswana", "brazil", "bulgaria", "cambodia", "canada", "chile", "colombia", "costa-rica", "croatia", "curacao", "cyprus", "czechia", "denmark", "dominican-republic", "ecuador", "estonia", "eswatini", "faroe-islands", "finland", "france", "germany", "ghana", "gibraltar", "greece", "greenland", "guatemala", "hawaii", "hong-kong", "hungary", "iceland", "india", "indonesia", "ireland", "isle-of-man", "israel-west-bank", "italy", "japan", "jersey", "jordan", "kazakhstan", "kenya", "kyrgyzstan", "laos", "latvia", "lebanon", "lesotho", "liechtenstein", "lithuania", "luxembourg", "madeira", "malaysia", "malta", "mexico", "monaco", "mongolia", "montenegro", "namibia", "nepal", "netherlands", "new-zealand", "nigeria", "north-macedonia", "norway", "oman", "pakistan", "panama", "peru", "philippines", "poland", "portugal", "puerto-rico", "qatar", "reunion", "romania", "russia", "rwanda", "san-marino", "sao-tome-and-principe", "senegal", "serbia", "singapore", "slovakia", "slovenia", "south-africa", "spain", "sri-lanka", "sweden", "switzerland", "taiwan", "thailand", "tunisia", "turkey", "uganda", "ukraine", "united-arab-emirates", "united-kingdom", "united-states", "uruguay", "us-virgin-islands", "vietnam"]
|
guess.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 2 β search + country vote + template reasoning. No LLM, fully offline.
|
| 2 |
+
|
| 3 |
+
Pure logic given an index (N,512 fp32, L2-normed), meta rows, and centroids.
|
| 4 |
+
The same module is imported by app.py (the Space) and eval/eval.py (the PC), so
|
| 5 |
+
the served algorithm and the evaluated algorithm are byte-identical.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class Config:
|
| 15 |
+
K: int = 15 # top matches per view
|
| 16 |
+
P: float = 8.0 # similarity sharpening exponent
|
| 17 |
+
ALPHA: float = 0.4 # large-guide bias correction (S_c /= max(count[c], MIN_COUNT)**ALPHA)
|
| 18 |
+
MIN_COUNT: int = 20 # floor on the per-country image count used for ALPHA normalization,
|
| 19 |
+
# so tiny guides (5β9 imgs) don't get a huge score boost and beat
|
| 20 |
+
# large countries on weak matches (the "India β mid-Pacific" bug)
|
| 21 |
+
PRIOR_BETA: float = 0.3 # strength of the game-location prior (S_c *= available[c]**BETA).
|
| 22 |
+
# 0 disables the prior (filter still applies). Higher = trust the
|
| 23 |
+
# "how many real locations exist in this country" signal more.
|
| 24 |
+
TEXT_WEIGHT: float = 0.25 # blend weight for the zero-shot TEXT branch (geo only):
|
| 25 |
+
# final = (1-W)*image_retrieval + W*imageβcountry_text. 0 disables.
|
| 26 |
+
P_TEXT: float = 4.0 # sharpening exponent for zero-shot text sims
|
| 27 |
+
JITTER_DEG: float = 0.0
|
| 28 |
+
MAX_CLUES: int = 3
|
| 29 |
+
RUNNER_SENTENCE_RATIO: float = 0.5 # show runner-up note when its score >= 50% of winner
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# --- reasoning templates, keyed by confidence band; chosen deterministically ---
|
| 33 |
+
ASSERTIVE = [
|
| 34 |
+
"Both views match features documented for {country} β {clue1} and {clue2}.{runner} Placing my guess in central {country}.",
|
| 35 |
+
"This is {country} for me: I'm seeing {clue1}, and {clue2} backs it up.{runner} Dropping my pin in central {country}.",
|
| 36 |
+
"Clear {country} signals here β {clue1} together with {clue2}.{runner} I'll guess central {country}.",
|
| 37 |
+
"Confident on {country}: {clue1} and {clue2} both line up.{runner} Going central {country}.",
|
| 38 |
+
]
|
| 39 |
+
HEDGED = [
|
| 40 |
+
"This looks most like {country} to me β I matched {clue1}, though I also saw similarities to {runner_c}. Going with {country}.",
|
| 41 |
+
"Leaning {country}: {clue1} points that way, but {runner_c} crossed my mind too. I'll commit to {country}.",
|
| 42 |
+
"Probably {country} β {clue1} is the strongest hint, with {runner_c} as a maybe. Guessing {country}.",
|
| 43 |
+
"My read is {country} based on {clue1}, even if {runner_c} isn't far off. Placing it in {country}.",
|
| 44 |
+
]
|
| 45 |
+
UNCERTAIN = [
|
| 46 |
+
"Tough one. Weak matches all around, but {clue1} nudges me toward {country}, so that's my guess.",
|
| 47 |
+
"Not much to go on here β {clue1} is the only real hint, pointing at {country}. Rolling with it.",
|
| 48 |
+
"Low confidence on this. {clue1} loosely suggests {country}, so I'll take the shot.",
|
| 49 |
+
"Hard to read. {clue1} is faint, but it leans {country} β guessing there.",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _pick(variants, seed_text):
|
| 54 |
+
return variants[abs(hash(seed_text)) % len(variants)]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _distinct_clues(matches, k):
|
| 58 |
+
"""Up to k distinct clue texts, ordered by sim desc."""
|
| 59 |
+
out, seen = [], set()
|
| 60 |
+
for m in sorted(matches, key=lambda x: -x["sim"]):
|
| 61 |
+
c = m["clue"]
|
| 62 |
+
if c not in seen:
|
| 63 |
+
seen.add(c)
|
| 64 |
+
out.append(m)
|
| 65 |
+
if len(out) >= k:
|
| 66 |
+
break
|
| 67 |
+
return out
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def build_reasoning(country_name, confidence, winner_matches, runner_name, runner_matches, cfg):
|
| 71 |
+
clues = _distinct_clues(winner_matches, cfg.MAX_CLUES)
|
| 72 |
+
clue1 = clues[0]["clue"] if clues else "a few subtle details"
|
| 73 |
+
clue2 = clues[1]["clue"] if len(clues) > 1 else clue1
|
| 74 |
+
seed = "|".join(c["clue"] for c in clues) + country_name
|
| 75 |
+
|
| 76 |
+
if confidence >= 0.55:
|
| 77 |
+
runner = ""
|
| 78 |
+
if runner_name and runner_matches:
|
| 79 |
+
runner = f" I also weighed {runner_matches[0]['country_name']}, but those matches were weaker."
|
| 80 |
+
return _pick(ASSERTIVE, seed).format(
|
| 81 |
+
country=country_name, clue1=clue1.lower(), clue2=clue2.lower(), runner=runner)
|
| 82 |
+
if confidence >= 0.35:
|
| 83 |
+
runner_c = runner_matches[0]["country_name"] if runner_matches else "a neighbour"
|
| 84 |
+
return _pick(HEDGED, seed).format(country=country_name, clue1=clue1.lower(), runner_c=runner_c)
|
| 85 |
+
return _pick(UNCERTAIN, seed).format(country=country_name, clue1=clue1.lower())
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def guess(embs, index, rows, centroids, count, cfg=Config(), priors=None,
|
| 89 |
+
text_vecs=None, text_countries=None):
|
| 90 |
+
"""embs: list of (D,) float32 L2-normed view embeddings (1+; e.g. 5 frames around
|
| 91 |
+
the spot + a downward car view). Returns the response dict.
|
| 92 |
+
|
| 93 |
+
priors: optional {slug: available_location_count}. When given, the bot can ONLY
|
| 94 |
+
guess countries present here (those the game actually has locations for) and
|
| 95 |
+
weights each by available_count**PRIOR_BETA.
|
| 96 |
+
|
| 97 |
+
text_vecs/(text_countries): optional (C,D) L2-normed country text embeddings and
|
| 98 |
+
their aligned slugs (StreetCLIP zero-shot). When given, the final score blends
|
| 99 |
+
image retrieval with imageβcountry-text similarity (cfg.TEXT_WEIGHT).
|
| 100 |
+
"""
|
| 101 |
+
if not isinstance(embs, (list, tuple)):
|
| 102 |
+
embs = [embs]
|
| 103 |
+
matches = {} # index row -> {sim, ...meta}
|
| 104 |
+
for e in embs:
|
| 105 |
+
sims = index @ e
|
| 106 |
+
top = np.argpartition(-sims, cfg.K)[: cfg.K] if len(sims) > cfg.K else np.arange(len(sims))
|
| 107 |
+
for idx in top:
|
| 108 |
+
s = float(sims[idx])
|
| 109 |
+
# if a row appears in both views, keep the larger sim but it still counts once here;
|
| 110 |
+
# union semantics with double-contribution handled by summing weights per view below
|
| 111 |
+
prev = matches.get(idx)
|
| 112 |
+
if prev is None:
|
| 113 |
+
r = rows[idx]
|
| 114 |
+
matches[idx] = {"sim": s, "country": r["country"],
|
| 115 |
+
"country_name": r["country_name"], "clue": r["clue"],
|
| 116 |
+
"page": r["page"], "_w": max(s, 0.0) ** cfg.P}
|
| 117 |
+
else:
|
| 118 |
+
# second view also matched this row: add its weight (intended double vote)
|
| 119 |
+
prev["_w"] += max(s, 0.0) ** cfg.P
|
| 120 |
+
prev["sim"] = max(prev["sim"], s)
|
| 121 |
+
|
| 122 |
+
# country scores
|
| 123 |
+
scores, by_country = {}, {}
|
| 124 |
+
for m in matches.values():
|
| 125 |
+
c = m["country"]
|
| 126 |
+
if priors is not None and priors.get(c, 0) <= 0:
|
| 127 |
+
continue # country has no game locations β the bot must never guess it
|
| 128 |
+
scores[c] = scores.get(c, 0.0) + m["_w"]
|
| 129 |
+
by_country.setdefault(c, []).append(m)
|
| 130 |
+
for c in scores:
|
| 131 |
+
scores[c] /= (max(count.get(c, 1), cfg.MIN_COUNT) ** cfg.ALPHA)
|
| 132 |
+
if priors is not None:
|
| 133 |
+
scores[c] *= priors[c] ** cfg.PRIOR_BETA
|
| 134 |
+
|
| 135 |
+
if not scores:
|
| 136 |
+
# None of the matched countries are in the game's pool β fall back to the
|
| 137 |
+
# most location-rich country so we still return a valid, in-pool guess.
|
| 138 |
+
winner = max(priors, key=priors.get) if priors else rows[0]["country"]
|
| 139 |
+
cen = centroids[winner]
|
| 140 |
+
return {
|
| 141 |
+
"lat": cen["lat"], "lon": cen["lon"],
|
| 142 |
+
"country": winner, "country_name": cen["name"],
|
| 143 |
+
"confidence": 0.0, "runner_up": None,
|
| 144 |
+
"reasoning": "Couldn't match these views to anywhere I know β taking a blind guess.",
|
| 145 |
+
"clues": [],
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
# --- zero-shot TEXT branch (geo): blend imageβcountry-text with retrieval ---
|
| 149 |
+
if text_vecs is not None and text_countries is not None and cfg.TEXT_WEIGHT > 0:
|
| 150 |
+
sims_t = np.max(np.stack([text_vecs @ e for e in embs]), axis=0) # best view per country
|
| 151 |
+
zs = {}
|
| 152 |
+
for i, c in enumerate(text_countries):
|
| 153 |
+
if priors is not None and priors.get(c, 0) <= 0:
|
| 154 |
+
continue
|
| 155 |
+
s = max(float(sims_t[i]), 0.0) ** cfg.P_TEXT
|
| 156 |
+
if priors is not None:
|
| 157 |
+
s *= priors[c] ** cfg.PRIOR_BETA
|
| 158 |
+
zs[c] = s
|
| 159 |
+
rsum = sum(scores.values()) or 1.0
|
| 160 |
+
zsum = sum(zs.values()) or 1.0
|
| 161 |
+
W = cfg.TEXT_WEIGHT
|
| 162 |
+
final = {
|
| 163 |
+
c: (1 - W) * (scores.get(c, 0.0) / rsum) + W * (zs.get(c, 0.0) / zsum)
|
| 164 |
+
for c in set(scores) | set(zs)
|
| 165 |
+
}
|
| 166 |
+
else:
|
| 167 |
+
final = scores
|
| 168 |
+
|
| 169 |
+
ranked = sorted(final.items(), key=lambda kv: -kv[1])
|
| 170 |
+
winner, win_score = ranked[0]
|
| 171 |
+
total = sum(final.values()) or 1.0
|
| 172 |
+
confidence = win_score / total
|
| 173 |
+
runner = ranked[1][0] if len(ranked) > 1 else None
|
| 174 |
+
runner_score = ranked[1][1] if len(ranked) > 1 else 0.0
|
| 175 |
+
|
| 176 |
+
win_matches = sorted(by_country.get(winner, []), key=lambda x: -x["sim"])
|
| 177 |
+
runner_matches = sorted(by_country.get(runner, []), key=lambda x: -x["sim"]) if runner else []
|
| 178 |
+
|
| 179 |
+
cen = centroids[winner]
|
| 180 |
+
# The ratio gate only governs the optional extra runner-up SENTENCE in the
|
| 181 |
+
# assertive band; the hedged band always names the actual runner-up.
|
| 182 |
+
show_runner = bool(runner) and runner_score >= cfg.RUNNER_SENTENCE_RATIO * win_score
|
| 183 |
+
if confidence >= 0.55:
|
| 184 |
+
reasoning = build_reasoning(cen["name"], confidence, win_matches,
|
| 185 |
+
runner if show_runner else None,
|
| 186 |
+
runner_matches if show_runner else [], cfg)
|
| 187 |
+
else:
|
| 188 |
+
reasoning = build_reasoning(cen["name"], confidence, win_matches,
|
| 189 |
+
runner, runner_matches, cfg)
|
| 190 |
+
|
| 191 |
+
clue_list = []
|
| 192 |
+
for m in _distinct_clues(win_matches, cfg.MAX_CLUES):
|
| 193 |
+
clue_list.append({"text": m["clue"], "country": m["country"],
|
| 194 |
+
"page": m["page"], "sim": round(m["sim"], 3)})
|
| 195 |
+
if runner_matches:
|
| 196 |
+
m = runner_matches[0]
|
| 197 |
+
clue_list.append({"text": m["clue"], "country": m["country"],
|
| 198 |
+
"page": m["page"], "sim": round(m["sim"], 3)})
|
| 199 |
+
|
| 200 |
+
return {
|
| 201 |
+
"lat": cen["lat"], "lon": cen["lon"],
|
| 202 |
+
"country": winner, "country_name": cen["name"],
|
| 203 |
+
"confidence": round(confidence, 3),
|
| 204 |
+
"runner_up": runner,
|
| 205 |
+
"reasoning": reasoning,
|
| 206 |
+
"clues": clue_list,
|
| 207 |
+
}
|
region.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Region kNN locator (Nomad-style, but for one country's scraped Street View).
|
| 2 |
+
|
| 3 |
+
A region model holds N reference embeddings + their exact lat/lng. Inference:
|
| 4 |
+
embed the round's frames β cosine-nearest reference across ALL frames β predict
|
| 5 |
+
that reference's coordinates. Best for in-country precision (e.g. the Serbia model
|
| 6 |
+
guessing where in Serbia a panorama is), not country classification.
|
| 7 |
+
"""
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def load_region(path):
|
| 12 |
+
"""Load a region index .npz β (ref_emb f32 normalised, lat, lng)."""
|
| 13 |
+
z = np.load(path, allow_pickle=True)
|
| 14 |
+
emb = z["emb"].astype(np.float32)
|
| 15 |
+
emb /= np.linalg.norm(emb, axis=1, keepdims=True) + 1e-8
|
| 16 |
+
return emb, z["lat"].astype(np.float64), z["lng"].astype(np.float64)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def predict(embs, ref_emb, ref_lat, ref_lng, country_slug, country_name, k=5):
|
| 20 |
+
"""embs: list of (D,) query-frame embeddings. Returns a scoreable guess dict
|
| 21 |
+
(same shape the bot/guess.py emits): predicted lat/lon from the nearest
|
| 22 |
+
reference image, confidence from cosine similarity."""
|
| 23 |
+
Q = np.stack([e / (np.linalg.norm(e) + 1e-8) for e in embs]) # (F, D)
|
| 24 |
+
sims = Q @ ref_emb.T # (F, N)
|
| 25 |
+
fi, ni = np.unravel_index(int(np.argmax(sims)), sims.shape)
|
| 26 |
+
best = float(sims[fi, ni])
|
| 27 |
+
|
| 28 |
+
# Top-k across all frames β geo-medoid (robust to a single odd neighbour).
|
| 29 |
+
flat = sims.reshape(-1)
|
| 30 |
+
kk = min(k, flat.shape[0])
|
| 31 |
+
top = np.argpartition(-flat, kk - 1)[:kk]
|
| 32 |
+
cols = (top % ref_emb.shape[0])
|
| 33 |
+
cla, cln = ref_lat[cols], ref_lng[cols]
|
| 34 |
+
R = 6371.0088; p = np.pi / 180.0
|
| 35 |
+
dsum = np.empty(len(cols))
|
| 36 |
+
for i in range(len(cols)):
|
| 37 |
+
a = (np.sin((cla - cla[i]) * p / 2) ** 2
|
| 38 |
+
+ np.cos(cla[i] * p) * np.cos(cla * p) * np.sin((cln - cln[i]) * p / 2) ** 2)
|
| 39 |
+
dsum[i] = (2 * R * np.arcsin(np.sqrt(np.clip(a, 0, 1)))).sum()
|
| 40 |
+
m = int(dsum.argmin())
|
| 41 |
+
lat, lon = float(cla[m]), float(cln[m])
|
| 42 |
+
|
| 43 |
+
conf = round(max(0.0, min(1.0, best)), 3)
|
| 44 |
+
return {
|
| 45 |
+
"country": country_slug,
|
| 46 |
+
"country_name": country_name,
|
| 47 |
+
"lat": lat,
|
| 48 |
+
"lon": lon,
|
| 49 |
+
"confidence": conf,
|
| 50 |
+
"runner_up": None,
|
| 51 |
+
"runner_up_conf": None,
|
| 52 |
+
"script": None,
|
| 53 |
+
"reasoning": f"Matched the closest of {ref_emb.shape[0]} {country_name} street views "
|
| 54 |
+
f"(similarity {conf}).",
|
| 55 |
+
"clues": [],
|
| 56 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# torch CPU is installed separately in the Dockerfile (pinned to the CPU index).
|
| 2 |
+
transformers==5.11.0
|
| 3 |
+
huggingface_hub
|
| 4 |
+
fastapi==0.115.6
|
| 5 |
+
uvicorn[standard]==0.34.0
|
| 6 |
+
pillow==11.1.0
|
| 7 |
+
numpy==2.2.1
|
| 8 |
+
python-multipart==0.0.20
|
shared/__init__.py
ADDED
|
File without changes
|
shared/embedder.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The ONE embedder. Imported by both the indexer (PC) and the server (Space).
|
| 2 |
+
|
| 3 |
+
Any divergence between index-time and serve-time preprocessing silently
|
| 4 |
+
destroys retrieval accuracy, so there must be exactly one implementation.
|
| 5 |
+
Vision-only: the server never needs the text tower.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from PIL import Image
|
| 11 |
+
|
| 12 |
+
from transformers import CLIPVisionModelWithProjection, CLIPImageProcessor
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from .version import MODEL_ID
|
| 16 |
+
except ImportError: # allow running as a loose script
|
| 17 |
+
from version import MODEL_ID
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Embedder:
|
| 21 |
+
def __init__(self, model_id: str = MODEL_ID):
|
| 22 |
+
self.model_id = model_id
|
| 23 |
+
self.proc = CLIPImageProcessor.from_pretrained(model_id)
|
| 24 |
+
self.model = CLIPVisionModelWithProjection.from_pretrained(model_id).eval()
|
| 25 |
+
|
| 26 |
+
@torch.no_grad()
|
| 27 |
+
def embed(self, images: list[Image.Image]) -> np.ndarray:
|
| 28 |
+
"""Return (B, 512) float32, L2-normalized rows. Input PIL images (any mode)."""
|
| 29 |
+
rgb = [im.convert("RGB") for im in images]
|
| 30 |
+
inputs = self.proc(images=rgb, return_tensors="pt")
|
| 31 |
+
emb = self.model(**inputs).image_embeds
|
| 32 |
+
emb = emb / emb.norm(dim=-1, keepdim=True)
|
| 33 |
+
return emb.float().cpu().numpy()
|
shared/version.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single source of truth for model + index versioning.
|
| 2 |
+
|
| 3 |
+
The indexer (PC) and the server (HF Space) both import these constants.
|
| 4 |
+
The server asserts that meta.json carries matching values before it starts,
|
| 5 |
+
so a stale index can never be served against a different model.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# The indexer (build_index.py) builds THIS model's index (currently the "geo" index).
|
| 9 |
+
MODEL_ID = "geolocal/StreetCLIP"
|
| 10 |
+
INDEX_VERSION = "v3"
|
| 11 |
+
|
| 12 |
+
# The Space serves multiple switchable models. Each has its own baked weights and
|
| 13 |
+
# its own index/meta files in data/. Switch per request via /guess?model=<key>.
|
| 14 |
+
MODELS = {
|
| 15 |
+
"fast": {
|
| 16 |
+
"model_id": "openai/clip-vit-base-patch32",
|
| 17 |
+
"index_version": "v1",
|
| 18 |
+
"index_file": "index_fast.npy",
|
| 19 |
+
"meta_file": "meta_fast.json",
|
| 20 |
+
"label": "Lite β ViT-B/32 (fast, less accurate)",
|
| 21 |
+
},
|
| 22 |
+
"pro": {
|
| 23 |
+
"model_id": "openai/clip-vit-large-patch14",
|
| 24 |
+
"index_version": "v2",
|
| 25 |
+
"index_file": "index_pro.npy",
|
| 26 |
+
"meta_file": "meta_pro.json",
|
| 27 |
+
"label": "Pro β ViT-L/14 (slower, more accurate)",
|
| 28 |
+
},
|
| 29 |
+
"geo": {
|
| 30 |
+
"model_id": "geolocal/StreetCLIP",
|
| 31 |
+
"index_version": "v3",
|
| 32 |
+
"index_file": "index_geo.npy",
|
| 33 |
+
"meta_file": "meta_geo.json",
|
| 34 |
+
"label": "Geo β StreetCLIP image retrieval only",
|
| 35 |
+
},
|
| 36 |
+
"geoplus": {
|
| 37 |
+
"model_id": "geolocal/StreetCLIP",
|
| 38 |
+
"index_version": "v3",
|
| 39 |
+
"index_file": "index_geo.npy",
|
| 40 |
+
"meta_file": "meta_geo.json",
|
| 41 |
+
"text_file": "text_geo.npy",
|
| 42 |
+
"text_countries_file": "text_geo_countries.json",
|
| 43 |
+
"label": "Geo+ β StreetCLIP + clue-text",
|
| 44 |
+
},
|
| 45 |
+
"atlas": {
|
| 46 |
+
"model_id": "geolocal/StreetCLIP",
|
| 47 |
+
"head_file": "atlas_head.npz",
|
| 48 |
+
"label": "Atlas β learned classifier (best)",
|
| 49 |
+
},
|
| 50 |
+
"serbia": {
|
| 51 |
+
"model_id": "geolocal/StreetCLIP",
|
| 52 |
+
"region_file": "serbia_index.npz",
|
| 53 |
+
"country_slug": "serbia",
|
| 54 |
+
"country_name": "Serbia",
|
| 55 |
+
"label": "Serbia β pinpoint locator (Serbia map only)",
|
| 56 |
+
},
|
| 57 |
+
"krajina": {
|
| 58 |
+
"model_id": "geolocal/StreetCLIP",
|
| 59 |
+
"region_file": "krajina_index.npz",
|
| 60 |
+
"country_slug": "krajina",
|
| 61 |
+
"country_name": "Krajina",
|
| 62 |
+
"label": "Krajina β pinpoint locator (Serbian Krajina map only)",
|
| 63 |
+
},
|
| 64 |
+
"usa": {
|
| 65 |
+
"model_id": "geolocal/StreetCLIP",
|
| 66 |
+
"region_file": "usa_index.npz",
|
| 67 |
+
"country_slug": "usa",
|
| 68 |
+
"country_name": "USA",
|
| 69 |
+
"map_id": "69c14929fc130ff0dfb0b916",
|
| 70 |
+
"label": "USA β pinpoint locator (USA map only)",
|
| 71 |
+
},
|
| 72 |
+
"canada": {
|
| 73 |
+
"model_id": "geolocal/StreetCLIP",
|
| 74 |
+
"region_file": "canada_index.npz",
|
| 75 |
+
"country_slug": "canada",
|
| 76 |
+
"country_name": "Canada",
|
| 77 |
+
"map_id": "69c13f8a66d5a179c340dd12",
|
| 78 |
+
"label": "Canada β pinpoint locator (Canada map only)",
|
| 79 |
+
},
|
| 80 |
+
"brazil": {
|
| 81 |
+
"model_id": "geolocal/StreetCLIP",
|
| 82 |
+
"region_file": "brazil_index.npz",
|
| 83 |
+
"country_slug": "brazil",
|
| 84 |
+
"country_name": "Brazil",
|
| 85 |
+
"map_id": "69c13f7066d5a179c340ac38",
|
| 86 |
+
"label": "Brazil β pinpoint locator (Brazil map only)",
|
| 87 |
+
},
|
| 88 |
+
"argentina": {
|
| 89 |
+
"model_id": "geolocal/StreetCLIP",
|
| 90 |
+
"region_file": "argentina_index.npz",
|
| 91 |
+
"country_slug": "argentina",
|
| 92 |
+
"country_name": "Argentina",
|
| 93 |
+
"map_id": "69c13c6466d5a179c33f9e16",
|
| 94 |
+
"label": "Argentina β pinpoint locator (Argentina map only)",
|
| 95 |
+
},
|
| 96 |
+
"russia": {
|
| 97 |
+
"model_id": "geolocal/StreetCLIP",
|
| 98 |
+
"region_file": "russia_index.npz",
|
| 99 |
+
"country_slug": "russia",
|
| 100 |
+
"country_name": "Russia",
|
| 101 |
+
"map_id": "69c1457b047406872f3c80f4",
|
| 102 |
+
"label": "Russia β pinpoint locator (Russia map only)",
|
| 103 |
+
},
|
| 104 |
+
"indonesia": {
|
| 105 |
+
"model_id": "geolocal/StreetCLIP",
|
| 106 |
+
"region_file": "indonesia_index.npz",
|
| 107 |
+
"country_slug": "indonesia",
|
| 108 |
+
"country_name": "Indonesia",
|
| 109 |
+
"map_id": "69c141f7200aba567e5352a8",
|
| 110 |
+
"label": "Indonesia β pinpoint locator (Indonesia map only)",
|
| 111 |
+
},
|
| 112 |
+
"peru": {
|
| 113 |
+
"model_id": "geolocal/StreetCLIP",
|
| 114 |
+
"region_file": "peru_index.npz",
|
| 115 |
+
"country_slug": "peru",
|
| 116 |
+
"country_name": "Peru",
|
| 117 |
+
"map_id": "69c144a7047406872f3be463",
|
| 118 |
+
"label": "Peru β pinpoint locator (Peru map only)",
|
| 119 |
+
},
|
| 120 |
+
"italy": {
|
| 121 |
+
"model_id": "geolocal/StreetCLIP",
|
| 122 |
+
"region_file": "italy_index.npz",
|
| 123 |
+
"country_slug": "italy",
|
| 124 |
+
"country_name": "Italy",
|
| 125 |
+
"map_id": "69c14219200aba567e538c4c",
|
| 126 |
+
"label": "Italy β pinpoint locator (Italy map only)",
|
| 127 |
+
},
|
| 128 |
+
"malaysia": {
|
| 129 |
+
"model_id": "geolocal/StreetCLIP",
|
| 130 |
+
"region_file": "malaysia_index.npz",
|
| 131 |
+
"country_slug": "malaysia",
|
| 132 |
+
"country_name": "Malaysia",
|
| 133 |
+
"map_id": "69c14385047406872f3af85c",
|
| 134 |
+
"label": "Malaysia β pinpoint locator (Malaysia map only)",
|
| 135 |
+
},
|
| 136 |
+
"colombia": {
|
| 137 |
+
"model_id": "geolocal/StreetCLIP",
|
| 138 |
+
"region_file": "colombia_index.npz",
|
| 139 |
+
"country_slug": "colombia",
|
| 140 |
+
"country_name": "Colombia",
|
| 141 |
+
"label": "Colombia β pinpoint locator (Colombia map only)",
|
| 142 |
+
},
|
| 143 |
+
"chile": {
|
| 144 |
+
"model_id": "geolocal/StreetCLIP",
|
| 145 |
+
"region_file": "chile_index.npz",
|
| 146 |
+
"country_slug": "chile",
|
| 147 |
+
"country_name": "Chile",
|
| 148 |
+
"map_id": "69c140b8200aba567e525880",
|
| 149 |
+
"label": "Chile β pinpoint locator (Chile map only)",
|
| 150 |
+
},
|
| 151 |
+
"new-zealand": {
|
| 152 |
+
"model_id": "geolocal/StreetCLIP",
|
| 153 |
+
"region_file": "new-zealand_index.npz",
|
| 154 |
+
"country_slug": "new-zealand",
|
| 155 |
+
"country_name": "New Zealand",
|
| 156 |
+
"map_id": "69c14422047406872f3b6e4f",
|
| 157 |
+
"label": "New Zealand β pinpoint locator (New Zealand map only)",
|
| 158 |
+
},
|
| 159 |
+
}
|
| 160 |
+
DEFAULT_MODEL = "atlas"
|