Fffffd commited on
Commit
47d331f
·
verified ·
1 Parent(s): 6a5ebd7

Delete drishx.py

Browse files
Files changed (1) hide show
  1. drishx.py +0 -594
drishx.py DELETED
@@ -1,594 +0,0 @@
1
- import os
2
- import time
3
- import json
4
- import logging
5
- import pickle
6
- import requests
7
- import imageio.v3 as imageio
8
- import numpy as np
9
- import geopandas as gpd
10
- from datetime import datetime, timedelta
11
- from typing import List
12
- from concurrent.futures import ThreadPoolExecutor, as_completed
13
- from shapely.geometry import LineString
14
- from requests.adapters import HTTPAdapter
15
- from urllib3.util.retry import Retry
16
-
17
- from fastapi import FastAPI, HTTPException
18
- from fastapi.responses import StreamingResponse, JSONResponse
19
- from fastapi.staticfiles import StaticFiles
20
- from fastapi.middleware.cors import CORSMiddleware
21
- from pydantic import BaseModel
22
- import uvicorn
23
-
24
- import osmnx as ox
25
- from sentinelhub import (
26
- SHConfig, SentinelHubRequest, DataCollection,
27
- BBox, CRS, MimeType, SentinelHubCatalog,
28
- )
29
- from dotenv import load_dotenv
30
-
31
- # ─────────────────────────────────────────────────────────────────────────────
32
- # Setup
33
- # ─────────────────────────────────────────────────────────────────────────────
34
-
35
- load_dotenv()
36
-
37
- logging.basicConfig(level=logging.INFO)
38
- logger = logging.getLogger("ARGUS")
39
-
40
- SECONDS_OFFSET_B02_B04 = 1.01
41
-
42
- # Hugging Face Spaces için Storage
43
- DATA_DIR = os.path.join(os.getcwd(), "data")
44
- DETECTION_DIR = os.path.join(DATA_DIR, "sentinel_data/detections")
45
- os.makedirs(DETECTION_DIR, exist_ok=True)
46
-
47
- # OSM verileri için cache
48
- os.makedirs(os.path.join(DATA_DIR, "osm_cache"), exist_ok=True)
49
-
50
- OVERPASS_MIRRORS = [
51
- "https://lz4.overpass-api.de/api/interpreter",
52
- "https://z.overpass-api.de/api/interpreter",
53
- "https://overpass.osm.ch/api/interpreter",
54
- "https://overpass-api.de/api/interpreter",
55
- ]
56
-
57
- _session = requests.Session()
58
- _retry = Retry(
59
- total=2, backoff_factor=1.0,
60
- status_forcelist=[429, 500, 502, 503, 504],
61
- allowed_methods=["GET", "POST"],
62
- )
63
- _adapter = HTTPAdapter(max_retries=_retry)
64
- _session.mount("http://", _adapter)
65
- _session.mount("https://", _adapter)
66
-
67
- ox.settings.requests_session = _session
68
- ox.settings.requests_timeout = 30
69
- ox.settings.overpass_rate_limit = False
70
- ox.settings.max_query_area_size = 1_000_000_000_000
71
- ox.settings.log_console = False
72
- ox.settings.use_cache = True
73
- ox.settings.cache_folder = os.path.join(DATA_DIR, "osm_cache")
74
-
75
- # Copernicus Data Space config - Hugging Face env'den oku
76
- CONFIG = SHConfig()
77
- CONFIG.sh_client_id = os.getenv("COPERNICUS_CLIENT_ID", "")
78
- CONFIG.sh_client_secret = os.getenv("COPERNICUS_CLIENT_SECRET", "")
79
- CONFIG.sh_base_url = "https://sh.dataspace.copernicus.eu"
80
- CONFIG.sh_token_url = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
81
- CONFIG.cache_dir = os.path.join(DATA_DIR, "sh_cache")
82
- os.makedirs(CONFIG.cache_dir, exist_ok=True)
83
-
84
- logger.info(f"DrishX Storage Link: {DATA_DIR}")
85
-
86
- # Hugging Face Spaces'te token'ı env'den al
87
- if os.getenv("COPERNICUS_TOKEN"):
88
- CONFIG.sh_token = os.getenv("COPERNICUS_TOKEN")
89
- logger.info("Copernicus token loaded from environment")
90
-
91
- FEATURED_SITES = [
92
- {"id": "v1", "name": "Braunschweig A7", "bbox": [52.25, 10.45, 52.32, 10.55], "country": "Germany", "type": "high_volume"},
93
- {"id": "v2", "name": "Frankfurt A3", "bbox": [50.05, 8.55, 50.12, 8.65], "country": "Germany", "type": "high_volume"},
94
- {"id": "v3", "name": "Karlsruhe A5", "bbox": [48.95, 8.35, 49.05, 8.45], "country": "Germany", "type": "standard"},
95
- ]
96
-
97
- # ─────────────────────────────────────────────────────────────────────────────
98
- # Helper functions (kısaltılmış - aynı kod)
99
- # ─────────────────────────────────────────────────────────────────────────────
100
-
101
- def normalized_ratio(a, b):
102
- denom = a + b
103
- with np.errstate(divide="ignore", invalid="ignore"):
104
- result = np.where(denom != 0, (a - b) / denom, 0.0)
105
- return result.astype(np.float32)
106
-
107
- def rescale_s2(bands):
108
- bands = bands.astype(np.float32)
109
- if np.nanmax(bands) > 10:
110
- bands /= 10000.0
111
- return bands
112
-
113
- def pick_arr_subset(arr, y, x, size):
114
- size_low = size // 2
115
- size_up = size // 2
116
- if size_low + size_up < size:
117
- size_up += 1
118
- ymin = max(0, y - size_low)
119
- ymax = max(0, y + size_up)
120
- xmin = max(0, x - size_low)
121
- xmax = max(0, x + size_up)
122
- if arr.ndim == 2:
123
- return arr[ymin:ymax, xmin:xmax]
124
- elif arr.ndim == 3:
125
- return arr[:, ymin:ymax, xmin:xmax]
126
- return arr
127
-
128
- def build_feature_stack(data):
129
- R = data[:, :, 0].astype(np.float32)
130
- G = data[:, :, 1].astype(np.float32)
131
- B = data[:, :, 2].astype(np.float32)
132
- NIR = data[:, :, 3].astype(np.float32)
133
- CLM = data[:, :, 4]
134
-
135
- bands = np.stack([R, G, B, NIR], axis=0)
136
- bands = rescale_s2(bands)
137
- R, G, B, NIR = bands[0], bands[1], bands[2], bands[3]
138
-
139
- cloud = CLM > 0
140
- R[cloud] = np.nan
141
- G[cloud] = np.nan
142
- B[cloud] = np.nan
143
- NIR[cloud] = np.nan
144
-
145
- H, W = R.shape
146
- fs = np.zeros((7, H, W), dtype=np.float32)
147
-
148
- if np.any(~cloud):
149
- import warnings
150
- with warnings.catch_warnings():
151
- warnings.simplefilter("ignore", category=RuntimeWarning)
152
- fs[0] = np.nanvar(np.stack([R, G, B], axis=0), axis=0, ddof=0)
153
- fs[1] = normalized_ratio(R, B)
154
- fs[2] = normalized_ratio(G, B)
155
- fs[3] = R - np.nanmean(R)
156
- fs[4] = G - np.nanmean(G)
157
- fs[5] = B - np.nanmean(B)
158
- fs[6] = NIR - np.nanmean(NIR)
159
- else:
160
- fs.fill(np.nan)
161
-
162
- nan_mask = np.isnan(fs[3])
163
- fs[:, nan_mask] = np.nan
164
-
165
- return {
166
- "feature_stack": fs,
167
- "bands": {"R": R, "G": G, "B": B, "NIR": NIR},
168
- "cloud_mask": cloud,
169
- }
170
-
171
- # ─────────────────────────────────────────────────────────────────────────────
172
- # RF Model (basitleştirilmiş proxy)
173
- # ─────────────────────────────────────────────────────────────────────────────
174
-
175
- _rf_model = None
176
-
177
- def load_rf_model(path=None):
178
- global _rf_model
179
- return None # Hugging Face'te model yok, proxy kullan
180
-
181
- def proxy_classify(feature_stack, road_mask):
182
- fs = feature_stack
183
- H, W = fs.shape[1], fs.shape[2]
184
- probs = np.zeros((4, H, W), dtype=np.float32)
185
-
186
- centered_R = fs[3]
187
- centered_G = fs[4]
188
- centered_B = fs[5]
189
- var_feat = fs[0]
190
- nratio_rb = fs[1]
191
- nratio_gb = fs[2]
192
-
193
- rm = road_mask.astype(bool)
194
- nan_mask = np.isnan(centered_R)
195
-
196
- blue_score = np.clip(-nratio_rb * 2 + centered_B * 5 + var_feat * 10, 0, None)
197
- blue_score[~rm | nan_mask] = 0
198
-
199
- green_score = np.clip(nratio_gb * 2 + centered_G * 5 + var_feat * 10, 0, None)
200
- green_score[~rm | nan_mask] = 0
201
-
202
- red_score = np.clip(nratio_rb * 2 + centered_R * 5 + var_feat * 10, 0, None)
203
- red_score[~rm | nan_mask] = 0
204
-
205
- total = blue_score + green_score + red_score + 1e-8
206
- probs[1] = blue_score / total
207
- probs[2] = green_score / total
208
- probs[3] = red_score / total
209
- probs[0] = 1.0 - np.max(probs[1:], axis=0)
210
-
211
- probs[0][probs[0] < 0.75] = 0
212
-
213
- classification = np.nanargmax(probs, axis=0).astype(np.int8) + 1
214
- classification[np.max(probs, axis=0) == 0] = 0
215
- classification[nan_mask] = 0
216
- classification[~rm] = 0
217
-
218
- return probs, classification
219
-
220
- # ─────────────────────────────────────────────────────────────────────────────
221
- # ObjectExtractor (kısaltılmış)
222
- # ─────────────────────────────────────────────────────────────────────────────
223
-
224
- class ObjectExtractor:
225
- def __init__(self, probabilities, lat_arr, lon_arr):
226
- self.probabilities = probabilities
227
- self.lat = lat_arr
228
- self.lon = lon_arr
229
-
230
- def extract(self, predictions_arr):
231
- preds = predictions_arr.copy()
232
- probs = self.probabilities.copy()
233
- preds[preds == 1] = 0
234
- blue_ys, blue_xs = np.where(preds == 2)
235
- detections = []
236
- sub_size = 9
237
-
238
- for i in range(len(blue_ys)):
239
- y_blue, x_blue = int(blue_ys[i]), int(blue_xs[i])
240
- if preds[y_blue, x_blue] == 0:
241
- continue
242
-
243
- subset_9 = pick_arr_subset(preds, y_blue, x_blue, sub_size).copy()
244
- subset_3 = pick_arr_subset(preds, y_blue, x_blue, 3).copy()
245
- subset_9_probs = pick_arr_subset(probs, y_blue, x_blue, sub_size).copy()
246
-
247
- half_idx_y = y_blue if subset_9.shape[0] < sub_size else subset_9.shape[0] // 2
248
- half_idx_x = x_blue if subset_9.shape[1] < sub_size else subset_9.shape[1] // 2
249
-
250
- if not all(v in subset_9 for v in [2, 3, 4]):
251
- continue
252
-
253
- det = {
254
- "lat": float(self.lat[y_blue]),
255
- "lon": float(self.lon[x_blue]),
256
- "confidence": 0.7,
257
- "speed_kmh": 80.0,
258
- "heading": 0.0,
259
- "timestamp": datetime.now().isoformat(),
260
- "id": f"truck_{int(time.time()*1000)}_{y_blue}_{x_blue}.png",
261
- }
262
- detections.append(det)
263
-
264
- return detections
265
-
266
- # ─���───────────────────────────────────────────────────────────────────────────
267
- # ARGUS Engine
268
- # ─────────────────────────────────────────────────────────────────────────────
269
-
270
- class ARGUSEngine:
271
- def __init__(self):
272
- self.history = []
273
- self.rf_model = load_rf_model()
274
-
275
- def fetch_roads(self, bbox_coords, progress_cb=None):
276
- min_lat, min_lon, max_lat, max_lon = bbox_coords
277
- center_lat = (min_lat + max_lat) / 2
278
- center_lon = (min_lon + max_lon) / 2
279
-
280
- lat_span = (max_lat - min_lat) * 111000
281
- lon_span = (max_lon - min_lon) * 111000 * np.cos(np.radians(center_lat))
282
- dist_m = int(max(lat_span, lon_span) * 0.6) + 1000
283
-
284
- for mirror in OVERPASS_MIRRORS:
285
- ox.settings.overpass_url = mirror
286
- try:
287
- graph = ox.graph_from_point(
288
- (center_lat, center_lon), dist=dist_m,
289
- network_type="drive", simplify=True,
290
- retain_all=False, truncate_by_edge=True,
291
- )
292
- roads = ox.graph_to_gdfs(graph, nodes=False)
293
- major_types = ["motorway", "trunk", "primary", "secondary", "motorway_link", "trunk_link"]
294
- roads = roads[roads["highway"].isin(major_types)].copy()
295
- if not roads.empty:
296
- return roads
297
- except Exception as e:
298
- logger.warning(f"Mirror failed: {e}")
299
- time.sleep(1)
300
-
301
- return gpd.GeoDataFrame()
302
-
303
- def detect_trucks(self, data, bbox_coords, timestamp, road_mask):
304
- min_lat, min_lon, max_lat, max_lon = bbox_coords
305
- H, W = data.shape[:2]
306
-
307
- feat = build_feature_stack(data)
308
- feature_stack = feat["feature_stack"]
309
-
310
- probs, prediction = proxy_classify(feature_stack, road_mask)
311
-
312
- lat_arr = np.linspace(max_lat, min_lat, H)
313
- lon_arr = np.linspace(min_lon, max_lon, W)
314
-
315
- extractor = ObjectExtractor(probs, lat_arr, lon_arr)
316
- detections = extractor.extract(prediction)
317
-
318
- for det in detections:
319
- det["timestamp"] = timestamp
320
- self._save_crop(data, det, H, W)
321
-
322
- return detections
323
-
324
- def _save_crop(self, data, det, H, W):
325
- try:
326
- rgb = data[:20, :20, :3].astype(np.float32)
327
- rgb = rescale_s2(rgb)
328
- rgb = (np.clip(rgb, 0, 0.3) / 0.3 * 255).astype(np.uint8)
329
- path = os.path.join(DETECTION_DIR, det["id"])
330
- imageio.imwrite(path, rgb)
331
- except Exception as e:
332
- logger.warning(f"Could not save crop: {e}")
333
-
334
- # ─────────────────────────────────────────────────────────────────────────────
335
- # FastAPI Application
336
- # ─────────────────────────────────────────────────────────────────────────────
337
-
338
- app = FastAPI(title="ARGUS Corridor Intelligence")
339
- app.add_middleware(
340
- CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
341
- )
342
-
343
- engine = ARGUSEngine()
344
-
345
- class AnalyzeRequest(BaseModel):
346
- bbox: List[float]
347
- label: str = "New Mission"
348
- months: int = 4
349
- max_frames: int = 10
350
-
351
- @app.post("/api/analyze")
352
- async def analyze(req: AnalyzeRequest):
353
- async def event_generator():
354
- try:
355
- def progress(msg, pct):
356
- return json.dumps({"type": "progress", "message": msg, "percent": pct}) + "\n"
357
-
358
- yield progress(f"Starting analysis for: {req.label}", 0)
359
-
360
- min_lat, min_lon, max_lat, max_lon = req.bbox
361
- if abs(max_lat - min_lat) > 0.5 or abs(max_lon - min_lon) > 0.5:
362
- yield json.dumps({"type": "error", "message": "AOI too large."}) + "\n"
363
- return
364
-
365
- yield progress("Running Road Discovery Pipeline...", 10)
366
- roads = engine.fetch_roads(req.bbox)
367
- if roads.empty:
368
- yield json.dumps({"type": "error", "message": "No major roads found."}) + "\n"
369
- return
370
- yield progress(f"Found {len(roads)} road corridors.", 25)
371
-
372
- sh_bbox = BBox(bbox=[min_lon, min_lat, max_lon, max_lat], crs=CRS.WGS84)
373
- yield progress("Searching Copernicus catalog...", 30)
374
-
375
- catalog = SentinelHubCatalog(config=CONFIG)
376
- end_date = datetime.now()
377
- start_date = end_date - timedelta(days=max(1, req.months) * 30)
378
-
379
- cdse_collection = DataCollection.SENTINEL2_L2A.define_from(
380
- "s2l2a", service_url=CONFIG.sh_base_url,
381
- )
382
-
383
- search_results = list(catalog.search(
384
- cdse_collection, bbox=sh_bbox,
385
- datetime=f"{start_date.strftime('%Y-%m-%dT00:00:00Z')}/{end_date.strftime('%Y-%m-%dT23:59:59Z')}",
386
- filter="eo:cloud_cover < 60",
387
- fields={"include": ["properties.datetime", "id"], "exclude": []}
388
- ))
389
-
390
- unique_scenes = {}
391
- for res in search_results:
392
- date_key = res["properties"]["datetime"][:10]
393
- if date_key not in unique_scenes:
394
- unique_scenes[date_key] = res
395
-
396
- final_obs = [unique_scenes[d] for d in sorted(unique_scenes.keys(), reverse=True)]
397
- final_obs = final_obs[:req.max_frames]
398
-
399
- if not final_obs:
400
- yield json.dumps({"type": "error", "message": "No clear imagery found."}) + "\n"
401
- return
402
-
403
- yield progress(f"Found {len(final_obs)} unique clear overpasses.", 40)
404
-
405
- # İlk frame'i işle
406
- first_obs = final_obs[0]
407
- date_str = first_obs["properties"]["datetime"]
408
-
409
- req_sh = SentinelHubRequest(
410
- evalscript="""//VERSION=3
411
- function setup() {
412
- return {
413
- input: ["B02", "B03", "B04", "B08", "CLM"],
414
- output: { id: "default", bands: 5, sampleType: "FLOAT32" }
415
- };
416
- }
417
- function evaluatePixel(s) {
418
- return [s.B04, s.B03, s.B02, s.B08, s.CLM];
419
- }""",
420
- input_data=[SentinelHubRequest.input_data(
421
- data_collection=cdse_collection,
422
- time_interval=(date_str, date_str),
423
- )],
424
- responses=[SentinelHubRequest.output_response("default", MimeType.TIFF)],
425
- bbox=sh_bbox, config=CONFIG,
426
- size=(512, 512), # Hugging Face için boyut sınırı
427
- )
428
-
429
- data_list = req_sh.get_data()
430
- if not data_list:
431
- yield json.dumps({"type": "error", "message": "Failed to get satellite data."}) + "\n"
432
- return
433
-
434
- seed_data = data_list[0]
435
- h, w = seed_data.shape[:2]
436
-
437
- # Road mask
438
- from rasterio import features as rio_features, transform as rio_transform
439
- roads_buf = roads.to_crs(epsg=3857).buffer(20).to_crs(epsg=4326)
440
- trans = rio_transform.from_bounds(min_lon, min_lat, max_lon, max_lat, w, h)
441
- road_mask = rio_features.rasterize(
442
- [(geom.__geo_interface__, 1) for geom in roads_buf.geometry],
443
- out_shape=(h, w), transform=trans, fill=0, all_touched=True,
444
- )
445
-
446
- seed_dets = engine.detect_trucks(seed_data, req.bbox, date_str, road_mask)
447
-
448
- detections = seed_dets
449
- mission_id = str(int(time.time()))
450
-
451
- engine.history.append({
452
- "mission_id": mission_id,
453
- "label": req.label,
454
- "bbox": req.bbox,
455
- "road_count": len(roads),
456
- "detections": detections,
457
- "timestamp": datetime.now().isoformat(),
458
- })
459
-
460
- yield json.dumps({
461
- "type": "result",
462
- "mission_id": mission_id,
463
- "road_count": len(roads),
464
- "detection_count": len(detections),
465
- "message": f"Complete: {len(detections)} truck signatures detected.",
466
- }) + "\n"
467
-
468
- except Exception as e:
469
- logger.error(f"Stream error: {e}", exc_info=True)
470
- yield json.dumps({"type": "error", "message": str(e)}) + "\n"
471
-
472
- return StreamingResponse(event_generator(), media_type="application/x-ndjson")
473
-
474
- @app.get("/api/sites")
475
- async def get_sites():
476
- sites = [
477
- {
478
- "id": s["id"], "name": s["name"],
479
- "lat": (s["bbox"][0] + s["bbox"][2]) / 2,
480
- "lng": (s["bbox"][1] + s["bbox"][3]) / 2,
481
- "bbox": s["bbox"], "country": s["country"], "type": s["type"],
482
- }
483
- for s in FEATURED_SITES
484
- ]
485
- for h in engine.history:
486
- sites.append({
487
- "id": h["mission_id"], "name": h["label"],
488
- "lat": (h["bbox"][0] + h["bbox"][2]) / 2,
489
- "lng": (h["bbox"][1] + h["bbox"][3]) / 2,
490
- "bbox": h["bbox"], "country": "ROI", "type": "history",
491
- })
492
- return sites
493
-
494
- @app.get("/api/detections/{mission_id}")
495
- async def get_detections(mission_id: str):
496
- mission = next((h for h in engine.history if h["mission_id"] == mission_id), None)
497
- if not mission:
498
- raise HTTPException(status_code=404, detail="Mission not found")
499
- return mission["detections"]
500
-
501
- @app.get("/api/health")
502
- async def health_check():
503
- return {"status": "healthy", "detections": sum(len(h["detections"]) for h in engine.history)}
504
-
505
- # Static files
506
- os.makedirs("frontend", exist_ok=True)
507
- with open("frontend/index.html", "w") as f:
508
- f.write("""<!DOCTYPE html>
509
- <html>
510
- <head><title>ARGUS - DrishX</title>
511
- <style>
512
- body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; background: #0a0a0f; color: #e0e0e0; }
513
- h1 { color: #3b82f6; }
514
- button { background: #3b82f6; color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; }
515
- input, textarea { background: #1a1a2e; color: white; border: 1px solid #333; padding: 8px; border-radius: 4px; width: 100%; margin: 8px 0; }
516
- .card { background: #1a1a2e; padding: 20px; border-radius: 12px; margin: 16px 0; }
517
- .status { color: #10b981; }
518
- </style>
519
- </head>
520
- <body>
521
- <h1>🚛 ARGUS - DrishX</h1>
522
- <p>Corridor Intelligence & Truck Detection</p>
523
-
524
- <div class="card">
525
- <h3>Analyze Area</h3>
526
- <input id="label" placeholder="Mission Label" value="Test Mission">
527
- <textarea id="bbox" rows="2">[52.25, 10.45, 52.32, 10.55]</textarea>
528
- <button onclick="analyze()">Start Analysis</button>
529
- </div>
530
-
531
- <div id="result" class="card" style="display:none;"></div>
532
- <div id="progress" class="card" style="display:none;"></div>
533
-
534
- <script>
535
- async function analyze() {
536
- const bbox = JSON.parse(document.getElementById('bbox').value);
537
- const label = document.getElementById('label').value;
538
-
539
- document.getElementById('result').style.display = 'none';
540
- document.getElementById('progress').style.display = 'block';
541
- document.getElementById('progress').innerHTML = 'Starting analysis...';
542
-
543
- try {
544
- const response = await fetch('/api/analyze', {
545
- method: 'POST',
546
- headers: { 'Content-Type': 'application/json' },
547
- body: JSON.stringify({ bbox, label, months: 4, max_frames: 3 })
548
- });
549
-
550
- const reader = response.body.getReader();
551
- const decoder = new TextDecoder();
552
- let result = '';
553
-
554
- while (true) {
555
- const {done, value} = await reader.read();
556
- if (done) break;
557
-
558
- const chunk = decoder.decode(value);
559
- const lines = chunk.split('\\n').filter(l => l.trim());
560
-
561
- for (const line of lines) {
562
- try {
563
- const data = JSON.parse(line);
564
- if (data.type === 'progress') {
565
- document.getElementById('progress').innerHTML =
566
- `<b>${data.percent}%</b> - ${data.message}`;
567
- } else if (data.type === 'result') {
568
- document.getElementById('result').style.display = 'block';
569
- document.getElementById('result').innerHTML =
570
- `<h3>✅ ${data.message}</h3>
571
- <p>Detections: ${data.detection_count}</p>
572
- <p>Mission ID: ${data.mission_id}</p>`;
573
- document.getElementById('progress').style.display = 'none';
574
- } else if (data.type === 'error') {
575
- document.getElementById('progress').innerHTML =
576
- `<b style="color:red;">❌ ${data.message}</b>`;
577
- }
578
- } catch(e) {}
579
- }
580
- }
581
- } catch(err) {
582
- document.getElementById('progress').innerHTML =
583
- `<b style="color:red;">Error: ${err.message}</b>`;
584
- }
585
- }
586
- </script>
587
- </body>
588
- </html>""")
589
-
590
- app.mount("/detections", StaticFiles(directory=DETECTION_DIR), name="detections")
591
- app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
592
-
593
- if __name__ == "__main__":
594
- uvicorn.run(app, host="0.0.0.0", port=7860)