Spaces:
Sleeping
Sleeping
Rename project to IndiDermaX, update README and Android integration guide, enhance FastAPI app structure, and adjust requirements for Gradio version.
80f5b17 | # IndiDermaX β Android Integration Guide | |
| ## Quick Start | |
| ``` | |
| Base URL: https://avishek8136-indidermax.hf.space | |
| No auth keys needed β HF Space is public. | |
| Warm-up: call GET /api/health first (cold start takes 20-30s). | |
| ``` | |
| ### Test with curl before writing Android code | |
| ```bash | |
| curl https://avishek8136-indidermax.hf.space/api/health | |
| ``` | |
| --- | |
| ## β οΈ Cold Start Warning | |
| HuggingFace Spaces sleep after ~15 minutes of inactivity. The first request after sleep **takes 20-30 seconds** while the container starts. | |
| **Fix:** Ping `/api/health` in `Application.onCreate()` as a warm-up. Show a spinner until it responds. | |
| ```kotlin | |
| class IndiDermaXApp : Application() { | |
| override fun onCreate() { | |
| super.onCreate() | |
| CoroutineScope(Dispatchers.IO).launch { | |
| try { | |
| val response = api.healthCheck() | |
| Log.i("IndiDermaX", "Warmed up: mode=${response.mode}") | |
| } catch (e: Exception) { | |
| Log.w("IndiDermaX", "Warm-up failed: ${e.message}") | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| Set Android network timeout to **60 seconds** (OkHttp/Retrofit default is 10s β too short). | |
| --- | |
| ## API Endpoints | |
| ### 1. Health Check | |
| ``` | |
| GET /api/health | |
| ``` | |
| **Response:** | |
| ```json | |
| { | |
| "status": "healthy", | |
| "mode": "neo4j_live", | |
| "neo4j": true, | |
| "nvidia": true, | |
| "timestamp": "2026-05-11T00:00:00Z" | |
| } | |
| ``` | |
| `mode` values: `"neo4j_live"` = full production | `"cache_fallback"` = Neo4j down, using local cache | `"kb_only"` = degraded | |
| --- | |
| ### 2. Diagnose (JSON β recommended for Android) | |
| Send text symptoms + optional base64 image. | |
| ``` | |
| POST /api/diagnose | |
| Content-Type: application/json | |
| ``` | |
| **Request:** | |
| ```json | |
| { | |
| "message": "Red scaly ring-shaped patch on my arm, very itchy, spreading for 2 weeks", | |
| "patient_age": 25, | |
| "image_base64": "/9j/4AAQSkZJRg...", | |
| "session_id": "android_session_001" | |
| } | |
| ``` | |
| | Field | Type | Required | Description | | |
| |-------|------|----------|-------------| | |
| | `message` | string | No | Symptom description (free text) | | |
| | `patient_age` | int | No | Age (or auto-extracted from message) | | |
| | `image_base64` | string | No | Base64-encoded JPEG (no data URI prefix) | | |
| | `session_id` | string | No | For tracking β use device ID | | |
| **Response:** | |
| ```json | |
| { | |
| "success": true, | |
| "top_disease": "Tinea Corporis", | |
| "top_score": 18.45, | |
| "candidates": [ | |
| {"disease": "Tinea Corporis", "score": 18.45}, | |
| {"disease": "Psoriasis", "score": 15.20}, | |
| {"disease": "Eczema", "score": 12.80}, | |
| {"disease": "Contact Dermatitis", "score": 10.50}, | |
| {"disease": "Tinea Cruris", "score": 9.30} | |
| ], | |
| "differentials": [ | |
| {"disease": "Psoriasis", "score": 15.20}, | |
| {"disease": "Eczema", "score": 12.80}, | |
| {"disease": "Contact Dermatitis", "score": 10.50} | |
| ], | |
| "evidence": [ | |
| {"title": "Clinical: annular ring-shaped erythematous-plaque central-clearing", "source": "CLINICAL_KB"}, | |
| {"title": "Dermatology reference: tinea corporis", "source": "PubMed"} | |
| ], | |
| "answer": "## π₯ Diagnosis: **Tinea Corporis**\n\n...", | |
| "log_text": "[0.0s] 1_input/parser: ...\n...", | |
| "pipeline": { | |
| "stages": 6, | |
| "agents": 5, | |
| "neo4j": true, | |
| "vision": true | |
| } | |
| } | |
| ``` | |
| ### 3. Diagnose (Multipart Upload) | |
| Easier for Android camera/gallery images β no base64 encoding needed. | |
| ``` | |
| POST /api/diagnose/upload | |
| Content-Type: multipart/form-data | |
| ``` | |
| **Form Fields:** | |
| | Field | Type | Required | Description | | |
| |-------|------|----------|-------------| | |
| | `message` | string | No | Symptom description | | |
| | `patient_age` | int | No | Patient age | | |
| | `image` | file | No | JPEG/PNG image file | | |
| **Response:** Same JSON structure as `/api/diagnose`. | |
| ### 4. Chat (Multi-turn Conversation) | |
| ``` | |
| POST /api/chat | |
| Content-Type: application/json | |
| ``` | |
| **Request:** | |
| ```json | |
| { | |
| "message": "It's very itchy and spreading to other areas", | |
| "session_id": "android_session_001", | |
| "image_base64": null, | |
| "patient_age": 25, | |
| "history": [] | |
| } | |
| ``` | |
| **Response:** | |
| ```json | |
| { | |
| "response": "## π₯ Diagnosis: **Tinea Corporis**\n...", | |
| "session_id": "android_session_001", | |
| "top_disease": "Tinea Corporis", | |
| "top_score": 20.30, | |
| "follow_up_question": "Have you tried any antifungal treatments?" | |
| } | |
| ``` | |
| When `top_score < 3.0`, the API includes a `follow_up_question` β show this to the user as a prompt. | |
| --- | |
| ## Kotlin Integration | |
| ### Retrofit Setup | |
| ```kotlin | |
| // build.gradle.kts | |
| // implementation("com.squareup.retrofit2:retrofit:2.11.0") | |
| // implementation("com.squareup.retrofit2:converter-gson:2.11.0") | |
| // implementation("com.squareup.okhttp3:okhttp:4.12.0") | |
| // implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") | |
| val okHttp = OkHttpClient.Builder() | |
| .connectTimeout(60, TimeUnit.SECONDS) | |
| .readTimeout(60, TimeUnit.SECONDS) | |
| .writeTimeout(60, TimeUnit.SECONDS) | |
| .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) | |
| .build() | |
| val retrofit = Retrofit.Builder() | |
| .baseUrl("https://avishek8136-indidermax.hf.space/") | |
| .client(okHttp) | |
| .addConverterFactory(GsonConverterFactory.create()) | |
| .build() | |
| val api = retrofit.create(IndiDermaXApi::class.java) | |
| ``` | |
| ### Retrofit Interface | |
| ```kotlin | |
| interface IndiDermaXApi { | |
| @GET("api/health") | |
| suspend fun healthCheck(): HealthResponse | |
| @POST("api/diagnose") | |
| suspend fun diagnose(@Body request: DiagnoseRequest): DiagnoseResponse | |
| @Multipart | |
| @POST("api/diagnose/upload") | |
| suspend fun diagnoseUpload( | |
| @Part("message") message: RequestBody, | |
| @Part("patient_age") age: RequestBody, | |
| @Part image: MultipartBody.Part | |
| ): DiagnoseResponse | |
| @POST("api/chat") | |
| suspend fun chat(@Body request: ChatRequest): ChatResponse | |
| } | |
| ``` | |
| ### Data Classes | |
| ```kotlin | |
| data class HealthResponse( | |
| val status: String, | |
| val mode: String, // "neo4j_live" | "cache_fallback" | "kb_only" | |
| val neo4j: Boolean, | |
| val nvidia: Boolean, | |
| val timestamp: String | |
| ) | |
| data class DiagnoseRequest( | |
| val message: String = "", | |
| val patient_age: Int? = null, | |
| val image_base64: String? = null, | |
| val session_id: String = "android" | |
| ) | |
| data class DiagnoseResponse( | |
| val success: Boolean, | |
| val top_disease: String, | |
| val top_score: Double, | |
| val candidates: List<Candidate>, | |
| val differentials: List<Candidate>, | |
| val evidence: List<EvidenceItem>, | |
| val answer: String, // Markdown β render in a WebView or parse | |
| val log_text: String, | |
| val pipeline: PipelineInfo | |
| ) | |
| data class Candidate( | |
| val disease: String, | |
| val score: Double | |
| ) | |
| data class EvidenceItem( | |
| val title: String, | |
| val source: String | |
| ) | |
| data class PipelineInfo( | |
| val stages: Int, | |
| val agents: Int, | |
| val neo4j: Boolean, | |
| val vision: Boolean | |
| ) | |
| data class ChatRequest( | |
| val message: String, | |
| val session_id: String = "chat", | |
| val image_base64: String? = null, | |
| val patient_age: Int? = null, | |
| val history: List<ChatMessage> = emptyList() | |
| ) | |
| data class ChatMessage( | |
| val role: String, // "user" | "assistant" | |
| val content: String | |
| ) | |
| data class ChatResponse( | |
| val response: String, | |
| val session_id: String, | |
| val top_disease: String, | |
| val top_score: Double, | |
| val follow_up_question: String | |
| ) | |
| ``` | |
| ### Image to Base64 | |
| ```kotlin | |
| fun Bitmap.toBase64Diagnose(): String { | |
| val stream = ByteArrayOutputStream() | |
| this.compress(Bitmap.CompressFormat.JPEG, 85, stream) | |
| return Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP) | |
| } | |
| fun File.toBase64Diagnose(): String { | |
| return Base64.encodeToString(this.readBytes(), Base64.NO_WRAP) | |
| } | |
| ``` | |
| ### Usage Example (ViewModel) | |
| ```kotlin | |
| class DiagnoseViewModel(private val api: IndiDermaXApi) : ViewModel() { | |
| private val _result = MutableLiveData<DiagnoseResponse>() | |
| val result: LiveData<DiagnoseResponse> = _result | |
| private val _loading = MutableLiveData(false) | |
| val loading: LiveData<Boolean> = _loading | |
| fun diagnose(text: String, age: Int?, imageBase64: String?) { | |
| viewModelScope.launch { | |
| _loading.value = true | |
| try { | |
| val response = api.diagnose( | |
| DiagnoseRequest( | |
| message = text, | |
| patient_age = age, | |
| image_base64 = imageBase64, | |
| session_id = UUID.randomUUID().toString() | |
| ) | |
| ) | |
| _result.value = response | |
| } catch (e: Exception) { | |
| // Handle network error β show retry button | |
| Log.e("Diagnose", "Failed", e) | |
| } finally { | |
| _loading.value = false | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| --- | |
| ## Error Handling | |
| | HTTP Code | Meaning | Body | | |
| |-----------|---------|------| | |
| | `200` | Success | Normal response JSON | | |
| | `400` | Bad request (invalid base64, missing fields) | `{"success":false,"error":"..."}` | | |
| | `422` | Validation error | `{"detail":[{"loc":[...],"msg":"..."}]}` | | |
| | `500` | Server error | `{"success":false,"error":"...","endpoint":"..."}` | | |
| | `504` | Timeout (>45s) | `{"success":false,"error":"Request timed out..."}` | | |
| ### Error Parsing (Retrofit) | |
| ```kotlin | |
| // In your ViewModel or repository: | |
| try { | |
| val response = api.diagnose(request) | |
| if (response.success) { | |
| // Show diagnosis | |
| } else { | |
| // response.success == false β show error message | |
| } | |
| } catch (e: HttpException) { | |
| when (e.code()) { | |
| 400, 422 -> showError("Invalid input. Check your message and image.") | |
| 500 -> showError("Server error. Please try again.") | |
| 504 -> showError("Request timed out. The image may be too large. Try again.") | |
| else -> showError("Network error: ${e.message}") | |
| } | |
| } catch (e: IOException) { | |
| showError("No internet connection. Check your network.") | |
| } | |
| ``` | |
| --- | |
| ## Pipeline Architecture | |
| ``` | |
| Android App | |
| β | |
| βββ POST /api/diagnose βββββΊ FastAPI Server (port 7860) | |
| β (text + base64 image) β | |
| β βββ Stage 1: Input Processing | |
| β βββ Stage 2: Feature Extraction (text) | |
| β βββ Stage 2b: NVIDIA NIM Vision (image β descriptors + body location) | |
| β βββ Stage 3: Neo4j Graph Candidate Retrieval | |
| β βββ Stage 4a: Visual Concept Agent | |
| β βββ Stage 4b: Symptom Analyst | |
| β βββ Stage 4c: Temporal Pattern Matcher (DTW) | |
| β βββ Stage 4d: Differential Diagnosis Debater | |
| β βββ Stage 4e: Evidence Synthesizer | |
| β βββ Stage 5: Final Output | |
| β | |
| βββ Response βββββββββββββββββ JSON | |
| { | |
| success, top_disease, top_score, | |
| candidates[5], differentials[3], | |
| evidence[5], answer, log_text, pipeline | |
| } | |
| ``` | |
| --- | |
| ## Testing with curl (before writing Android code) | |
| ```bash | |
| # 1. Warm up (do this first β cold start takes 20-30s) | |
| curl -s https://avishek8136-indidermax.hf.space/api/health | python3 -m json.tool | |
| # 2. Text-only diagnosis | |
| curl -s -X POST https://avishek8136-indidermax.hf.space/api/diagnose \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"message":"red scaly ring-shaped patch on arm, very itchy for 2 weeks","patient_age":25}' \ | |
| | python3 -m json.tool | |
| # 3. Diagnosis with image (multipart) | |
| curl -s -X POST https://avishek8136-indidermax.hf.space/api/diagnose/upload \ | |
| -F "message=ring shaped rash on arm, itchy" \ | |
| -F "patient_age=30" \ | |
| -F "image=@/path/to/skin_image.jpg" \ | |
| | python3 -m json.tool | |
| # 4. Diagnosis with image (base64) | |
| IMG_BASE64=$(base64 -w0 /path/to/skin_image.jpg) | |
| curl -s -X POST https://avishek8136-indidermax.hf.space/api/diagnose \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"message\":\"itchy red patch\",\"image_base64\":\"$IMG_BASE64\",\"patient_age\":25}" \ | |
| | python3 -m json.tool | |
| # 5. Chat (multi-turn) | |
| curl -s -X POST https://avishek8136-indidermax.hf.space/api/chat \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"message":"it is spreading and very painful now","session_id":"test_001","patient_age":25}' \ | |
| | python3 -m json.tool | |
| ``` | |
| --- | |
| ## Disclaimer | |
| β οΈ **AI-assisted decision support tool for educational purposes only.** Always consult a qualified dermatologist for in-person examination and diagnosis. This app does not replace professional medical advice. | |