SmoothTraining-FaceScanning
Adding visual clarity for duplicates detected
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
is PersonInventoryViewModel.InventoryUiState.Success -> {
|
||||
if (state.persons.isEmpty()) {
|
||||
if (personsWithModels.isEmpty()) {
|
||||
EmptyState()
|
||||
} else {
|
||||
PersonList(
|
||||
persons = state.persons,
|
||||
onScan = { personId, faceModelId ->
|
||||
viewModel.scanLibraryForPerson(personId, faceModelId)
|
||||
},
|
||||
onImprove = { personId, faceModelId ->
|
||||
viewModel.startModelImprovement(personId, faceModelId)
|
||||
persons = personsWithModels,
|
||||
onScan = { personId ->
|
||||
viewModel.scanForPerson(personId)
|
||||
},
|
||||
onView = { personId ->
|
||||
if (isScanningInBackground) {
|
||||
showNavigationWarning = true
|
||||
pendingNavigation = { onNavigateToPersonDetail(personId) }
|
||||
} else {
|
||||
onNavigateToPersonDetail(personId)
|
||||
}
|
||||
},
|
||||
onDelete = { personId, faceModelId ->
|
||||
viewModel.deletePerson(personId, faceModelId)
|
||||
onDelete = { personId ->
|
||||
viewModel.deletePerson(personId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is PersonInventoryViewModel.InventoryUiState.Error -> {
|
||||
ErrorState(state.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
|
||||
private fun StatsCard(persons: List<PersonWithModelInfo>) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -187,7 +125,7 @@ fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
|
||||
)
|
||||
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<PersonInventoryViewModel.PersonWithStats>) {
|
||||
}
|
||||
|
||||
@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
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(
|
||||
"Scan Complete!",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"Found ${state.facesFound} photos of ${state.personName} in ${String.format("%.1f", state.durationSeconds)}s",
|
||||
"Found ${state.personName} in ${state.facesFound} photos",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Default.Close, "Dismiss")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PersonList(
|
||||
persons: List<PersonInventoryViewModel.PersonWithStats>,
|
||||
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<PersonWithModelInfo>,
|
||||
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,
|
||||
@@ -545,37 +493,3 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Triple<String, String, Float>>() // (personId, imageId, confidence)
|
||||
val batchMatches = mutableListOf<Triple<String, String, Float>>()
|
||||
|
||||
// ✅ 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 {
|
||||
|
||||
Reference in New Issue
Block a user