from fastapi import APIRouter, UploadFile, File, Form from typing import Optional from app.agents.multimodal_agent import TongueDiagnosisAgent import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1", tags=["tongue"]) _agent: Optional[TongueDiagnosisAgent] = None def get_agent() -> TongueDiagnosisAgent: global _agent if _agent is None: _agent = TongueDiagnosisAgent() return _agent @router.post("/tongue/diagnose") async def tongue_diagnose( file: UploadFile = File(...), user_id: int = Form(...), prompt_template: Optional[str] = Form(None), ): """舌诊分析: 上传舌苔图片, 返回分析结果""" agent = get_agent() import tempfile, os ext = os.path.splitext(file.filename or "tongue.jpg")[1] or ".jpg" tmp = tempfile.NamedTemporaryFile(delete=False, suffix=ext) content = await file.read() tmp.write(content) tmp.close() try: import base64 b64 = base64.b64encode(content).decode() data_url = f"data:image/{ext[1:]};base64,{b64}" result = await agent.diagnose(image_url=data_url, user_id=user_id, prompt_template=prompt_template) return {"code": 200, "data": result} except Exception as e: logger.error("舌诊分析失败: %s", e, exc_info=True) return {"code": 500, "message": "舌诊分析失败"} finally: os.unlink(tmp.name) # ── 舌象快速分类(纯颜色启发式,<50ms)──────────────────────────── def _quick_tongue_detect(content: bytes) -> bool: """基于颜色分布的轻量级舌象分类,避免调用 LLM。 判据: 1. 全图中偏红/暖色像素占比 >= 30%(舌体是红/粉色) 2. 白色/近白像素占比 < 65%(排除大面积白纸报告) 3. 纯黑文字像素占比 < 25%(排除文档/报告截图) """ from PIL import Image import io try: img = Image.open(io.BytesIO(content)).convert("RGB") small = img.resize((200, 200), Image.LANCZOS) pixels = list(small.getdata()) n = len(pixels) reddish = warm = white_px = black_px = 0 for r, g, b in pixels: if r > g * 1.15 and r > b * 1.15: reddish += 1 if r > g and r > b and (r - b) > 15: warm += 1 if r > 200 and g > 200 and b > 200: white_px += 1 if r < 80 and g < 80 and b < 80: black_px += 1 red_ratio = reddish / n warm_ratio = warm / n white_ratio = white_px / n text_ratio = black_px / n # 舌象:≥30% 偏红 + 白色占比不高 + 文字占比不高 is_tongue = (red_ratio >= 0.30) and (white_ratio < 0.65) and (text_ratio < 0.25) logger.debug( "舌象分类 [red=%.3f warm=%.3f white=%.3f text=%.3f] → %s", red_ratio, warm_ratio, white_ratio, text_ratio, is_tongue, ) return is_tongue except Exception as e: logger.warning("舌象颜色分析失败,降级为 false: %s", e) return False @router.post("/tongue/detect") async def tongue_detect(file: UploadFile = File(...)): """舌象照片分类: 判断上传图片是否为舌头(舌象)照片,用于图片上传分流。 返回 {"is_tongue": bool}。非舌象(报告截图/文档/其他部位)返回 false。 分类失败时返回 false(安全降级:走通用报告流程,不误伤正常报告)。 """ content = await file.read() try: is_tongue = _quick_tongue_detect(content) logger.info("舌象分类结果 is_tongue=%s (filename=%s)", is_tongue, file.filename) return {"code": 200, "data": {"is_tongue": is_tongue}} except Exception as e: logger.error("舌象分类失败: %s", e, exc_info=True) return {"code": 200, "data": {"is_tongue": False}}