Spaces:
Running
Running
Load Space map assets from runtime URLs
#5
by Dontcryx - opened
- README.md +9 -1
- backend/Odin.py +14 -0
- backend/pathfinder.py +20 -3
- frontend/src/components/SimCanvas.jsx +26 -12
README.md
CHANGED
|
@@ -178,6 +178,14 @@ Set values in `.env` (copy from `.env.local` first).
|
|
| 178 |
| `SIM_GEMINI_MODEL` | `gemini-3.1-flash-lite` | Gemini model used by the simulation |
|
| 179 |
| `SIM_CREATIVITY` | `1.0` | Creativity dial for plans/dialogue |
|
| 180 |
| `SIM_WELLBEING_VARIABILITY` | `0.75` | Non-LLM variability in wellbeing updates |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
## Project Structure
|
| 183 |
|
|
@@ -210,4 +218,4 @@ requirements.txt # Python dependencies
|
|
| 210 |
1. Fork and create a feature branch.
|
| 211 |
2. Keep changes focused and documented.
|
| 212 |
3. Validate local run paths before opening a PR.
|
| 213 |
-
4. Include a clear summary of behavior changes and test notes.
|
|
|
|
| 178 |
| `SIM_GEMINI_MODEL` | `gemini-3.1-flash-lite` | Gemini model used by the simulation |
|
| 179 |
| `SIM_CREATIVITY` | `1.0` | Creativity dial for plans/dialogue |
|
| 180 |
| `SIM_WELLBEING_VARIABILITY` | `0.75` | Non-LLM variability in wellbeing updates |
|
| 181 |
+
| `MAP_IMAGE_URL` | `""` | Public HTTPS URL for the daytime map displayed in the browser |
|
| 182 |
+
| `MAP_NIGHT_IMAGE_URL` | `""` | Public HTTPS URL for the night-map overlay displayed in the browser |
|
| 183 |
+
| `PATH_IMAGE_URL` | `""` | Public HTTPS URL for the walkable-path PNG used by the backend |
|
| 184 |
+
|
| 185 |
+
For a Hugging Face Space that keeps images in GitHub, add these as **Variables**
|
| 186 |
+
(not Secrets) in **Settings → Variables and secrets**. Use GitHub raw-content
|
| 187 |
+
URLs, for example `https://raw.githubusercontent.com/OWNER/REPO/main/assets/map.png`.
|
| 188 |
+
Set all three URLs; `PATH_IMAGE_URL` is required for backend route calculation.
|
| 189 |
|
| 190 |
## Project Structure
|
| 191 |
|
|
|
|
| 218 |
1. Fork and create a feature branch.
|
| 219 |
2. Keep changes focused and documented.
|
| 220 |
3. Validate local run paths before opening a PR.
|
| 221 |
+
4. Include a clear summary of behavior changes and test notes.
|
backend/Odin.py
CHANGED
|
@@ -962,6 +962,20 @@ def get_entrypoints():
|
|
| 962 |
return json.load(f)
|
| 963 |
|
| 964 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
# Serve React production build (dist/) if it exists, else fallback to frontend/
|
| 966 |
import mimetypes
|
| 967 |
# On Windows, the registry often maps .js to text/plain, which makes browsers
|
|
|
|
| 962 |
return json.load(f)
|
| 963 |
|
| 964 |
|
| 965 |
+
@app.get("/api/assets")
|
| 966 |
+
def get_asset_urls():
|
| 967 |
+
"""Return public map URLs configured on the running Space.
|
| 968 |
+
|
| 969 |
+
Hugging Face Space variables are runtime environment variables, while the
|
| 970 |
+
Vite bundle is built earlier. The browser reads this endpoint so changing
|
| 971 |
+
a Space variable does not require committing image files to the Space.
|
| 972 |
+
"""
|
| 973 |
+
return {
|
| 974 |
+
"map_image_url": os.environ.get("MAP_IMAGE_URL", "").strip(),
|
| 975 |
+
"night_image_url": os.environ.get("MAP_NIGHT_IMAGE_URL", "").strip(),
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
|
| 979 |
# Serve React production build (dist/) if it exists, else fallback to frontend/
|
| 980 |
import mimetypes
|
| 981 |
# On Windows, the registry often maps .js to text/plain, which makes browsers
|
backend/pathfinder.py
CHANGED
|
@@ -6,7 +6,10 @@ BFS shortest_path(), stats(), and is_walkable() helpers.
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
import threading
|
|
|
|
| 9 |
from collections import deque
|
|
|
|
|
|
|
| 10 |
from PIL import Image
|
| 11 |
|
| 12 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
@@ -31,10 +34,24 @@ def _load():
|
|
| 31 |
os.path.join(ROOT, "frontend", "path.png"),
|
| 32 |
os.path.join(ROOT, "frontend", "dist", "path.png"),
|
| 33 |
]
|
|
|
|
|
|
|
| 34 |
try:
|
| 35 |
-
if path_file
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
rgb = image.convert("RGB")
|
| 39 |
_W, _H = rgb.size
|
| 40 |
pix = rgb.load()
|
|
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
import threading
|
| 9 |
+
from io import BytesIO
|
| 10 |
from collections import deque
|
| 11 |
+
from urllib.parse import urlparse
|
| 12 |
+
from urllib.request import Request, urlopen
|
| 13 |
from PIL import Image
|
| 14 |
|
| 15 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
| 34 |
os.path.join(ROOT, "frontend", "path.png"),
|
| 35 |
os.path.join(ROOT, "frontend", "dist", "path.png"),
|
| 36 |
]
|
| 37 |
+
path_file = next((p for p in candidates if os.path.exists(p)), None)
|
| 38 |
+
path_url = os.environ.get("PATH_IMAGE_URL", "").strip()
|
| 39 |
try:
|
| 40 |
+
if path_file:
|
| 41 |
+
image_source = path_file
|
| 42 |
+
else:
|
| 43 |
+
if not path_url:
|
| 44 |
+
raise FileNotFoundError("path.png not found and PATH_IMAGE_URL is not configured")
|
| 45 |
+
if urlparse(path_url).scheme != "https":
|
| 46 |
+
raise ValueError("PATH_IMAGE_URL must be an https URL")
|
| 47 |
+
request = Request(path_url, headers={"User-Agent": "Valhalla/1.0"})
|
| 48 |
+
with urlopen(request, timeout=20) as response:
|
| 49 |
+
image_bytes = response.read(20 * 1024 * 1024 + 1)
|
| 50 |
+
if len(image_bytes) > 20 * 1024 * 1024:
|
| 51 |
+
raise ValueError("PATH_IMAGE_URL exceeds the 20 MB download limit")
|
| 52 |
+
image_source = BytesIO(image_bytes)
|
| 53 |
+
|
| 54 |
+
with Image.open(image_source) as image:
|
| 55 |
rgb = image.convert("RGB")
|
| 56 |
_W, _H = rgb.size
|
| 57 |
pix = rgb.load()
|
frontend/src/components/SimCanvas.jsx
CHANGED
|
@@ -26,32 +26,46 @@ export default function SimCanvas({ snapshot, focusedId, onFocus }) {
|
|
| 26 |
|
| 27 |
useEffect(() => { focusRef.current = focusedId; }, [focusedId]);
|
| 28 |
|
| 29 |
-
//
|
| 30 |
useEffect(() => {
|
| 31 |
-
|
|
|
|
| 32 |
const baseUrl = import.meta.env?.BASE_URL || "/";
|
| 33 |
const cleanBase = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
|
| 34 |
-
const candidates = [
|
| 35 |
-
filename,
|
| 36 |
-
`./${filename}`,
|
| 37 |
-
`/${filename}`,
|
| 38 |
-
`${cleanBase}${filename}`
|
| 39 |
-
];
|
| 40 |
let idx = 0;
|
| 41 |
const loadNext = () => {
|
| 42 |
if (idx >= candidates.length) return;
|
| 43 |
const img = new Image();
|
| 44 |
const src = candidates[idx++];
|
| 45 |
-
img.onload = () => onLoaded(img);
|
| 46 |
img.onerror = () => loadNext();
|
| 47 |
img.src = src;
|
| 48 |
};
|
| 49 |
loadNext();
|
| 50 |
};
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}, []);
|
| 56 |
|
| 57 |
// Update targets from snapshot
|
|
|
|
| 26 |
|
| 27 |
useEffect(() => { focusRef.current = focusedId; }, [focusedId]);
|
| 28 |
|
| 29 |
+
// Read URLs from the backend so Hugging Face Space variables work at runtime.
|
| 30 |
useEffect(() => {
|
| 31 |
+
let disposed = false;
|
| 32 |
+
const tryLoadImage = (urls, filename, onLoaded) => {
|
| 33 |
const baseUrl = import.meta.env?.BASE_URL || "/";
|
| 34 |
const cleanBase = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
|
| 35 |
+
const candidates = [...urls, filename, `./${filename}`, `/${filename}`, `${cleanBase}${filename}`];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
let idx = 0;
|
| 37 |
const loadNext = () => {
|
| 38 |
if (idx >= candidates.length) return;
|
| 39 |
const img = new Image();
|
| 40 |
const src = candidates[idx++];
|
| 41 |
+
img.onload = () => { if (!disposed) onLoaded(img); };
|
| 42 |
img.onerror = () => loadNext();
|
| 43 |
img.src = src;
|
| 44 |
};
|
| 45 |
loadNext();
|
| 46 |
};
|
| 47 |
|
| 48 |
+
async function loadMaps() {
|
| 49 |
+
let assets = {};
|
| 50 |
+
try {
|
| 51 |
+
const response = await fetch(`${import.meta.env.BASE_URL}api/assets`);
|
| 52 |
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 53 |
+
assets = await response.json();
|
| 54 |
+
} catch (error) {
|
| 55 |
+
console.warn("Could not read remote map configuration; using local assets.", error);
|
| 56 |
+
}
|
| 57 |
+
if (disposed) return;
|
| 58 |
+
tryLoadImage(assets.map_image_url ? [assets.map_image_url] : [], "map.png", (img) => {
|
| 59 |
+
mapImgRef.current = img;
|
| 60 |
+
});
|
| 61 |
+
tryLoadImage(assets.night_image_url ? [assets.night_image_url] : [], "map_night.png", (img) => {
|
| 62 |
+
nightImgRef.current = img;
|
| 63 |
+
});
|
| 64 |
+
startLoop();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
loadMaps();
|
| 68 |
+
return () => { disposed = true; };
|
| 69 |
}, []);
|
| 70 |
|
| 71 |
// Update targets from snapshot
|