diff --git a/src/main/kotlin/com/android/trisolarisserver/component/ExtractionQueue.kt b/src/main/kotlin/com/android/trisolarisserver/component/ExtractionQueue.kt new file mode 100644 index 0000000..57fc5ec --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/ExtractionQueue.kt @@ -0,0 +1,27 @@ +package com.android.trisolarisserver.component + +import jakarta.annotation.PreDestroy +import org.springframework.stereotype.Component +import java.util.concurrent.Executors + +@Component +class ExtractionQueue { + private val executor = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "doc-extraction-queue").apply { isDaemon = true } + } + + fun enqueue(task: () -> Unit) { + executor.submit { + try { + task() + } catch (_: Exception) { + // Best-effort processing; failures should not crash the worker. + } + } + } + + @PreDestroy + fun shutdown() { + executor.shutdown() + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt index 895aeef..6b21c8a 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/GuestDocuments.kt @@ -2,6 +2,7 @@ package com.android.trisolarisserver.controller import com.android.trisolarisserver.component.DocumentStorage import com.android.trisolarisserver.component.DocumentTokenService +import com.android.trisolarisserver.component.ExtractionQueue import com.android.trisolarisserver.component.LlamaClient import com.android.trisolarisserver.component.PropertyAccess import com.android.trisolarisserver.db.repo.BookingRepo @@ -38,6 +39,7 @@ class GuestDocuments( private val appUserRepo: AppUserRepo, private val storage: DocumentStorage, private val tokenService: DocumentTokenService, + private val extractionQueue: ExtractionQueue, private val llamaClient: LlamaClient, private val objectMapper: ObjectMapper, @org.springframework.beans.factory.annotation.Value("\${storage.documents.publicBaseUrl}") @@ -88,8 +90,8 @@ class GuestDocuments( storagePath = stored.storagePath ) val saved = guestDocumentRepo.save(document) - runExtraction(saved, propertyId, guestId) - return guestDocumentRepo.save(saved).toResponse(objectMapper) + runExtraction(saved.id!!, propertyId, guestId) + return saved.toResponse(objectMapper) } @GetMapping @@ -135,63 +137,67 @@ class GuestDocuments( .body(resource) } - private fun runExtraction(document: GuestDocument, propertyId: UUID, guestId: UUID) { - try { - val token = tokenService.createToken(document.id.toString()) - val imageUrl = - "${publicBaseUrl}/properties/$propertyId/guests/$guestId/documents/${document.id}/file?token=$token" + private fun runExtraction(documentId: UUID, propertyId: UUID, guestId: UUID) { + extractionQueue.enqueue { + val document = guestDocumentRepo.findById(documentId).orElse(null) ?: return@enqueue + try { + val token = tokenService.createToken(document.id.toString()) + val imageUrl = + "${publicBaseUrl}/properties/$propertyId/guests/$guestId/documents/${document.id}/file?token=$token" - val results = linkedMapOf() - results["hasAadhar"] = llamaClient.ask(imageUrl, "CONTAINS AADHAAR? Answer YES or NO only.") - results["hasUidai"] = llamaClient.ask(imageUrl, "CONTAINS UIDAI? Answer YES or NO only.") - results["hasTransportDept"] = llamaClient.ask(imageUrl, "CONTAINS TRANSPORT DEPARTMENT? Answer YES or NO only.") - results["hasIncomeTaxDept"] = llamaClient.ask(imageUrl, "CONTAINS INCOME TAX DEPARTMENT? Answer YES or NO only.") - results["hasElectionCommission"] = llamaClient.ask(imageUrl, "CONTAINS ELECTION COMMISSION OF INDIA? Answer YES or NO only.") - results["hasDrivingLicence"] = llamaClient.ask(imageUrl, "CONTAINS DRIVING LICENCE? Answer YES or NO only.") - results["hasPassport"] = llamaClient.ask(imageUrl, "CONTAINS PASSPORT? Answer YES or NO only.") - results["hasPolice"] = llamaClient.ask(imageUrl, "CONTAINS POLICE? Answer YES or NO only.") - results["hasCourt"] = llamaClient.ask(imageUrl, "CONTAINS COURT? Answer YES or NO only.") - results["hasHighCourt"] = llamaClient.ask(imageUrl, "CONTAINS HIGH COURT? Answer YES or NO only.") - results["hasSupremeCourt"] = llamaClient.ask(imageUrl, "CONTAINS SUPREME COURT? Answer YES or NO only.") - results["hasJudiciary"] = llamaClient.ask(imageUrl, "CONTAINS JUDICIARY? Answer YES or NO only.") - results["hasAddress"] = llamaClient.ask(imageUrl, "ADDRESS PRESENT? Answer YES or NO only.") - results["hasGender"] = llamaClient.ask(imageUrl, "GENDER PRESENT? Answer YES or NO only.") - results["hasNationality"] = llamaClient.ask(imageUrl, "NATIONALITY PRESENT? Answer YES or NO only.") - results["name"] = llamaClient.ask(imageUrl, "NAME? Reply only the name or NONE.") - results["dob"] = llamaClient.ask(imageUrl, "DOB? Reply only date or NONE.") - results["idNumber"] = llamaClient.ask(imageUrl, "ID NUMBER? Reply only number or NONE.") - results["address"] = llamaClient.ask(imageUrl, "ADDRESS? Reply only address or NONE.") - results["vehicleNumber"] = llamaClient.ask(imageUrl, "VEHICLE NUMBER? Reply only number or NONE.") - results["isVehiclePhoto"] = llamaClient.ask(imageUrl, "IS THIS A VEHICLE PHOTO? Answer YES or NO only.") - results["pinCode"] = llamaClient.ask(imageUrl, "PIN CODE? Reply only pin or NONE.") - results["city"] = llamaClient.ask(imageUrl, "CITY? Reply only city or NONE.") - results["gender"] = llamaClient.ask(imageUrl, "GENDER? Reply only MALE/FEMALE/OTHER or NONE.") - results["nationality"] = llamaClient.ask(imageUrl, "NATIONALITY? Reply only nationality or NONE.") + val results = linkedMapOf() + results["hasAadhar"] = llamaClient.ask(imageUrl, "CONTAINS AADHAAR? Answer YES or NO only.") + results["hasUidai"] = llamaClient.ask(imageUrl, "CONTAINS UIDAI? Answer YES or NO only.") + results["hasTransportDept"] = llamaClient.ask(imageUrl, "CONTAINS TRANSPORT DEPARTMENT? Answer YES or NO only.") + results["hasIncomeTaxDept"] = llamaClient.ask(imageUrl, "CONTAINS INCOME TAX DEPARTMENT? Answer YES or NO only.") + results["hasElectionCommission"] = llamaClient.ask(imageUrl, "CONTAINS ELECTION COMMISSION OF INDIA? Answer YES or NO only.") + results["hasDrivingLicence"] = llamaClient.ask(imageUrl, "CONTAINS DRIVING LICENCE? Answer YES or NO only.") + results["hasPassport"] = llamaClient.ask(imageUrl, "CONTAINS PASSPORT? Answer YES or NO only.") + results["hasPolice"] = llamaClient.ask(imageUrl, "CONTAINS POLICE? Answer YES or NO only.") + results["hasCourt"] = llamaClient.ask(imageUrl, "CONTAINS COURT? Answer YES or NO only.") + results["hasHighCourt"] = llamaClient.ask(imageUrl, "CONTAINS HIGH COURT? Answer YES or NO only.") + results["hasSupremeCourt"] = llamaClient.ask(imageUrl, "CONTAINS SUPREME COURT? Answer YES or NO only.") + results["hasJudiciary"] = llamaClient.ask(imageUrl, "CONTAINS JUDICIARY? Answer YES or NO only.") + results["hasAddress"] = llamaClient.ask(imageUrl, "ADDRESS PRESENT? Answer YES or NO only.") + results["hasGender"] = llamaClient.ask(imageUrl, "GENDER PRESENT? Answer YES or NO only.") + results["hasNationality"] = llamaClient.ask(imageUrl, "NATIONALITY PRESENT? Answer YES or NO only.") + results["name"] = llamaClient.ask(imageUrl, "NAME? Reply only the name or NONE.") + results["dob"] = llamaClient.ask(imageUrl, "DOB? Reply only date or NONE.") + results["idNumber"] = llamaClient.ask(imageUrl, "ID NUMBER? Reply only number or NONE.") + results["address"] = llamaClient.ask(imageUrl, "ADDRESS? Reply only address or NONE.") + results["vehicleNumber"] = llamaClient.ask(imageUrl, "VEHICLE NUMBER? Reply only number or NONE.") + results["isVehiclePhoto"] = llamaClient.ask(imageUrl, "IS THIS A VEHICLE PHOTO? Answer YES or NO only.") + results["pinCode"] = llamaClient.ask(imageUrl, "PIN CODE? Reply only pin or NONE.") + results["city"] = llamaClient.ask(imageUrl, "CITY? Reply only city or NONE.") + results["gender"] = llamaClient.ask(imageUrl, "GENDER? Reply only MALE/FEMALE/OTHER or NONE.") + results["nationality"] = llamaClient.ask(imageUrl, "NATIONALITY? Reply only nationality or NONE.") - results["docType"] = when { - results["hasCourt"].orEmpty().contains("YES", ignoreCase = true) || - results["hasHighCourt"].orEmpty().contains("YES", ignoreCase = true) || - results["hasSupremeCourt"].orEmpty().contains("YES", ignoreCase = true) || - results["hasJudiciary"].orEmpty().contains("YES", ignoreCase = true) -> "COURT_ID" - results["hasPolice"].orEmpty().contains("YES", ignoreCase = true) -> "POLICE_ID" - results["hasPassport"].orEmpty().contains("YES", ignoreCase = true) -> "PASSPORT" - results["hasTransportDept"].orEmpty().contains("YES", ignoreCase = true) || - results["hasDrivingLicence"].orEmpty().contains("YES", ignoreCase = true) -> "TRANSPORT" - results["hasIncomeTaxDept"].orEmpty().contains("YES", ignoreCase = true) -> "PAN" - results["hasElectionCommission"].orEmpty().contains("YES", ignoreCase = true) -> "VOTER_ID" - results["hasAadhar"].orEmpty().contains("YES", ignoreCase = true) || - results["hasUidai"].orEmpty().contains("YES", ignoreCase = true) -> { - if (results["hasAddress"].orEmpty().contains("YES", ignoreCase = true)) "AADHAR_BACK" else "AADHAR_FRONT" + results["docType"] = when { + results["hasCourt"].orEmpty().contains("YES", ignoreCase = true) || + results["hasHighCourt"].orEmpty().contains("YES", ignoreCase = true) || + results["hasSupremeCourt"].orEmpty().contains("YES", ignoreCase = true) || + results["hasJudiciary"].orEmpty().contains("YES", ignoreCase = true) -> "COURT_ID" + results["hasPolice"].orEmpty().contains("YES", ignoreCase = true) -> "POLICE_ID" + results["hasPassport"].orEmpty().contains("YES", ignoreCase = true) -> "PASSPORT" + results["hasTransportDept"].orEmpty().contains("YES", ignoreCase = true) || + results["hasDrivingLicence"].orEmpty().contains("YES", ignoreCase = true) -> "TRANSPORT" + results["hasIncomeTaxDept"].orEmpty().contains("YES", ignoreCase = true) -> "PAN" + results["hasElectionCommission"].orEmpty().contains("YES", ignoreCase = true) -> "VOTER_ID" + results["hasAadhar"].orEmpty().contains("YES", ignoreCase = true) || + results["hasUidai"].orEmpty().contains("YES", ignoreCase = true) -> { + if (results["hasAddress"].orEmpty().contains("YES", ignoreCase = true)) "AADHAR_BACK" else "AADHAR_FRONT" + } + results["vehicleNumber"].orEmpty().isNotBlank() && !results["vehicleNumber"]!!.contains("NONE", true) -> "VEHICLE" + results["isVehiclePhoto"].orEmpty().contains("YES", ignoreCase = true) -> "VEHICLE_PHOTO" + else -> "UNKNOWN" } - results["vehicleNumber"].orEmpty().isNotBlank() && !results["vehicleNumber"]!!.contains("NONE", true) -> "VEHICLE" - results["isVehiclePhoto"].orEmpty().contains("YES", ignoreCase = true) -> "VEHICLE_PHOTO" - else -> "UNKNOWN" - } - document.extractedData = objectMapper.writeValueAsString(results) - document.extractedAt = OffsetDateTime.now() - } catch (_: Exception) { - // Keep upload successful even if AI extraction fails. + document.extractedData = objectMapper.writeValueAsString(results) + document.extractedAt = OffsetDateTime.now() + guestDocumentRepo.save(document) + } catch (_: Exception) { + // Keep upload successful even if AI extraction fails. + } } }