IndiDermaX / android.md
Avishek8136's picture
Rename project to IndiDermaX, update README and Android integration guide, enhance FastAPI app structure, and adjust requirements for Gradio version.
80f5b17
|
Raw
History Blame Contribute Delete
12.6 kB
# 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.