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:
Warren
2026-04-30 15:11:53 +08:00
parent 4d75b2e251
commit b54c2def30
192 changed files with 46721 additions and 0 deletions
+195
View File
@@ -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>