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

View File

@@ -0,0 +1,176 @@
<template>
<div class="space-y-6">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold">人物管理</h2>
<button
@click="loadPersons"
class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg transition"
>
重新整理
</button>
</div>
<!-- Search Filter -->
<div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
<input
v-model="filterQuery"
@keyup.enter="loadPersons"
type="text"
placeholder="搜尋人物..."
class="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-blue-500"
/>
</div>
<!-- Person List -->
<div v-if="persons.length > 0" class="grid gap-4">
<div
v-for="person in persons"
:key="person.id"
class="bg-gray-800 rounded-lg p-6 border border-gray-700"
>
<div class="flex items-start gap-6">
<!-- Thumbnail -->
<PersonThumbnail :personId="person.person_id" :videoUuid="person.file_uuid" />
<div class="flex-1">
<div class="flex items-center space-x-3 mb-2">
<h3 class="text-xl font-semibold text-blue-400">
{{ person.profile.name || '未命名' }}
</h3>
<span
v-if="person.face_identity_id"
class="bg-green-900 text-green-300 px-2 py-1 rounded text-xs"
>
已註冊
</span>
<span
v-else
class="bg-yellow-900 text-yellow-300 px-2 py-1 rounded text-xs"
>
未註冊
</span>
</div>
<div class="grid grid-cols-2 gap-4 text-sm text-gray-400 mb-3">
<div>角色: {{ person.profile.character_name || '-' }}</div>
<div>Speaker: {{ person.profile.speaker_id || '-' }}</div>
<div>影片: {{ person.file_uuid }}</div>
<div>出現次數: {{ person.stats.appearance_count }}</div>
</div>
<div v-if="person.profile.original_name" class="text-sm text-gray-500">
原始名稱: {{ person.profile.original_name }}
</div>
</div>
<!-- Actions -->
<div class="flex flex-col space-y-2">
<button
v-if="!person.face_identity_id"
@click="registerPerson(person.person_id, person.file_uuid)"
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg text-sm transition"
>
註冊為全域身份
</button>
<button
@click="viewDetails(person)"
class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg text-sm transition"
>
{{ person.face_identity_id ? '查看全域詳情' : '查看影片' }}
</button>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div v-else-if="loading" class="text-center py-12 text-gray-500">
載入中...
</div>
<!-- Empty State -->
<div v-else class="text-center py-12 text-gray-500">
尚無人物資料
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { listIdentities } from '@/api/client'
import { useRouter } from 'vue-router'
import PersonThumbnail from '@/components/PersonThumbnail.vue'
import { invoke } from '@tauri-apps/api/core'
interface PersonProfile {
name: string | null
original_name: string | null
character_name: string | null
speaker_id: string | null
}
interface PersonStats {
appearance_count: number
total_duration: number
first_appearance: number | null
last_appearance: number | null
}
interface Person {
id: number
person_id: string
face_identity_id: number | null
file_uuid: string
profile: PersonProfile
stats: PersonStats
is_confirmed: boolean
}
const persons = ref<Person[]>([])
const loading = ref(false)
const filterQuery = ref('')
const router = useRouter()
const loadPersons = async () => {
loading.value = true
try {
const result = await listIdentities()
persons.value = result.identities
} catch (error) {
console.error('Failed to load persons:', error)
alert('載入失敗: ' + error)
} finally {
loading.value = false
}
}
const registerPerson = async (personId: string, videoUuid: string) => {
try {
await invoke('register_identity', {
personId,
videoUuid
})
alert('註冊成功!')
await loadPersons()
} catch (error) {
console.error('Registration failed:', error)
alert('註冊失敗: ' + error)
}
}
const viewDetails = (person: Person) => {
if (person.face_identity_id) {
router.push({
name: 'identity-detail',
params: { id: person.face_identity_id }
})
} else {
router.push({
name: 'video-detail',
params: { uuid: person.file_uuid }
})
}
}
onMounted(() => {
loadPersons()
})
</script>