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

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.

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:

{
  "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:

{
  "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:

{
  "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:

{
  "message": "It's very itchy and spreading to other areas",
  "session_id": "android_session_001",
  "image_base64": null,
  "patient_age": 25,
  "history": []
}

Response:

{
  "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

// 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

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

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

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)

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)

// 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)

# 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.