d10g commited on
Commit
7b65184
ยท
1 Parent(s): e0189b8

Refactored APIs

Browse files
reachy_f1_commentator/main.py CHANGED
@@ -46,7 +46,8 @@ _app_instance = None
46
  class ReachyF1Commentator(ReachyMiniApp):
47
  """Main Reachy Mini app for F1 commentary generation."""
48
 
49
- custom_app_url: str = "/static" # Serve web UI from static directory
 
50
 
51
  def __init__(self):
52
  """Initialize the F1 commentator app."""
@@ -78,9 +79,11 @@ class ReachyF1Commentator(ReachyMiniApp):
78
  logger.info("Starting F1 Commentator app")
79
 
80
  self.reachy_mini_instance = reachy_mini
81
- # state_tracker already initialized in __init__
82
 
83
- # Web server is started automatically by framework when custom_app_url is set
 
 
 
84
  logger.info(f"Web UI available at {self.custom_app_url}")
85
 
86
  # Wait for stop_event or user interaction
@@ -92,6 +95,275 @@ class ReachyF1Commentator(ReachyMiniApp):
92
  finally:
93
  self._cleanup()
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def start_commentary(self, config: WebUIConfiguration) -> dict:
96
  """
97
  Start commentary playback with given configuration.
@@ -648,8 +920,9 @@ class ReachyF1Commentator(ReachyMiniApp):
648
 
649
 
650
 
651
- # FastAPI app for web UI endpoints
652
- app = FastAPI(title="Reachy F1 Commentator API")
 
653
 
654
  # WebSocket connections for live dashboard
655
  from fastapi import WebSocket, WebSocketDisconnect
@@ -692,13 +965,8 @@ class ConnectionManager:
692
  for conn in disconnected:
693
  self.disconnect(conn)
694
 
695
- dashboard_manager = ConnectionManager()
696
-
697
- # Mount static files
698
- import os
699
- static_path = os.path.join(os.path.dirname(__file__), "static")
700
- if os.path.exists(static_path):
701
- app.mount("/static", StaticFiles(directory=static_path, html=True), name="static")
702
 
703
 
704
  # Pydantic models for API
@@ -716,7 +984,12 @@ class ConfigSaveRequest(BaseModel):
716
  elevenlabs_voice_id: str = 'HSSEHuB5EziJgTfCVmC6'
717
 
718
 
 
 
 
 
719
  # Configuration file path
 
720
  CONFIG_DIR = os.path.expanduser("~/.reachy_f1_commentator")
721
  CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
722
 
@@ -747,259 +1020,6 @@ def save_config(config: dict) -> bool:
747
  return False
748
 
749
 
750
- @app.get("/api/config")
751
- async def get_config():
752
- """Get saved configuration."""
753
- try:
754
- config = load_saved_config()
755
- return {
756
- "elevenlabs_api_key": config.get("elevenlabs_api_key", ""),
757
- "elevenlabs_voice_id": config.get("elevenlabs_voice_id", "HSSEHuB5EziJgTfCVmC6")
758
- }
759
- except Exception as e:
760
- logger.error(f"Failed to get config: {e}")
761
- raise HTTPException(status_code=500, detail=str(e))
762
-
763
-
764
- @app.post("/api/config")
765
- async def save_config_endpoint(request: ConfigSaveRequest):
766
- """Save configuration."""
767
- try:
768
- config = {
769
- "elevenlabs_api_key": request.elevenlabs_api_key,
770
- "elevenlabs_voice_id": request.elevenlabs_voice_id
771
- }
772
-
773
- if save_config(config):
774
- return {"status": "saved", "message": "Configuration saved successfully"}
775
- else:
776
- raise HTTPException(status_code=500, detail="Failed to save configuration")
777
- except Exception as e:
778
- logger.error(f"Failed to save config: {e}")
779
- raise HTTPException(status_code=500, detail=str(e))
780
-
781
-
782
- @app.get("/api/races/years")
783
- async def get_years():
784
- """Get list of available years with race data."""
785
- try:
786
- if _app_instance is None:
787
- raise HTTPException(status_code=503, detail="App not initialized")
788
-
789
- years = _app_instance.openf1_client.get_years()
790
- return {"years": years}
791
- except Exception as e:
792
- logger.error(f"Failed to get years: {e}")
793
- raise HTTPException(status_code=500, detail=str(e))
794
-
795
-
796
- @app.get("/api/races/{year}")
797
- async def get_races(year: int):
798
- """Get all races for a specific year."""
799
- try:
800
- if _app_instance is None:
801
- raise HTTPException(status_code=503, detail="App not initialized")
802
-
803
- logger.info(f"Fetching races for year {year}")
804
- races = _app_instance.openf1_client.get_races_by_year(year)
805
-
806
- if not races:
807
- logger.warning(f"No races found for year {year}")
808
- return {"races": []}
809
-
810
- # Convert to dict format
811
- races_data = [
812
- {
813
- "session_key": race.session_key,
814
- "date": race.date,
815
- "country": race.country,
816
- "circuit": race.circuit,
817
- "name": race.name
818
- }
819
- for race in races
820
- ]
821
-
822
- logger.info(f"Returning {len(races_data)} races for year {year}")
823
- return {"races": races_data}
824
- except Exception as e:
825
- logger.error(f"Failed to get races for year {year}: {e}", exc_info=True)
826
- raise HTTPException(status_code=500, detail=f"Failed to load races: {str(e)}")
827
-
828
-
829
- @app.post("/api/commentary/start")
830
- async def start_commentary(request: CommentaryStartRequest):
831
- """Start commentary playback."""
832
- try:
833
- if _app_instance is None:
834
- raise HTTPException(status_code=503, detail="App not initialized")
835
-
836
- # Convert request to WebUIConfiguration
837
- config = WebUIConfiguration(
838
- mode=request.mode,
839
- session_key=request.session_key,
840
- commentary_mode=request.commentary_mode,
841
- playback_speed=request.playback_speed,
842
- elevenlabs_api_key=request.elevenlabs_api_key,
843
- elevenlabs_voice_id=request.elevenlabs_voice_id
844
- )
845
-
846
- result = _app_instance.start_commentary(config)
847
- return result
848
- except Exception as e:
849
- logger.error(f"Failed to start commentary: {e}")
850
- raise HTTPException(status_code=500, detail=str(e))
851
-
852
-
853
- @app.post("/api/commentary/stop")
854
- async def stop_commentary():
855
- """Stop active commentary playback."""
856
- try:
857
- if _app_instance is None:
858
- raise HTTPException(status_code=503, detail="App not initialized")
859
-
860
- result = _app_instance.stop_commentary()
861
- return result
862
- except Exception as e:
863
- logger.error(f"Failed to stop commentary: {e}")
864
- raise HTTPException(status_code=500, detail=str(e))
865
-
866
-
867
- @app.get("/api/commentary/status")
868
- async def get_status():
869
- """Get current playback status."""
870
- try:
871
- if _app_instance is None:
872
- raise HTTPException(status_code=503, detail="App not initialized")
873
-
874
- status = _app_instance.get_status()
875
- return status
876
- except Exception as e:
877
- logger.error(f"Failed to get status: {e}")
878
- raise HTTPException(status_code=500, detail=str(e))
879
-
880
-
881
- class QuestionRequest(BaseModel):
882
- question: str
883
-
884
-
885
- @app.post("/api/qa/ask")
886
- async def ask_question(request: QuestionRequest):
887
- """Handle Q&A question from user."""
888
- try:
889
- if _app_instance is None:
890
- raise HTTPException(status_code=503, detail="App not initialized")
891
-
892
- if not request.question or not request.question.strip():
893
- raise HTTPException(status_code=400, detail="Question cannot be empty")
894
-
895
- # Process question using QA manager
896
- answer = _app_instance.process_question(request.question)
897
-
898
- return {"question": request.question, "answer": answer}
899
- except Exception as e:
900
- logger.error(f"Failed to process question: {e}", exc_info=True)
901
- raise HTTPException(status_code=500, detail=str(e))
902
-
903
-
904
- @app.get("/")
905
- async def root():
906
- """Redirect to static UI."""
907
- from fastapi.responses import RedirectResponse
908
- return RedirectResponse(url="/static/index.html")
909
-
910
-
911
- # Health check endpoint
912
- @app.get("/health")
913
- async def health():
914
- """Health check endpoint."""
915
- return {"status": "healthy", "app": "reachy-f1-commentator"}
916
-
917
-
918
- @app.websocket("/ws/dashboard")
919
- async def dashboard_websocket(websocket: WebSocket):
920
- """WebSocket endpoint for live dashboard updates."""
921
- await dashboard_manager.connect(websocket)
922
- try:
923
- while True:
924
- # Process any queued broadcasts
925
- while dashboard_manager.broadcast_queue:
926
- message = dashboard_manager.broadcast_queue.pop(0)
927
- await dashboard_manager.broadcast(message)
928
-
929
- # Wait a bit before checking queue again
930
- await asyncio.sleep(0.1)
931
-
932
- # Check if client sent any messages (for keep-alive)
933
- try:
934
- data = await asyncio.wait_for(websocket.receive_text(), timeout=0.1)
935
- # Echo back for ping/pong
936
- await websocket.send_text(data)
937
- except asyncio.TimeoutError:
938
- # No message received, continue
939
- pass
940
-
941
- except WebSocketDisconnect:
942
- dashboard_manager.disconnect(websocket)
943
- except Exception as e:
944
- logger.error(f"WebSocket error: {e}")
945
- dashboard_manager.disconnect(websocket)
946
-
947
-
948
- @app.get("/api/dashboard/state")
949
- async def get_dashboard_state():
950
- """Get current race state for dashboard."""
951
- try:
952
- if _app_instance is None or _app_instance.state_tracker is None:
953
- return {
954
- "positions": [],
955
- "race_info": {
956
- "current_lap": 0,
957
- "total_laps": 0,
958
- "leader": None,
959
- "fastest_lap_holder": None,
960
- "race_phase": "START"
961
- },
962
- "recent_events": []
963
- }
964
-
965
- # Get positions
966
- positions = _app_instance.state_tracker.get_positions()
967
- positions_data = [
968
- {
969
- "position": p.position,
970
- "driver": p.name,
971
- "gap_to_leader": f"+{p.gap_to_leader:.3f}s" if p.gap_to_leader > 0 else "Leader",
972
- "tire_compound": p.current_tire or "Unknown",
973
- "pit_stops": p.pit_count,
974
- "team": "Unknown" # Team info not available in DriverState
975
- }
976
- for p in positions[:20] # Top 20
977
- ]
978
-
979
- # Get race info
980
- leader = _app_instance.state_tracker.get_leader()
981
- state = _app_instance.state_tracker._state # Access private _state attribute
982
-
983
- race_info = {
984
- "current_lap": state.current_lap,
985
- "total_laps": state.total_laps,
986
- "leader": leader.name if leader else None,
987
- "fastest_lap_holder": state.fastest_lap_driver,
988
- "fastest_lap_time": f"{state.fastest_lap_time:.3f}s" if state.fastest_lap_time else None,
989
- "race_phase": state.race_phase.value if state.race_phase else "START",
990
- "safety_car": state.safety_car_active
991
- }
992
-
993
- return {
994
- "positions": positions_data,
995
- "race_info": race_info,
996
- "recent_events": [] # TODO: Add event history
997
- }
998
- except Exception as e:
999
- logger.error(f"Failed to get dashboard state: {e}", exc_info=True)
1000
- raise HTTPException(status_code=500, detail=str(e))
1001
-
1002
-
1003
  # ============================================================================
1004
  # Standalone Mode Entry Point
1005
  # ============================================================================
@@ -1014,6 +1034,7 @@ if __name__ == "__main__":
1014
  The app will auto-detect and connect to Reachy if available.
1015
  """
1016
  import uvicorn
 
1017
 
1018
  logger.info("=" * 60)
1019
  logger.info("Starting Reachy F1 Commentator in standalone mode")
@@ -1038,15 +1059,28 @@ if __name__ == "__main__":
1038
  logger.info(" Running without Reachy - audio playback disabled")
1039
  logger.info(" This is normal for development/testing")
1040
 
 
 
 
 
 
 
 
 
 
1041
  # Run FastAPI server on port 8080 (port 8000 is used by Reachy)
1042
  logger.info("")
1043
  logger.info("Starting web server on http://localhost:8080")
1044
  logger.info("Open http://localhost:8080 in your browser")
1045
  logger.info("=" * 60)
1046
 
1047
- uvicorn.run(
1048
- app,
1049
- host="0.0.0.0",
1050
- port=8080,
1051
- log_level="info"
1052
- )
 
 
 
 
 
46
  class ReachyF1Commentator(ReachyMiniApp):
47
  """Main Reachy Mini app for F1 commentary generation."""
48
 
49
+ custom_app_url: str = "http://0.0.0.0:8080" # Root path where the app is served
50
+ dont_start_webserver: bool = False # Let framework handle the web server
51
 
52
  def __init__(self):
53
  """Initialize the F1 commentator app."""
 
79
  logger.info("Starting F1 Commentator app")
80
 
81
  self.reachy_mini_instance = reachy_mini
 
82
 
83
+ # Setup FastAPI routes using the framework-provided settings_app
84
+ self._setup_api_routes()
85
+
86
+ # Web server is started automatically by framework
87
  logger.info(f"Web UI available at {self.custom_app_url}")
88
 
89
  # Wait for stop_event or user interaction
 
95
  finally:
96
  self._cleanup()
97
 
98
+ def _setup_api_routes(self):
99
+ """Setup FastAPI routes and static file serving."""
100
+ import os
101
+ from fastapi import WebSocket, WebSocketDisconnect
102
+ from fastapi.staticfiles import StaticFiles
103
+
104
+ app = self.settings_app
105
+
106
+ # Mount static files
107
+ static_path = os.path.join(os.path.dirname(__file__), "static")
108
+ if os.path.exists(static_path):
109
+ app.mount("/static", StaticFiles(directory=static_path, html=True), name="static")
110
+ logger.info(f"Mounted static files from {static_path}")
111
+
112
+ # Initialize dashboard manager
113
+ global dashboard_manager
114
+ dashboard_manager = ConnectionManager()
115
+
116
+ # Add all API routes
117
+ self._add_api_routes(app)
118
+
119
+ logger.info("API routes configured")
120
+
121
+ def _add_api_routes(self, app: FastAPI):
122
+ """Add all API routes to the FastAPI app."""
123
+ from fastapi import WebSocket, WebSocketDisconnect
124
+ import asyncio
125
+
126
+ # Configuration endpoints
127
+ @app.get("/api/config")
128
+ async def get_config():
129
+ """Get saved configuration."""
130
+ try:
131
+ config = load_saved_config()
132
+ return {
133
+ "elevenlabs_api_key": config.get("elevenlabs_api_key", ""),
134
+ "elevenlabs_voice_id": config.get("elevenlabs_voice_id", "HSSEHuB5EziJgTfCVmC6")
135
+ }
136
+ except Exception as e:
137
+ logger.error(f"Failed to get config: {e}")
138
+ raise HTTPException(status_code=500, detail=str(e))
139
+
140
+ @app.post("/api/config")
141
+ async def save_config_endpoint(request: ConfigSaveRequest):
142
+ """Save configuration."""
143
+ try:
144
+ config = {
145
+ "elevenlabs_api_key": request.elevenlabs_api_key,
146
+ "elevenlabs_voice_id": request.elevenlabs_voice_id
147
+ }
148
+
149
+ if save_config(config):
150
+ return {"status": "saved", "message": "Configuration saved successfully"}
151
+ else:
152
+ raise HTTPException(status_code=500, detail="Failed to save configuration")
153
+ except Exception as e:
154
+ logger.error(f"Failed to save config: {e}")
155
+ raise HTTPException(status_code=500, detail=str(e))
156
+
157
+ # Race data endpoints
158
+ @app.get("/api/races/years")
159
+ async def get_years():
160
+ """Get list of available years with race data."""
161
+ try:
162
+ if _app_instance is None:
163
+ raise HTTPException(status_code=503, detail="App not initialized")
164
+
165
+ years = _app_instance.openf1_client.get_years()
166
+ return {"years": years}
167
+ except Exception as e:
168
+ logger.error(f"Failed to get years: {e}")
169
+ raise HTTPException(status_code=500, detail=str(e))
170
+
171
+ @app.get("/api/races/{year}")
172
+ async def get_races(year: int):
173
+ """Get all races for a specific year."""
174
+ try:
175
+ if _app_instance is None:
176
+ raise HTTPException(status_code=503, detail="App not initialized")
177
+
178
+ logger.info(f"Fetching races for year {year}")
179
+ races = _app_instance.openf1_client.get_races_by_year(year)
180
+
181
+ if not races:
182
+ logger.warning(f"No races found for year {year}")
183
+ return {"races": []}
184
+
185
+ # Convert to dict format
186
+ races_data = [
187
+ {
188
+ "session_key": race.session_key,
189
+ "date": race.date,
190
+ "country": race.country,
191
+ "circuit": race.circuit,
192
+ "name": race.name
193
+ }
194
+ for race in races
195
+ ]
196
+
197
+ logger.info(f"Returning {len(races_data)} races for year {year}")
198
+ return {"races": races_data}
199
+ except Exception as e:
200
+ logger.error(f"Failed to get races for year {year}: {e}", exc_info=True)
201
+ raise HTTPException(status_code=500, detail=f"Failed to load races: {str(e)}")
202
+
203
+ # Commentary control endpoints
204
+ @app.post("/api/commentary/start")
205
+ async def start_commentary(request: CommentaryStartRequest):
206
+ """Start commentary playback."""
207
+ try:
208
+ if _app_instance is None:
209
+ raise HTTPException(status_code=503, detail="App not initialized")
210
+
211
+ # Convert request to WebUIConfiguration
212
+ config = WebUIConfiguration(
213
+ mode=request.mode,
214
+ session_key=request.session_key,
215
+ commentary_mode=request.commentary_mode,
216
+ playback_speed=request.playback_speed,
217
+ elevenlabs_api_key=request.elevenlabs_api_key,
218
+ elevenlabs_voice_id=request.elevenlabs_voice_id
219
+ )
220
+
221
+ result = _app_instance.start_commentary(config)
222
+ return result
223
+ except Exception as e:
224
+ logger.error(f"Failed to start commentary: {e}")
225
+ raise HTTPException(status_code=500, detail=str(e))
226
+
227
+ @app.post("/api/commentary/stop")
228
+ async def stop_commentary():
229
+ """Stop active commentary playback."""
230
+ try:
231
+ if _app_instance is None:
232
+ raise HTTPException(status_code=503, detail="App not initialized")
233
+
234
+ result = _app_instance.stop_commentary()
235
+ return result
236
+ except Exception as e:
237
+ logger.error(f"Failed to stop commentary: {e}")
238
+ raise HTTPException(status_code=500, detail=str(e))
239
+
240
+ @app.get("/api/commentary/status")
241
+ async def get_status():
242
+ """Get current playback status."""
243
+ try:
244
+ if _app_instance is None:
245
+ raise HTTPException(status_code=503, detail="App not initialized")
246
+
247
+ status = _app_instance.get_status()
248
+ return status
249
+ except Exception as e:
250
+ logger.error(f"Failed to get status: {e}")
251
+ raise HTTPException(status_code=500, detail=str(e))
252
+
253
+ # Q&A endpoint
254
+ @app.post("/api/qa/ask")
255
+ async def ask_question(request: QuestionRequest):
256
+ """Handle Q&A question from user."""
257
+ try:
258
+ if _app_instance is None:
259
+ raise HTTPException(status_code=503, detail="App not initialized")
260
+
261
+ if not request.question or not request.question.strip():
262
+ raise HTTPException(status_code=400, detail="Question cannot be empty")
263
+
264
+ # Process question using QA manager
265
+ answer = _app_instance.process_question(request.question)
266
+
267
+ return {"question": request.question, "answer": answer}
268
+ except Exception as e:
269
+ logger.error(f"Failed to process question: {e}", exc_info=True)
270
+ raise HTTPException(status_code=500, detail=str(e))
271
+
272
+ # Dashboard endpoints
273
+ @app.websocket("/ws/dashboard")
274
+ async def dashboard_websocket(websocket: WebSocket):
275
+ """WebSocket endpoint for live dashboard updates."""
276
+ await dashboard_manager.connect(websocket)
277
+ try:
278
+ while True:
279
+ # Process any queued broadcasts
280
+ while dashboard_manager.broadcast_queue:
281
+ message = dashboard_manager.broadcast_queue.pop(0)
282
+ await dashboard_manager.broadcast(message)
283
+
284
+ # Wait a bit before checking queue again
285
+ await asyncio.sleep(0.1)
286
+
287
+ # Check if client sent any messages (for keep-alive)
288
+ try:
289
+ data = await asyncio.wait_for(websocket.receive_text(), timeout=0.1)
290
+ # Echo back for ping/pong
291
+ await websocket.send_text(data)
292
+ except asyncio.TimeoutError:
293
+ # No message received, continue
294
+ pass
295
+
296
+ except WebSocketDisconnect:
297
+ dashboard_manager.disconnect(websocket)
298
+ except Exception as e:
299
+ logger.error(f"WebSocket error: {e}")
300
+ dashboard_manager.disconnect(websocket)
301
+
302
+ @app.get("/api/dashboard/state")
303
+ async def get_dashboard_state():
304
+ """Get current race state for dashboard."""
305
+ try:
306
+ if _app_instance is None or _app_instance.state_tracker is None:
307
+ return {
308
+ "positions": [],
309
+ "race_info": {
310
+ "current_lap": 0,
311
+ "total_laps": 0,
312
+ "leader": None,
313
+ "fastest_lap_holder": None,
314
+ "race_phase": "START"
315
+ },
316
+ "recent_events": []
317
+ }
318
+
319
+ # Get positions
320
+ positions = _app_instance.state_tracker.get_positions()
321
+ positions_data = [
322
+ {
323
+ "position": p.position,
324
+ "driver": p.name,
325
+ "gap_to_leader": f"+{p.gap_to_leader:.3f}s" if p.gap_to_leader > 0 else "Leader",
326
+ "tire_compound": p.current_tire or "Unknown",
327
+ "pit_stops": p.pit_count,
328
+ "team": "Unknown" # Team info not available in DriverState
329
+ }
330
+ for p in positions[:20] # Top 20
331
+ ]
332
+
333
+ # Get race info
334
+ leader = _app_instance.state_tracker.get_leader()
335
+ state = _app_instance.state_tracker._state # Access private _state attribute
336
+
337
+ race_info = {
338
+ "current_lap": state.current_lap,
339
+ "total_laps": state.total_laps,
340
+ "leader": leader.name if leader else None,
341
+ "fastest_lap_holder": state.fastest_lap_driver,
342
+ "fastest_lap_time": f"{state.fastest_lap_time:.3f}s" if state.fastest_lap_time else None,
343
+ "race_phase": state.race_phase.value if state.race_phase else "START",
344
+ "safety_car": state.safety_car_active
345
+ }
346
+
347
+ return {
348
+ "positions": positions_data,
349
+ "race_info": race_info,
350
+ "recent_events": [] # TODO: Add event history
351
+ }
352
+ except Exception as e:
353
+ logger.error(f"Failed to get dashboard state: {e}", exc_info=True)
354
+ raise HTTPException(status_code=500, detail=str(e))
355
+
356
+ # Root and health endpoints
357
+ @app.get("/")
358
+ async def root():
359
+ """Redirect to static UI."""
360
+ return RedirectResponse(url="/static/index.html")
361
+
362
+ @app.get("/health")
363
+ async def health():
364
+ """Health check endpoint."""
365
+ return {"status": "healthy", "app": "reachy-f1-commentator"}
366
+
367
  def start_commentary(self, config: WebUIConfiguration) -> dict:
368
  """
369
  Start commentary playback with given configuration.
 
920
 
921
 
922
 
923
+ # ============================================================================
924
+ # Helper Classes and Functions for API Routes
925
+ # ============================================================================
926
 
927
  # WebSocket connections for live dashboard
928
  from fastapi import WebSocket, WebSocketDisconnect
 
965
  for conn in disconnected:
966
  self.disconnect(conn)
967
 
968
+ # Global dashboard manager (initialized in _setup_api_routes)
969
+ dashboard_manager = None
 
 
 
 
 
970
 
971
 
972
  # Pydantic models for API
 
984
  elevenlabs_voice_id: str = 'HSSEHuB5EziJgTfCVmC6'
985
 
986
 
987
+ class QuestionRequest(BaseModel):
988
+ question: str
989
+
990
+
991
  # Configuration file path
992
+ import os
993
  CONFIG_DIR = os.path.expanduser("~/.reachy_f1_commentator")
994
  CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
995
 
 
1020
  return False
1021
 
1022
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
  # ============================================================================
1024
  # Standalone Mode Entry Point
1025
  # ============================================================================
 
1034
  The app will auto-detect and connect to Reachy if available.
1035
  """
1036
  import uvicorn
1037
+ from fastapi import FastAPI
1038
 
1039
  logger.info("=" * 60)
1040
  logger.info("Starting Reachy F1 Commentator in standalone mode")
 
1059
  logger.info(" Running without Reachy - audio playback disabled")
1060
  logger.info(" This is normal for development/testing")
1061
 
1062
+ # Create a standalone FastAPI app for development
1063
+ standalone_app = FastAPI(title="Reachy F1 Commentator API (Standalone)")
1064
+
1065
+ # Manually set up the app's settings_app to use our standalone app
1066
+ commentator.settings_app = standalone_app
1067
+
1068
+ # Setup API routes
1069
+ commentator._setup_api_routes()
1070
+
1071
  # Run FastAPI server on port 8080 (port 8000 is used by Reachy)
1072
  logger.info("")
1073
  logger.info("Starting web server on http://localhost:8080")
1074
  logger.info("Open http://localhost:8080 in your browser")
1075
  logger.info("=" * 60)
1076
 
1077
+ try:
1078
+ uvicorn.run(
1079
+ standalone_app,
1080
+ host="0.0.0.0",
1081
+ port=8080,
1082
+ log_level="info"
1083
+ )
1084
+ except KeyboardInterrupt:
1085
+ logger.info("Shutting down...")
1086
+ commentator._cleanup()
reachy_f1_commentator/static/dashboard.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Live Race Dashboard - Reachy F1 Commentator</title>
7
- <link rel="stylesheet" href="dashboard.css">
8
  </head>
9
  <body>
10
  <div class="dashboard-container">
@@ -69,6 +69,6 @@
69
  </footer>
70
  </div>
71
 
72
- <script src="dashboard.js"></script>
73
  </body>
74
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Live Race Dashboard - Reachy F1 Commentator</title>
7
+ <link rel="stylesheet" href="/static/dashboard.css">
8
  </head>
9
  <body>
10
  <div class="dashboard-container">
 
69
  </footer>
70
  </div>
71
 
72
+ <script src="/static/dashboard.js"></script>
73
  </body>
74
  </html>
reachy_f1_commentator/static/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Reachy F1 Commentator</title>
7
- <link rel="stylesheet" href="style.css">
8
  </head>
9
  <body>
10
  <div class="container">
@@ -14,7 +14,7 @@
14
  <h1>๐ŸŽ๏ธ Reachy F1 Commentator</h1>
15
  <p class="subtitle">Interactive F1 Race Commentary</p>
16
  </div>
17
- <a href="dashboard.html" class="dashboard-link" target="_blank">
18
  ๐Ÿ“Š Live Dashboard
19
  </a>
20
  </div>
@@ -142,6 +142,6 @@
142
  </div>
143
  </div>
144
 
145
- <script src="main.js"></script>
146
  </body>
147
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Reachy F1 Commentator</title>
7
+ <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
  <body>
10
  <div class="container">
 
14
  <h1>๐ŸŽ๏ธ Reachy F1 Commentator</h1>
15
  <p class="subtitle">Interactive F1 Race Commentary</p>
16
  </div>
17
+ <a href="/static/dashboard.html" class="dashboard-link" target="_blank">
18
  ๐Ÿ“Š Live Dashboard
19
  </a>
20
  </div>
 
142
  </div>
143
  </div>
144
 
145
+ <script src="/static/main.js"></script>
146
  </body>
147
  </html>