Spaces:
Running
Running
| # 🩺 Skin Disease Classifier — API Documentation (v2.0 — Dynamic AI Recommendations) | |
| > ResNet50 (HAM10000) for classification + Groq (fast LLM inference) for personalized, dynamic risks & recommendations · Deployed on Hugging Face Spaces | |
|  | |
|  | |
|  | |
|  | |
| --- | |
| ## 🆕 What's new in v2.0 | |
| This version replaces the old **static** recommendations with a **two-step, dynamic** flow: | |
| 1. **`POST /predict`** — classifies the image and returns a **set of follow-up questions specific to the predicted class** (e.g. Melanoma gets different questions than a Mole). | |
| 2. **`POST /recommend`** — takes the predicted class + the user's answers to those questions, sends them to **Groq**, and returns **personalized risks & recommendations in both English and Arabic**, plus an `urgency_level`. | |
| This means recommendations are no longer hardcoded — they adapt to each patient's actual situation (how long the lesion has been there, whether it's bleeding, family history, etc). | |
| --- | |
| ## 📊 Model Overview (Classifier) | |
| | Property | Value | | |
| |---|---| | |
| | Architecture | ResNet50 (pretrained on ImageNet, fine-tuned) | | |
| | Final layer | Linear(2048 → 7) | | |
| | Input size | 224 × 224 × 3 (RGB) | | |
| | Normalization | mean=[0.485, 0.456, 0.406] · std=[0.229, 0.224, 0.225] | | |
| | Dataset | HAM10000 — oversampled to 6,000 samples/class | | |
| | Saved weights | `skin_resnet50.pth` | | |
| | Recommendation engine | Groq — model `llama-3.3-70b-versatile` | | |
| --- | |
| ## 🔗 Base URL | |
| ``` | |
| https://refaat9900-skin-detection.hf.space | |
| ``` | |
| Swagger UI: | |
| ``` | |
| https://refaat9900-skin-detection.hf.space/docs | |
| ``` | |
| A working **demo web page** (`index.html`) that calls this exact API end-to-end — upload → predict → answer questions → recommend — is included alongside this documentation. Open it in any browser to see the full flow live. | |
| --- | |
| ## 🔑 Required Server Configuration | |
| The server needs a **Groq API key** set as an environment variable named `GROK_API_KEY`. | |
| > ⚠️ Despite the variable name `GROK_API_KEY` (kept for backward compatibility), this must be a key from **Groq** (console.groq.com) — **not** xAI's Grok (console.x.ai). These are two different companies and the keys are not interchangeable. | |
| ### On Hugging Face Spaces: | |
| 1. Go to your Space → **Settings** → **Variables and secrets** | |
| 2. Click **New secret** | |
| 3. Name: `GROK_API_KEY` | |
| 4. Value: *(your key from [console.groq.com/keys](https://console.groq.com/keys))* | |
| 5. Save — the Space will restart automatically | |
| > ⚠️ Never hardcode the API key in `main.py` or commit it to a public repo. Always use Space secrets / environment variables. | |
| --- | |
| ## 📌 Endpoints | |
| | Method | Path | Description | | |
| |---|---|---| | |
| | `GET` | `/` | Health check | | |
| | `POST` | `/predict` | Upload image → get class + confidence + **follow-up questions** | | |
| | `POST` | `/recommend` | Send class + answers → get **AI-generated risks & recommendations** (EN + AR) | | |
| --- | |
| ## 1️⃣ POST `/predict` | |
| ### Request | |
| - `Content-Type: multipart/form-data` | |
| - Field name: `file` | |
| - Accepted: `image/jpeg`, `image/png`, `image/jpg`, `image/webp` | |
| ### Response | |
| | Field | Type | Description | | |
| |---|---|---| | |
| | `predicted_class` | `string` | Predicted disease name (English) | | |
| | `predicted_class_index` | `int` | Class index (0–6) — **save this, you need it for `/recommend`** | | |
| | `confidence` | `float` | Confidence of top prediction (0–100%) | | |
| | `description` | `string` | General medical description (English) | | |
| | `all_probabilities` | `object[]` | Confidence for all 7 classes, sorted descending | | |
| | `followup_questions` | `object[]` | **Questions to ask the user**, specific to the predicted class | | |
| ### `followup_questions` item structure | |
| | Field | Type | Description | | |
| |---|---|---| | |
| | `id` | `string` | Stable identifier — **send this back exactly** in `/recommend` | | |
| | `question_en` | `string` | Question text in English | | |
| | `question_ar` | `string` | Question text in Arabic | | |
| | `options` | `string[]` | Suggested answer choices (show as buttons/dropdown) | | |
| ### Example response | |
| ```json | |
| { | |
| "predicted_class": "Melanoma", | |
| "predicted_class_index": 5, | |
| "confidence": 94.32, | |
| "description": "Melanoma is the most dangerous form of skin cancer...", | |
| "all_probabilities": [ | |
| { "class_index": 5, "class_name": "Melanoma", "confidence": 94.32 }, | |
| { "class_index": 4, "class_name": "Melanocytic Nevi (Moles)", "confidence": 3.11 } | |
| ], | |
| "followup_questions": [ | |
| { | |
| "id": "duration", | |
| "question_en": "How long have you noticed this spot or change?", | |
| "question_ar": "البقعة أو التغيير ده موجود من قد إيه؟", | |
| "options": ["Less than 1 month", "1–6 months", "More than 6 months"] | |
| }, | |
| { | |
| "id": "abcde_change", | |
| "question_en": "Has it changed in shape, border, color, or size recently (ABCDE rule)?", | |
| "question_ar": "حصل تغيير في الشكل أو الحدود أو اللون أو الحجم مؤخرًا؟", | |
| "options": ["Yes, significantly", "Slightly", "No"] | |
| }, | |
| { | |
| "id": "bleeding_itching", | |
| "question_en": "Is it bleeding, itching, or crusting?", | |
| "question_ar": "بينزف أو بيحكك أو فيه قشور عليه؟", | |
| "options": ["Yes", "No"] | |
| }, | |
| { | |
| "id": "family_history", | |
| "question_en": "Any personal or family history of melanoma?", | |
| "question_ar": "في تاريخ شخصي أو عائلي للميلانوما؟", | |
| "options": ["Yes", "No", "Not sure"] | |
| }, | |
| { | |
| "id": "sun_history", | |
| "question_en": "History of severe sunburns or frequent tanning bed use?", | |
| "question_ar": "في تاريخ لحروق شمس شديدة أو استخدام متكرر لأجهزة التسمير؟", | |
| "options": ["Yes", "No"] | |
| } | |
| ] | |
| } | |
| ``` | |
| > 💡 Each of the 7 classes has its **own set of 4–5 tailored questions** (Melanoma's questions differ from Mole's, which differ from Dermatofibroma's, etc). | |
| --- | |
| ## 2️⃣ POST `/recommend` | |
| Call this **after** the user answers the `followup_questions` from `/predict`. | |
| ### Request body (JSON) | |
| ```json | |
| { | |
| "predicted_class_index": 5, | |
| "answers": { | |
| "duration": "1–6 months", | |
| "abcde_change": "Yes, significantly", | |
| "bleeding_itching": "Yes", | |
| "family_history": "No", | |
| "sun_history": "Yes" | |
| } | |
| } | |
| ``` | |
| | Field | Type | Required | Description | | |
| |---|---|---|---| | |
| | `predicted_class_index` | `int` | ✅ Yes | The index returned by `/predict` | | |
| | `answers` | `object` | ✅ Yes | Key = question `id` from `/predict`, Value = the option the user picked | | |
| > ⚠️ If you skip a question, just omit its key — the backend will mark it as "Not answered" when building the AI prompt. | |
| ### Response | |
| | Field | Type | Description | | |
| |---|---|---| | |
| | `predicted_class` | `string` | Class name (English) | | |
| | `risks_en` | `string[]` | 3–5 personalized risk points (English) | | |
| | `risks_ar` | `string[]` | Same risks in Arabic | | |
| | `recommendations_en` | `string[]` | 4–6 personalized action recommendations (English) | | |
| | `recommendations_ar` | `string[]` | Same recommendations in Arabic | | |
| | `urgency_level` | `string` | One of: `"low"`, `"moderate"`, `"high"`, `"urgent"` | | |
| ### Example response | |
| ```json | |
| { | |
| "predicted_class": "Melanoma", | |
| "risks_en": [ | |
| "Recent changes in shape, color, and size strongly increase concern for malignant melanoma", | |
| "Bleeding or crusting can indicate active tissue change requiring urgent biopsy", | |
| "History of severe sunburns is a known major risk factor for melanoma", | |
| "Melanoma can spread quickly to lymph nodes if not treated early" | |
| ], | |
| "risks_ar": [ | |
| "التغييرات الأخيرة في الشكل واللون والحجم تزيد من احتمالية وجود ميلانوما خبيثة", | |
| "النزيف أو القشور قد يدل على تغير نشط في النسيج يحتاج خزعة عاجلة", | |
| "تاريخ حروق الشمس الشديدة عامل خطر معروف للميلانوما", | |
| "الميلانوما ممكن تنتشر بسرعة للغدد الليمفاوية لو ماتعالجتش بدري" | |
| ], | |
| "recommendations_en": [ | |
| "⚠️ See a dermatologist or oncologist within the next few days — do not delay", | |
| "Avoid any further sun exposure or trauma to the area", | |
| "Take a clear photo dated today to track any further changes", | |
| "Ask your doctor about a biopsy given the recent ABCDE changes", | |
| "Inform close family members to get their skin checked too" | |
| ], | |
| "recommendations_ar": [ | |
| "⚠️ يجب زيارة طبيب جلدية أو أورام في أقرب وقت ممكن — لا تتأخر", | |
| "تجنب أي تعرض إضافي للشمس أو إصابة في المنطقة", | |
| "خد صورة واضحة بتاريخ اليوم لتتابع أي تغييرات إضافية", | |
| "اسأل الطبيب عن إجراء خزعة بسبب التغييرات الأخيرة", | |
| "أخبر أفراد العائلة المقربين بعمل فحص للجلد كذلك" | |
| ], | |
| "urgency_level": "urgent" | |
| } | |
| ``` | |
| --- | |
| ## 🔄 Full Flow Diagram | |
| ``` | |
| ┌─────────────┐ image ┌──────────────────┐ | |
| │ Frontend │ ─────────────► │ POST /predict │ | |
| │ (Flutter/JS)│ │ (ResNet50 model) │ | |
| └─────────────┘ └──────────────────┘ | |
| ▲ │ | |
| │ class + confidence + │ | |
| │ followup_questions ▼ | |
| │ Show questions to user | |
| │ (buttons from `options`) | |
| │ │ | |
| │ ▼ | |
| │ User answers all/some | |
| │ │ | |
| │ class_index + answers ▼ | |
| │ ┌──────────────────┐ | |
| └────────────────────── │ POST /recommend │ | |
| risks + recommendations│ (calls Groq) │ | |
| (EN + AR) + urgency └──────────────────┘ | |
| ``` | |
| --- | |
| ## 🏷️ Disease classes (label mapping) | |
| | Index | Class name (EN) | | |
| |---|---| | |
| | 0 | Actinic Keratoses | | |
| | 1 | Basal Cell Carcinoma | | |
| | 2 | Benign Keratosis-like Lesions | | |
| | 3 | Dermatofibroma | | |
| | 4 | Melanocytic Nevi (Moles) | | |
| | 5 | Melanoma | | |
| | 6 | Vascular Lesions | | |
| --- | |
| ## 💻 Integration Examples | |
| ### Flutter (Dart) — Full flow | |
| ```dart | |
| import 'dart:io'; | |
| import 'dart:convert'; | |
| import 'package:http/http.dart' as http; | |
| const String baseUrl = 'https://refaat9900-skin-detection.hf.space'; | |
| // Step 1: Predict | |
| Future<Map<String, dynamic>> predictSkinDisease(File imageFile) async { | |
| final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/predict')); | |
| request.files.add(await http.MultipartFile.fromPath('file', imageFile.path)); | |
| final streamed = await request.send(); | |
| final response = await http.Response.fromStream(streamed); | |
| if (response.statusCode != 200) { | |
| throw Exception('Predict failed: ${response.statusCode} — ${response.body}'); | |
| } | |
| return jsonDecode(response.body) as Map<String, dynamic>; | |
| } | |
| // Step 2: Recommend (after user answers followup_questions) | |
| Future<Map<String, dynamic>> getRecommendation( | |
| int predictedClassIndex, | |
| Map<String, String> answers, | |
| ) async { | |
| final response = await http.post( | |
| Uri.parse('$baseUrl/recommend'), | |
| headers: {'Content-Type': 'application/json'}, | |
| body: jsonEncode({ | |
| 'predicted_class_index': predictedClassIndex, | |
| 'answers': answers, | |
| }), | |
| ); | |
| if (response.statusCode != 200) { | |
| throw Exception('Recommend failed: ${response.statusCode} — ${response.body}'); | |
| } | |
| return jsonDecode(response.body) as Map<String, dynamic>; | |
| } | |
| // Usage example | |
| void example() async { | |
| final file = File('/path/to/skin_image.jpg'); | |
| // 1. Predict | |
| final prediction = await predictSkinDisease(file); | |
| print('Predicted: ${prediction['predicted_class']}'); | |
| final classIndex = prediction['predicted_class_index']; | |
| final questions = prediction['followup_questions'] as List; | |
| // 2. Collect answers (in a real app, show these as UI — e.g. buttons per `options`) | |
| final Map<String, String> answers = {}; | |
| for (final q in questions) { | |
| // Example: just pick the first option for demo purposes | |
| answers[q['id']] = q['options'][0]; | |
| } | |
| // 3. Get personalized recommendation | |
| final result = await getRecommendation(classIndex, answers); | |
| print('Urgency: ${result['urgency_level']}'); | |
| print('Risks (EN): ${result['risks_en']}'); | |
| print('Risks (AR): ${result['risks_ar']}'); | |
| print('Recommendations (EN): ${result['recommendations_en']}'); | |
| print('Recommendations (AR): ${result['recommendations_ar']}'); | |
| } | |
| ``` | |
| #### pubspec.yaml | |
| ```yaml | |
| dependencies: | |
| flutter: | |
| sdk: flutter | |
| http: ^1.2.1 | |
| image_picker: ^1.0.7 | |
| ``` | |
| --- | |
| ### JavaScript (Fetch) — Full flow | |
| ```javascript | |
| const BASE_URL = 'https://refaat9900-skin-detection.hf.space'; | |
| // Step 1: Predict | |
| async function predictSkinDisease(imageFile) { | |
| const formData = new FormData(); | |
| formData.append('file', imageFile); | |
| const res = await fetch(`${BASE_URL}/predict`, { method: 'POST', body: formData }); | |
| if (!res.ok) throw new Error(`Predict failed: ${res.status}`); | |
| return await res.json(); | |
| } | |
| // Step 2: Recommend | |
| async function getRecommendation(predictedClassIndex, answers) { | |
| const res = await fetch(`${BASE_URL}/recommend`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ predicted_class_index: predictedClassIndex, answers }), | |
| }); | |
| if (!res.ok) throw new Error(`Recommend failed: ${res.status}`); | |
| return await res.json(); | |
| } | |
| // Usage | |
| const fileInput = document.querySelector('#upload'); | |
| fileInput.addEventListener('change', async (e) => { | |
| const file = e.target.files[0]; | |
| const prediction = await predictSkinDisease(file); | |
| // Render prediction.followup_questions as a form (radio buttons per `options`) | |
| renderQuestionsForm(prediction.followup_questions, async (answers) => { | |
| const result = await getRecommendation(prediction.predicted_class_index, answers); | |
| console.log('Urgency:', result.urgency_level); | |
| console.log('Risks (EN):', result.risks_en); | |
| console.log('Recommendations (AR):', result.recommendations_ar); | |
| }); | |
| }); | |
| ``` | |
| --- | |
| ### React — Full flow component | |
| ```jsx | |
| import { useState } from 'react'; | |
| const BASE_URL = 'https://refaat9900-skin-detection.hf.space'; | |
| export default function SkinChecker() { | |
| const [prediction, setPrediction] = useState(null); | |
| const [answers, setAnswers] = useState({}); | |
| const [recommendation, setRecommendation] = useState(null); | |
| const [loading, setLoading] = useState(false); | |
| const handleUpload = async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| setLoading(true); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| const res = await fetch(`${BASE_URL}/predict`, { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| setPrediction(data); | |
| setAnswers({}); | |
| setRecommendation(null); | |
| setLoading(false); | |
| }; | |
| const submitAnswers = async () => { | |
| setLoading(true); | |
| const res = await fetch(`${BASE_URL}/recommend`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| predicted_class_index: prediction.predicted_class_index, | |
| answers, | |
| }), | |
| }); | |
| const data = await res.json(); | |
| setRecommendation(data); | |
| setLoading(false); | |
| }; | |
| return ( | |
| <div> | |
| <input type="file" accept="image/jpeg,image/png,image/webp" onChange={handleUpload} /> | |
| {prediction && ( | |
| <div> | |
| <h2>{prediction.predicted_class} — {prediction.confidence}%</h2> | |
| <p>{prediction.description}</p> | |
| <h3>A few quick questions</h3> | |
| {prediction.followup_questions.map((q) => ( | |
| <div key={q.id}> | |
| <p>{q.question_en} / {q.question_ar}</p> | |
| {q.options.map((opt) => ( | |
| <button | |
| key={opt} | |
| onClick={() => setAnswers((prev) => ({ ...prev, [q.id]: opt }))} | |
| style={{ fontWeight: answers[q.id] === opt ? 'bold' : 'normal' }} | |
| > | |
| {opt} | |
| </button> | |
| ))} | |
| </div> | |
| ))} | |
| <button onClick={submitAnswers} disabled={loading}> | |
| {loading ? 'Analyzing...' : 'Get Recommendation'} | |
| </button> | |
| </div> | |
| )} | |
| {recommendation && ( | |
| <div> | |
| <h3>Urgency: {recommendation.urgency_level}</h3> | |
| <h4>Risks</h4> | |
| <ul>{recommendation.risks_en.map((r, i) => <li key={i}>{r}</li>)}</ul> | |
| <h4>التوصيات</h4> | |
| <ul>{recommendation.recommendations_ar.map((r, i) => <li key={i}>{r}</li>)}</ul> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| ``` | |
| --- | |
| ### cURL (terminal testing) | |
| ```bash | |
| # Step 1: Predict | |
| curl -X POST "https://refaat9900-skin-detection.hf.space/predict" \ | |
| -F "file=@skin_image.jpg" | |
| # Step 2: Recommend (use predicted_class_index & question ids from step 1's response) | |
| curl -X POST "https://refaat9900-skin-detection.hf.space/recommend" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "predicted_class_index": 5, | |
| "answers": { | |
| "duration": "1–6 months", | |
| "abcde_change": "Yes, significantly", | |
| "bleeding_itching": "Yes", | |
| "family_history": "No", | |
| "sun_history": "Yes" | |
| } | |
| }' | |
| ``` | |
| --- | |
| ## ⚠️ Error Responses | |
| | Endpoint | HTTP code | Cause | Fix | | |
| |---|---|---|---| | |
| | `/predict` | `400` | Invalid file type or corrupted image | Send valid JPEG/PNG/WebP | | |
| | `/predict` | `422` | Missing `file` field | Field name must be exactly `file` | | |
| | `/recommend` | `400` | Invalid `predicted_class_index` | Must be an integer 0–6 | | |
| | `/recommend` | `422` | Missing `predicted_class_index` or `answers` | Both fields are required in the JSON body | | |
| | `/recommend` | `500` | `GROK_API_KEY` not set on server | Add it as a Space secret | | |
| | `/recommend` | `502` | Groq API unreachable or returned a bad response | Check API key validity / Groq service status | | |
| --- | |
| ## 📋 Medical Disclaimer | |
| > This API is intended for **research and educational purposes only**. The AI-generated risks and recommendations (including those from Groq) do **not** replace professional medical diagnosis. Always consult a qualified dermatologist for any skin-related health concerns. | |