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 @asynccontextmanager 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) @app.get("/health") 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, } @app.get("/", response_class=HTMLResponse) async def root(): return """ 🏀 篮球高光工坊 API

🏀 篮球高光工坊

✅ 服务运行中

AI 自动识别篮球高光,生成带特效的高光集锦

这是后端 API 服务,请访问前端页面使用完整功能

API 端点

POST /api/auth/guest - 游客登录
POST /api/auth/email - 邮箱登录
POST /api/upload/create - 创建上传
POST /api/upload/{id}/file - 上传文件
POST /api/analyze/start - 开始分析
GET /api/analyze/status - 查询状态
GET /api/download/{id}/stream - 下载视频
GET /health - 健康检查
WS /ws/progress/{id} - 进度推送
""" 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() @app.websocket("/ws/progress/{video_id}") 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, )