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,102 @@
<template>
<div v-if="apiCall" class="mt-6 border-t border-gray-600 bg-gray-800 rounded-lg p-4 text-sm shadow-lg">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-bold text-blue-400 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
API 呼叫示範 (Real-time)
</h3>
<div class="flex items-center gap-2">
<button
@click="copyToClipboard"
class="px-2 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 text-xs rounded border border-gray-600 transition flex items-center gap-1"
>
<svg v-if="!isCopied" class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg>
<svg v-else class="w-3 h-3 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
{{ isCopied ? '已複製' : 'Copy Text' }}
</button>
<span class="px-2 py-0.5 rounded text-xs font-mono" :class="statusColor">
{{ apiCall.status }}
</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- Request -->
<div class="bg-gray-900 p-3 rounded border border-gray-700">
<div class="text-gray-400 mb-1 text-xs uppercase tracking-wider">Request ({{ apiCall.type }})</div>
<div class="text-white font-mono break-all">
<span v-if="apiCall.type === 'HTTP'" class="text-green-400">{{ apiCall.method }}</span>
<span v-if="apiCall.type === 'HTTP'" class="text-gray-500 mx-1"></span>
<span v-if="apiCall.type === 'HTTP'" class="text-blue-300">{{ apiCall.url }}</span>
<span v-if="apiCall.type === 'TAURI'" class="text-green-400">Command:</span>
<span v-if="apiCall.type === 'TAURI'" class="text-blue-300 ml-1">{{ apiCall.command }}</span>
</div>
<div v-if="apiCall.body" class="mt-2 text-xs text-gray-300 font-mono bg-gray-800 p-2 rounded overflow-x-auto">
Body: {{ JSON.stringify(apiCall.body, null, 2) }}
</div>
<div v-if="apiCall.args && apiCall.type === 'TAURI'" class="mt-2 text-xs text-gray-300 font-mono bg-gray-800 p-2 rounded overflow-x-auto">
Args: {{ JSON.stringify(apiCall.args, null, 2) }}
</div>
</div>
<!-- Response -->
<div class="bg-gray-900 p-3 rounded border border-gray-700 relative">
<div class="text-gray-400 mb-1 text-xs uppercase tracking-wider">Response</div>
<pre class="text-xs text-gray-300 font-mono overflow-auto max-h-48 whitespace-pre-wrap break-all">{{ formatResponse(apiCall.data) }}</pre>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { lastApiCall } from '@/api/client'
import { computed, ref } from 'vue'
const apiCall = lastApiCall
const isCopied = ref(false)
const statusColor = computed(() => {
const s = apiCall.value?.status || ''
if (s === 'loading') return 'bg-yellow-900 text-yellow-300'
if (s.includes('OK') || s === 'Success') return 'bg-green-900 text-green-300'
return 'bg-red-900 text-red-300'
})
const copyToClipboard = async () => {
if (!apiCall.value) return
const data = apiCall.value
const text = `
=== Momentry API Call Details ===
Type: ${data.type}
Status: ${data.status}
Time: ${data.timestamp}
${data.type === 'HTTP' ? `[${data.method}] ${data.url}` : `Command: ${data.command}`}
Arguments:
${JSON.stringify(data.args || data.body, null, 2)}
Response:
${JSON.stringify(data.data, null, 2)}
`.trim()
try {
await navigator.clipboard.writeText(text)
isCopied.value = true
setTimeout(() => isCopied.value = false, 2000)
} catch (err) {
console.error('Failed to copy text:', err)
}
}
function formatResponse(data: any): string {
if (!data) return 'Empty'
try {
return JSON.stringify(data, null, 2)
} catch {
return String(data)
}
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="w-16 h-16 bg-gray-700 rounded-lg overflow-hidden border border-gray-600 flex-shrink-0 flex items-center justify-center">
<img
v-if="src"
:src="src"
alt="Person"
class="w-full h-full object-cover"
loading="lazy"
/>
<span v-else-if="loading" class="text-gray-500 text-xs">...</span>
<svg v-else class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getPersonThumbnail } from '@/api/client'
const props = defineProps<{
personId: string
videoUuid?: string
}>()
const src = ref('')
const loading = ref(false)
onMounted(async () => {
loading.value = true
try {
src.value = await getPersonThumbnail(props.personId)
} catch (e) {
console.error('Failed to load thumbnail', e)
} finally {
loading.value = false
}
})
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div class="mt-2">
<div class="flex items-center gap-2">
<select
v-model="targetLang"
class="bg-gray-700 text-white text-xs px-2 py-1 rounded border border-gray-600 focus:outline-none focus:border-blue-500"
>
<option value="zh-TW">繁體中文</option>
<option value="zh-CN">简体中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
<option value="fr">Français</option>
<option value="en">English</option>
</select>
<button
@click="translate"
:disabled="loading || !props.text"
class="text-xs bg-blue-900 text-blue-300 hover:bg-blue-800 px-2 py-1 rounded transition flex items-center gap-1 disabled:opacity-50"
>
<span v-if="loading" class="animate-pulse">翻譯中...</span>
<span v-else>🌐 翻譯</span>
</button>
</div>
<div v-if="showTranslation" class="mt-3 p-3 bg-gray-900 border border-green-600 rounded text-green-300 text-sm leading-relaxed">
<div class="flex justify-between items-center mb-1">
<span class="text-xs font-bold text-green-500 uppercase">Translation ({{ targetLang }})</span>
<button @click="showTranslation = false" class="text-gray-500 hover:text-white text-xs"></button>
</div>
{{ translatedText }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { translateText } from '@/api/client'
const props = defineProps<{
text: string
}>()
const targetLang = ref('zh-TW')
const translatedText = ref('')
const loading = ref(false)
const showTranslation = ref(false)
const translate = async () => {
if (!props.text.trim()) return
loading.value = true
try {
translatedText.value = await translateText(props.text, targetLang.value)
showTranslation.value = true
} catch (error) {
console.error('Translation failed:', error)
} finally {
loading.value = false
}
}
</script>