From 22c25d5cedf9a20aad5c41b479cbdbbd10305254 Mon Sep 17 00:00:00 2001
From: genki <123@1234.com>
Date: Thu, 1 Jan 2026 01:30:08 -0500
Subject: [PATCH] TODO - end of time - need to revisit anlysis results window -
broke it adding the uh faePicker (needs to go in AppRoutes)
---
.idea/deviceManager.xml | 13 +++
.idea/inspectionProfiles/Project_Default.xml | 61 +++++++++++
.../ui/trainingprep/FacePickerScreen.kt | 56 ++++++++++
.../ui/trainingprep/ScanResultsScreen.kt | 38 +++++--
.../ui/trainingprep/TrainViewModel.kt | 103 ++++++++++--------
5 files changed, 218 insertions(+), 53 deletions(-)
create mode 100644 .idea/deviceManager.xml
create mode 100644 .idea/inspectionProfiles/Project_Default.xml
create mode 100644 app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7061a0d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt
new file mode 100644
index 0000000..fc1a53b
--- /dev/null
+++ b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/FacePickerScreen.kt
@@ -0,0 +1,56 @@
+package com.placeholder.sherpai2.ui.trainingprep
+import android.graphics.Rect
+import android.net.Uri
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberAsyncImagePainter
+
+@Composable
+fun FacePickerScreen(
+ uri: Uri,
+ faceBoxes: List,
+ onFaceSelected: (Rect) -> Unit
+) {
+ Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
+ Text("Multiple faces detected!", style = MaterialTheme.typography.headlineSmall)
+ Text("Tap the person you want to train on.")
+
+ Box(modifier = Modifier.weight(1f).fillMaxWidth().padding(vertical = 16.dp)) {
+ // Main Image
+ Image(
+ painter = rememberAsyncImagePainter(uri),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Fit
+ )
+
+ // Overlay Clickable Boxes
+ // Note: In a production app, you'd need to map Rect coordinates
+ // from the Bitmap scale to the UI View scale.
+ Canvas(modifier = Modifier.fillMaxSize().clickable { /* Handle general tap */ }) {
+ // Implementation of coordinate mapping goes here
+ }
+
+ // Simplified: Just show the options as a list of crops if Canvas mapping is too complex for now
+ LazyRow {
+ items(faceBoxes) { box ->
+ Card(modifier = Modifier.padding(8.dp).size(100.dp).clickable { onFaceSelected(box) }) {
+ Text("Face ${faceBoxes.indexOf(box) + 1}", Modifier.align(Alignment.CenterHorizontally))
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/ScanResultsScreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/ScanResultsScreen.kt
index 88345bf..9b61b5b 100644
--- a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/ScanResultsScreen.kt
+++ b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/ScanResultsScreen.kt
@@ -2,9 +2,10 @@ package com.placeholder.sherpai2.ui.trainingprep
import android.net.Uri
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.items // CRITICAL: This is the correct import for List items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
@@ -14,14 +15,20 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
+import androidx.compose.foundation.lazy.items
+/**
+ * Displays the outcome of the face detection process.
+ */
@Composable
fun ScanResultsScreen(
state: ScanningState,
onFinish: () -> Unit
) {
Column(
- modifier = Modifier.fillMaxSize().padding(16.dp),
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -37,16 +44,25 @@ fun ScanResultsScreen(
style = MaterialTheme.typography.headlineMedium
)
- LazyColumn(modifier = Modifier.weight(1f).padding(vertical = 16.dp)) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .padding(vertical = 16.dp)
+ ) {
+ // FIX: Ensure 'items' is the one that takes a List, not a count
items(state.results) { result ->
Row(
- modifier = Modifier.fillMaxWidth().padding(8.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberAsyncImagePainter(result.uri),
contentDescription = null,
- modifier = Modifier.size(60.dp).clip(RoundedCornerShape(8.dp)),
+ modifier = Modifier
+ .size(64.dp)
+ .clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
Spacer(Modifier.width(16.dp))
@@ -54,7 +70,7 @@ fun ScanResultsScreen(
Text(if (result.faceCount > 0) "✅ Face Detected" else "❌ No Face")
if (result.hasMultipleFaces) {
Text(
- "⚠️ Multiple faces (${result.faceCount})",
+ text = "⚠️ Multiple faces (${result.faceCount})",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
@@ -64,11 +80,17 @@ fun ScanResultsScreen(
}
}
- Button(onClick = onFinish, modifier = Modifier.fillMaxWidth()) {
+ Button(
+ onClick = onFinish,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(8.dp)
+ ) {
Text("Done")
}
}
- else -> {}
+ // Add fallback for other states (Idle/RequiresCrop)
+ // so the compiler doesn't complain about non-exhaustive 'when'
+ else -> { }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/TrainViewModel.kt b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/TrainViewModel.kt
index bb68472..572712e 100644
--- a/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/TrainViewModel.kt
+++ b/app/src/main/java/com/placeholder/sherpai2/ui/trainingprep/TrainViewModel.kt
@@ -1,6 +1,7 @@
package com.placeholder.sherpai2.ui.trainingprep
import android.content.Context
+import android.graphics.Rect
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -12,8 +13,6 @@ import com.placeholder.sherpai2.domain.repository.TaggingRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -25,9 +24,11 @@ import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import javax.inject.Inject
+// 1. DEFINE THESE AT TOP LEVEL (Outside the class) so the UI can see them
sealed class ScanningState {
object Idle : ScanningState()
data class Processing(val current: Int, val total: Int) : ScanningState()
+ data class RequiresCrop(val uri: Uri, val faceBoxes: List, val remainingUris: List) : ScanningState()
data class Success(val results: List) : ScanningState()
}
@@ -48,60 +49,72 @@ class TrainViewModel @Inject constructor(
val uiState: StateFlow = _uiState.asStateFlow()
private val semaphore = Semaphore(2)
+ private val finalResults = mutableListOf()
fun scanAndTagFaces(uris: List) = viewModelScope.launch {
- val total = uris.size
- _uiState.value = ScanningState.Processing(0, total)
-
- val detector = FaceDetection.getClient(faceOptions())
+ // Goal: Deduplicate by SHA256 before starting
val allImages = imageRepository.getAllImages().first()
- val uriToIdMap = allImages.associate { it.image.imageUri to it.image.imageId }
+ val uriToShaMap = allImages.associate { it.image.imageUri to it.image.sha256 }
- var completedCount = 0
-
- val scanResults = withContext(Dispatchers.Default) {
- uris.map { uri ->
- async {
- semaphore.withPermit {
- val faceCount = detectFaceCount(detector, uri)
-
- // Tagging logic
- if (faceCount > 0) {
- uriToIdMap[uri.toString()]?.let { id ->
- taggingRepository.addTagToImage(id, "face", "ML_KIT", 1.0f)
- if (faceCount > 1) {
- taggingRepository.addTagToImage(id, "multiple_faces", "ML_KIT", 1.0f)
- }
- }
- }
-
- completedCount++
- _uiState.value = ScanningState.Processing(completedCount, total)
-
- ScanResult(uri, faceCount)
- }
- }
- }.awaitAll()
+ val uniqueUris = uris.distinctBy { uri ->
+ uriToShaMap[uri.toString()] ?: uri.toString()
}
- detector.close()
- _uiState.value = ScanningState.Success(scanResults)
+ processNext(uniqueUris)
}
- private suspend fun detectFaceCount(
- detector: com.google.mlkit.vision.face.FaceDetector,
- uri: Uri
- ): Int = withContext(Dispatchers.IO) {
- return@withContext try {
- val image = InputImage.fromFilePath(context, uri)
- val faces = detector.process(image).await()
- faces.size // Returns actual count
- } catch (e: Exception) {
- 0
+ private suspend fun processNext(remaining: List) {
+ if (remaining.isEmpty()) {
+ _uiState.value = ScanningState.Success(finalResults.toList())
+ return
}
+
+ val currentUri = remaining.first()
+ val nextList = remaining.drop(1)
+
+ _uiState.value = ScanningState.Processing(finalResults.size + 1, finalResults.size + remaining.size)
+
+ val detector = FaceDetection.getClient(faceOptions())
+ try {
+ val image = InputImage.fromFilePath(context, currentUri)
+ val faces = detector.process(image).await()
+
+ if (faces.size > 1) {
+ // FORCE USER TO CROP: Transition to RequiresCrop state
+ _uiState.value = ScanningState.RequiresCrop(
+ uri = currentUri,
+ faceBoxes = faces.map { it.boundingBox },
+ remainingUris = nextList
+ )
+ } else {
+ val faceCount = faces.size
+ if (faceCount > 0) tagImage(currentUri)
+
+ finalResults.add(ScanResult(currentUri, faceCount))
+ processNext(nextList)
+ }
+ } catch (e: Exception) {
+ processNext(nextList)
+ } finally {
+ detector.close()
+ }
+ }
+
+ private suspend fun tagImage(uri: Uri) {
+ val allImages = imageRepository.getAllImages().first()
+ val imageId = allImages.find { it.image.imageUri == uri.toString() }?.image?.imageId
+ if (imageId != null) {
+ taggingRepository.addTagToImage(imageId, "face", "ML_KIT", 1.0f)
+ }
+ }
+
+ fun onFaceSelected(uri: Uri, box: Rect, remaining: List) = viewModelScope.launch {
+ tagImage(uri)
+ finalResults.add(ScanResult(uri, 1))
+ processNext(remaining)
}
private fun faceOptions() = FaceDetectorOptions.Builder()
- .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
+ .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
.build()
}
\ No newline at end of file