feat: Library 批量注册 + PeopleView 修复 + 国际化
Library: - 添加 checkbox 和鼠标框选功能 - Unregistered 卡片添加快速注册按钮 - 状态标签移至左下方,人脸图标移至左侧中间 - Deep Scan 改为 unregister -> register -> process - 完整国际化(日语、繁体中文、英语) PeopleView: - 修复卡片不显示问题(使用 get_file_traces API) - 移除对 identity_id 字段的依赖 其他: - 添加日语语言包 - 添加 useDragSelect composable - 新增文档:Face Bbox Null Bug Report
This commit is contained in:
242
src/store.ts
242
src/store.ts
@@ -1,7 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { apiCall } from '@/api'
|
||||
import { isTauri } from './api/config'
|
||||
import type { PoseResponse, AppearanceResponse } from '@/api/types'
|
||||
import { isTauri, getApiBase } from './api/config'
|
||||
import type { PoseResponse, AppearanceResponse, FaceLandmarksResponse } from '@/api/types'
|
||||
|
||||
export const filesCache = ref<any[]>([])
|
||||
export const filesLoaded = ref(false)
|
||||
@@ -16,13 +16,37 @@ export const unassignedTracesCache = ref<any[]>([])
|
||||
export const unassignedTracesLoaded = ref(false)
|
||||
export const unassignedTracesTotal = ref(0)
|
||||
export const unassignedTracesPage = ref(1)
|
||||
export const faceCountsCache = ref<Record<string, number>>({})
|
||||
const UNASSIGNED_PER_PAGE = 20
|
||||
|
||||
export async function loadFaceCount(fileUuid: string) {
|
||||
if (faceCountsCache.value[fileUuid] !== undefined) return
|
||||
try {
|
||||
const result = await apiCall('get_file_traces', { fileUuid, page: 1, page_size: 1, limit: 1 })
|
||||
faceCountsCache.value[fileUuid] = result.total || 0
|
||||
} catch (e) {
|
||||
console.error('loadFaceCount error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
export const thumbnailsCache = ref<Record<string, string>>({})
|
||||
export const profilesCache = ref<Record<string, string>>({})
|
||||
export const faceThumbsCache = ref<Record<string, string>>({})
|
||||
export const poseCache = ref<Record<string, PoseResponse>>({})
|
||||
export const appearanceCache = ref<Record<string, AppearanceResponse>>({})
|
||||
export const faceLandmarksCache = ref<Record<string, FaceLandmarksResponse>>({})
|
||||
|
||||
let thumbCacheBust = 0
|
||||
export function bumpThumbCacheBust() {
|
||||
thumbCacheBust += 1
|
||||
}
|
||||
export function clearFaceThumbs() {
|
||||
for (const k of Object.keys(faceThumbsCache.value)) {
|
||||
const v = faceThumbsCache.value[k]
|
||||
if (v && v.startsWith('blob:')) URL.revokeObjectURL(v)
|
||||
}
|
||||
faceThumbsCache.value = {}
|
||||
}
|
||||
|
||||
export interface ProcessorOutputInfo {
|
||||
has_json: boolean
|
||||
@@ -63,6 +87,36 @@ export interface PipelineStats {
|
||||
stages: PipelineStage[]
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
cpu_used_percent: number
|
||||
memory_used_percent: number
|
||||
memory_available_mb: number
|
||||
memory_total_mb: number
|
||||
swap_used_percent: number
|
||||
swap_available_mb: number
|
||||
swap_total_mb: number
|
||||
gpu_available: boolean
|
||||
gpu_utilization: number | null
|
||||
gpu_memory_used_pct: number | null
|
||||
ane_available: boolean
|
||||
ane_utilization: number | null
|
||||
source: string
|
||||
chip_name?: string
|
||||
cpu_power?: number
|
||||
gpu_power?: number
|
||||
ane_power?: number
|
||||
sys_power?: number
|
||||
ram_power?: number
|
||||
cpu_temp?: number
|
||||
gpu_temp?: number
|
||||
ecpu_freq_mhz?: number
|
||||
pcpu_freq_mhz?: number
|
||||
gpu_freq_mhz?: number
|
||||
ecpu_cores?: number
|
||||
pcpu_cores?: number
|
||||
gpu_cores?: number
|
||||
}
|
||||
|
||||
// File详细统计接口
|
||||
export interface JsonProcessorInfo {
|
||||
status: string
|
||||
@@ -131,13 +185,27 @@ export interface IdentityAgentStats {
|
||||
confirmations: number
|
||||
}
|
||||
|
||||
export interface FileStats {
|
||||
json: JsonStats
|
||||
postgresql: PostgresqlStats
|
||||
qdrant: QdrantStats
|
||||
tkg: TkgStats
|
||||
identity_agent: IdentityAgentStats
|
||||
}
|
||||
export interface FileStats {
|
||||
json: JsonStats
|
||||
postgresql: PostgresqlStats
|
||||
qdrant: QdrantStats
|
||||
tkg: TkgStats
|
||||
identity_agent: IdentityAgentStats
|
||||
vlm_summary: VlmSummaryStats
|
||||
scene_vlm: SceneVlmStats
|
||||
}
|
||||
|
||||
export interface VlmSummaryStats {
|
||||
total: number
|
||||
done: number
|
||||
}
|
||||
|
||||
export interface SceneVlmStats {
|
||||
total_scenes: number
|
||||
status: string
|
||||
vlm_done: number
|
||||
vlm_total: number
|
||||
}
|
||||
|
||||
export const processorCountsCache = ref<Record<string, Record<string, ProcessorOutputInfo>>>({})
|
||||
export const fileProgressCache = ref<Record<string, FileProgress>>({})
|
||||
@@ -146,22 +214,47 @@ export const fileStatusCache = ref<Record<string, string>>({})
|
||||
// Cluster agent state (persistent across navigation)
|
||||
export const clusterResultsCache = ref<Record<string, any[]>>({})
|
||||
export const clusterRunningState = ref<Record<string, boolean>>({})
|
||||
const clusterRunningSince = ref<Record<string, number>>({})
|
||||
const CLUSTER_RUNNING_STALE_MS = 5 * 60 * 1000
|
||||
|
||||
function loadClusterStateFromStorage() {
|
||||
try {
|
||||
const saved = localStorage.getItem('momentry_cluster_running')
|
||||
if (saved) clusterRunningState.value = JSON.parse(saved)
|
||||
} catch (e) {}
|
||||
try {
|
||||
const since = localStorage.getItem('momentry_cluster_running_since')
|
||||
if (since) clusterRunningSince.value = JSON.parse(since)
|
||||
} catch (e) {}
|
||||
const now = Date.now()
|
||||
let changed = false
|
||||
for (const uuid of Object.keys(clusterRunningState.value)) {
|
||||
const sinceTs = clusterRunningSince.value[uuid]
|
||||
if (clusterRunningState.value[uuid] && (!sinceTs || now - sinceTs > CLUSTER_RUNNING_STALE_MS)) {
|
||||
clusterRunningState.value[uuid] = false
|
||||
delete clusterRunningSince.value[uuid]
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) saveClusterStateToStorage()
|
||||
}
|
||||
function saveClusterStateToStorage() {
|
||||
try {
|
||||
localStorage.setItem('momentry_cluster_running', JSON.stringify(clusterRunningState.value))
|
||||
} catch (e) {}
|
||||
try {
|
||||
localStorage.setItem('momentry_cluster_running_since', JSON.stringify(clusterRunningSince.value))
|
||||
} catch (e) {}
|
||||
}
|
||||
loadClusterStateFromStorage()
|
||||
|
||||
export function setClusterRunning(fileUuid: string, running: boolean) {
|
||||
clusterRunningState.value[fileUuid] = running
|
||||
if (running) {
|
||||
clusterRunningSince.value[fileUuid] = Date.now()
|
||||
} else {
|
||||
delete clusterRunningSince.value[fileUuid]
|
||||
}
|
||||
saveClusterStateToStorage()
|
||||
}
|
||||
|
||||
@@ -170,7 +263,7 @@ export function isClusterRunning(fileUuid: string): boolean {
|
||||
}
|
||||
|
||||
export async function pollClusterResults(fileUuid: string, onDone?: (results: any[]) => void) {
|
||||
if (!fileUuid || isClusterRunning(fileUuid)) return
|
||||
if (!fileUuid) return
|
||||
setClusterRunning(fileUuid, true)
|
||||
try {
|
||||
for (let i = 0; i < 120; i++) {
|
||||
@@ -196,7 +289,7 @@ export async function pollClusterResults(fileUuid: string, onDone?: (results: an
|
||||
export async function loadClusterNames(fileUuid: string, clusters: any[]) {
|
||||
console.log('[loadClusterNames] loading names for', clusters.length, 'clusters')
|
||||
const promises = clusters.map(async (c) => {
|
||||
const repTid = c.representative_trace || c.trace_ids?.[0]
|
||||
const repTid = c.representative_trace !== undefined && c.representative_trace !== null ? c.representative_trace : c.trace_ids?.[0]
|
||||
if (repTid != null) {
|
||||
try {
|
||||
const key = `${fileUuid}:${repTid}`
|
||||
@@ -340,13 +433,14 @@ const MAX_PROFILE_CONCURRENT = 4
|
||||
|
||||
const loadingThumbs = new Set<string>()
|
||||
const loadingProfiles = new Set<string>()
|
||||
const loadingFaceThumbs = new Set<string>()
|
||||
export const loadingFaceThumbs = reactive(new Set<string>())
|
||||
const loadingProcessorCounts = new Set<string>()
|
||||
const pollingProgress = new Set<string>()
|
||||
export const pollingProgressSet = pollingProgress
|
||||
|
||||
export const pipelineStatsCache = ref<Record<string, PipelineStats>>({})
|
||||
export const fileStatsCache = ref<Record<string, FileStats>>({})
|
||||
export const systemStatsCache = ref<SystemStats | null>(null)
|
||||
const loadingPipelineStats = new Set<string>()
|
||||
const loadingFileStats = new Set<string>()
|
||||
|
||||
@@ -410,6 +504,7 @@ export function startFilesWatch() {
|
||||
}
|
||||
|
||||
// Check status changes for existing files
|
||||
let hasStatusChange = false
|
||||
for (const newFile of newFiles) {
|
||||
if (!newFile.file_uuid) continue
|
||||
const oldFile = filesCache.value.find((f: any) => f.file_uuid === newFile.file_uuid)
|
||||
@@ -417,14 +512,19 @@ export function startFilesWatch() {
|
||||
// Status changed, update cache
|
||||
const idx = filesCache.value.findIndex((f: any) => f.file_uuid === newFile.file_uuid)
|
||||
if (idx >= 0) {
|
||||
filesCache.value[idx] = newFile
|
||||
filesCache.value.splice(idx, 1, newFile)
|
||||
hasStatusChange = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStatusChange) {
|
||||
console.log('[startFilesWatch] Status changes detected and updated')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[startFilesWatch] Error:', e)
|
||||
}
|
||||
}, 5000)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
export function stopFilesWatch() {
|
||||
@@ -570,8 +670,8 @@ export async function ensureUnassignedTraces(fileUuid: string, page?: number) {
|
||||
}
|
||||
_unassignedTracesLoading = true
|
||||
try {
|
||||
const args: any = { page: targetPage, perPage: UNASSIGNED_PER_PAGE, fileUuid }
|
||||
const batch: any = await apiCall('get_unassigned_traces', args)
|
||||
const args: any = { page: 1, page_size: 3000, limit: 3000, fileUuid }
|
||||
const batch: any = await apiCall('get_file_traces', args)
|
||||
const traces = batch?.traces || []
|
||||
unassignedTracesCache.value = traces
|
||||
unassignedTracesTotal.value = batch?.total ?? traces.length
|
||||
@@ -586,19 +686,21 @@ export async function ensureUnassignedTraces(fileUuid: string, page?: number) {
|
||||
}
|
||||
|
||||
export function loadTraceThumb(t: any) {
|
||||
if (!t.file_uuid || !t.trace_id) return
|
||||
const key = `${t.trace_id}-${t.file_uuid}-${t.best_face_id || 0}`
|
||||
if (!t.file_uuid || t.trace_id === undefined || t.trace_id === null) return
|
||||
const key = `${t.file_uuid}-${t.trace_id}`
|
||||
if (faceThumbsCache.value[key] || loadingFaceThumbs.has(key)) return
|
||||
loadingFaceThumbs.add(key)
|
||||
queueThumb(async () => {
|
||||
try {
|
||||
// Use direct trace-thumb endpoint for fast keyFace.jpg loading (~8ms)
|
||||
const url = `/api/v1/trace-thumb/${t.file_uuid}/${t.trace_id}`
|
||||
// 改用自己的 robust face-thumbnail 端點(三層 fallback:.faces cache →
|
||||
// key_face.jpg → live 重切格),避免 trace-thumb 缺檔時 404 破圖。
|
||||
const base = getApiBase()
|
||||
const ver = thumbCacheBust > 0 ? `&t=${thumbCacheBust}` : ''
|
||||
const url = `${base}/api/v1/file/${t.file_uuid}/thumbnail?trace_id=${t.trace_id}${ver}`
|
||||
const resp = await fetch(url)
|
||||
if (resp.ok) {
|
||||
const blob = await resp.blob()
|
||||
const dataUrl = URL.createObjectURL(blob)
|
||||
faceThumbsCache.value[key] = dataUrl
|
||||
faceThumbsCache.value[key] = URL.createObjectURL(blob)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('loadTraceThumb failed:', key, e)
|
||||
@@ -795,15 +897,11 @@ export async function pollProgress(fileUuid: string) {
|
||||
gpu_percent: data.gpu_percent,
|
||||
memory_percent: data.memory_percent,
|
||||
}
|
||||
fileStatusCache.value[fileUuid] = 'processing'
|
||||
const f = filesCache.value.find((x: any) => x.file_uuid === fileUuid)
|
||||
if (f) f.status = 'processing'
|
||||
// No local status promotion - status comes from Core API only
|
||||
saveProgressCacheToStorage()
|
||||
const allComplete = data.processors?.every((p: any) => p.status === 'complete' || p.status === 'failed')
|
||||
if (allComplete || data.overall_progress >= 100) {
|
||||
stopPolling(fileUuid)
|
||||
fileStatusCache.value[fileUuid] = 'completed'
|
||||
if (f) f.status = 'completed'
|
||||
saveProgressCacheToStorage()
|
||||
await loadProcessorCounts(fileUuid)
|
||||
break
|
||||
@@ -838,22 +936,8 @@ export async function refreshFileStatus(fileUuid: string) {
|
||||
try {
|
||||
const pipeline = await loadPipelineStats(fileUuid, true)
|
||||
if (pipeline) {
|
||||
const isComplete = pipeline.overall_progress >= 1.0 || pipeline.stages?.every((s: any) => s.status === 'completed' || s.status === 'failed')
|
||||
const isRunning = pipeline.stages?.some((s: any) => s.status === 'running')
|
||||
|
||||
if (isComplete) {
|
||||
fileStatusCache.value[fileUuid] = 'completed'
|
||||
await loadFileStats(fileUuid, true)
|
||||
} else if (isRunning) {
|
||||
fileStatusCache.value[fileUuid] = 'processing'
|
||||
}
|
||||
|
||||
// Also update the file object in filesCache
|
||||
const f = filesCache.value.find((x: any) => x.file_uuid === fileUuid)
|
||||
if (f) {
|
||||
if (isComplete) f.status = 'completed'
|
||||
else if (isRunning) f.status = 'processing'
|
||||
}
|
||||
// No local status promotion - status comes from Core API only
|
||||
await loadFileStats(fileUuid, true)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('refreshFileStatus failed:', fileUuid, e)
|
||||
@@ -921,10 +1005,12 @@ export async function loadPipelineStats(fileUuid: string, force = false): Promis
|
||||
processors: 0.30,
|
||||
rule1_ingestion: 0.05,
|
||||
face_tracing: 0.05,
|
||||
identity_agent: 0.10,
|
||||
face_dedup: 0.10,
|
||||
tkg_nodes: 0.20,
|
||||
tkg_edges: 0.15,
|
||||
rule2_ingestion: 0.15
|
||||
rule2_ingestion: 0.10,
|
||||
vlm_summary: 0.04,
|
||||
moment_chunks: 0.01
|
||||
}
|
||||
|
||||
const mappedStages = stages.map((s: any) => ({
|
||||
@@ -934,7 +1020,6 @@ export async function loadPipelineStats(fileUuid: string, force = false): Promis
|
||||
status: s.status || 'pending'
|
||||
}))
|
||||
|
||||
// 用前端权重重新计算 overall_progress(不信任 API 返回的值)
|
||||
const calculatedProgress = mappedStages.reduce((sum, s) => sum + s.weight * s.progress, 0)
|
||||
|
||||
const pipelineStats: PipelineStats = {
|
||||
@@ -946,14 +1031,7 @@ export async function loadPipelineStats(fileUuid: string, force = false): Promis
|
||||
|
||||
pipelineStatsCache.value = { ...pipelineStatsCache.value, [fileUuid]: pipelineStats }
|
||||
|
||||
// If pipeline is complete, update file status
|
||||
if (calculatedProgress >= 1.0) {
|
||||
const f = filesCache.value.find((x: any) => x.file_uuid === fileUuid)
|
||||
if (f && f.status !== 'completed') {
|
||||
f.status = 'completed'
|
||||
fileStatusCache.value[fileUuid] = 'completed'
|
||||
}
|
||||
}
|
||||
// No local status promotion - status comes from Core API only
|
||||
|
||||
return pipelineStats
|
||||
} catch (e) {
|
||||
@@ -961,11 +1039,9 @@ export async function loadPipelineStats(fileUuid: string, force = false): Promis
|
||||
try {
|
||||
const oldData: any = await apiCall('get_progress', { fileUuid })
|
||||
|
||||
// 检查所有处理器是否都完成
|
||||
const allComplete = oldData.processors?.length > 0 && oldData.processors?.every((p: any) => p.status === 'complete' || p.status === 'failed')
|
||||
const hasRunning = oldData.processors?.some((p: any) => p.status === 'running')
|
||||
|
||||
// 如果都完成,显示100%
|
||||
const progress = allComplete ? 1.0 : (oldData.overall_progress || 0) / 100
|
||||
const status = allComplete ? 'completed' : (hasRunning ? 'running' : 'pending')
|
||||
|
||||
@@ -988,6 +1064,19 @@ export async function loadPipelineStats(fileUuid: string, force = false): Promis
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSystemStats(): Promise<SystemStats | null> {
|
||||
try {
|
||||
const resp = await fetch('/api/v1/system-stats')
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data: SystemStats = await resp.json()
|
||||
systemStatsCache.value = data
|
||||
return data
|
||||
} catch (e) {
|
||||
console.error('loadSystemStats failed:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadFileStats(fileUuid: string, force = false): Promise<FileStats | null> {
|
||||
if (!fileUuid) return null
|
||||
if (!force && fileStatsCache.value[fileUuid]) return fileStatsCache.value[fileUuid]
|
||||
@@ -1060,6 +1149,16 @@ export async function loadFileStats(fileUuid: string, force = false): Promise<Fi
|
||||
tmdb_matches: data.identity_agent?.tmdb_matches || 0,
|
||||
speaker_bindings: data.identity_agent?.speaker_bindings || 0,
|
||||
confirmations: data.identity_agent?.confirmations || 0
|
||||
},
|
||||
vlm_summary: {
|
||||
total: data.vlm_summary?.total || 0,
|
||||
done: data.vlm_summary?.done || 0
|
||||
},
|
||||
scene_vlm: {
|
||||
total_scenes: data.scene_vlm?.total_scenes || 0,
|
||||
status: data.scene_vlm?.status || '',
|
||||
vlm_done: data.scene_vlm?.vlm_done || 0,
|
||||
vlm_total: data.scene_vlm?.vlm_total || 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,7 +1189,9 @@ export async function loadFileStats(fileUuid: string, force = false): Promise<Fi
|
||||
postgresql: { sentence_chunks: 0, trace_chunks: 0, relationship_chunks: 0, identities: 0, file_identities: 0 },
|
||||
qdrant: { faces: 0, face_traces: 0, face_identities: 0, text_chunks: 0, speakers: 0 },
|
||||
tkg: { nodes: { character: 0, object: 0, scene: 0, action: 0, location: 0, event: 0, dialogue: 0, emotion: 0, relation: 0 }, edges: { appear_in: 0, interact_with: 0, located_at: 0, perform_action: 0, cause_effect: 0, temporal_link: 0, speak_to: 0, feel: 0 }, total_nodes: 0, total_edges: 0 },
|
||||
identity_agent: { clusters: 0, identities_created: 0, tmdb_matches: 0, speaker_bindings: 0, confirmations: 0 }
|
||||
identity_agent: { clusters: 0, identities_created: 0, tmdb_matches: 0, speaker_bindings: 0, confirmations: 0 },
|
||||
vlm_summary: { total: 0, done: 0 },
|
||||
scene_vlm: { total_scenes: 0, status: '', vlm_done: 0, vlm_total: 0 }
|
||||
}
|
||||
|
||||
// 从processorCounts填充json.processors
|
||||
@@ -1132,15 +1233,12 @@ export async function pollPipelineProgress(fileUuid: string) {
|
||||
const idx = filesCache.value.findIndex((x: any) => x.file_uuid === fileUuid)
|
||||
if (idx === -1) continue
|
||||
|
||||
fileStatusCache.value[fileUuid] = 'processing'
|
||||
filesCache.value.splice(idx, 1, { ...filesCache.value[idx], status: 'processing' })
|
||||
// No local status promotion - status comes from Core API only
|
||||
saveProgressCacheToStorage()
|
||||
|
||||
const allComplete = stats.stages.every(s => s.status === 'completed' || s.status === 'failed')
|
||||
if (allComplete || stats.overall_progress >= 1.0) {
|
||||
stopPolling(fileUuid)
|
||||
fileStatusCache.value[fileUuid] = 'completed'
|
||||
filesCache.value.splice(idx, 1, { ...filesCache.value[idx], status: 'completed' })
|
||||
saveProgressCacheToStorage()
|
||||
await loadFileStats(fileUuid, true)
|
||||
break
|
||||
@@ -1210,4 +1308,24 @@ export async function getAppearance(fileUuid: string, frame: number, bbox?: { x:
|
||||
console.error('getAppearance failed:', fileUuid, frame, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFaceLandmarks(fileUuid: string, frame: number, bbox?: { x: number; y: number; width: number; height: number } | null): Promise<FaceLandmarksResponse | null> {
|
||||
const key = bbox ? `${fileUuid}:${frame}:${bbox.x}:${bbox.y}` : `${fileUuid}:${frame}`
|
||||
if (faceLandmarksCache.value[key]) return faceLandmarksCache.value[key]
|
||||
try {
|
||||
const args: any = { fileUuid, frame }
|
||||
if (bbox) {
|
||||
args.bboxX = bbox.x
|
||||
args.bboxY = bbox.y
|
||||
args.bboxW = bbox.width
|
||||
args.bboxH = bbox.height
|
||||
}
|
||||
const data = await apiCall('get_face_landmarks', args)
|
||||
if (data) faceLandmarksCache.value[key] = data
|
||||
return data
|
||||
} catch (e) {
|
||||
console.error('getFaceLandmarks failed:', fileUuid, frame, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user