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:
102
portal/src/components/ApiDemo.vue
Normal file
102
portal/src/components/ApiDemo.vue
Normal 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>
|
||||
37
portal/src/components/PersonThumbnail.vue
Normal file
37
portal/src/components/PersonThumbnail.vue
Normal 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>
|
||||
62
portal/src/components/TranslatableText.vue
Normal file
62
portal/src/components/TranslatableText.vue
Normal 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>
|
||||
Reference in New Issue
Block a user