From 9312fcf645929c5387416973ffa38630fa3ec46c Mon Sep 17 00:00:00 2001 From: genki <123@1234.com> Date: Fri, 16 Jan 2026 09:30:39 -0500 Subject: [PATCH] SmoothTraining-FaceScanning Adding visual clarity for duplicates detected --- .../modelinventory/Personinventoryscreen.kt | 432 +++++++----------- .../Personinventoryviewmodel.kt | 22 +- 2 files changed, 187 insertions(+), 267 deletions(-) diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryscreen.kt b/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryscreen.kt index 1f2a2d8..b56951c 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryscreen.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryscreen.kt @@ -1,6 +1,5 @@ package com.placeholder.sherpai2.ui.modelinventory -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* @@ -14,17 +13,19 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle /** - * PersonInventoryScreen - SUPERCHARGED UI with warnings and improved buttons + * PersonInventoryScreen - Simplified to match corrected ViewModel * * Features: - * - Background scanning with navigation warnings - * - Real-time performance stats (images/sec) - * - Fixed button layout (no text wrapping) - * - Progress tracking + * - List of all persons with face models + * - Scan button to find person in library + * - Real-time scanning progress + * - Delete person functionality */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -32,53 +33,8 @@ fun PersonInventoryScreen( viewModel: PersonInventoryViewModel = hiltViewModel(), onNavigateToPersonDetail: (String) -> Unit ) { - val uiState by viewModel.uiState.collectAsState() - val scanningState by viewModel.scanningState.collectAsState() - val isScanningInBackground by viewModel.isScanningInBackground.collectAsState() - - // Navigation warning dialog - var showNavigationWarning by remember { mutableStateOf(false) } - var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) } - - // Intercept back button when scanning - BackHandler(enabled = isScanningInBackground) { - showNavigationWarning = true - } - - // Navigation warning dialog - if (showNavigationWarning) { - AlertDialog( - onDismissRequest = { showNavigationWarning = false }, - title = { Text("Scan in Progress") }, - text = { - Column { - Text("A face scanning operation is running in the background.") - Spacer(Modifier.height(8.dp)) - Text( - "Leaving now will cancel the scan and you'll need to restart it.", - style = MaterialTheme.typography.bodySmall - ) - } - }, - confirmButton = { - TextButton( - onClick = { - viewModel.cancelScan() - showNavigationWarning = false - pendingNavigation?.invoke() - pendingNavigation = null - } - ) { - Text("Cancel Scan & Leave", color = MaterialTheme.colorScheme.error) - } - }, - dismissButton = { - TextButton(onClick = { showNavigationWarning = false }) { - Text("Continue Scanning") - } - } - ) - } + val personsWithModels by viewModel.personsWithModels.collectAsStateWithLifecycle() + val scanningState by viewModel.scanningState.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -86,9 +42,9 @@ fun PersonInventoryScreen( title = { Column { Text("People") - if (isScanningInBackground) { + if (scanningState is ScanningState.Scanning) { Text( - "⚡ Scanning in background", + "⚡ Scanning...", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary ) @@ -103,69 +59,51 @@ fun PersonInventoryScreen( ) { padding -> Column(Modifier.padding(padding)) { // Stats card - when (uiState) { - is PersonInventoryViewModel.InventoryUiState.Success -> { - StatsCard((uiState as PersonInventoryViewModel.InventoryUiState.Success).persons) + if (personsWithModels.isNotEmpty()) { + StatsCard(personsWithModels) + } + + // Scanning progress (if active) + when (val state = scanningState) { + is ScanningState.Scanning -> { + ScanningProgressCard(state) + } + is ScanningState.Complete -> { + CompletionCard(state) { + viewModel.resetScanningState() + } + } + is ScanningState.Error -> { + ErrorCard(state) { + viewModel.resetScanningState() + } } else -> {} } - // Scanning progress (if active) - if (scanningState is PersonInventoryViewModel.ScanningState.Scanning) { - ScanningProgressCard(scanningState as PersonInventoryViewModel.ScanningState.Scanning) - } - - // Completion message - if (scanningState is PersonInventoryViewModel.ScanningState.Complete) { - CompletionCard(scanningState as PersonInventoryViewModel.ScanningState.Complete) - } - // Person list - when (val state = uiState) { - is PersonInventoryViewModel.InventoryUiState.Loading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() + if (personsWithModels.isEmpty()) { + EmptyState() + } else { + PersonList( + persons = personsWithModels, + onScan = { personId -> + viewModel.scanForPerson(personId) + }, + onView = { personId -> + onNavigateToPersonDetail(personId) + }, + onDelete = { personId -> + viewModel.deletePerson(personId) } - } - is PersonInventoryViewModel.InventoryUiState.Success -> { - if (state.persons.isEmpty()) { - EmptyState() - } else { - PersonList( - persons = state.persons, - onScan = { personId, faceModelId -> - viewModel.scanLibraryForPerson(personId, faceModelId) - }, - onImprove = { personId, faceModelId -> - viewModel.startModelImprovement(personId, faceModelId) - }, - onView = { personId -> - if (isScanningInBackground) { - showNavigationWarning = true - pendingNavigation = { onNavigateToPersonDetail(personId) } - } else { - onNavigateToPersonDetail(personId) - } - }, - onDelete = { personId, faceModelId -> - viewModel.deletePerson(personId, faceModelId) - } - ) - } - } - is PersonInventoryViewModel.InventoryUiState.Error -> { - ErrorState(state.message) - } + ) } } } } @Composable -fun StatsCard(persons: List) { +private fun StatsCard(persons: List) { Card( modifier = Modifier .fillMaxWidth() @@ -187,7 +125,7 @@ fun StatsCard(persons: List) { ) StatItem( icon = Icons.Default.Collections, - value = persons.sumOf { it.stats.taggedPhotoCount }.toString(), + value = persons.sumOf { it.taggedPhotoCount }.toString(), label = "Tagged" ) } @@ -195,7 +133,11 @@ fun StatsCard(persons: List) { } @Composable -fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: String, label: String) { +private fun StatItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + value: String, + label: String +) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp) @@ -210,6 +152,7 @@ fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: Strin Text( value, style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onPrimaryContainer ) Text( @@ -221,16 +164,21 @@ fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: Strin } @Composable -fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning) { +private fun ScanningProgressCard(state: ScanningState.Scanning) { Card( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(horizontal = 16.dp, vertical = 8.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.secondaryContainer ) ) { - Column(Modifier.padding(16.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -238,45 +186,34 @@ fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning) ) { Text( "Scanning for ${state.personName}", - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) Text( - "${state.progress}/${state.total}", - style = MaterialTheme.typography.bodySmall + "${state.completed} / ${state.total}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary ) } - Spacer(Modifier.height(8.dp)) - LinearProgressIndicator( - progress = { state.progress.toFloat() / state.total.toFloat() }, - modifier = Modifier.fillMaxWidth() + progress = { if (state.total > 0) state.completed.toFloat() / state.total.toFloat() else 0f }, + modifier = Modifier.fillMaxWidth(), ) - Spacer(Modifier.height(8.dp)) - - // Performance stats Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text( - "⚡ ${String.format("%.1f", state.imagesPerSecond)} images/sec", + "✓ ${state.facesFound} matches found", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary ) Text( - "Found: ${state.facesFound}", - style = MaterialTheme.typography.bodySmall - ) - } - - if (state.imagesSkipped > 0) { - Spacer(Modifier.height(4.dp)) - Text( - "Skipped: ${state.imagesSkipped} (no faces)", + "%.1f img/sec".format(state.speed), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline + color = MaterialTheme.colorScheme.onSecondaryContainer ) } } @@ -284,73 +221,127 @@ fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning) } @Composable -fun CompletionCard(state: PersonInventoryViewModel.ScanningState.Complete) { +private fun CompletionCard(state: ScanningState.Complete, onDismiss: () -> Unit) { Card( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(horizontal = 16.dp, vertical = 8.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer + containerColor = MaterialTheme.colorScheme.primaryContainer ) ) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Icon( - Icons.Default.CheckCircle, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = MaterialTheme.colorScheme.tertiary - ) - Spacer(Modifier.width(12.dp)) - Column { - Text( - "Scan Complete!", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onTertiaryContainer - ) - Text( - "Found ${state.facesFound} photos of ${state.personName} in ${String.format("%.1f", state.durationSeconds)}s", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onTertiaryContainer + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) ) + Column { + Text( + "Scan Complete!", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + "Found ${state.personName} in ${state.facesFound} photos", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + IconButton(onClick = onDismiss) { + Icon(Icons.Default.Close, "Dismiss") } } } } @Composable -fun PersonList( - persons: List, - onScan: (String, String) -> Unit, - onImprove: (String, String) -> Unit, +private fun ErrorCard(state: ScanningState.Error, onDismiss: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(32.dp) + ) + Column { + Text( + "Scan Failed", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + state.message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + IconButton(onClick = onDismiss) { + Icon(Icons.Default.Close, "Dismiss") + } + } + } +} + +@Composable +private fun PersonList( + persons: List, + onScan: (String) -> Unit, onView: (String) -> Unit, - onDelete: (String, String) -> Unit + onDelete: (String) -> Unit ) { LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 16.dp) + contentPadding = PaddingValues(vertical = 8.dp) ) { - items(persons, key = { it.person.id }) { personWithStats -> + items( + items = persons, + key = { it.person.id } + ) { person -> PersonCard( - person = personWithStats, - onScan = { onScan(personWithStats.person.id, personWithStats.stats.faceModelId) }, - onImprove = { onImprove(personWithStats.person.id, personWithStats.stats.faceModelId) }, - onView = { onView(personWithStats.person.id) }, - onDelete = { onDelete(personWithStats.person.id, personWithStats.stats.faceModelId) } + person = person, + onScan = { onScan(person.person.id) }, + onView = { onView(person.person.id) }, + onDelete = { onDelete(person.person.id) } ) } } } @Composable -fun PersonCard( - person: PersonInventoryViewModel.PersonWithStats, +private fun PersonCard( + person: PersonWithModelInfo, onScan: () -> Unit, - onImprove: () -> Unit, onView: () -> Unit, onDelete: () -> Unit ) { @@ -410,10 +401,13 @@ fun PersonCard( Column(Modifier.weight(1f)) { Text( person.person.name, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold ) + + val trainingCount = person.faceModel?.trainingImageCount ?: 0 Text( - "${person.stats.taggedPhotoCount} photos • ${person.stats.trainingImageCount} trained", + "${person.taggedPhotoCount} photos • $trainingCount trained", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline ) @@ -431,12 +425,12 @@ fun PersonCard( Spacer(Modifier.height(12.dp)) - // IMPROVED BUTTON LAYOUT - No text wrapping! + // Action buttons Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - // Primary action - Scan + // Scan button Button( onClick = onScan, modifier = Modifier.weight(1f) @@ -447,10 +441,10 @@ fun PersonCard( modifier = Modifier.size(18.dp) ) Spacer(Modifier.width(4.dp)) - Text("Scan", maxLines = 1) + Text("Scan Library", maxLines = 1) } - // Secondary action - View + // View button OutlinedButton( onClick = onView, modifier = Modifier.weight(1f) @@ -461,51 +455,7 @@ fun PersonCard( modifier = Modifier.size(18.dp) ) Spacer(Modifier.width(4.dp)) - Text("View", maxLines = 1) - } - - // Tertiary - More menu - var showMenu by remember { mutableStateOf(false) } - - Box { - IconButton( - onClick = { showMenu = true }, - modifier = Modifier - .size(48.dp) - .border( - 1.dp, - MaterialTheme.colorScheme.outline, - MaterialTheme.shapes.small - ) - ) { - Icon(Icons.Default.MoreVert, "More") - } - - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - DropdownMenuItem( - text = { Text("Improve Model") }, - onClick = { - showMenu = false - onImprove() - }, - leadingIcon = { - Icon(Icons.Default.TrendingUp, null) - } - ) - DropdownMenuItem( - text = { Text("Export Data") }, - onClick = { - showMenu = false - // TODO: Handle export - }, - leadingIcon = { - Icon(Icons.Default.Share, null) - } - ) - } + Text("View Photos", maxLines = 1) } } } @@ -513,7 +463,7 @@ fun PersonCard( } @Composable -fun EmptyState() { +private fun EmptyState() { Box( modifier = Modifier .fillMaxSize() @@ -522,21 +472,19 @@ fun EmptyState() { ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.spacedBy(16.dp) ) { Icon( Icons.Default.PersonAdd, contentDescription = null, - modifier = Modifier.size(64.dp), + modifier = Modifier.size(72.dp), tint = MaterialTheme.colorScheme.outline ) - Spacer(Modifier.height(16.dp)) Text( "No People Yet", style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface + fontWeight = FontWeight.Bold ) - Spacer(Modifier.height(8.dp)) Text( "Train your first face model to get started", style = MaterialTheme.typography.bodyMedium, @@ -544,38 +492,4 @@ fun EmptyState() { ) } } -} - -@Composable -fun ErrorState(message: String) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - Icons.Default.Error, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.error - ) - Spacer(Modifier.height(16.dp)) - Text( - "Error Loading People", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(Modifier.height(8.dp)) - Text( - message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline - ) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryviewmodel.kt b/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryviewmodel.kt index 5a57826..8484e32 100644 --- a/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryviewmodel.kt +++ b/app/src/main/java/com/placeholder/sherpai2/ui/modelinventory/Personinventoryviewmodel.kt @@ -19,6 +19,8 @@ import com.placeholder.sherpai2.ml.FaceNetModel 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.* import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -186,26 +188,27 @@ class PersonInventoryViewModel @Inject constructor( val startTime = System.currentTimeMillis() // Batch collection for DB writes (mutex-protected) - val batchMatches = mutableListOf>() // (personId, imageId, confidence) + val batchMatches = mutableListOf>() // ✅ MASSIVE PARALLELIZATION: Process all images concurrently // Semaphore(50) limits to 50 simultaneous operations - untaggedImages.map { image -> - kotlinx.coroutines.async(Dispatchers.IO) { + val deferredResults = untaggedImages.map { image -> + async(Dispatchers.IO) { semaphore.withPermit { try { // Load and detect faces val uri = Uri.parse(image.imageUri) - val inputStream = context.contentResolver.openInputStream(uri) ?: return@withPermit + val inputStream = context.contentResolver.openInputStream(uri) + if (inputStream == null) return@withPermit + val bitmap = BitmapFactory.decodeStream(inputStream) inputStream.close() if (bitmap == null) return@withPermit val mlImage = InputImage.fromBitmap(bitmap, 0) - val faces = com.google.android.gms.tasks.Tasks.await( - detector.process(mlImage) - ) + val facesTask = detector.process(mlImage) + val faces = com.google.android.gms.tasks.Tasks.await(facesTask) // Check each detected face for (face in faces) { @@ -271,7 +274,10 @@ class PersonInventoryViewModel @Inject constructor( } } } - }.forEach { it.await() } // Wait for all to complete + } + + // Wait for all to complete + deferredResults.awaitAll() // Flush remaining batch batchUpdateMutex.withLock {