Files
pifu/api_server.py
2026-09-09 16:25:22 +08:00

319 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
皮肤分析 API 接口服务
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
from pydantic import BaseModel, Field
import uvicorn
import logging
from datetime import datetime
from typing import Optional
import subprocess
from pathlib import Path
from config.settings import API_HOST, API_CALLBACK
# ==================== 日志配置 ====================
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s | %(levelname)s | %(message)s'
)
logger = logging.getLogger(__name__)
# ==================== FastAPI 应用 ====================
app = FastAPI(
title="皮肤分析任务接口",
description="接收文件夹名称,触发皮肤分析任务",
version="1.0.0"
)
# ==================== 数据存储 ====================
task_store = {}
# ==================== 数据模型 ====================
class TaskRequest(BaseModel):
folder_name: str = Field(..., description="文件夹全路径")
task_id: str = Field(..., description="目标文件夹名称(任务 ID)")
class TaskResponse(BaseModel):
code: int = Field(..., description="状态码")
message: str = Field(..., description="消息")
task_id: str = Field(..., description="任务 ID")
status: str = Field(..., description="任务状态")
class TaskStatus(BaseModel):
task_id: str = Field(..., description="任务 ID")
status: str = Field(..., description="状态")
created_at: str = Field(..., description="创建时间")
completed_at: Optional[str] = Field(default=None, description="完成时间")
error: Optional[str] = Field(default=None, description="错误信息")
class CallbackRequest(BaseModel):
task_id: str = Field(..., description="任务 ID")
status: int = Field(..., description="处理状态,1=成功,0=失败")
# ==================== 后台任务 ====================
def run_analysis_task(task_id: str, request: TaskRequest):
"""后台运行分析任务"""
logger.info(f"🚀 开始执行任务:{task_id}")
try:
task_store[task_id]["status"] = "running"
# ==================== 输入目录 ====================
if Path(request.folder_name).is_absolute():
# 绝对路径
input_dir = Path(request.folder_name)
else:
# 相对路径
base_dir = "/data/analysis"
input_dir = (
Path(base_dir)
/ datetime.now().strftime("%Y%m%d")
/ task_id
)
# 检查目录是否存在
if not input_dir.exists():
raise Exception(f"输入目录不存在: {input_dir}")
# ==================== 脚本路径 ====================
script_path = Path(__file__).parent / "wanzheng.py"
if not script_path.exists():
raise Exception(f"分析脚本不存在: {script_path}")
# ==================== 模型路径 ====================
model_path = "/app/best.pt"
if not Path(model_path).exists():
raise Exception(f"模型文件不存在: {model_path}")
# ==================== 构建命令 ====================
cmd = [
sys.executable,
str(script_path),
str(input_dir),
"--model", model_path,
"--workers", "4",
"--api-url", API_HOST + API_CALLBACK,
"--task-id", task_id,
]
logger.info(f"🔧 命令参数:{cmd}")
logger.info(f"📋 执行命令:{' '.join(cmd)}")
logger.info(f"📁 输入目录:{input_dir}")
# ==================== 执行脚本 ====================
result = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=3600
)
# ==================== 输出日志 ====================
if result.stdout:
logger.info(f"📋 标准输出:\n{result.stdout}")
if result.stderr:
logger.error(f"📋 错误输出:\n{result.stderr}")
# ==================== 判断执行结果 ====================
if result.returncode == 0:
task_store[task_id]["status"] = "completed"
logger.info(f"✅ 任务完成:{task_id}")
else:
task_store[task_id]["status"] = "failed"
task_store[task_id]["error"] = result.stderr
logger.error(
f"❌ 任务失败:{task_id} "
f"(code={result.returncode})"
)
task_store[task_id]["completed_at"] = (
datetime.now().isoformat()
)
except subprocess.TimeoutExpired:
task_store[task_id]["status"] = "failed"
task_store[task_id]["error"] = "任务超时(超过1小时)"
task_store[task_id]["completed_at"] = (
datetime.now().isoformat()
)
logger.error(f"⏰ 任务超时:{task_id}")
except Exception as e:
task_store[task_id]["status"] = "failed"
task_store[task_id]["error"] = str(e)
task_store[task_id]["completed_at"] = (
datetime.now().isoformat()
)
logger.exception(f"❌ 任务异常:{task_id}")
# ==================== API 接口 ====================
@app.get("/")
async def root():
return {
"service": "皮肤分析任务接口",
"version": "1.0.0",
"status": "running"
}
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"tasks": {
"total": len(task_store),
"running": sum(1 for t in task_store.values() if t["status"] == "running"),
"completed": sum(1 for t in task_store.values() if t["status"] == "completed"),
"failed": sum(1 for t in task_store.values() if t["status"] == "failed")
}
}
@app.post("/api/v1/analysis/submit", response_model=TaskResponse)
async def submit_task(request: TaskRequest, background_tasks: BackgroundTasks):
# 使用传入的 task_id,如果没有则从 folder_name 提取
task_id = request.task_id if request.task_id else Path(request.folder_name).name
if not task_id or len(task_id) < 3:
raise HTTPException(status_code=400, detail="任务 ID 格式错误,至少 3 个字符")
if task_id in task_store:
existing_task = task_store[task_id]
if existing_task["status"] in ["pending", "running"]:
return TaskResponse(
code=200,
message="任务已存在,正在运行中",
task_id=task_id,
status="pending"
)
task_store[task_id] = {
"task_id": task_id,
"status": "pending",
"created_at": datetime.now().isoformat(),
"completed_at": None,
"error": None,
"request": {
"folder_name": request.folder_name,
"task_id": request.task_id
}
}
background_tasks.add_task(run_analysis_task, task_id, request)
logger.info(f"📝 任务已提交:{task_id}")
return TaskResponse(
code=200,
message="任务提交成功",
task_id=task_id,
status="pending"
)
@app.post("/api/v1/analysis/callback")
async def analysis_callback(request: CallbackRequest):
task_id = request.task_id
status = "completed" if request.status == 1 else "failed"
if task_id not in task_store:
task_store[task_id] = {
"task_id": task_id,
"status": status,
"created_at": datetime.now().isoformat(),
"completed_at": datetime.now().isoformat(),
"error": None if request.status == 1 else "分析任务回调失败",
"request": None
}
else:
task_store[task_id]["status"] = status
task_store[task_id]["completed_at"] = datetime.now().isoformat()
task_store[task_id]["error"] = None if request.status == 1 else "分析任务回调失败"
logger.info(f"📩 收到分析回调:{task_id} status={request.status}")
return {"code": 200, "message": "回调接收成功", "task_id": task_id, "status": status}
@app.get("/api/v1/analysis/status/{task_id}", response_model=TaskStatus)
async def get_task_status(task_id: str):
if task_id not in task_store:
raise HTTPException(status_code=404, detail=f"任务不存在:{task_id}")
task = task_store[task_id]
return TaskStatus(
task_id=task["task_id"],
status=task["status"],
created_at=task["created_at"],
completed_at=task.get("completed_at"),
error=task.get("error")
)
@app.get("/api/v1/analysis/list")
async def list_tasks(
status: Optional[str] = Query(default=None),
limit: int = Query(default=20, ge=1, le=100)
):
tasks = list(task_store.values())
if status:
tasks = [t for t in tasks if t["status"] == status]
tasks = sorted(tasks, key=lambda x: x["created_at"], reverse=True)[:limit]
return {
"total": len(tasks),
"tasks": tasks
}
# ==================== 启动服务 ====================
if __name__ == "__main__":
print("="*60)
print("皮肤分析 API 服务")
print("="*60)
print("监听地址:http://0.0.0.0:8000")
print("提交接口:POST /api/v1/analysis/submit")
print("状态查询:GET /api/v1/analysis/status/{task_id}")
print("健康检查:GET /health")
print("="*60)
print()
uvicorn.run(app, host="0.0.0.0", port=8000)