Spaces:
Sleeping
Sleeping
| """ | |
| 智能垃圾分类系统 - FastAPI 后端 | |
| Smart Garbage Classification System - FastAPI Backend | |
| """ | |
| import os | |
| import sys | |
| import tempfile | |
| import logging | |
| from pathlib import Path | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, File, UploadFile, HTTPException, Body | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from model import GarbageClassifier | |
| from knowledge_base import query_dna, get_knowledge_base | |
| # 日志配置 | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # 全局分类器(延迟加载) | |
| classifier: GarbageClassifier = None | |
| # 获取前端目录 | |
| FRONTEND_DIR = Path(__file__).parent / "frontend" | |
| # 允许的图片格式 | |
| ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} | |
| MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB | |
| async def lifespan(app: FastAPI): | |
| """应用生命周期管理""" | |
| logger.info("🚀 智能垃圾分类系统启动中...") | |
| # 预加载 DNA 知识库 | |
| kb = get_knowledge_base() | |
| logger.info(f"🧬 垃圾 DNA 知识库已加载 ({kb.count} 条记录)") | |
| yield | |
| logger.info("👋 系统已关闭") | |
| app = FastAPI( | |
| title="智能垃圾分类系统", | |
| description="上传垃圾照片,AI自动识别垃圾分类", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| ) | |
| # CORS - 允许手机等设备跨域访问 | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def get_classifier() -> GarbageClassifier: | |
| """获取分类器(延迟加载单例)""" | |
| global classifier | |
| if classifier is None: | |
| logger.info("正在初始化垃圾分类模型...") | |
| classifier = GarbageClassifier() | |
| return classifier | |
| # 服务前端静态文件 | |
| if FRONTEND_DIR.exists(): | |
| app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static") | |
| async def index(): | |
| """返回主页面""" | |
| index_path = FRONTEND_DIR / "index.html" | |
| if index_path.exists(): | |
| with open(index_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| return HTMLResponse("<h1>智能垃圾分类系统</h1><p>前端文件未找到</p>", status_code=200) | |
| async def health(): | |
| """健康检查""" | |
| return { | |
| "status": "ok", | |
| "model_loaded": classifier is not None, | |
| "frontend_exists": FRONTEND_DIR.exists(), | |
| } | |
| async def classify_image(file: UploadFile = File(...)): | |
| """ | |
| 上传图片并进行垃圾分类识别 | |
| - **file**: 图片文件(jpg, png, webp, bmp),最大 10MB | |
| """ | |
| # 验证文件类型 | |
| if not file.content_type or not file.content_type.startswith("image/"): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="请上传图片文件(支持 JPG、PNG、WebP、BMP 格式)" | |
| ) | |
| # 验证文件扩展名 | |
| ext = os.path.splitext(file.filename or "image.jpg")[1].lower() | |
| if ext not in ALLOWED_EXTENSIONS and ext: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"不支持的图片格式: {ext}。支持: {', '.join(ALLOWED_EXTENSIONS)}" | |
| ) | |
| # 读取文件内容 | |
| content = await file.read() | |
| if len(content) > MAX_FILE_SIZE: | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"图片太大({len(content)/1024/1024:.1f}MB),请上传小于 10MB 的图片" | |
| ) | |
| if len(content) == 0: | |
| raise HTTPException(status_code=400, detail="文件为空") | |
| # 保存到临时文件 | |
| suffix = ext if ext else ".jpg" | |
| tmp_path = None | |
| try: | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: | |
| tmp.write(content) | |
| tmp_path = tmp.name | |
| # 分类 | |
| clf = get_classifier() | |
| result = clf.classify(tmp_path) | |
| return JSONResponse(result) | |
| except RuntimeError as e: | |
| if "transformers" in str(e) or "model" in str(e).lower(): | |
| raise HTTPException( | |
| status_code=503, | |
| detail=f"模型加载失败: {str(e)}。请确保已安装依赖: pip install -r requirements.txt" | |
| ) | |
| raise HTTPException(status_code=500, detail=f"分类失败: {str(e)}") | |
| except Exception as e: | |
| logger.error(f"分类过程中出错: {e}", exc_info=True) | |
| raise HTTPException(status_code=500, detail=f"分类失败: {str(e)}") | |
| finally: | |
| # 清理临时文件 | |
| if tmp_path and os.path.exists(tmp_path): | |
| try: | |
| os.unlink(tmp_path) | |
| except Exception as e: | |
| logger.warning(f"清理临时文件失败: {e}") | |
| async def classify_text(data: dict = Body(...)): | |
| """ | |
| 文本垃圾分类 - 根据物品名称判断垃圾分类 | |
| """ | |
| item_name = data.get("item", "").strip() | |
| if not item_name: | |
| raise HTTPException(status_code=400, detail="请提供物品名称(item 参数)") | |
| clf = get_classifier() | |
| result = clf.classify_text(item_name) | |
| return JSONResponse(result) | |
| async def get_item_dna(item: str = None, item_zh: str = None): | |
| """查询物品的 DNA 知识 | |
| - **item**: 英文物品名(模型预测标签) | |
| - **item_zh**: 中文物品名 | |
| """ | |
| if not item and not item_zh: | |
| return { | |
| "dna_entries": get_knowledge_base().count, | |
| "detail": "请提供 item 或 item_zh 参数进行查询" | |
| } | |
| dna = query_dna(item, item_zh) | |
| if dna: | |
| return {"found": True, "dna": dna} | |
| return {"found": False, "detail": f"未找到「{item_zh or item}」的 DNA 信息"} | |
| async def get_daily_challenge(): | |
| """获取今日每日挑战题目(10 件物品)""" | |
| from datetime import date | |
| clf = get_classifier() | |
| items = clf.generate_daily_challenge(date.today()) | |
| return { | |
| "date": date.today().isoformat(), | |
| "items": items, | |
| "total": len(items), | |
| } | |
| async def submit_answer(data: dict = Body(...)): | |
| """提交单题答案,返回对错及趣味知识 | |
| Body: { item_en: str, guess: str } | |
| - item_en: 物品英文标签 | |
| - guess: 用户猜测的分类 | |
| Returns: { correct: bool, correct_category: str, dna: {...} | null } | |
| """ | |
| item_en = data.get("item_en", "") | |
| guess = data.get("guess", "") | |
| if not item_en or not guess: | |
| return {"error": "缺少 item_en 或 guess 参数"} | |
| clf = get_classifier() | |
| correct_category = clf.label_to_category.get(item_en, "其他垃圾") | |
| is_correct = guess == correct_category | |
| # 查询 DNA | |
| item_zh = clf._get_label_zh(item_en) | |
| dna = query_dna(item_en, item_zh) | |
| return { | |
| "correct": is_correct, | |
| "correct_category": correct_category, | |
| "item_zh": item_zh, | |
| "dna": dna, | |
| } | |
| async def get_categories(): | |
| """获取分类信息""" | |
| clf = get_classifier() | |
| return { | |
| "categories": [ | |
| { | |
| "name": name, | |
| **info | |
| } | |
| for name, info in clf.category_info.items() | |
| ] | |
| } | |
| # 错误处理 | |
| async def not_found(request, exc): | |
| return JSONResponse( | |
| status_code=404, | |
| content={"detail": "请求的资源不存在"} | |
| ) | |
| async def server_error(request, exc): | |
| return JSONResponse( | |
| status_code=500, | |
| content={"detail": "服务器内部错误,请稍后重试"} | |
| ) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| host = os.getenv("HOST", "0.0.0.0") | |
| port = int(os.getenv("PORT", "8000")) | |
| print("=" * 50) | |
| print(" 🌱 智能垃圾分类系统") | |
| print("=" * 50) | |
| print(f" 本地访问: http://localhost:{port}") | |
| print(f" 局域网访问: http://<本机IP>:{port}") | |
| print(f" 上传图片即可识别垃圾类别") | |
| print("=" * 50) | |
| uvicorn.run(app, host=host, port=port, log_level="info") | |