MohamedTry commited on
Commit
31160e7
·
verified ·
1 Parent(s): 96e90bd

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -0
app.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from typing import Optional, List, Dict, Any
4
+
5
+ import httpx
6
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel
9
+
10
+ # =========================
11
+ # إعدادات عامة
12
+ # =========================
13
+
14
+ HF_API_TOKEN = os.getenv("HF_API_TOKEN")
15
+ if not HF_API_TOKEN:
16
+ raise RuntimeError("Please set HF_API_TOKEN as a Secret in your Space.")
17
+
18
+ # موديلات Hugging Face المستخدمة
19
+ DETECTOR_MODEL = os.getenv("DETECTOR_MODEL", "Tinny-Robot/acne")
20
+ SEVERITY_MODEL = os.getenv("SEVERITY_MODEL", "imfarzanansari/skintelligent-acne")
21
+ CONDITION_MODEL = os.getenv("CONDITION_MODEL", "Tanishq77/skin-condition-classifier")
22
+
23
+ HF_API_BASE = "https://api-inference.huggingface.co/models"
24
+
25
+ HEADERS = {"Authorization": f"Bearer {HF_API_TOKEN}"}
26
+ TIMEOUT = 60.0 # ثواني
27
+
28
+
29
+ # =========================
30
+ # Schemas
31
+ # =========================
32
+
33
+ class SeverityOut(BaseModel):
34
+ raw: str
35
+ label_ar: str
36
+ score: float
37
+
38
+
39
+ class ConditionOut(BaseModel):
40
+ label: str
41
+ score: float
42
+
43
+
44
+ class AnalysisResponse(BaseModel):
45
+ num_lesions: int
46
+ severity: SeverityOut
47
+ condition: ConditionOut
48
+ meta: dict
49
+ # ممكن تضيف حقول أخرى لاحقًا (مثلاً توزيع الحبوب)
50
+
51
+
52
+ # =========================
53
+ # Utilities
54
+ # =========================
55
+
56
+ def map_severity_label_ar(label: str) -> str:
57
+ m = {
58
+ "clear": "صافية",
59
+ "mild": "خفيفة",
60
+ "moderate": "متوسطة",
61
+ "severe": "شديدة",
62
+ }
63
+ return m.get(label.lower(), label)
64
+
65
+
66
+ async def hf_object_detection(client: httpx.AsyncClient, image_bytes: bytes) -> List[Dict[str, Any]]:
67
+ url = f"{HF_API_BASE}/{DETECTOR_MODEL}"
68
+ resp = await client.post(url, headers=HEADERS, content=image_bytes)
69
+ if resp.status_code != 200:
70
+ raise HTTPException(status_code=500, detail=f"Detector error: {resp.text}")
71
+ data = resp.json()
72
+ # نتوقع list
73
+ if not isinstance(data, list):
74
+ # بعض الموديلات ترجع dict مع "error"
75
+ raise HTTPException(status_code=500, detail=f"Unexpected detector response: {data}")
76
+ return data
77
+
78
+
79
+ async def hf_image_classification(client: httpx.AsyncClient, image_bytes: bytes, model_name: str) -> List[Dict[str, Any]]:
80
+ url = f"{HF_API_BASE}/{model_name}"
81
+ resp = await client.post(url, headers=HEADERS, content=image_bytes)
82
+ if resp.status_code != 200:
83
+ raise HTTPException(status_code=500, detail=f"{model_name} error: {resp.text}")
84
+ data = resp.json()
85
+ if not isinstance(data, list):
86
+ raise HTTPException(status_code=500, detail=f"Unexpected response from {model_name}: {data}")
87
+ return data
88
+
89
+
90
+ # =========================
91
+ # FastAPI app
92
+ # =========================
93
+
94
+ app = FastAPI(title="Acne Orchestrator", version="0.1.0")
95
+
96
+ # CORS عشان تربطه مع Lovable / فرونت ثاني
97
+ app.add_middleware(
98
+ CORSMiddleware,
99
+ allow_origins=["*"], # عدلها لو تبي Origins محددة
100
+ allow_credentials=True,
101
+ allow_methods=["*"],
102
+ allow_headers=["*"],
103
+ )
104
+
105
+
106
+ @app.get("/")
107
+ async def root():
108
+ return {
109
+ "message": "Acne Orchestrator is running.",
110
+ "models": {
111
+ "detector": DETECTOR_MODEL,
112
+ "severity": SEVERITY_MODEL,
113
+ "condition": CONDITION_MODEL,
114
+ },
115
+ }
116
+
117
+
118
+ @app.post("/analyze", response_model=AnalysisResponse)
119
+ async def analyze(
120
+ file: UploadFile = File(...),
121
+ age: Optional[int] = Form(None),
122
+ skin_type: Optional[str] = Form(None),
123
+ notes: Optional[str] = Form(None),
124
+ ):
125
+ """
126
+ Endpoint رئيسي:
127
+ يستقبل صورة + معلومات بسيطة ويرجع:
128
+ - عدد الحبوب (من object detection)
129
+ - شدة الحالة (severity model)
130
+ - نوع الحالة الجلدية (condition model)
131
+ """
132
+
133
+ # قراءة الصورة كـ bytes
134
+ image_bytes = await file.read()
135
+ if not image_bytes:
136
+ raise HTTPException(status_code=400, detail="Empty image file.")
137
+
138
+ async with httpx.AsyncClient(timeout=TIMEOUT) as client:
139
+ # نطلق النداءات الثلاثة بالتوازي
140
+ det_task = hf_object_detection(client, image_bytes)
141
+ sev_task = hf_image_classification(client, image_bytes, SEVERITY_MODEL)
142
+ cond_task = hf_image_classification(client, image_bytes, CONDITION_MODEL)
143
+
144
+ det_json, sev_json, cond_json = await asyncio.gather(det_task, sev_task, cond_task)
145
+
146
+ # ========== عدد الحبوب ==========
147
+ # HF object-detection عادة ترجع list of dicts
148
+ num_lesions = len(det_json)
149
+
150
+ # ========== severity ==========
151
+ # HF image-classification: list of {"label": ..., "score": ...}
152
+ sev_top = max(sev_json, key=lambda x: x.get("score", 0))
153
+ severity_raw = str(sev_top.get("label", "unknown"))
154
+ severity_score = float(sev_top.get("score", 0.0))
155
+ severity_ar = map_severity_label_ar(severity_raw)
156
+
157
+ severity_obj = SeverityOut(
158
+ raw=severity_raw,
159
+ label_ar=severity_ar,
160
+ score=severity_score,
161
+ )
162
+
163
+ # ========== condition ==========
164
+ cond_top = max(cond_json, key=lambda x: x.get("score", 0))
165
+ condition_label = str(cond_top.get("label", "unknown"))
166
+ condition_score = float(cond_top.get("score", 0.0))
167
+
168
+ condition_obj = ConditionOut(
169
+ label=condition_label,
170
+ score=condition_score,
171
+ )
172
+
173
+ meta = {
174
+ "age": age,
175
+ "skin_type": skin_type,
176
+ "notes": notes,
177
+ "filename": file.filename,
178
+ "content_type": file.content_type,
179
+ }
180
+
181
+ return AnalysisResponse(
182
+ num_lesions=num_lesions,
183
+ severity=severity_obj,
184
+ condition=condition_obj,
185
+ meta=meta,
186
+ )