GitHub Actions commited on
Commit
63cc479
·
0 Parent(s):

Deploy App to HF Spaces

Browse files
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Create the user according to Hugging Face specs
4
+ RUN useradd -m -u 1000 user
5
+ USER user
6
+ ENV HOME=/home/user \
7
+ PATH=/home/user/.local/bin:$PATH \
8
+ PYTHONUNBUFFERED=1
9
+
10
+ WORKDIR $HOME/app
11
+
12
+ # Install poetry
13
+ RUN pip install --no-cache-dir poetry
14
+
15
+ # Copy dependency files
16
+ COPY --chown=user pyproject.toml poetry.lock* ./
17
+
18
+ # Install dependencies without virtualenvs setup for container
19
+ RUN poetry config virtualenvs.create false && \
20
+ poetry install --no-interaction --no-ansi --no-root
21
+
22
+ # Copy rest of the application
23
+ COPY --chown=user src/ ./src/
24
+
25
+ EXPOSE 7860
26
+
27
+ # Command to run the application space
28
+ CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: TrafficSimApp
3
+ emoji: 🚗
4
+ colorFrom: gray
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ short_description: app for traffic simulation project
10
+ ---
pyproject.toml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "sim-backend"
3
+ version = "0.1.0"
4
+ description = "Simulation backend for vehicular traffic offloading"
5
+ authors = ["Author <author@example.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "app", from = "src"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ fastapi = "^0.103.0"
12
+ uvicorn = "^0.23.0"
13
+ pydantic = "^2.3.0"
14
+ osmnx = "^1.6.0"
15
+ networkx = "^3.1"
16
+
17
+ [build-system]
18
+ requires = ["poetry-core"]
19
+ build-backend = "poetry.core.masonry.api"
src/app/__init__.py ADDED
File without changes
src/app/api/__init__.py ADDED
File without changes
src/app/api/routes.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from app.core.state import sim_state
3
+ from app.models.schemas import ConfigState, ResetConfig
4
+ from app.simulation.engine import run_step, init_sim_entities
5
+
6
+ router = APIRouter()
7
+
8
+ @router.post("/start")
9
+ def start_sim():
10
+ sim_state.running = True
11
+ return {"status": "started"}
12
+
13
+ @router.post("/pause")
14
+ def pause_sim():
15
+ sim_state.running = False
16
+ return {"status": "paused"}
17
+
18
+ @router.post("/step")
19
+ def step_sim():
20
+ run_step()
21
+ return {"status": "stepped"}
22
+
23
+ @router.post("/algorithm")
24
+ def set_algorithm(config: ConfigState):
25
+ sim_state.algorithm = config.algorithm
26
+ return {"status": "success", "algorithm": sim_state.algorithm}
27
+
28
+ @router.post("/reset")
29
+ def reset_sim(config: ResetConfig):
30
+ sim_state.num_hubs = config.num_hubs
31
+ sim_state.num_vehicles = config.num_vehicles
32
+ sim_state.event_rate = config.event_rate
33
+ init_sim_entities()
34
+ return {"status": "reset_successful"}
35
+
36
+ @router.get("/roads")
37
+ def get_roads():
38
+ return sim_state.roads
39
+
40
+ @router.get("/state")
41
+ def get_state():
42
+ return {
43
+ "running": sim_state.running,
44
+ "step": sim_state.step,
45
+ "algorithm": sim_state.algorithm,
46
+ "total_generated_mb": sim_state.total_generated_mb,
47
+ "total_offloaded_mb": sim_state.total_offloaded_mb,
48
+ "vehicles": sim_state.vehicles,
49
+ "hubs": sim_state.hubs,
50
+ "recent_events": sim_state.recent_events
51
+ }
52
+
53
+ @router.get("/report")
54
+ def export_report():
55
+ avg_latency = 0
56
+ if sim_state.priority_events_completed > 0:
57
+ avg_latency = sim_state.priority_latency_sum / sim_state.priority_events_completed
58
+
59
+ hub_utilization = {f"Hub_{h['id']}": round(h["total_offloaded_mb"], 2) for h in sim_state.hubs}
60
+
61
+ return {
62
+ "algorithm": sim_state.algorithm,
63
+ "total_steps": sim_state.step,
64
+ "completed_events": sim_state.priority_events_completed,
65
+ "average_priority_latency_steps": round(avg_latency, 2),
66
+ "total_buffer_overflows": sim_state.total_buffer_overflows,
67
+ "hub_utilization_mb": hub_utilization,
68
+ "total_generated_mb": round(sim_state.total_generated_mb, 2),
69
+ "total_offloaded_mb": round(sim_state.total_offloaded_mb, 2),
70
+ "overall_efficiency": f"{round((sim_state.total_offloaded_mb / max(1, sim_state.total_generated_mb)) * 100, 2)}%"
71
+ }
src/app/core/__init__.py ADDED
File without changes
src/app/core/config.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Configuration
2
+ LOCATION = "Faizabad Interchange, Islamabad, Pakistan"
3
+ WIFI_RANGE_METERS = 100
4
+ WIFI_RATE_MB_S = 1.5 # MB per 0.1s step (effectively 15 MB/s)
5
+ DATA_GEN_RATE_MB_STEP = 0.1 # effectively 1 MB/s
6
+ NUM_VEHICLES = 50
7
+ NUM_HUBS = 15
8
+ MAX_BUFFER_MB = 50.0
src/app/core/state.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.core.config import NUM_HUBS, NUM_VEHICLES
2
+
3
+ class SimulationState:
4
+ def __init__(self):
5
+ self.running = False
6
+ self.step = 0
7
+ self.algorithm = "greedy" # 'greedy' or 'predictive'
8
+ self.total_generated_mb = 0.0
9
+ self.total_offloaded_mb = 0.0
10
+ self.total_buffer_overflows = 0
11
+ self.priority_latency_sum = 0
12
+ self.priority_events_completed = 0
13
+ self.num_hubs = NUM_HUBS
14
+ self.num_vehicles = NUM_VEHICLES
15
+ self.event_rate = 0.0005
16
+ self.graph = None
17
+ self.hubs = []
18
+ self.vehicles = []
19
+ self.roads = []
20
+ self.recent_events = []
21
+
22
+ # Single global instance
23
+ sim_state = SimulationState()
src/app/main.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import asyncio
4
+ import osmnx as ox
5
+
6
+ from app.api.routes import router as api_router
7
+ from app.core.state import sim_state
8
+ from app.core.config import LOCATION
9
+ from app.simulation.engine import init_sim_entities, simulation_loop
10
+
11
+ app = FastAPI(title="Simulation Backend")
12
+
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["*"],
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ app.include_router(api_router)
21
+
22
+ @app.on_event("startup")
23
+ async def startup_event():
24
+ print(f"Loading graph for {LOCATION}...")
25
+ try:
26
+ sim_state.graph = ox.graph_from_place(LOCATION, network_type="drive")
27
+ except Exception as e:
28
+ print("Failed to download exact place, using bounding box or default fallback...", e)
29
+ sim_state.graph = ox.graph_from_point((33.6515, 73.0801), dist=2500, network_type="drive")
30
+
31
+ # Extract roads for frontend rendering
32
+ roads = []
33
+ for u, v, data in sim_state.graph.edges(data=True):
34
+ if 'geometry' in data:
35
+ coords = list(data['geometry'].coords)
36
+ roads.append([[pt[1], pt[0]] for pt in coords])
37
+ else:
38
+ u_node = sim_state.graph.nodes[u]
39
+ v_node = sim_state.graph.nodes[v]
40
+ roads.append([[u_node['y'], u_node['x']], [v_node['y'], v_node['x']]])
41
+ sim_state.roads = roads
42
+
43
+ init_sim_entities()
44
+
45
+ print("Simulation initialized.")
46
+ asyncio.create_task(simulation_loop())
47
+
48
+ if __name__ == "__main__":
49
+ import uvicorn
50
+ # Make sure to run the file inside `src/` to correctly access `app.main:app` module.
51
+ uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
src/app/models/__init__.py ADDED
File without changes
src/app/models/schemas.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class ConfigState(BaseModel):
4
+ algorithm: str
5
+
6
+ class ResetConfig(BaseModel):
7
+ num_hubs: int
8
+ num_vehicles: int
9
+ event_rate: float
src/app/rl/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # This file initializes the RL module
src/app/rl/train.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ import os
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ async def start_training_loop():
8
+ """
9
+ Minimal dummy RL training loop that runs asynchronously.
10
+ """
11
+ logger.info("Initializing RL Training environment...")
12
+
13
+ # Ensure models directory exists
14
+ os.makedirs("models", exist_ok=True)
15
+
16
+ epoch = 0
17
+ while True:
18
+ logger.info(f"Running training epoch {epoch}...")
19
+
20
+ # Simulate training work
21
+ await asyncio.sleep(10)
22
+
23
+ # Save a dummy model
24
+ with open(f"models/dummy_model_v{epoch}.pkl", "w") as f:
25
+ f.write(f"Model parameters for epoch {epoch}")
26
+
27
+ logger.info(f"Epoch {epoch} complete and model saved.")
28
+ epoch += 1
src/app/simulation/__init__.py ADDED
File without changes
src/app/simulation/engine.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import random
3
+ import math
4
+ import networkx as nx
5
+
6
+ from app.core.state import sim_state
7
+ from app.core.config import DATA_GEN_RATE_MB_STEP, MAX_BUFFER_MB, WIFI_RANGE_METERS
8
+ from app.simulation.utils import euclidean_dist
9
+
10
+ def init_sim_entities():
11
+ nodes = list(sim_state.graph.nodes(data=True))
12
+ sim_state.hubs = []
13
+ sim_state.vehicles = []
14
+ sim_state.step = 0
15
+ sim_state.total_generated_mb = 0.0
16
+ sim_state.total_offloaded_mb = 0.0
17
+ sim_state.total_buffer_overflows = 0
18
+ sim_state.priority_latency_sum = 0
19
+ sim_state.priority_events_completed = 0
20
+ sim_state.recent_events = []
21
+
22
+ center_lat = 33.6515
23
+ center_lon = 73.0801
24
+ top_right_nodes = [n for n in nodes if n[1]["y"] > center_lat + 0.005 and n[1]["x"] > center_lon + 0.005]
25
+ if len(top_right_nodes) >= 2 and sim_state.num_hubs >= 2:
26
+ tr_hubs = random.sample(top_right_nodes, 2)
27
+ other_nodes = [n for n in nodes if n not in tr_hubs]
28
+ hub_nodes = tr_hubs + random.sample(other_nodes, sim_state.num_hubs - 2)
29
+ else:
30
+ hub_nodes = random.sample(nodes, sim_state.num_hubs)
31
+
32
+ for hid, node in enumerate(hub_nodes):
33
+ sim_state.hubs.append({
34
+ "id": hid,
35
+ "lat": node[1]["y"],
36
+ "lon": node[1]["x"],
37
+ "range": WIFI_RANGE_METERS,
38
+ "rate": random.choice([1.0, 2.5, 5.0]),
39
+ "active": False,
40
+ "online": True,
41
+ "total_offloaded_mb": 0.0
42
+ })
43
+
44
+ node_ids = list(sim_state.graph.nodes())
45
+ for vid in range(sim_state.num_vehicles):
46
+ n1 = random.choice(node_ids)
47
+ n2 = random.choice(node_ids)
48
+ try:
49
+ path = nx.shortest_path(sim_state.graph, n1, n2, weight='length')
50
+ except:
51
+ path = [n1]
52
+
53
+ start_data = sim_state.graph.nodes[n1]
54
+ target_lat = start_data["y"]
55
+ target_lon = start_data["x"]
56
+
57
+ if len(path) > 1:
58
+ next_node = path[1]
59
+ target_lat = sim_state.graph.nodes[next_node]["y"]
60
+ target_lon = sim_state.graph.nodes[next_node]["x"]
61
+
62
+ base_spd = random.uniform(0.00004, 0.00008)
63
+ sim_state.vehicles.append({
64
+ "id": vid,
65
+ "lat": start_data["y"],
66
+ "lon": start_data["x"],
67
+ "buffer_mb": 0.0,
68
+ "routine_mb": 0.0,
69
+ "priority_mb": 0.0,
70
+ "panic": False,
71
+ "target_lat": target_lat,
72
+ "target_lon": target_lon,
73
+ "path": path,
74
+ "path_idx": 1 if len(path) > 1 else 0,
75
+ "base_speed": base_spd,
76
+ "speed": base_spd,
77
+ "events": []
78
+ })
79
+
80
+ async def simulation_loop():
81
+ while True:
82
+ if sim_state.running:
83
+ run_step()
84
+ await asyncio.sleep(0.05)
85
+
86
+ def run_step():
87
+ sim_state.step += 1
88
+
89
+ for hub in sim_state.hubs:
90
+ hub["active"] = False
91
+ if random.random() < 0.001:
92
+ hub["online"] = not hub["online"]
93
+
94
+ node_ids = list(sim_state.graph.nodes())
95
+ edge_traffic = {}
96
+ for v in sim_state.vehicles:
97
+ if v["path"] and v["path_idx"] < len(v["path"]):
98
+ prev = v["path"][v["path_idx"]-1] if v["path_idx"] > 0 else v["path"][0]
99
+ curr = v["path"][v["path_idx"]]
100
+ edge_traffic[(prev, curr)] = edge_traffic.get((prev, curr), 0) + 1
101
+
102
+ EVENT_TYPES = ["Pothole", "Minor Crash", "Debris", "Speed Trap", "Traffic Jam"]
103
+
104
+ for v in sim_state.vehicles:
105
+ v["routine_mb"] += DATA_GEN_RATE_MB_STEP
106
+ sim_state.total_generated_mb += DATA_GEN_RATE_MB_STEP
107
+
108
+ if v["path"] and v["path_idx"] < len(v["path"]):
109
+ prev = v["path"][v["path_idx"]-1] if v["path_idx"] > 0 else v["path"][0]
110
+ curr = v["path"][v["path_idx"]]
111
+ vehicles_on_edge = edge_traffic.get((prev, curr), 1)
112
+ v["speed"] = v["base_speed"] / max(1, vehicles_on_edge)
113
+
114
+ if random.random() < sim_state.event_rate:
115
+ evt_type = random.choice(EVENT_TYPES)
116
+ evt_size = random.uniform(5.0, 10.0)
117
+ v["events"].append({"type": evt_type, "size": evt_size, "timestamp": sim_state.step})
118
+ v["priority_mb"] += evt_size
119
+ sim_state.total_generated_mb += evt_size
120
+
121
+ total_buf = v["routine_mb"] + v["priority_mb"]
122
+ if total_buf > MAX_BUFFER_MB:
123
+ overflow_amt = total_buf - MAX_BUFFER_MB
124
+ dropped_mb = min(overflow_amt, v["routine_mb"])
125
+ if dropped_mb > 0:
126
+ v["routine_mb"] -= dropped_mb
127
+ sim_state.total_buffer_overflows += 1
128
+
129
+ v["buffer_mb"] = v["routine_mb"] + v["priority_mb"]
130
+
131
+ dx = v["target_lon"] - v["lon"]
132
+ dy = v["target_lat"] - v["lat"]
133
+ dist = math.sqrt(dx*dx + dy*dy)
134
+ if dist < v["speed"]:
135
+ v["lon"] = v["target_lon"]
136
+ v["lat"] = v["target_lat"]
137
+ v["path_idx"] += 1
138
+ if v["path_idx"] < len(v["path"]):
139
+ next_node = v["path"][v["path_idx"]]
140
+ v["target_lat"] = sim_state.graph.nodes[next_node]["y"]
141
+ v["target_lon"] = sim_state.graph.nodes[next_node]["x"]
142
+ else:
143
+ start_node = v["path"][-1] if v["path"] else random.choice(node_ids)
144
+ end_node = random.choice(node_ids)
145
+ try:
146
+ new_path = nx.shortest_path(sim_state.graph, start_node, end_node, weight='length')
147
+ except:
148
+ new_path = [start_node]
149
+
150
+ v["path"] = new_path
151
+ v["path_idx"] = 1
152
+ if len(new_path) > 1:
153
+ next_node = new_path[1]
154
+ v["target_lat"] = sim_state.graph.nodes[next_node]["y"]
155
+ v["target_lon"] = sim_state.graph.nodes[next_node]["x"]
156
+ else:
157
+ v["target_lat"] = v["lat"]
158
+ v["target_lon"] = v["lon"]
159
+ else:
160
+ v["lon"] += (dx/dist) * v["speed"]
161
+ v["lat"] += (dy/dist) * v["speed"]
162
+
163
+ closest_hub = None
164
+ min_dist = float('inf')
165
+ for hub in sim_state.hubs:
166
+ d = euclidean_dist(v["lat"], v["lon"], hub["lat"], hub["lon"])
167
+ if d < min_dist:
168
+ min_dist = d
169
+ closest_hub = hub
170
+
171
+ if closest_hub and min_dist <= closest_hub["range"]:
172
+ if not closest_hub["online"]:
173
+ v["panic"] = True
174
+ else:
175
+ skip_offload = False
176
+
177
+ if sim_state.algorithm == "predictive" and not v.get("panic", False) and (v["routine_mb"] + v["priority_mb"]) < MAX_BUFFER_MB * 0.8:
178
+ future_nodes = v["path"][v["path_idx"]:v["path_idx"]+10]
179
+ for f_node in future_nodes:
180
+ ny = sim_state.graph.nodes[f_node]["y"]
181
+ nx_coord = sim_state.graph.nodes[f_node]["x"]
182
+ for h in sim_state.hubs:
183
+ if h["online"] and h["rate"] > closest_hub["rate"] and euclidean_dist(ny, nx_coord, h["lat"], h["lon"]) <= h["range"]:
184
+ skip_offload = True
185
+ break
186
+ if skip_offload:
187
+ break
188
+
189
+ if not skip_offload:
190
+ amt_to_offload = closest_hub["rate"]
191
+
192
+ while v["events"] and amt_to_offload > 0:
193
+ evt = v["events"][0]
194
+ if evt["size"] <= amt_to_offload:
195
+ amt_to_offload -= evt["size"]
196
+ v["priority_mb"] -= evt["size"]
197
+ sim_state.total_offloaded_mb += evt["size"]
198
+ closest_hub["total_offloaded_mb"] += evt["size"]
199
+ latency = sim_state.step - evt["timestamp"]
200
+ sim_state.priority_latency_sum += latency
201
+ sim_state.priority_events_completed += 1
202
+ v["events"].pop(0)
203
+ sim_state.recent_events.insert(0, f"[{sim_state.step}] Veh {v['id']} offloaded '{evt['type']}' to Hub {closest_hub['id']} (Lat: {latency}s)")
204
+ else:
205
+ evt["size"] -= amt_to_offload
206
+ v["priority_mb"] -= amt_to_offload
207
+ sim_state.total_offloaded_mb += amt_to_offload
208
+ closest_hub["total_offloaded_mb"] += amt_to_offload
209
+ amt_to_offload = 0
210
+
211
+ if amt_to_offload > 0 and v["routine_mb"] > 0:
212
+ off_routine = min(v["routine_mb"], amt_to_offload)
213
+ v["routine_mb"] -= off_routine
214
+ sim_state.total_offloaded_mb += off_routine
215
+ closest_hub["total_offloaded_mb"] += off_routine
216
+
217
+ closest_hub["active"] = True
218
+ v["panic"] = False
219
+
220
+ v["buffer_mb"] = v["routine_mb"] + v["priority_mb"]
221
+ sim_state.recent_events = sim_state.recent_events[:30]
src/app/simulation/utils.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ def euclidean_dist(lat1, lon1, lat2, lon2):
4
+ # Rough approximation for small distances, returning meters
5
+ # 1 deg lat = 111,320m
6
+ lat_mid = (lat1 + lat2) / 2.0
7
+ dx = (lon2 - lon1) * 111320.0 * math.cos(math.radians(lat_mid))
8
+ dy = (lat2 - lat1) * 111320.0
9
+ return math.sqrt(dx*dx + dy*dy)
src/app/train_main.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ from fastapi import FastAPI
4
+ from app.rl.train import start_training_loop
5
+
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
9
+ app = FastAPI(title="RL Training Headless Space")
10
+
11
+ @app.on_event("startup")
12
+ async def startup_event():
13
+ logger.info("Starting headless RL training server...")
14
+ # Launch the RL training loop as a background task
15
+ asyncio.create_task(start_training_loop())
16
+
17
+ @app.get("/health")
18
+ def health_check():
19
+ """
20
+ Minimal health check endpoint required by Hugging Face Spaces on port 7860.
21
+ """
22
+ return {"status": "ok", "message": "Training is running"}
23
+
24
+ if __name__ == "__main__":
25
+ import uvicorn
26
+ # Hugging Face spaces expect port 7860
27
+ uvicorn.run("app.train_main:app", host="0.0.0.0", port=7860)