blanchon commited on
Commit
c764bfc
Β·
1 Parent(s): de6f5a0

Fix hf by merging both service

Browse files
DOCKER_README.md CHANGED
@@ -2,35 +2,83 @@
2
 
3
  This Docker setup provides a containerized environment that runs both the Python backend and Svelte frontend for LeRobot Arena using modern tools like Bun, uv, and box-packager.
4
 
 
 
 
 
 
 
 
5
  ## πŸš€ Quick Start
6
 
7
- ### Using Docker Compose (Recommended)
8
 
9
  ```bash
10
- # Build and start both services
11
- docker-compose up --build
 
 
 
12
 
13
- # Or run in detached mode
14
- docker-compose up -d --build
15
  ```
16
 
17
- ### Using Docker directly
18
 
19
  ```bash
20
- # Build the image
21
- docker build -t lerobot-arena .
22
 
23
- # Run the container
24
- docker run -p 8080:8080 -p 7860:7860 lerobot-arena
25
  ```
26
 
27
- ## 🌐 Access the Application
 
 
 
 
28
 
29
- After starting the container, you can access:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- - **Frontend (Svelte)**: http://localhost:7860
32
- - **Backend (Python/FastAPI)**: http://localhost:8080
33
- - **Backend API Docs**: http://localhost:8080/docs
34
 
35
  ## πŸ“‹ What's Included
36
 
 
2
 
3
  This Docker setup provides a containerized environment that runs both the Python backend and Svelte frontend for LeRobot Arena using modern tools like Bun, uv, and box-packager.
4
 
5
+ ## πŸ—οΈ Architecture
6
+
7
+ The application consists of:
8
+ - **Svelte Frontend**: Built as static files and served by FastAPI
9
+ - **FastAPI Backend**: WebSocket server with HTTP API for robot control
10
+ - **Single Port**: Both frontend and backend served on port 7860
11
+
12
  ## πŸš€ Quick Start
13
 
14
+ ### Build and Run with Docker
15
 
16
  ```bash
17
+ # Build the image
18
+ docker build -t lerobot-arena .
19
+
20
+ # Run the container
21
+ docker run -p 7860:7860 lerobot-arena
22
 
23
+ # Access the application
24
+ open http://localhost:7860
25
  ```
26
 
27
+ ### Using Docker Compose
28
 
29
  ```bash
30
+ # Start the application
31
+ docker-compose up
32
 
33
+ # Access the application
34
+ open http://localhost:7860
35
  ```
36
 
37
+ ## 🌐 Deployment
38
+
39
+ ### Hugging Face Spaces
40
+
41
+ This project is configured for deployment on Hugging Face Spaces:
42
 
43
+ 1. Push to a Hugging Face Space repository
44
+ 2. The Dockerfile will automatically build and serve the application
45
+ 3. Both frontend and backend run on port 7860 (HF Spaces default)
46
+ 4. Environment detection automatically configures URLs for HF Spaces
47
+
48
+ ### Local Development vs Production
49
+
50
+ - **Local Development**: Frontend dev server on 5173, backend on 7860
51
+ - **Docker/Production**: Both frontend and backend served by FastAPI on 7860
52
+ - **HF Spaces**: Single FastAPI server on 7860 with automatic HTTPS and domain detection
53
+
54
+ ## πŸ”§ Environment Variables
55
+
56
+ - `SPACE_HOST`: Set automatically by Hugging Face Spaces for proper URL configuration
57
+ - `PORT`: Override the default port (7860)
58
+
59
+ ## πŸ“‘ API Endpoints
60
+
61
+ - **Frontend**: `http://localhost:7860/` (Served by FastAPI)
62
+ - **API**: `http://localhost:7860/api/*`
63
+ - **WebSocket**: `ws://localhost:7860/ws/*`
64
+ - **Status**: `http://localhost:7860/status`
65
+
66
+ ## πŸ› οΈ Development
67
+
68
+ For local development without Docker:
69
+
70
+ ```bash
71
+ # Start backend
72
+ cd src-python
73
+ uv sync
74
+ uv run python start_server.py
75
+
76
+ # Start frontend (in another terminal)
77
+ bun install
78
+ bun run dev
79
+ ```
80
 
81
+ The frontend dev server (port 5173) will automatically proxy API requests to the backend (port 7860).
 
 
82
 
83
  ## πŸ“‹ What's Included
84
 
Dockerfile CHANGED
@@ -51,24 +51,11 @@ RUN uv sync
51
  # Copy built frontend from previous stage with proper ownership
52
  COPY --chown=user --from=frontend-builder /app/build $HOME/app/static-frontend
53
 
54
- # Create a startup script that runs both services
55
  WORKDIR $HOME/app
56
- RUN echo '#!/bin/bash\n\
57
- echo "Starting LeRobot Arena services..."\n\
58
- echo "πŸš€ Starting Python backend on port 8080..."\n\
59
- cd $HOME/app/src-python && uv run python start_server.py &\n\
60
- echo "🌐 Starting static file server on port 7860..."\n\
61
- cd $HOME/app/static-frontend && python -m http.server 7860 --bind 0.0.0.0 &\n\
62
- echo "βœ… Both services started!"\n\
63
- echo "Backend: http://localhost:8080"\n\
64
- echo "Frontend: http://localhost:7860"\n\
65
- if [ ! -z "$SPACE_HOST" ]; then\n\
66
- echo "🌐 Hugging Face Space: https://$SPACE_HOST"\n\
67
- fi\n\
68
- wait' > start_services.sh && chmod +x start_services.sh
69
-
70
- # Expose both ports (7860 is default for HF Spaces)
71
- EXPOSE 7860 8080
72
-
73
- # Start both services
74
- CMD ["./start_services.sh"]
 
51
  # Copy built frontend from previous stage with proper ownership
52
  COPY --chown=user --from=frontend-builder /app/build $HOME/app/static-frontend
53
 
54
+ # Set working directory back to app root
55
  WORKDIR $HOME/app
56
+
57
+ # Expose port 7860 (HF Spaces default)
58
+ EXPOSE 7860
59
+
60
+ # Start the FastAPI server (serves both frontend and backend)
61
+ CMD ["sh", "-c", "cd src-python && uv run python start_server.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
docker-compose.yml CHANGED
@@ -2,13 +2,13 @@ version: '3.8'
2
 
3
  services:
4
  lerobot-arena:
5
- build:
6
- context: .
7
- dockerfile: Dockerfile
8
  ports:
9
- - "8080:8080" # Python backend
10
- - "7860:7860" # Svelte frontend (HF Spaces default)
11
  environment:
12
  - NODE_ENV=production
13
- - PYTHONUNBUFFERED=1
 
 
 
14
  restart: unless-stopped
 
2
 
3
  services:
4
  lerobot-arena:
5
+ build: .
 
 
6
  ports:
7
+ - "7860:7860" # FastAPI server serving both frontend and backend
 
8
  environment:
9
  - NODE_ENV=production
10
+ # volumes:
11
+ # # Optional: Mount local development files for debugging
12
+ # - ./src-python:/home/user/app/src-python
13
+ # - ./static-frontend:/home/user/app/static-frontend
14
  restart: unless-stopped
src-python/src/main.py CHANGED
@@ -6,6 +6,8 @@ from datetime import UTC, datetime
6
 
7
  from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
8
  from fastapi.middleware.cors import CORSMiddleware
 
 
9
 
10
  from .connection_manager import ConnectionManager
11
  from .models import (
@@ -60,6 +62,51 @@ app.add_middleware(
60
  connection_manager = ConnectionManager()
61
  robot_manager = RobotManager()
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  @app.on_event("startup")
65
  async def startup_event():
@@ -73,15 +120,65 @@ async def startup_event():
73
 
74
 
75
  @app.get("/")
76
- async def root():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return {
78
  "message": "LeRobot Arena Server",
79
  "version": "1.0.0",
80
  "robots_connected": len(robot_manager.robots),
81
  "active_connections": connection_manager.get_connection_count(),
 
82
  }
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # ============= ROBOT MANAGEMENT API =============
86
 
87
 
@@ -563,11 +660,11 @@ if __name__ == "__main__":
563
  logger = logging.getLogger("lerobot-arena")
564
  logger.info("πŸš€ Starting LeRobot Arena WebSocket Server...")
565
 
566
- # Start the server
567
  uvicorn.run(
568
  app,
569
  host="0.0.0.0",
570
- port=8080,
571
  log_level="info",
572
- reload=False, # Auto-reload on code changes
573
  )
 
6
 
7
  from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
8
  from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import FileResponse
10
+ from fastapi.staticfiles import StaticFiles
11
 
12
  from .connection_manager import ConnectionManager
13
  from .models import (
 
62
  connection_manager = ConnectionManager()
63
  robot_manager = RobotManager()
64
 
65
+ # Mount static files for the frontend
66
+ # Try different possible locations for the static frontend
67
+ static_dir_candidates = [
68
+ os.path.join(
69
+ os.path.dirname(os.path.dirname(__file__)), "..", "static-frontend"
70
+ ), # From src-python/src
71
+ os.path.join(
72
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "static-frontend"
73
+ ), # From src-python
74
+ "/home/user/app/static-frontend", # HF Spaces absolute path
75
+ "static-frontend", # Relative to working directory
76
+ ]
77
+
78
+ static_dir = None
79
+ for candidate in static_dir_candidates:
80
+ if os.path.exists(candidate):
81
+ static_dir = candidate
82
+ break
83
+
84
+ if static_dir:
85
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
86
+ logger.info(f"πŸ“ Serving static files from: {static_dir}")
87
+ else:
88
+ logger.warning(f"⚠️ Static directory not found in any of: {static_dir_candidates}")
89
+
90
+
91
+ def get_static_dir() -> str | None:
92
+ """Get the static directory path"""
93
+ static_dir_candidates = [
94
+ os.path.join(
95
+ os.path.dirname(os.path.dirname(__file__)), "..", "static-frontend"
96
+ ), # From src-python/src
97
+ os.path.join(
98
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
99
+ "static-frontend",
100
+ ), # From src-python
101
+ "/home/user/app/static-frontend", # HF Spaces absolute path
102
+ "static-frontend", # Relative to working directory
103
+ ]
104
+
105
+ for candidate in static_dir_candidates:
106
+ if os.path.exists(candidate):
107
+ return candidate
108
+ return None
109
+
110
 
111
  @app.on_event("startup")
112
  async def startup_event():
 
120
 
121
 
122
  @app.get("/")
123
+ async def serve_frontend():
124
+ """Serve the main frontend page"""
125
+ static_dir = get_static_dir()
126
+ if not static_dir:
127
+ return {
128
+ "message": "Frontend not built. Run 'bun run build' to build the frontend."
129
+ }
130
+
131
+ index_file = os.path.join(static_dir, "index.html")
132
+ if os.path.exists(index_file):
133
+ return FileResponse(index_file)
134
+ return {"message": "Frontend not built. Run 'bun run build' to build the frontend."}
135
+
136
+
137
+ @app.get("/status")
138
+ async def get_status():
139
+ """Get server status for health checks"""
140
  return {
141
  "message": "LeRobot Arena Server",
142
  "version": "1.0.0",
143
  "robots_connected": len(robot_manager.robots),
144
  "active_connections": connection_manager.get_connection_count(),
145
+ "status": "healthy",
146
  }
147
 
148
 
149
+ # Serve static assets from the _app directory
150
+ @app.get("/_app/{path:path}")
151
+ async def serve_app_assets(path: str):
152
+ """Serve Svelte app assets"""
153
+ static_dir = get_static_dir()
154
+ if not static_dir:
155
+ raise HTTPException(status_code=404, detail="Frontend not found")
156
+
157
+ file_path = os.path.join(static_dir, "_app", path)
158
+ if os.path.exists(file_path) and os.path.isfile(file_path):
159
+ return FileResponse(file_path)
160
+ raise HTTPException(status_code=404, detail="File not found")
161
+
162
+
163
+ # Catch-all route for client-side routing (SPA fallback)
164
+ @app.get("/{path:path}")
165
+ async def serve_spa_fallback(path: str):
166
+ """Serve the frontend for client-side routing"""
167
+ # If it's an API or WebSocket path, let it pass through to other handlers
168
+ if path.startswith(("api/", "ws/")):
169
+ raise HTTPException(status_code=404, detail="Not found")
170
+
171
+ # For all other paths, serve the index.html (SPA)
172
+ static_dir = get_static_dir()
173
+ if not static_dir:
174
+ raise HTTPException(status_code=404, detail="Frontend not found")
175
+
176
+ index_file = os.path.join(static_dir, "index.html")
177
+ if os.path.exists(index_file):
178
+ return FileResponse(index_file)
179
+ raise HTTPException(status_code=404, detail="Frontend not found")
180
+
181
+
182
  # ============= ROBOT MANAGEMENT API =============
183
 
184
 
 
660
  logger = logging.getLogger("lerobot-arena")
661
  logger.info("πŸš€ Starting LeRobot Arena WebSocket Server...")
662
 
663
+ # Start the server on port 7860 for HF Spaces compatibility
664
  uvicorn.run(
665
  app,
666
  host="0.0.0.0",
667
+ port=7860,
668
  log_level="info",
669
+ reload=False,
670
  )
src-python/start_server.py CHANGED
@@ -5,34 +5,37 @@ LeRobot Arena WebSocket Server
5
  Run with: python start_server.py
6
  """
7
 
8
- import uvicorn
9
  import logging
10
  import sys
11
  from pathlib import Path
12
 
 
 
13
  # Add src to path
14
  sys.path.insert(0, str(Path(__file__).parent / "src"))
15
 
16
  from src.main import app
17
 
 
18
  def main():
19
  """Start the LeRobot Arena server"""
20
  logging.basicConfig(
21
  level=logging.INFO,
22
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
23
  )
24
-
25
  logger = logging.getLogger("lerobot-arena")
26
  logger.info("πŸš€ Starting LeRobot Arena WebSocket Server...")
27
-
28
- # Start the server
29
  uvicorn.run(
30
- app,
31
- host="0.0.0.0",
32
- port=8080,
33
  log_level="info",
34
- reload=False # Auto-reload on code changes
35
  )
36
 
 
37
  if __name__ == "__main__":
38
- main()
 
5
  Run with: python start_server.py
6
  """
7
 
 
8
  import logging
9
  import sys
10
  from pathlib import Path
11
 
12
+ import uvicorn
13
+
14
  # Add src to path
15
  sys.path.insert(0, str(Path(__file__).parent / "src"))
16
 
17
  from src.main import app
18
 
19
+
20
  def main():
21
  """Start the LeRobot Arena server"""
22
  logging.basicConfig(
23
  level=logging.INFO,
24
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
25
  )
26
+
27
  logger = logging.getLogger("lerobot-arena")
28
  logger.info("πŸš€ Starting LeRobot Arena WebSocket Server...")
29
+
30
+ # Start the server on port 7860 for HF Spaces compatibility
31
  uvicorn.run(
32
+ app,
33
+ host="0.0.0.0",
34
+ port=7860,
35
  log_level="info",
36
+ reload=False, # Auto-reload on code changes
37
  )
38
 
39
+
40
  if __name__ == "__main__":
41
+ main()
src/lib/utils/config.ts CHANGED
@@ -29,7 +29,7 @@ function getSpaceHost(): string | undefined {
29
  * Get the base URL for API requests
30
  */
31
  export function getApiBaseUrl(): string {
32
- if (!isBrowser) return 'http://localhost:8080';
33
 
34
  // Check for Hugging Face Spaces
35
  const spaceHost = getSpaceHost();
@@ -38,22 +38,27 @@ export function getApiBaseUrl(): string {
38
  }
39
 
40
  // In browser, check current location
41
- const { protocol, hostname } = window.location;
42
 
43
- // If we're on localhost or serving from file, use localhost:8080
44
- if (hostname === 'localhost' || hostname === '127.0.0.1' || protocol === 'file:') {
45
- return 'http://localhost:8080';
 
 
 
 
 
46
  }
47
 
48
- // Default fallback - same origin but port 8080
49
- return `${protocol}//${hostname}:8080`;
50
  }
51
 
52
  /**
53
  * Get the WebSocket URL for real-time connections
54
  */
55
  export function getWebSocketBaseUrl(): string {
56
- if (!isBrowser) return 'ws://localhost:8080';
57
 
58
  // Check for Hugging Face Spaces
59
  const spaceHost = getSpaceHost();
@@ -61,18 +66,24 @@ export function getWebSocketBaseUrl(): string {
61
  return `wss://${spaceHost}`;
62
  }
63
 
64
- const { protocol, hostname } = window.location;
65
 
66
- // If we're on localhost, use ws://localhost:8080
67
  if (hostname === 'localhost' || hostname === '127.0.0.1') {
68
- return 'ws://localhost:8080';
 
 
 
 
 
 
69
  }
70
 
71
  // For HTTPS sites, use WSS; for HTTP sites, use WS
72
  const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
73
 
74
- // Default fallback - same origin but port 8080
75
- return `${wsProtocol}//${hostname}:8080`;
76
  }
77
 
78
  /**
 
29
  * Get the base URL for API requests
30
  */
31
  export function getApiBaseUrl(): string {
32
+ if (!isBrowser) return 'http://localhost:7860';
33
 
34
  // Check for Hugging Face Spaces
35
  const spaceHost = getSpaceHost();
 
38
  }
39
 
40
  // In browser, check current location
41
+ const { protocol, hostname, port } = window.location;
42
 
43
+ // If we're on the same host and port, use same origin (both frontend and backend on same server)
44
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
45
+ // In development, frontend might be on 5173 and backend on 7860
46
+ if (port === '5173') {
47
+ return 'http://localhost:7860';
48
+ }
49
+ // If frontend is served from backend (port 7860), use same origin
50
+ return `${protocol}//${hostname}:${port}`;
51
  }
52
 
53
+ // For production, use same origin (both served from same FastAPI server)
54
+ return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
55
  }
56
 
57
  /**
58
  * Get the WebSocket URL for real-time connections
59
  */
60
  export function getWebSocketBaseUrl(): string {
61
+ if (!isBrowser) return 'ws://localhost:7860';
62
 
63
  // Check for Hugging Face Spaces
64
  const spaceHost = getSpaceHost();
 
66
  return `wss://${spaceHost}`;
67
  }
68
 
69
+ const { protocol, hostname, port } = window.location;
70
 
71
+ // If we're on localhost
72
  if (hostname === 'localhost' || hostname === '127.0.0.1') {
73
+ // In development, frontend might be on 5173 and backend on 7860
74
+ if (port === '5173') {
75
+ return 'ws://localhost:7860';
76
+ }
77
+ // If frontend is served from backend (port 7860), use same origin
78
+ const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
79
+ return `${wsProtocol}//${hostname}:${port}`;
80
  }
81
 
82
  // For HTTPS sites, use WSS; for HTTP sites, use WS
83
  const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
84
 
85
+ // For production, use same origin (both served from same FastAPI server)
86
+ return `${wsProtocol}//${hostname}${port ? `:${port}` : ''}`;
87
  }
88
 
89
  /**