Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| sys.path.insert(0, os.path.dirname(__file__)) | |
| import asyncio | |
| import json | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse | |
| from services.config_service import load_config, get_config | |
| from services.database_service import init_db | |
| from services.progress_service import register_progress_callback, unregister_progress_callback, set_event_loop | |
| from api.auth import router as auth_router | |
| from api.upload import router as upload_router | |
| from api.analyze import router as analyze_router | |
| from api.download import router as download_router | |
| from api.history import router as history_router | |
| from api.internal import router as internal_router | |
| def _register_engines(): | |
| for module_name in [ | |
| "engines.detectors.motion_pose_audio_detector", | |
| "engines.detectors.xclip_slowfast_detector", | |
| "engines.renderers.cinematic_renderer", | |
| ]: | |
| try: | |
| __import__(module_name) | |
| except ImportError: | |
| pass | |
| async def lifespan(app: FastAPI): | |
| load_config() | |
| await init_db() | |
| _register_engines() | |
| set_event_loop(asyncio.get_running_loop()) | |
| keepalive_task = None | |
| cleanup_task = None | |
| space_url = os.environ.get("SPACE_URL", "") | |
| if space_url: | |
| async def _keepalive(): | |
| import aiohttp | |
| while True: | |
| await asyncio.sleep(300) | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| async with session.get(f"{space_url}/health", timeout=aiohttp.ClientTimeout(total=10)) as resp: | |
| if resp.status == 200: | |
| pass | |
| except Exception: | |
| pass | |
| keepalive_task = asyncio.create_task(_keepalive()) | |
| async def _periodic_cleanup(): | |
| """每小时执行一次过期清理和 LRU 清理""" | |
| from services.cleanup_service import cleanup_expired, cleanup_lru | |
| while True: | |
| await asyncio.sleep(3600) # 每小时 | |
| try: | |
| await cleanup_expired() | |
| except Exception as e: | |
| import logging | |
| logging.getLogger(__name__).error(f"Periodic cleanup_expired failed: {e}", exc_info=True) | |
| try: | |
| await cleanup_lru() | |
| except Exception as e: | |
| import logging | |
| logging.getLogger(__name__).error(f"Periodic cleanup_lru failed: {e}", exc_info=True) | |
| # 定期清理孤儿 GitHub Release(删除失败时残留的 Release) | |
| try: | |
| from services.github_release_service import cleanup_expired_releases | |
| deleted = await cleanup_expired_releases() | |
| if deleted > 0: | |
| logging.getLogger(__name__).info(f"Periodic cleanup_expired_releases: deleted {deleted} orphan releases") | |
| except Exception as e: | |
| import logging | |
| logging.getLogger(__name__).error(f"Periodic cleanup_expired_releases failed: {e}", exc_info=True) | |
| cleanup_task = asyncio.create_task(_periodic_cleanup()) | |
| yield | |
| if keepalive_task: | |
| keepalive_task.cancel() | |
| if cleanup_task: | |
| cleanup_task.cancel() | |
| app = FastAPI( | |
| title="篮球高光工坊", | |
| description="AI自动识别篮球高光,生成带特效的高光集锦", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| config = get_config() | |
| cors_origins = config.get("server", {}).get("cors_origins", ["http://localhost:5173"]) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=cors_origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| expose_headers=["Content-Range", "Content-Length", "Accept-Ranges", "Content-Type", "ETag", "Cache-Control"], | |
| ) | |
| app.include_router(auth_router) | |
| app.include_router(upload_router) | |
| app.include_router(analyze_router) | |
| app.include_router(download_router) | |
| app.include_router(history_router) | |
| app.include_router(internal_router) | |
| async def health(): | |
| gpu_available = False | |
| gpu_info = {} | |
| try: | |
| import torch | |
| gpu_available = torch.cuda.is_available() | |
| if gpu_available: | |
| gpu_info = { | |
| "device": torch.cuda.get_device_name(0), | |
| "memory_gb": round(torch.cuda.get_device_properties(0).total_mem / 1024**3, 1), | |
| } | |
| except ImportError: | |
| pass | |
| from services.gpu_gateway_service import get_gpu_status | |
| gateway_status = get_gpu_status() | |
| # 片头片尾渲染依赖检查 | |
| intro_deps = {"pil": False, "numpy": False, "ffmpeg": False, "sfx_count": 0, "cjk_font": False} | |
| try: | |
| from PIL import Image # noqa | |
| intro_deps["pil"] = True | |
| except ImportError: | |
| pass | |
| try: | |
| import numpy # noqa | |
| intro_deps["numpy"] = True | |
| except ImportError: | |
| pass | |
| try: | |
| import subprocess | |
| r = subprocess.run(["ffmpeg", "-version"], capture_output=True, timeout=5) | |
| intro_deps["ffmpeg"] = r.returncode == 0 | |
| except Exception: | |
| pass | |
| try: | |
| from services.video_processor import _ASSETS_DIR | |
| sfx_dir = os.path.join(_ASSETS_DIR, "sfx") | |
| if os.path.isdir(sfx_dir): | |
| intro_deps["sfx_count"] = len([f for f in os.listdir(sfx_dir) if f.endswith(".mp3")]) | |
| except Exception: | |
| pass | |
| try: | |
| from services.video_processor import _find_cjk_font | |
| intro_deps["cjk_font"] = os.path.isfile(_find_cjk_font()) | |
| except Exception: | |
| pass | |
| return { | |
| "status": "ok", | |
| "service": "basketball-highlight", | |
| "gpu_available": gpu_available, | |
| "gpu_info": gpu_info, | |
| "gpu_gateway": gateway_status, | |
| "intro_deps": intro_deps, | |
| } | |
| async def root(): | |
| return """<!DOCTYPE html> | |
| <html lang="zh"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>🏀 篮球高光工坊 API</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #eee; display: flex; justify-content: center; align-items: center; min-height: 100vh; } | |
| .container { text-align: center; max-width: 600px; padding: 40px; } | |
| h1 { font-size: 2.5em; margin-bottom: 16px; background: linear-gradient(135deg, #ff6b2b, #ff2d2d); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| p { color: #aaa; line-height: 1.8; margin-bottom: 12px; } | |
| .status { display: inline-block; padding: 8px 20px; background: rgba(0,200,83,0.15); color: #00C853; border-radius: 20px; font-weight: 600; margin: 16px 0; } | |
| .api-list { text-align: left; margin-top: 24px; background: rgba(255,255,255,0.05); border-radius: 12px; padding: 20px; } | |
| .api-list h3 { color: #ff6b2b; margin-bottom: 12px; } | |
| .api-item { padding: 6px 0; color: #ccc; font-family: monospace; font-size: 14px; } | |
| .api-item span { color: #00d4ff; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🏀 篮球高光工坊</h1> | |
| <div class="status">✅ 服务运行中</div> | |
| <p>AI 自动识别篮球高光,生成带特效的高光集锦</p> | |
| <p>这是后端 API 服务,请访问前端页面使用完整功能</p> | |
| <div class="api-list"> | |
| <h3>API 端点</h3> | |
| <div class="api-item"><span>POST</span> /api/auth/guest - 游客登录</div> | |
| <div class="api-item"><span>POST</span> /api/auth/email - 邮箱登录</div> | |
| <div class="api-item"><span>POST</span> /api/upload/create - 创建上传</div> | |
| <div class="api-item"><span>POST</span> /api/upload/{id}/file - 上传文件</div> | |
| <div class="api-item"><span>POST</span> /api/analyze/start - 开始分析</div> | |
| <div class="api-item"><span>GET</span> /api/analyze/status - 查询状态</div> | |
| <div class="api-item"><span>GET</span> /api/download/{id}/stream - 下载视频</div> | |
| <div class="api-item"><span>GET</span> /health - 健康检查</div> | |
| <div class="api-item"><span>WS</span> /ws/progress/{id} - 进度推送</div> | |
| </div> | |
| </div> | |
| </body> | |
| </html>""" | |
| class ProgressManager: | |
| def __init__(self): | |
| self.connections: dict = {} | |
| async def connect(self, video_id: str, websocket: WebSocket): | |
| await websocket.accept() | |
| if video_id not in self.connections: | |
| self.connections[video_id] = [] | |
| self.connections[video_id].append(websocket) | |
| def disconnect(self, video_id: str, websocket: WebSocket): | |
| if video_id in self.connections: | |
| self.connections[video_id] = [ | |
| ws for ws in self.connections[video_id] if ws != websocket | |
| ] | |
| if not self.connections[video_id]: | |
| del self.connections[video_id] | |
| async def broadcast(self, video_id: str, data: dict): | |
| if video_id in self.connections: | |
| for ws in self.connections[video_id]: | |
| try: | |
| await ws.send_json(data) | |
| except Exception: | |
| pass | |
| progress_manager = ProgressManager() | |
| async def ws_progress(websocket: WebSocket, video_id: str): | |
| await progress_manager.connect(video_id, websocket) | |
| callback = None | |
| try: | |
| async def on_progress(data: dict): | |
| await progress_manager.broadcast(video_id, data) | |
| callback = on_progress | |
| register_progress_callback(video_id, callback) | |
| while True: | |
| try: | |
| await websocket.receive_text() | |
| except WebSocketDisconnect: | |
| break | |
| finally: | |
| if callback: | |
| unregister_progress_callback(video_id, callback) | |
| progress_manager.disconnect(video_id, websocket) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| server_config = config.get("server", {}) | |
| uvicorn.run( | |
| "main:app", | |
| host=server_config.get("host", "0.0.0.0"), | |
| port=server_config.get("port", 7860), | |
| reload=True, | |
| ) | |