hiro1997's picture
Upload main.py with huggingface_hub
d5d10f6 verified
Raw
History Blame Contribute Delete
10.4 kB
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 """<!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()
@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,
)