SmoothTraining-FaceScanning

Adding visual clarity for duplicates detected
This commit is contained in:
genki
2026-01-16 09:30:39 -05:00
parent 4325f7f178
commit 9312fcf645
2 changed files with 187 additions and 267 deletions

View File

@@ -1,6 +1,5 @@
package com.placeholder.sherpai2.ui.modelinventory package com.placeholder.sherpai2.ui.modelinventory
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -14,17 +13,19 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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: * Features:
* - Background scanning with navigation warnings * - List of all persons with face models
* - Real-time performance stats (images/sec) * - Scan button to find person in library
* - Fixed button layout (no text wrapping) * - Real-time scanning progress
* - Progress tracking * - Delete person functionality
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -32,53 +33,8 @@ fun PersonInventoryScreen(
viewModel: PersonInventoryViewModel = hiltViewModel(), viewModel: PersonInventoryViewModel = hiltViewModel(),
onNavigateToPersonDetail: (String) -> Unit onNavigateToPersonDetail: (String) -> Unit
) { ) {
val uiState by viewModel.uiState.collectAsState() val personsWithModels by viewModel.personsWithModels.collectAsStateWithLifecycle()
val scanningState by viewModel.scanningState.collectAsState() val scanningState by viewModel.scanningState.collectAsStateWithLifecycle()
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")
}
}
)
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -86,9 +42,9 @@ fun PersonInventoryScreen(
title = { title = {
Column { Column {
Text("People") Text("People")
if (isScanningInBackground) { if (scanningState is ScanningState.Scanning) {
Text( Text(
"⚡ Scanning in background", "⚡ Scanning...",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
@@ -103,69 +59,51 @@ fun PersonInventoryScreen(
) { padding -> ) { padding ->
Column(Modifier.padding(padding)) { Column(Modifier.padding(padding)) {
// Stats card // Stats card
when (uiState) { if (personsWithModels.isNotEmpty()) {
is PersonInventoryViewModel.InventoryUiState.Success -> { StatsCard(personsWithModels)
StatsCard((uiState as PersonInventoryViewModel.InventoryUiState.Success).persons) }
// 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 -> {} 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 // Person list
when (val state = uiState) { if (personsWithModels.isEmpty()) {
is PersonInventoryViewModel.InventoryUiState.Loading -> { EmptyState()
Box( } else {
modifier = Modifier.fillMaxSize(), PersonList(
contentAlignment = Alignment.Center persons = personsWithModels,
) { onScan = { personId ->
CircularProgressIndicator() 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 @Composable
fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) { private fun StatsCard(persons: List<PersonWithModelInfo>) {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -187,7 +125,7 @@ fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
) )
StatItem( StatItem(
icon = Icons.Default.Collections, icon = Icons.Default.Collections,
value = persons.sumOf { it.stats.taggedPhotoCount }.toString(), value = persons.sumOf { it.taggedPhotoCount }.toString(),
label = "Tagged" label = "Tagged"
) )
} }
@@ -195,7 +133,11 @@ fun StatsCard(persons: List<PersonInventoryViewModel.PersonWithStats>) {
} }
@Composable @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( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
@@ -210,6 +152,7 @@ fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: Strin
Text( Text(
value, value,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.onPrimaryContainer
) )
Text( Text(
@@ -221,16 +164,21 @@ fun StatItem(icon: androidx.compose.ui.graphics.vector.ImageVector, value: Strin
} }
@Composable @Composable
fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning) { private fun ScanningProgressCard(state: ScanningState.Scanning) {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer
) )
) { ) {
Column(Modifier.padding(16.dp)) { Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -238,45 +186,34 @@ fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning)
) { ) {
Text( Text(
"Scanning for ${state.personName}", "Scanning for ${state.personName}",
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
) )
Text( Text(
"${state.progress}/${state.total}", "${state.completed} / ${state.total}",
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary
) )
} }
Spacer(Modifier.height(8.dp))
LinearProgressIndicator( LinearProgressIndicator(
progress = { state.progress.toFloat() / state.total.toFloat() }, progress = { if (state.total > 0) state.completed.toFloat() / state.total.toFloat() else 0f },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
Spacer(Modifier.height(8.dp))
// Performance stats
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
" ${String.format("%.1f", state.imagesPerSecond)} images/sec", " ${state.facesFound} matches found",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Text( Text(
"Found: ${state.facesFound}", "%.1f img/sec".format(state.speed),
style = MaterialTheme.typography.bodySmall
)
}
if (state.imagesSkipped > 0) {
Spacer(Modifier.height(4.dp))
Text(
"Skipped: ${state.imagesSkipped} (no faces)",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline color = MaterialTheme.colorScheme.onSecondaryContainer
) )
} }
} }
@@ -284,73 +221,127 @@ fun ScanningProgressCard(state: PersonInventoryViewModel.ScanningState.Scanning)
} }
@Composable @Composable
fun CompletionCard(state: PersonInventoryViewModel.ScanningState.Complete) { private fun CompletionCard(state: ScanningState.Complete, onDismiss: () -> Unit) {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer containerColor = MaterialTheme.colorScheme.primaryContainer
) )
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Row(
Icons.Default.CheckCircle, horizontalArrangement = Arrangement.spacedBy(12.dp),
contentDescription = null, verticalAlignment = Alignment.CenterVertically
modifier = Modifier.size(32.dp), ) {
tint = MaterialTheme.colorScheme.tertiary Icon(
) Icons.Default.CheckCircle,
Spacer(Modifier.width(12.dp)) contentDescription = null,
Column { tint = MaterialTheme.colorScheme.primary,
Text( modifier = Modifier.size(32.dp)
"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
) )
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 @Composable
fun PersonList( private fun ErrorCard(state: ScanningState.Error, onDismiss: () -> Unit) {
persons: List<PersonInventoryViewModel.PersonWithStats>, Card(
onScan: (String, String) -> Unit, modifier = Modifier
onImprove: (String, String) -> Unit, .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, onView: (String) -> Unit,
onDelete: (String, String) -> Unit onDelete: (String) -> Unit
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = 8.dp)
contentPadding = PaddingValues(bottom = 16.dp)
) { ) {
items(persons, key = { it.person.id }) { personWithStats -> items(
items = persons,
key = { it.person.id }
) { person ->
PersonCard( PersonCard(
person = personWithStats, person = person,
onScan = { onScan(personWithStats.person.id, personWithStats.stats.faceModelId) }, onScan = { onScan(person.person.id) },
onImprove = { onImprove(personWithStats.person.id, personWithStats.stats.faceModelId) }, onView = { onView(person.person.id) },
onView = { onView(personWithStats.person.id) }, onDelete = { onDelete(person.person.id) }
onDelete = { onDelete(personWithStats.person.id, personWithStats.stats.faceModelId) }
) )
} }
} }
} }
@Composable @Composable
fun PersonCard( private fun PersonCard(
person: PersonInventoryViewModel.PersonWithStats, person: PersonWithModelInfo,
onScan: () -> Unit, onScan: () -> Unit,
onImprove: () -> Unit,
onView: () -> Unit, onView: () -> Unit,
onDelete: () -> Unit onDelete: () -> Unit
) { ) {
@@ -410,10 +401,13 @@ fun PersonCard(
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text( Text(
person.person.name, person.person.name,
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
) )
val trainingCount = person.faceModel?.trainingImageCount ?: 0
Text( Text(
"${person.stats.taggedPhotoCount} photos • ${person.stats.trainingImageCount} trained", "${person.taggedPhotoCount} photos • $trainingCount trained",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline color = MaterialTheme.colorScheme.outline
) )
@@ -431,12 +425,12 @@ fun PersonCard(
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
// IMPROVED BUTTON LAYOUT - No text wrapping! // Action buttons
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
// Primary action - Scan // Scan button
Button( Button(
onClick = onScan, onClick = onScan,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@@ -447,10 +441,10 @@ fun PersonCard(
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text("Scan", maxLines = 1) Text("Scan Library", maxLines = 1)
} }
// Secondary action - View // View button
OutlinedButton( OutlinedButton(
onClick = onView, onClick = onView,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@@ -461,51 +455,7 @@ fun PersonCard(
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text("View", maxLines = 1) Text("View Photos", 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)
}
)
}
} }
} }
} }
@@ -513,7 +463,7 @@ fun PersonCard(
} }
@Composable @Composable
fun EmptyState() { private fun EmptyState() {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -522,21 +472,19 @@ fun EmptyState() {
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Icon( Icon(
Icons.Default.PersonAdd, Icons.Default.PersonAdd,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(64.dp), modifier = Modifier.size(72.dp),
tint = MaterialTheme.colorScheme.outline tint = MaterialTheme.colorScheme.outline
) )
Spacer(Modifier.height(16.dp))
Text( Text(
"No People Yet", "No People Yet",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface fontWeight = FontWeight.Bold
) )
Spacer(Modifier.height(8.dp))
Text( Text(
"Train your first face model to get started", "Train your first face model to get started",
style = MaterialTheme.typography.bodyMedium, 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
)
}
}
} }

View File

@@ -19,6 +19,8 @@ import com.placeholder.sherpai2.ml.FaceNetModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -186,26 +188,27 @@ class PersonInventoryViewModel @Inject constructor(
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// Batch collection for DB writes (mutex-protected) // 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 // ✅ MASSIVE PARALLELIZATION: Process all images concurrently
// Semaphore(50) limits to 50 simultaneous operations // Semaphore(50) limits to 50 simultaneous operations
untaggedImages.map { image -> val deferredResults = untaggedImages.map { image ->
kotlinx.coroutines.async(Dispatchers.IO) { async(Dispatchers.IO) {
semaphore.withPermit { semaphore.withPermit {
try { try {
// Load and detect faces // Load and detect faces
val uri = Uri.parse(image.imageUri) 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) val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close() inputStream.close()
if (bitmap == null) return@withPermit if (bitmap == null) return@withPermit
val mlImage = InputImage.fromBitmap(bitmap, 0) val mlImage = InputImage.fromBitmap(bitmap, 0)
val faces = com.google.android.gms.tasks.Tasks.await( val facesTask = detector.process(mlImage)
detector.process(mlImage) val faces = com.google.android.gms.tasks.Tasks.await(facesTask)
)
// Check each detected face // Check each detected face
for (face in faces) { 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 // Flush remaining batch
batchUpdateMutex.withLock { batchUpdateMutex.withLock {