from fastapi import FastAPI, Request, UploadFile, File, APIRouter
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
import uvicorn
import requests
import os
from pathlib import Path
import uuid
app = FastAPI(title="Video Analysis UI")
# Setup directories
BASE_DIR = Path(__file__).resolve().parent.parent
RESULTS_DIR = BASE_DIR / "results"
RESULTS_DIR.mkdir(exist_ok=True)
# Define the subpath prefix
PREFIX = "/analyze"
# Create a router to handle all /analyze requests
router = APIRouter(prefix=PREFIX)
# Worker URL (Docker service name)
WORKER_URL = os.getenv("WORKER_URL", "<http://worker:8000/process>")
HTML_CONTENT = f"""
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Skate Ollie Analyse</title>
<style>
:root {{
--bg: #121212;
--surface: #1e1e1e;
--primary: #bb86fc;
--text: #e0e0e0;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg);
color: var(--text);
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}}
.container {{
width: 90%;
max-width: 900px;
margin-top: 40px;
}}
h1 {{ color: var(--primary); text-align: center; }}
.upload-card {{
background: var(--surface);
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
text-align: center;
margin-bottom: 30px;
}}
input[type="file"] {{ margin: 20px 0; }}
button {{
background-color: var(--primary);
color: #000;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
}}
button:disabled {{ opacity: 0.5; cursor: not-allowed; }}
#status {{ margin-top: 20px; font-style: italic; }}
.result-container {{
display: none;
width: 100%;
}}
video {{
width: 100%;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.6);
}}
.loading-spinner {{
border: 4px solid rgba(255, 255, 255, 0.1);
border-left-color: var(--primary);
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
display: none;
margin: 20px auto;
}}
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
</style>
</head>
<body>
<div class="container">
<h1>Skate Ollie Analyse</h1>
<div class="upload-card">
<p>Wähle ein Video deines Ollies aus zur Analyse.</p>
<input type="file" id="videoInput" accept="video/*">
<br>
<button id="uploadBtn">Analyse starten</button>
<div class="loading-spinner" id="spinner"></div>
<p id="status"></p>
</div>
<div class="result-container" id="resultContainer">
<h2>Deine Analyse:</h2>
<video id="videoPlayer" controls autoplay loop playsinline>
<source id="videoSource" src="" type="video/mp4">
Ihr Browser unterstützt das Video-Tag nicht.
</video>
</div>
</div>
<script>
const uploadBtn = document.getElementById('uploadBtn');
const videoInput = document.getElementById('videoInput');
const status = document.getElementById('status');
const spinner = document.getElementById('spinner');
const resultContainer = document.getElementById('resultContainer');
const videoPlayer = document.getElementById('videoPlayer');
const videoSource = document.getElementById('videoSource');
uploadBtn.addEventListener('click', async () => {{
const file = videoInput.files[0];
if (!file) {{
alert('Bitte wähle ein Video aus.');
return;
}}
const formData = new FormData();
formData.append('file', file);
// UI Update
uploadBtn.disabled = true;
spinner.style.display = 'block';
status.innerText = 'Verarbeitung läuft... Das kann bis zu 1 Minute dauern.';
resultContainer.style.display = 'none';
try {{
const response = await fetch('{PREFIX}/upload', {{
method: 'POST',
body: formData
}});
const data = await response.json();
if (response.ok) {{
status.innerText = 'Fertig!';
videoSource.src = data.video_url + '?t=' + Date.now();
videoPlayer.load();
resultContainer.style.display = 'block';
}} else {{
status.innerText = 'Fehler: ' + (data.error || 'Unbekannt');
}}
}} catch (err) {{
status.innerText = 'Verbindungsfehler: ' + err.message;
}} finally {{
uploadBtn.disabled = false;
spinner.style.display = 'none';
}}
}});
</script>
</body>
</html>
"""
@router.get("/", response_class=HTMLResponse)
async def index():
return HTML_CONTENT
@router.post("/upload")
async def upload_video(file: UploadFile = File(...)):
job_id = str(uuid.uuid4())
# Forward to Worker
try:
files = {"file": (file.filename, file.file, file.content_type)}
worker_response = requests.post(
WORKER_URL,
files=files,
timeout=300 # 5 minutes max
)
except Exception as e:
return JSONResponse(status_code=500, content={"error": f"Worker unreachable: {str(e)}"})
if worker_response.status_code != 200:
return JSONResponse(
status_code=500,
content={"error": "Worker failed", "details": worker_response.text},
)
# Save result
output_path = RESULTS_DIR / f"{job_id}.mp4"
output_path.write_bytes(worker_response.content)
return {
"job_id": job_id,
"video_url": f"{PREFIX}/results/{job_id}.mp4"
}
# Mount results for static access
app.mount(f"{PREFIX}/results", StaticFiles(directory=str(RESULTS_DIR)), name="results")
# Include the router
app.include_router(router)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)