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,119 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button @click="$router.back()" class="text-gray-400 hover:text-white">
|
||||
← 返回
|
||||
</button>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">{{ profile.name || '未命名身份' }}</h2>
|
||||
<p class="text-sm text-gray-400">全域身份 ID: {{ identityId }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="text-center py-12 text-gray-500">載入中...</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else-if="detail" class="grid gap-6">
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-blue-400 mb-4">人物檔案</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<span class="text-gray-500 text-sm">本名</span>
|
||||
<p class="text-white text-lg">{{ profile.name || '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500 text-sm">角色名</span>
|
||||
<p class="text-white text-lg">{{ profile.character_name || '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500 text-sm">別名 (Aliases)</span>
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
<span v-for="(alias, idx) in profile.aliases" :key="idx" class="bg-gray-700 text-gray-300 px-2 py-1 rounded text-sm">
|
||||
{{ alias }}
|
||||
</span>
|
||||
<span v-if="!profile.aliases || profile.aliases.length === 0" class="text-gray-600 text-sm">無</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500 text-sm">Speaker ID</span>
|
||||
<p class="text-white text-lg font-mono">{{ profile.speaker_id || '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500 text-sm">性別</span>
|
||||
<p class="text-white">{{ profile.gender || '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500 text-sm">年齡</span>
|
||||
<p class="text-white">{{ profile.age || '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Videos List -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-green-400 mb-4">出現影片 ({{ videos.length }})</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-400">
|
||||
<thead class="text-xs text-gray-500 uppercase bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 rounded-l-lg">影片名稱</th>
|
||||
<th scope="col" class="px-6 py-3">區域 ID</th>
|
||||
<th scope="col" class="px-6 py-3">出現次數</th>
|
||||
<th scope="col" class="px-6 py-3 rounded-r-lg">首次出現</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="video in videos" :key="video.file_uuid" class="bg-gray-800 border-b border-gray-700 hover:bg-gray-750">
|
||||
<td class="px-6 py-4 font-medium text-white">{{ video.file_name }}</td>
|
||||
<td class="px-6 py-4 font-mono text-blue-300">{{ video.person_id }}</td>
|
||||
<td class="px-6 py-4">{{ video.appearance_count }}</td>
|
||||
<td class="px-6 py-4">{{ video.first_appearance?.toFixed(2) || '-' }}s</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const identityId = ref('')
|
||||
const loading = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
const profile = ref<any>({})
|
||||
const videos = ref<any[]>([])
|
||||
|
||||
const loadDetail = async () => {
|
||||
identityId.value = route.params.id as string
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const config = JSON.parse(localStorage.getItem('portal_config') || '{}')
|
||||
const baseUrl = config.api_base_url || 'http://127.0.0.1:3003'
|
||||
const resp = await fetch(`${baseUrl}/api/v1/identities/${identityId.value}`, {
|
||||
headers: { 'X-API-Key': config.api_key || '' }
|
||||
})
|
||||
const result = await resp.json()
|
||||
detail.value = result
|
||||
profile.value = result.profile || {}
|
||||
videos.value = result.videos || []
|
||||
} catch (error) {
|
||||
console.error('Failed to load identity detail:', error)
|
||||
alert('載入失敗: ' + error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user