Spaces:
Runtime error
Runtime error
JakgritB commited on
Commit ·
e72b932
1
Parent(s): 102f4d2
Deploy safe hackathon demo with proxy
Browse files- Dockerfile +1 -0
- README.md +20 -0
- backend/main.py +97 -2
- docker-compose.yml +1 -0
- frontend/app/page.tsx +3 -7
- frontend/lib/api.ts +1 -0
Dockerfile
CHANGED
|
@@ -34,6 +34,7 @@ COPY frontend/ /app/frontend/
|
|
| 34 |
ENV NEXT_PUBLIC_API_URL=""
|
| 35 |
ENV NEXT_PUBLIC_DEMO_ENABLED="true"
|
| 36 |
ENV NEXT_PUBLIC_DEMO_ONLY="true"
|
|
|
|
| 37 |
|
| 38 |
RUN cd /app/frontend && npm run build
|
| 39 |
|
|
|
|
| 34 |
ENV NEXT_PUBLIC_API_URL=""
|
| 35 |
ENV NEXT_PUBLIC_DEMO_ENABLED="true"
|
| 36 |
ENV NEXT_PUBLIC_DEMO_ONLY="true"
|
| 37 |
+
ENV REMOTE_BACKEND_URL="http://129.212.178.101:8080"
|
| 38 |
|
| 39 |
RUN cd /app/frontend && npm run build
|
| 40 |
|
README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# ElevenClip AI ✂️
|
| 2 |
|
| 3 |
> **AMD Developer Hackathon 2026 — Track 3: Vision & Multimodal AI**
|
|
@@ -233,6 +244,15 @@ For a self-contained HuggingFace GPU Space, leave `NEXT_PUBLIC_API_URL=""` so ng
|
|
| 233 |
|
| 234 |
For the public HuggingFace Space, set `NEXT_PUBLIC_DEMO_ONLY=true`. Visitors can open the UI and run the simulated demo without touching AMD GPU credits. Judges can enter the access code to run real generation against the protected AMD GPU Cloud backend.
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
---
|
| 237 |
|
| 238 |
## Hackathon Compliance
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ElevenClip AI
|
| 3 |
+
emoji: ✂️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
# ElevenClip AI ✂️
|
| 13 |
|
| 14 |
> **AMD Developer Hackathon 2026 — Track 3: Vision & Multimodal AI**
|
|
|
|
| 244 |
|
| 245 |
For the public HuggingFace Space, set `NEXT_PUBLIC_DEMO_ONLY=true`. Visitors can open the UI and run the simulated demo without touching AMD GPU credits. Judges can enter the access code to run real generation against the protected AMD GPU Cloud backend.
|
| 246 |
|
| 247 |
+
The current Docker setup keeps `NEXT_PUBLIC_API_URL=""` so the browser calls the HF Space on the same origin, then FastAPI forwards real judge requests to `REMOTE_BACKEND_URL`. This avoids browser mixed-content blocking from an HTTPS Space calling an HTTP AMD Cloud IP directly.
|
| 248 |
+
|
| 249 |
+
```env
|
| 250 |
+
# HF Space / Docker runtime
|
| 251 |
+
NEXT_PUBLIC_API_URL=
|
| 252 |
+
NEXT_PUBLIC_DEMO_ONLY=true
|
| 253 |
+
REMOTE_BACKEND_URL=http://129.212.178.101:8080
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
---
|
| 257 |
|
| 258 |
## Hackathon Compliance
|
backend/main.py
CHANGED
|
@@ -17,11 +17,12 @@ import uuid
|
|
| 17 |
from pathlib import Path
|
| 18 |
from typing import Optional
|
| 19 |
|
| 20 |
-
from fastapi import FastAPI, UploadFile, File, Form, Header, WebSocket, WebSocketDisconnect, HTTPException
|
| 21 |
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
from fastapi.staticfiles import StaticFiles
|
| 23 |
from pydantic import BaseModel
|
| 24 |
from loguru import logger
|
|
|
|
| 25 |
|
| 26 |
from src.gpu.rocm_utils import get_device, log_gpu_status
|
| 27 |
from src.gpu.vllm_manager import ensure_vllm_running, vllm_stop, vllm_status
|
|
@@ -50,8 +51,10 @@ WORK_DIR.mkdir(parents=True, exist_ok=True)
|
|
| 50 |
DEMO_ACCESS_CODE = os.getenv("DEMO_ACCESS_CODE", "").strip()
|
| 51 |
MAX_CONCURRENT_JOBS = int(os.getenv("MAX_CONCURRENT_JOBS", "1"))
|
| 52 |
MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "500"))
|
|
|
|
| 53 |
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
# In-memory session store + WebSocket registry
|
| 57 |
sessions: dict[str, dict] = {}
|
|
@@ -66,6 +69,15 @@ def _require_access(x_demo_key: Optional[str]) -> None:
|
|
| 66 |
raise HTTPException(403, "Access code required for generation")
|
| 67 |
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# ─── Startup ──────────────────────────────────────────────────────────────
|
| 70 |
|
| 71 |
@app.on_event("startup")
|
|
@@ -182,6 +194,14 @@ async def health():
|
|
| 182 |
@app.post("/api/video-info")
|
| 183 |
async def video_info(req: VideoInfoRequest, x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")):
|
| 184 |
_require_access(x_demo_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
try:
|
| 186 |
return get_video_info(req.url)
|
| 187 |
except Exception as e:
|
|
@@ -196,6 +216,27 @@ async def process(
|
|
| 196 |
):
|
| 197 |
"""Main pipeline endpoint. Returns session_id immediately; progress via WebSocket."""
|
| 198 |
_require_access(x_demo_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
if len(active_jobs) >= MAX_CONCURRENT_JOBS:
|
| 200 |
raise HTTPException(429, "GPU is busy. Please try again in a few minutes.")
|
| 201 |
|
|
@@ -383,6 +424,11 @@ async def _run_pipeline(
|
|
| 383 |
|
| 384 |
@app.get("/api/clips/{session_id}")
|
| 385 |
async def get_clips(session_id: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
session = sessions.get(session_id)
|
| 387 |
if not session:
|
| 388 |
raise HTTPException(404, "Session not found")
|
|
@@ -391,6 +437,14 @@ async def get_clips(session_id: str):
|
|
| 391 |
|
| 392 |
@app.patch("/api/clips/{session_id}/{clip_index}/subtitles")
|
| 393 |
async def patch_subtitle(session_id: str, clip_index: int, patch: SubtitlePatch):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
clip = _get_clip_or_404(session_id, clip_index)
|
| 395 |
if not clip.get("ass_path"):
|
| 396 |
raise HTTPException(404, "No subtitle file for this clip")
|
|
@@ -400,6 +454,14 @@ async def patch_subtitle(session_id: str, clip_index: int, patch: SubtitlePatch)
|
|
| 400 |
|
| 401 |
@app.patch("/api/clips/{session_id}/{clip_index}/style")
|
| 402 |
async def patch_global_style(session_id: str, clip_index: int, patch: GlobalStylePatch):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
clip = _get_clip_or_404(session_id, clip_index)
|
| 404 |
if not clip.get("ass_path"):
|
| 405 |
raise HTTPException(404, "No subtitle file for this clip")
|
|
@@ -409,6 +471,11 @@ async def patch_global_style(session_id: str, clip_index: int, patch: GlobalStyl
|
|
| 409 |
|
| 410 |
@app.post("/api/clips/{session_id}/{clip_index}/render")
|
| 411 |
async def render_clip(session_id: str, clip_index: int):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
clip = _get_clip_or_404(session_id, clip_index)
|
| 413 |
|
| 414 |
clip_path = Path(clip["clip_path"])
|
|
@@ -439,14 +506,34 @@ def _get_clip_or_404(session_id: str, clip_index: int) -> dict:
|
|
| 439 |
|
| 440 |
# ─── vLLM management endpoints ────────────────────────────────────────────────
|
| 441 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
@app.get("/api/vllm/status")
|
| 443 |
async def get_vllm_status():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
return vllm_status()
|
| 445 |
|
| 446 |
|
| 447 |
@app.post("/api/vllm/stop")
|
| 448 |
async def stop_vllm(x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")):
|
| 449 |
_require_access(x_demo_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
loop = asyncio.get_running_loop()
|
| 451 |
await loop.run_in_executor(None, vllm_stop)
|
| 452 |
return {"ok": True, "message": "vLLM stopped — will restart automatically on next job"}
|
|
@@ -455,6 +542,14 @@ async def stop_vllm(x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")
|
|
| 455 |
@app.post("/api/vllm/start")
|
| 456 |
async def start_vllm(x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")):
|
| 457 |
_require_access(x_demo_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
loop = asyncio.get_running_loop()
|
| 459 |
await loop.run_in_executor(None, ensure_vllm_running)
|
| 460 |
return {"ok": True, "status": vllm_status()}
|
|
|
|
| 17 |
from pathlib import Path
|
| 18 |
from typing import Optional
|
| 19 |
|
| 20 |
+
from fastapi import FastAPI, UploadFile, File, Form, Header, Response, WebSocket, WebSocketDisconnect, HTTPException
|
| 21 |
from fastapi.middleware.cors import CORSMiddleware
|
| 22 |
from fastapi.staticfiles import StaticFiles
|
| 23 |
from pydantic import BaseModel
|
| 24 |
from loguru import logger
|
| 25 |
+
import httpx
|
| 26 |
|
| 27 |
from src.gpu.rocm_utils import get_device, log_gpu_status
|
| 28 |
from src.gpu.vllm_manager import ensure_vllm_running, vllm_stop, vllm_status
|
|
|
|
| 51 |
DEMO_ACCESS_CODE = os.getenv("DEMO_ACCESS_CODE", "").strip()
|
| 52 |
MAX_CONCURRENT_JOBS = int(os.getenv("MAX_CONCURRENT_JOBS", "1"))
|
| 53 |
MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "500"))
|
| 54 |
+
REMOTE_BACKEND_URL = os.getenv("REMOTE_BACKEND_URL", "").rstrip("/")
|
| 55 |
|
| 56 |
+
if not REMOTE_BACKEND_URL:
|
| 57 |
+
app.mount("/downloads", StaticFiles(directory=str(WORK_DIR)), name="downloads")
|
| 58 |
|
| 59 |
# In-memory session store + WebSocket registry
|
| 60 |
sessions: dict[str, dict] = {}
|
|
|
|
| 69 |
raise HTTPException(403, "Access code required for generation")
|
| 70 |
|
| 71 |
|
| 72 |
+
def _demo_headers(x_demo_key: Optional[str]) -> dict[str, str]:
|
| 73 |
+
return {"X-Demo-Key": x_demo_key.strip()} if x_demo_key and x_demo_key.strip() else {}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _proxy_response(resp: httpx.Response) -> Response:
|
| 77 |
+
content_type = resp.headers.get("content-type", "application/octet-stream")
|
| 78 |
+
return Response(content=resp.content, status_code=resp.status_code, media_type=content_type)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
# ─── Startup ──────────────────────────────────────────────────────────────
|
| 82 |
|
| 83 |
@app.on_event("startup")
|
|
|
|
| 194 |
@app.post("/api/video-info")
|
| 195 |
async def video_info(req: VideoInfoRequest, x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")):
|
| 196 |
_require_access(x_demo_key)
|
| 197 |
+
if REMOTE_BACKEND_URL:
|
| 198 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 199 |
+
resp = await client.post(
|
| 200 |
+
f"{REMOTE_BACKEND_URL}/api/video-info",
|
| 201 |
+
json=req.model_dump(),
|
| 202 |
+
headers=_demo_headers(x_demo_key),
|
| 203 |
+
)
|
| 204 |
+
return _proxy_response(resp)
|
| 205 |
try:
|
| 206 |
return get_video_info(req.url)
|
| 207 |
except Exception as e:
|
|
|
|
| 216 |
):
|
| 217 |
"""Main pipeline endpoint. Returns session_id immediately; progress via WebSocket."""
|
| 218 |
_require_access(x_demo_key)
|
| 219 |
+
if REMOTE_BACKEND_URL:
|
| 220 |
+
file_bytes: Optional[bytes] = None
|
| 221 |
+
file_name: Optional[str] = None
|
| 222 |
+
file_type = "application/octet-stream"
|
| 223 |
+
if file:
|
| 224 |
+
file_bytes = await file.read()
|
| 225 |
+
file_name = file.filename or "upload.mp4"
|
| 226 |
+
file_type = file.content_type or file_type
|
| 227 |
+
if len(file_bytes) > MAX_UPLOAD_MB * 1024 * 1024:
|
| 228 |
+
raise HTTPException(413, f"File too large. Max upload size is {MAX_UPLOAD_MB} MB.")
|
| 229 |
+
|
| 230 |
+
files = {"file": (file_name, file_bytes, file_type)} if file_bytes and file_name else None
|
| 231 |
+
async with httpx.AsyncClient(timeout=900.0) as client:
|
| 232 |
+
resp = await client.post(
|
| 233 |
+
f"{REMOTE_BACKEND_URL}/api/process",
|
| 234 |
+
data={"settings_json": settings_json},
|
| 235 |
+
files=files,
|
| 236 |
+
headers=_demo_headers(x_demo_key),
|
| 237 |
+
)
|
| 238 |
+
return _proxy_response(resp)
|
| 239 |
+
|
| 240 |
if len(active_jobs) >= MAX_CONCURRENT_JOBS:
|
| 241 |
raise HTTPException(429, "GPU is busy. Please try again in a few minutes.")
|
| 242 |
|
|
|
|
| 424 |
|
| 425 |
@app.get("/api/clips/{session_id}")
|
| 426 |
async def get_clips(session_id: str):
|
| 427 |
+
if REMOTE_BACKEND_URL:
|
| 428 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 429 |
+
resp = await client.get(f"{REMOTE_BACKEND_URL}/api/clips/{session_id}")
|
| 430 |
+
return _proxy_response(resp)
|
| 431 |
+
|
| 432 |
session = sessions.get(session_id)
|
| 433 |
if not session:
|
| 434 |
raise HTTPException(404, "Session not found")
|
|
|
|
| 437 |
|
| 438 |
@app.patch("/api/clips/{session_id}/{clip_index}/subtitles")
|
| 439 |
async def patch_subtitle(session_id: str, clip_index: int, patch: SubtitlePatch):
|
| 440 |
+
if REMOTE_BACKEND_URL:
|
| 441 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 442 |
+
resp = await client.patch(
|
| 443 |
+
f"{REMOTE_BACKEND_URL}/api/clips/{session_id}/{clip_index}/subtitles",
|
| 444 |
+
json=patch.model_dump(),
|
| 445 |
+
)
|
| 446 |
+
return _proxy_response(resp)
|
| 447 |
+
|
| 448 |
clip = _get_clip_or_404(session_id, clip_index)
|
| 449 |
if not clip.get("ass_path"):
|
| 450 |
raise HTTPException(404, "No subtitle file for this clip")
|
|
|
|
| 454 |
|
| 455 |
@app.patch("/api/clips/{session_id}/{clip_index}/style")
|
| 456 |
async def patch_global_style(session_id: str, clip_index: int, patch: GlobalStylePatch):
|
| 457 |
+
if REMOTE_BACKEND_URL:
|
| 458 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 459 |
+
resp = await client.patch(
|
| 460 |
+
f"{REMOTE_BACKEND_URL}/api/clips/{session_id}/{clip_index}/style",
|
| 461 |
+
json=patch.model_dump(),
|
| 462 |
+
)
|
| 463 |
+
return _proxy_response(resp)
|
| 464 |
+
|
| 465 |
clip = _get_clip_or_404(session_id, clip_index)
|
| 466 |
if not clip.get("ass_path"):
|
| 467 |
raise HTTPException(404, "No subtitle file for this clip")
|
|
|
|
| 471 |
|
| 472 |
@app.post("/api/clips/{session_id}/{clip_index}/render")
|
| 473 |
async def render_clip(session_id: str, clip_index: int):
|
| 474 |
+
if REMOTE_BACKEND_URL:
|
| 475 |
+
async with httpx.AsyncClient(timeout=600.0) as client:
|
| 476 |
+
resp = await client.post(f"{REMOTE_BACKEND_URL}/api/clips/{session_id}/{clip_index}/render")
|
| 477 |
+
return _proxy_response(resp)
|
| 478 |
+
|
| 479 |
clip = _get_clip_or_404(session_id, clip_index)
|
| 480 |
|
| 481 |
clip_path = Path(clip["clip_path"])
|
|
|
|
| 506 |
|
| 507 |
# ─── vLLM management endpoints ────────────────────────────────────────────────
|
| 508 |
|
| 509 |
+
if REMOTE_BACKEND_URL:
|
| 510 |
+
@app.get("/downloads/{file_path:path}")
|
| 511 |
+
async def proxy_download(file_path: str):
|
| 512 |
+
async with httpx.AsyncClient(timeout=600.0) as client:
|
| 513 |
+
resp = await client.get(f"{REMOTE_BACKEND_URL}/downloads/{file_path}")
|
| 514 |
+
return _proxy_response(resp)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
@app.get("/api/vllm/status")
|
| 518 |
async def get_vllm_status():
|
| 519 |
+
if REMOTE_BACKEND_URL:
|
| 520 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 521 |
+
resp = await client.get(f"{REMOTE_BACKEND_URL}/api/vllm/status")
|
| 522 |
+
return _proxy_response(resp)
|
| 523 |
return vllm_status()
|
| 524 |
|
| 525 |
|
| 526 |
@app.post("/api/vllm/stop")
|
| 527 |
async def stop_vllm(x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")):
|
| 528 |
_require_access(x_demo_key)
|
| 529 |
+
if REMOTE_BACKEND_URL:
|
| 530 |
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 531 |
+
resp = await client.post(
|
| 532 |
+
f"{REMOTE_BACKEND_URL}/api/vllm/stop",
|
| 533 |
+
headers=_demo_headers(x_demo_key),
|
| 534 |
+
)
|
| 535 |
+
return _proxy_response(resp)
|
| 536 |
+
|
| 537 |
loop = asyncio.get_running_loop()
|
| 538 |
await loop.run_in_executor(None, vllm_stop)
|
| 539 |
return {"ok": True, "message": "vLLM stopped — will restart automatically on next job"}
|
|
|
|
| 542 |
@app.post("/api/vllm/start")
|
| 543 |
async def start_vllm(x_demo_key: Optional[str] = Header(None, alias="X-Demo-Key")):
|
| 544 |
_require_access(x_demo_key)
|
| 545 |
+
if REMOTE_BACKEND_URL:
|
| 546 |
+
async with httpx.AsyncClient(timeout=240.0) as client:
|
| 547 |
+
resp = await client.post(
|
| 548 |
+
f"{REMOTE_BACKEND_URL}/api/vllm/start",
|
| 549 |
+
headers=_demo_headers(x_demo_key),
|
| 550 |
+
)
|
| 551 |
+
return _proxy_response(resp)
|
| 552 |
+
|
| 553 |
loop = asyncio.get_running_loop()
|
| 554 |
await loop.run_in_executor(None, ensure_vllm_running)
|
| 555 |
return {"ok": True, "status": vllm_status()}
|
docker-compose.yml
CHANGED
|
@@ -14,6 +14,7 @@ services:
|
|
| 14 |
- VLLM_MODEL=Qwen/Qwen2.5-VL-7B-Instruct
|
| 15 |
- VLLM_PORT=8000
|
| 16 |
- VLLM_DOCKER_CONTAINER=
|
|
|
|
| 17 |
- NEXT_PUBLIC_API_URL=http://localhost:7860
|
| 18 |
volumes:
|
| 19 |
- /tmp/elevnclip:/tmp/elevnclip
|
|
|
|
| 14 |
- VLLM_MODEL=Qwen/Qwen2.5-VL-7B-Instruct
|
| 15 |
- VLLM_PORT=8000
|
| 16 |
- VLLM_DOCKER_CONTAINER=
|
| 17 |
+
- REMOTE_BACKEND_URL=
|
| 18 |
- NEXT_PUBLIC_API_URL=http://localhost:7860
|
| 19 |
volumes:
|
| 20 |
- /tmp/elevnclip:/tmp/elevnclip
|
frontend/app/page.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import VideoUpload from "@/components/VideoUpload";
|
|
| 5 |
import ClipSettings from "@/components/ClipSettings";
|
| 6 |
import SubtitleDesigner from "@/components/SubtitleDesigner";
|
| 7 |
import GenerationProgress from "@/components/GenerationProgress";
|
| 8 |
-
import { startProcessing, connectProgressWS, type StyleConfig, type ProcessSettings } from "@/lib/api";
|
| 9 |
import { Scissors, Check, ChevronLeft, ArrowRight, Zap, Sparkles, PlayCircle } from "lucide-react";
|
| 10 |
|
| 11 |
const LANGS = [
|
|
@@ -206,11 +206,9 @@ export default function HomePage() {
|
|
| 206 |
wsRef.current = ws;
|
| 207 |
|
| 208 |
// HTTP polling fallback — kicks in if WS doesn't deliver messages
|
| 209 |
-
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
| 210 |
const poll = setInterval(async () => {
|
| 211 |
try {
|
| 212 |
-
const
|
| 213 |
-
const data = await res.json();
|
| 214 |
const lp = data.last_progress;
|
| 215 |
if (!wsAlive && lp) setProgress(lp);
|
| 216 |
if (data.status === "done") {
|
|
@@ -248,7 +246,6 @@ export default function HomePage() {
|
|
| 248 |
const sessionId = await startProcessing(settings, undefined, accessCode);
|
| 249 |
localStorage.setItem("elevnclip_session", sessionId);
|
| 250 |
|
| 251 |
-
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
| 252 |
const ws = connectProgressWS(sessionId, (data) => {
|
| 253 |
setProgress(data);
|
| 254 |
if (data.stage === "done") { ws.close(); router.push(`/editor?session=${sessionId}`); }
|
|
@@ -258,8 +255,7 @@ export default function HomePage() {
|
|
| 258 |
|
| 259 |
const poll = setInterval(async () => {
|
| 260 |
try {
|
| 261 |
-
const
|
| 262 |
-
const data = await res.json();
|
| 263 |
if (data.last_progress) setProgress(data.last_progress);
|
| 264 |
if (data.status === "done") { clearInterval(poll); router.push(`/editor?session=${sessionId}`); }
|
| 265 |
if (data.status === "error") clearInterval(poll);
|
|
|
|
| 5 |
import ClipSettings from "@/components/ClipSettings";
|
| 6 |
import SubtitleDesigner from "@/components/SubtitleDesigner";
|
| 7 |
import GenerationProgress from "@/components/GenerationProgress";
|
| 8 |
+
import { startProcessing, connectProgressWS, getClips, type StyleConfig, type ProcessSettings } from "@/lib/api";
|
| 9 |
import { Scissors, Check, ChevronLeft, ArrowRight, Zap, Sparkles, PlayCircle } from "lucide-react";
|
| 10 |
|
| 11 |
const LANGS = [
|
|
|
|
| 206 |
wsRef.current = ws;
|
| 207 |
|
| 208 |
// HTTP polling fallback — kicks in if WS doesn't deliver messages
|
|
|
|
| 209 |
const poll = setInterval(async () => {
|
| 210 |
try {
|
| 211 |
+
const data = await getClips(sessionId);
|
|
|
|
| 212 |
const lp = data.last_progress;
|
| 213 |
if (!wsAlive && lp) setProgress(lp);
|
| 214 |
if (data.status === "done") {
|
|
|
|
| 246 |
const sessionId = await startProcessing(settings, undefined, accessCode);
|
| 247 |
localStorage.setItem("elevnclip_session", sessionId);
|
| 248 |
|
|
|
|
| 249 |
const ws = connectProgressWS(sessionId, (data) => {
|
| 250 |
setProgress(data);
|
| 251 |
if (data.stage === "done") { ws.close(); router.push(`/editor?session=${sessionId}`); }
|
|
|
|
| 255 |
|
| 256 |
const poll = setInterval(async () => {
|
| 257 |
try {
|
| 258 |
+
const data = await getClips(sessionId);
|
|
|
|
| 259 |
if (data.last_progress) setProgress(data.last_progress);
|
| 260 |
if (data.status === "done") { clearInterval(poll); router.push(`/editor?session=${sessionId}`); }
|
| 261 |
if (data.status === "error") clearInterval(poll);
|
frontend/lib/api.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface SessionResult {
|
|
| 66 |
status: "starting" | "done" | "error";
|
| 67 |
clips: ClipResult[];
|
| 68 |
error?: string;
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
export async function getVideoInfo(url: string, accessCode?: string) {
|
|
|
|
| 66 |
status: "starting" | "done" | "error";
|
| 67 |
clips: ClipResult[];
|
| 68 |
error?: string;
|
| 69 |
+
last_progress?: { stage: string; pct: number; message: string };
|
| 70 |
}
|
| 71 |
|
| 72 |
export async function getVideoInfo(url: string, accessCode?: string) {
|