TODO - end of time - need to revisit anlysis results window - broke it adding the uh faePicker (needs to go in AppRoutes)

This commit is contained in:
genki
2026-01-01 01:30:08 -05:00
parent dba64b89b6
commit 22c25d5ced
5 changed files with 218 additions and 53 deletions

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@@ -0,0 +1,61 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -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<Rect>,
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))
}
}
}
}
}
}

View File

@@ -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 -> { }
}
}
}

View File

@@ -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<Rect>, val remainingUris: List<Uri>) : ScanningState()
data class Success(val results: List<ScanResult>) : ScanningState()
}
@@ -48,60 +49,72 @@ class TrainViewModel @Inject constructor(
val uiState: StateFlow<ScanningState> = _uiState.asStateFlow()
private val semaphore = Semaphore(2)
private val finalResults = mutableListOf<ScanResult>()
fun scanAndTagFaces(uris: List<Uri>) = viewModelScope.launch {
val total = uris.size
_uiState.value = ScanningState.Processing(0, total)
// Goal: Deduplicate by SHA256 before starting
val allImages = imageRepository.getAllImages().first()
val uriToShaMap = allImages.associate { it.image.imageUri to it.image.sha256 }
val uniqueUris = uris.distinctBy { uri ->
uriToShaMap[uri.toString()] ?: uri.toString()
}
processNext(uniqueUris)
}
private suspend fun processNext(remaining: List<Uri>) {
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())
val allImages = imageRepository.getAllImages().first()
val uriToIdMap = allImages.associate { it.image.imageUri to it.image.imageId }
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()
}
detector.close()
_uiState.value = ScanningState.Success(scanResults)
}
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)
try {
val image = InputImage.fromFilePath(context, currentUri)
val faces = detector.process(image).await()
faces.size // Returns actual count
} catch (e: Exception) {
0
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<Uri>) = 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()
}