feat: add migrations, test scripts, and utility tools
- Add database migrations (006-028) for face recognition, identity, file_uuid - Add test scripts for ASR, face, search, processing - Add portal frontend (Tauri) - Add config, benchmark, and monitoring utilities - Add model checkpoints and pretrained model references
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold">Face Candidates</h2>
|
||||
<button
|
||||
@click="loadCandidates"
|
||||
class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg transition"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-gray-400 text-sm mb-1">Min Confidence</label>
|
||||
<input
|
||||
v-model.number="minConfidence"
|
||||
@change="loadCandidates"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-gray-400 text-sm mb-1">Page Size</label>
|
||||
<input
|
||||
v-model.number="pageSize"
|
||||
@change="loadCandidates"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center py-12 text-gray-500">
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<div v-else-if="candidates.length > 0">
|
||||
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700 mb-4">
|
||||
<div class="text-gray-400">
|
||||
Showing {{ candidates.length }} of {{ total }} candidates
|
||||
<span v-if="selectedFaces.length > 0" class="ml-4 text-green-400">
|
||||
{{ selectedFaces.length }} selected
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<div
|
||||
v-for="face in candidates"
|
||||
:key="face.id"
|
||||
@click="toggleSelection(face)"
|
||||
:class="[
|
||||
'bg-gray-800 rounded-lg border overflow-hidden cursor-pointer transition',
|
||||
selectedFaces.includes(face.id) ? 'border-green-500 bg-green-900/20' : 'border-gray-700 hover:border-gray-500'
|
||||
]"
|
||||
>
|
||||
<div class="aspect-square bg-gray-700 flex items-center justify-center overflow-hidden">
|
||||
<img
|
||||
:src="getThumbnailUrl(face.id)"
|
||||
alt="Face thumbnail"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
@error="onThumbnailError"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-xs text-gray-400">Conf:</span>
|
||||
<span class="text-sm font-mono" :class="getConfidenceColor(face.confidence)">
|
||||
{{ face.confidence.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="face.attributes" class="text-xs text-gray-500">
|
||||
<div v-if="face.attributes.gender">{{ face.attributes.gender }}</div>
|
||||
<div v-if="face.attributes.age">Age: {{ face.attributes.age }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="total > pageSize" class="flex justify-center mt-6 space-x-2">
|
||||
<button
|
||||
v-if="page > 1"
|
||||
@click="prevPage"
|
||||
class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span class="text-gray-400 py-2">
|
||||
Page {{ page }} of {{ Math.ceil(total / pageSize) }}
|
||||
</span>
|
||||
<button
|
||||
v-if="page * pageSize < total"
|
||||
@click="nextPage"
|
||||
class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 text-gray-500">
|
||||
No candidates found
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { listFaceCandidates, getCurrentConfig } from '@/api/client'
|
||||
|
||||
interface FaceCandidate {
|
||||
id: number
|
||||
face_id: string | null
|
||||
file_uuid: string
|
||||
frame_number: number
|
||||
confidence: number
|
||||
bbox: any
|
||||
attributes: any
|
||||
}
|
||||
|
||||
const candidates = ref<FaceCandidate[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const minConfidence = ref(0.8)
|
||||
const selectedFaces = ref<number[]>([])
|
||||
|
||||
const getThumbnailUrl = (faceId: number): string => {
|
||||
const config = getCurrentConfig()
|
||||
return `${config.api_base_url}/api/v1/faces/${faceId}/thumbnail`
|
||||
}
|
||||
|
||||
const onThumbnailError = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.style.display = 'none'
|
||||
const parent = img.parentElement
|
||||
if (parent) {
|
||||
parent.innerHTML = '<div class="text-center p-4"><div class="text-2xl">👤</div></div>'
|
||||
}
|
||||
}
|
||||
|
||||
const loadCandidates = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await listFaceCandidates(undefined, minConfidence.value, page.value, pageSize.value)
|
||||
candidates.value = result.candidates || []
|
||||
total.value = result.total || 0
|
||||
} catch (error) {
|
||||
console.error('Failed to load candidates:', error)
|
||||
alert('Load failed: ' + error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelection = (face: FaceCandidate) => {
|
||||
const idx = selectedFaces.value.indexOf(face.id)
|
||||
if (idx >= 0) {
|
||||
selectedFaces.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedFaces.value.push(face.id)
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
page.value++
|
||||
loadCandidates()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
page.value--
|
||||
loadCandidates()
|
||||
}
|
||||
|
||||
const getConfidenceColor = (conf: number): string => {
|
||||
if (conf >= 0.9) return 'text-green-400'
|
||||
if (conf >= 0.8) return 'text-blue-400'
|
||||
if (conf >= 0.7) return 'text-yellow-400'
|
||||
return 'text-gray-400'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCandidates()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user