问题: - Core API 新增 scene_vlm 阶段 - Studio 需要同步更新权重映射和显示名称 修复: - 更新 WEIGHT_MAP,添加 scene_vlm (0.03) - 调整 vlm_summary 权重从 0.04 到 0.03 - 更新 formatStageName 函数,添加 'Scene VLM' 显示名称 权重映射: - processors: 0.30 - rule1_ingestion: 0.05 - face_tracing: 0.05 - face_dedup: 0.10 - tkg_nodes: 0.20 - tkg_edges: 0.15 - rule2_ingestion: 0.10 - vlm_summary: 0.03 - scene_vlm: 0.03 - moment_chunks: 0.01 影响: - 文件处理流程显示 Scene VLM 阶段 - 深度扫描文件显示完整的处理进度 - 用户可以实时监控 Scene VLM 处理状态 测试: ✅ 编译通过 (npm run build) ✅ 阶段显示名称正确
1333 lines
43 KiB
TypeScript
1333 lines
43 KiB
TypeScript
import { ref, reactive } from 'vue'
|
||
import { apiCall } from '@/api'
|
||
import { isTauri, getApiBase } from './api/config'
|
||
import type { PoseResponse, AppearanceResponse, FaceLandmarksResponse } from '@/api/types'
|
||
|
||
export const filesCache = ref<any[]>([])
|
||
export const filesLoaded = ref(false)
|
||
|
||
export const peopleCache = ref<any[]>([])
|
||
export const peopleLoaded = ref(false)
|
||
|
||
export const faceCandidatesCache = ref<any[]>([])
|
||
export const faceCandidatesLoaded = ref(false)
|
||
|
||
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
|
||
frame_count: number | null
|
||
segment_count: number | null
|
||
chunk_count: number | null
|
||
last_modified: string | null
|
||
}
|
||
|
||
export interface ProcessorProgress {
|
||
processor_type: string
|
||
status: 'pending' | 'running' | 'complete' | 'failed'
|
||
progress: number
|
||
current?: number
|
||
total?: number
|
||
eta_seconds?: number
|
||
}
|
||
|
||
export interface FileProgress {
|
||
file_uuid: string
|
||
overall_progress: number
|
||
processors: ProcessorProgress[]
|
||
cpu_percent?: number
|
||
gpu_percent?: number
|
||
memory_percent?: number
|
||
}
|
||
|
||
// Pipeline阶段接口
|
||
export interface PipelineStage {
|
||
name: string
|
||
weight: number
|
||
progress: number
|
||
status: 'pending' | 'running' | 'completed' | 'failed'
|
||
}
|
||
|
||
export interface PipelineStats {
|
||
overall_progress: number
|
||
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
|
||
segment_count?: number
|
||
frame_count?: number
|
||
chunk_count?: number
|
||
}
|
||
|
||
export interface JsonStats {
|
||
file_name: string
|
||
status: string
|
||
processors: Record<string, JsonProcessorInfo>
|
||
}
|
||
|
||
export interface PostgresqlStats {
|
||
sentence_chunks: number
|
||
trace_chunks: number
|
||
relationship_chunks: number
|
||
identities: number
|
||
file_identities: number
|
||
}
|
||
|
||
export interface QdrantStats {
|
||
faces: number
|
||
face_traces: number
|
||
face_identities: number
|
||
text_chunks: number
|
||
speakers: number
|
||
}
|
||
|
||
export interface TkgNodeStats {
|
||
character: number
|
||
object: number
|
||
scene: number
|
||
action: number
|
||
location: number
|
||
event: number
|
||
dialogue: number
|
||
emotion: number
|
||
relation: number
|
||
}
|
||
|
||
export interface TkgEdgeStats {
|
||
appear_in: number
|
||
interact_with: number
|
||
located_at: number
|
||
perform_action: number
|
||
cause_effect: number
|
||
temporal_link: number
|
||
speak_to: number
|
||
feel: number
|
||
}
|
||
|
||
export interface TkgStats {
|
||
nodes: TkgNodeStats
|
||
edges: TkgEdgeStats
|
||
total_nodes: number
|
||
total_edges: number
|
||
}
|
||
|
||
export interface IdentityAgentStats {
|
||
clusters: number
|
||
identities_created: number
|
||
tmdb_matches: number
|
||
speaker_bindings: number
|
||
confirmations: number
|
||
}
|
||
|
||
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>>({})
|
||
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()
|
||
}
|
||
|
||
export function isClusterRunning(fileUuid: string): boolean {
|
||
return clusterRunningState.value[fileUuid] || false
|
||
}
|
||
|
||
export async function pollClusterResults(fileUuid: string, onDone?: (results: any[]) => void) {
|
||
if (!fileUuid) return
|
||
setClusterRunning(fileUuid, true)
|
||
try {
|
||
for (let i = 0; i < 120; i++) {
|
||
if (!isClusterRunning(fileUuid)) return
|
||
await new Promise(r => setTimeout(r, 3000))
|
||
try {
|
||
const results: any = await apiCall('get_cluster_results', { fileHash: fileUuid })
|
||
const groups = results?.face_groups || results?.clusters || []
|
||
if (groups.length) {
|
||
await loadClusterNames(fileUuid, groups)
|
||
clusterResultsCache.value[fileUuid] = groups
|
||
setClusterRunning(fileUuid, false)
|
||
onDone?.(groups)
|
||
return
|
||
}
|
||
} catch { /* not ready */ }
|
||
}
|
||
} finally {
|
||
setClusterRunning(fileUuid, false)
|
||
}
|
||
}
|
||
|
||
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 !== undefined && c.representative_trace !== null ? c.representative_trace : c.trace_ids?.[0]
|
||
if (repTid != null) {
|
||
try {
|
||
const key = `${fileUuid}:${repTid}`
|
||
delete traceProfileCache.value[key]
|
||
const profile = await getTraceProfile(fileUuid, repTid)
|
||
console.log('[loadClusterNames] cluster', c.cluster_id, 'trace', repTid, 'profile:', profile?.name)
|
||
if (profile?.name) c.name = profile.name
|
||
} catch (e) { console.error('[loadClusterNames] failed:', repTid, e) }
|
||
}
|
||
})
|
||
await Promise.all(promises)
|
||
console.log('[loadClusterNames] done, clusters:', clusters.map(c => ({ id: c.cluster_id, name: c.name })))
|
||
}
|
||
|
||
export function getClusterResults(fileUuid: string): any[] {
|
||
return clusterResultsCache.value[fileUuid] || []
|
||
}
|
||
|
||
export function setClusterResults(fileUuid: string, results: any[]) {
|
||
clusterResultsCache.value[fileUuid] = results
|
||
}
|
||
|
||
export function clearClusterState(fileUuid: string) {
|
||
delete clusterRunningState.value[fileUuid]
|
||
delete clusterResultsCache.value[fileUuid]
|
||
saveClusterStateToStorage()
|
||
}
|
||
|
||
// Trace & File Profile cache
|
||
export const traceProfileCache = ref<Record<string, any>>({})
|
||
export const fileProfileCache = ref<Record<string, any>>({})
|
||
|
||
export async function getTraceProfile(fileUuid: string, traceId: number) {
|
||
const key = `${fileUuid}:${traceId}`
|
||
if (traceProfileCache.value[key]) return traceProfileCache.value[key]
|
||
try {
|
||
const data = await apiCall('get_trace_profile', { fileUuid, traceId })
|
||
if (data) traceProfileCache.value[key] = data
|
||
return data
|
||
} catch (e) {
|
||
console.error('getTraceProfile failed:', fileUuid, traceId, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function updateTraceProfile(fileUuid: string, traceId: number, updates: any) {
|
||
try {
|
||
const data = await apiCall('update_trace_profile', { fileUuid, traceId, ...updates })
|
||
const key = `${fileUuid}:${traceId}`
|
||
if (data) traceProfileCache.value[key] = data
|
||
return data
|
||
} catch (e) {
|
||
console.error('updateTraceProfile failed:', fileUuid, traceId, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function updateTraceProfileGroup(fileUuid: string, traceIds: number[], updates: any) {
|
||
try {
|
||
const data = await apiCall('update_trace_profile_group', { fileUuid, traceIds, ...updates })
|
||
// Update cache for all traces
|
||
for (const tid of traceIds) {
|
||
const key = `${fileUuid}:${tid}`
|
||
if (traceProfileCache.value[key]) {
|
||
traceProfileCache.value[key] = { ...traceProfileCache.value[key], ...updates }
|
||
} else {
|
||
traceProfileCache.value[key] = { file_uuid: fileUuid, trace_id: tid, ...updates }
|
||
}
|
||
}
|
||
return data
|
||
} catch (e) {
|
||
console.error('updateTraceProfileGroup failed:', fileUuid, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function getFileProfile(fileUuid: string) {
|
||
if (fileProfileCache.value[fileUuid]) return fileProfileCache.value[fileUuid]
|
||
try {
|
||
const data = await apiCall('get_file_profile', { fileUuid })
|
||
if (data) fileProfileCache.value[fileUuid] = data
|
||
return data
|
||
} catch (e) {
|
||
console.error('getFileProfile failed:', fileUuid, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function updateFileProfile(fileUuid: string, updates: any) {
|
||
try {
|
||
const data = await apiCall('update_file_profile', { fileUuid, ...updates })
|
||
if (data) fileProfileCache.value[fileUuid] = { ...fileProfileCache.value[fileUuid], ...data }
|
||
return data
|
||
} catch (e) {
|
||
console.error('updateFileProfile failed:', fileUuid, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export function invalidateTraceProfile(fileUuid: string, traceId: number) {
|
||
delete traceProfileCache.value[`${fileUuid}:${traceId}`]
|
||
}
|
||
|
||
export function invalidateFileProfile(fileUuid: string) {
|
||
delete fileProfileCache.value[fileUuid]
|
||
}
|
||
|
||
function loadProgressCacheFromStorage() {
|
||
try {
|
||
const savedProgress = localStorage.getItem('momentry_file_progress')
|
||
const savedStatus = localStorage.getItem('momentry_file_status')
|
||
if (savedProgress) {
|
||
fileProgressCache.value = JSON.parse(savedProgress)
|
||
}
|
||
if (savedStatus) {
|
||
fileStatusCache.value = JSON.parse(savedStatus)
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load progress cache:', e)
|
||
}
|
||
}
|
||
|
||
function saveProgressCacheToStorage() {
|
||
try {
|
||
localStorage.setItem('momentry_file_progress', JSON.stringify(fileProgressCache.value))
|
||
localStorage.setItem('momentry_file_status', JSON.stringify(fileStatusCache.value))
|
||
} catch (e) {
|
||
console.error('Failed to save progress cache:', e)
|
||
}
|
||
}
|
||
|
||
loadProgressCacheFromStorage()
|
||
|
||
const thumbQueue: (() => Promise<void>)[] = []
|
||
let activeThumbLoads = 0
|
||
const MAX_CONCURRENT = 10
|
||
|
||
const profileQueue: (() => Promise<void>)[] = []
|
||
let activeProfileLoads = 0
|
||
const MAX_PROFILE_CONCURRENT = 4
|
||
|
||
const loadingThumbs = new Set<string>()
|
||
const loadingProfiles = 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>()
|
||
|
||
let statsRefreshInterval: ReturnType<typeof setInterval> | null = null
|
||
let processingPollInterval: ReturnType<typeof setInterval> | null = null
|
||
let filesWatchInterval: ReturnType<typeof setInterval> | null = null
|
||
|
||
export function startStatsAutoRefresh() {
|
||
if (statsRefreshInterval) return
|
||
statsRefreshInterval = setInterval(async () => {
|
||
for (const fileUuid of Object.keys(pipelineStatsCache.value)) {
|
||
await loadPipelineStats(fileUuid, true)
|
||
}
|
||
for (const fileUuid of Object.keys(fileStatsCache.value)) {
|
||
await loadFileStats(fileUuid, true)
|
||
}
|
||
}, 30000)
|
||
}
|
||
|
||
export function stopStatsAutoRefresh() {
|
||
if (statsRefreshInterval) {
|
||
clearInterval(statsRefreshInterval)
|
||
statsRefreshInterval = null
|
||
}
|
||
}
|
||
|
||
export function startProcessingFilesPolling() {
|
||
if (processingPollInterval) return
|
||
processingPollInterval = setInterval(async () => {
|
||
const processingFiles = filesCache.value.filter((f: any) => f.status === 'processing' && f.file_uuid)
|
||
if (processingFiles.length === 0) return
|
||
// Use Promise.all to sync in parallel, not sequentially
|
||
await Promise.all(processingFiles.map((f: any) => syncFileStatus(f.file_uuid)))
|
||
}, 10000) // Increased from 5s to 10s
|
||
}
|
||
|
||
export function stopProcessingFilesPolling() {
|
||
if (processingPollInterval) {
|
||
clearInterval(processingPollInterval)
|
||
processingPollInterval = null
|
||
}
|
||
}
|
||
|
||
export function startFilesWatch() {
|
||
if (filesWatchInterval) return
|
||
filesWatchInterval = setInterval(async () => {
|
||
try {
|
||
const result = await apiCall('get_files', { args: { pageSize: 500 } })
|
||
const newFiles = Array.isArray(result) ? result : []
|
||
|
||
// Check for new files
|
||
const oldUuids = new Set(filesCache.value.map((f: any) => f.file_uuid).filter(Boolean))
|
||
const newUuids = newFiles.map((f: any) => f.file_uuid).filter(Boolean)
|
||
|
||
// Detect new registered files
|
||
const added = newUuids.filter((uuid: string) => !oldUuids.has(uuid))
|
||
const removed = Array.from(oldUuids).filter((uuid: string) => !newUuids.includes(uuid))
|
||
|
||
if (added.length > 0 || removed.length > 0) {
|
||
filesCache.value = newFiles
|
||
}
|
||
|
||
// 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)
|
||
if (oldFile && oldFile.status !== newFile.status) {
|
||
// Status changed, update cache
|
||
const idx = filesCache.value.findIndex((f: any) => f.file_uuid === newFile.file_uuid)
|
||
if (idx >= 0) {
|
||
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)
|
||
}
|
||
}, 3000)
|
||
}
|
||
|
||
export function stopFilesWatch() {
|
||
if (filesWatchInterval) {
|
||
clearInterval(filesWatchInterval)
|
||
filesWatchInterval = null
|
||
}
|
||
}
|
||
|
||
function drainThumbQueue() {
|
||
if (activeThumbLoads >= MAX_CONCURRENT || thumbQueue.length === 0) return
|
||
activeThumbLoads++
|
||
const task = thumbQueue.shift()!
|
||
task().then(() => {
|
||
activeThumbLoads--
|
||
drainThumbQueue()
|
||
}).catch(() => {
|
||
activeThumbLoads--
|
||
drainThumbQueue()
|
||
})
|
||
}
|
||
|
||
function drainProfileQueue() {
|
||
if (activeProfileLoads >= MAX_PROFILE_CONCURRENT || profileQueue.length === 0) return
|
||
activeProfileLoads++
|
||
const task = profileQueue.shift()!
|
||
task().then(() => {
|
||
activeProfileLoads--
|
||
drainProfileQueue()
|
||
}).catch(() => {
|
||
activeProfileLoads--
|
||
drainProfileQueue()
|
||
})
|
||
}
|
||
|
||
function queueThumb(fn: () => Promise<void>) {
|
||
thumbQueue.push(fn)
|
||
drainThumbQueue()
|
||
}
|
||
|
||
function queueProfile(fn: () => Promise<void>) {
|
||
profileQueue.push(fn)
|
||
drainProfileQueue()
|
||
}
|
||
|
||
let _peopleLoading = false
|
||
let _filesLoading = false
|
||
let _faceCandidatesLoading = false
|
||
let _fileUuidForCandidates: string | undefined = undefined
|
||
let _unassignedTracesLoading = false
|
||
let _lastLoadedFileUuid: string | undefined = undefined
|
||
|
||
export async function ensureFiles() {
|
||
console.error('[ensureFiles] starting, filesLoaded.value:', filesLoaded.value)
|
||
if (filesLoaded.value) return
|
||
if (_filesLoading) {
|
||
await new Promise<void>(r => { const check = setInterval(() => { if (filesLoaded.value || !_filesLoading) { clearInterval(check); r() } }, 100) })
|
||
return
|
||
}
|
||
_filesLoading = true
|
||
try {
|
||
const result = await apiCall('get_files', { args: { pageSize: 500 } })
|
||
// API returns { success: true, total: number, data: File[] }
|
||
filesCache.value = Array.isArray(result?.data) ? result.data : (Array.isArray(result) ? result : [])
|
||
} catch (e) {
|
||
console.error('Failed to load files:', e)
|
||
} finally {
|
||
_filesLoading = false
|
||
filesLoaded.value = true
|
||
}
|
||
}
|
||
|
||
export async function ensurePeople(force = false) {
|
||
if (peopleLoaded.value && !force) return
|
||
if (_peopleLoading) {
|
||
await new Promise<void>(r => { const check = setInterval(() => { if (peopleLoaded.value || !_peopleLoading) { clearInterval(check); r() } }, 100) })
|
||
if (!force) return
|
||
}
|
||
_peopleLoading = true
|
||
try {
|
||
if (isTauri) {
|
||
const result: any = await apiCall('get_people', { page: 1, perPage: 100 })
|
||
peopleCache.value = Array.isArray(result) ? result : []
|
||
} else {
|
||
const allPeople: any[] = []
|
||
for (let page = 1; page <= 5; page++) {
|
||
const batch: any = await apiCall('get_people', { page, perPage: 20 })
|
||
const arr = Array.isArray(batch) ? batch : []
|
||
if (!arr.length) break
|
||
allPeople.push(...arr)
|
||
if (arr.length < 20) break
|
||
}
|
||
peopleCache.value = allPeople
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load people:', e)
|
||
} finally {
|
||
_peopleLoading = false
|
||
peopleLoaded.value = true
|
||
}
|
||
}
|
||
|
||
export async function ensureFaceCandidates(fileUuid: string) {
|
||
if (!fileUuid) return
|
||
if (faceCandidatesLoaded.value && _fileUuidForCandidates === fileUuid) return
|
||
if (_faceCandidatesLoading) {
|
||
await new Promise<void>(r => { const check = setInterval(() => { if (faceCandidatesLoaded.value || !_faceCandidatesLoading) { clearInterval(check); r() } }, 100) })
|
||
return
|
||
}
|
||
_faceCandidatesLoading = true
|
||
_fileUuidForCandidates = fileUuid
|
||
try {
|
||
if (isTauri) {
|
||
const args: any = { page: 1, perPage: 20, fileUuid }
|
||
const fc: any = await apiCall('get_face_candidates', args)
|
||
faceCandidatesCache.value = Array.isArray(fc) ? fc : []
|
||
} else {
|
||
const all: any[] = []
|
||
for (let page = 1; page <= 3; page++) {
|
||
const args: any = { page, perPage: 20, fileUuid }
|
||
const batch: any = await apiCall('get_face_candidates', args)
|
||
const arr = Array.isArray(batch) ? batch : []
|
||
if (!arr.length) break
|
||
all.push(...arr)
|
||
if (arr.length < 20) break
|
||
}
|
||
faceCandidatesCache.value = all
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load face candidates:', e)
|
||
} finally {
|
||
_faceCandidatesLoading = false
|
||
faceCandidatesLoaded.value = true
|
||
}
|
||
}
|
||
|
||
export async function ensureUnassignedTraces(fileUuid: string, page?: number) {
|
||
if (!fileUuid) return
|
||
const targetPage = page ?? unassignedTracesPage.value
|
||
if (unassignedTracesLoaded.value && _lastLoadedFileUuid === fileUuid && targetPage === unassignedTracesPage.value) return
|
||
if (_unassignedTracesLoading) {
|
||
await new Promise<void>(r => { const check = setInterval(() => { if (unassignedTracesLoaded.value || !_unassignedTracesLoading) { clearInterval(check); r() } }, 100) })
|
||
if (unassignedTracesLoaded.value && _lastLoadedFileUuid === fileUuid && targetPage === unassignedTracesPage.value) return
|
||
}
|
||
_unassignedTracesLoading = true
|
||
try {
|
||
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
|
||
unassignedTracesPage.value = targetPage
|
||
_lastLoadedFileUuid = fileUuid
|
||
} catch (e) {
|
||
console.error('Failed to load unassigned traces:', e)
|
||
} finally {
|
||
_unassignedTracesLoading = false
|
||
unassignedTracesLoaded.value = true
|
||
}
|
||
}
|
||
|
||
export function loadTraceThumb(t: any) {
|
||
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 {
|
||
// 改用自己的 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()
|
||
faceThumbsCache.value[key] = URL.createObjectURL(blob)
|
||
}
|
||
} catch (e) {
|
||
console.error('loadTraceThumb failed:', key, e)
|
||
} finally {
|
||
loadingFaceThumbs.delete(key)
|
||
}
|
||
})
|
||
}
|
||
|
||
export function loadThumbnail(uuid: string, frame = 30) {
|
||
const key = `${uuid}:${frame}`
|
||
if (!uuid || thumbnailsCache.value[key] || loadingThumbs.has(key)) return
|
||
loadingThumbs.add(key)
|
||
queueThumb(async () => {
|
||
try {
|
||
const result = await apiCall('get_thumbnail', { uuid, frame })
|
||
if (result) thumbnailsCache.value[key] = result
|
||
} catch (e) {
|
||
console.error('loadThumbnail failed:', uuid, frame, e)
|
||
} finally {
|
||
loadingThumbs.delete(key)
|
||
}
|
||
})
|
||
}
|
||
|
||
export function loadUnregisteredThumbnail(filePath: string, frame = 30) {
|
||
const key = `unreg:${filePath}:${frame}`
|
||
if (!filePath || thumbnailsCache.value[key] || loadingThumbs.has(key)) return
|
||
loadingThumbs.add(key)
|
||
queueThumb(async () => {
|
||
try {
|
||
const result = await apiCall('get_file_thumbnail_by_path', { path: filePath, frame })
|
||
if (result) thumbnailsCache.value[key] = result
|
||
} catch (e) {
|
||
console.error('loadUnregisteredThumbnail failed:', filePath, frame, e)
|
||
} finally {
|
||
loadingThumbs.delete(key)
|
||
}
|
||
})
|
||
}
|
||
|
||
export function loadProfile(uuid: string, forceReload = false) {
|
||
if (!uuid) { console.error('[loadProfile] called with empty uuid'); return }
|
||
if (!forceReload && profilesCache.value[uuid]) return
|
||
if (loadingProfiles.has(uuid)) return
|
||
loadingProfiles.add(uuid)
|
||
console.error('[loadProfile] requesting', uuid)
|
||
queueProfile(async () => {
|
||
try {
|
||
const result = await apiCall('get_identity_profile', { uuid })
|
||
console.error('[loadProfile] got result for', uuid, typeof result, result ? (result as string).substring(0, 30) : 'null')
|
||
if (result) profilesCache.value[uuid] = result
|
||
else console.error('[loadProfile] empty result for', uuid)
|
||
} catch (e) {
|
||
console.error('loadProfile failed:', uuid, e)
|
||
} finally {
|
||
loadingProfiles.delete(uuid)
|
||
}
|
||
})
|
||
}
|
||
|
||
export function loadFaceThumb(key: string, uuid: string, frame: number, bbox?: any) {
|
||
if (!uuid || faceThumbsCache.value[key] || loadingFaceThumbs.has(key)) return
|
||
loadingFaceThumbs.add(key)
|
||
queueThumb(async () => {
|
||
try {
|
||
const args: any = { uuid, frame }
|
||
if (bbox) {
|
||
args.bboxX = bbox.x
|
||
args.bboxY = bbox.y
|
||
args.bboxW = bbox.width
|
||
args.bboxH = bbox.height
|
||
}
|
||
const result = await apiCall('get_face_thumbnail', args)
|
||
if (result) faceThumbsCache.value[key] = result
|
||
} catch (e) {
|
||
console.error('loadFaceThumb failed:', key, uuid, e)
|
||
} finally {
|
||
loadingFaceThumbs.delete(key)
|
||
}
|
||
})
|
||
}
|
||
|
||
export async function loadProcessorCounts(fileUuid: string, force = false) {
|
||
if (!fileUuid) return
|
||
if (!force && processorCountsCache.value[fileUuid]) return
|
||
if (loadingProcessorCounts.has(fileUuid)) return
|
||
loadingProcessorCounts.add(fileUuid)
|
||
try {
|
||
const data: any = await apiCall('get_processor_counts', { uuid: fileUuid })
|
||
const counts: Record<string, ProcessorOutputInfo> = {}
|
||
for (const p of data.processors || []) {
|
||
counts[p.processor] = {
|
||
has_json: p.has_json ?? false,
|
||
frame_count: p.frame_count ?? null,
|
||
segment_count: p.segment_count ?? null,
|
||
chunk_count: p.chunk_count ?? null,
|
||
last_modified: p.last_modified ?? null,
|
||
}
|
||
}
|
||
processorCountsCache.value = { ...processorCountsCache.value, [fileUuid]: counts }
|
||
} catch (e) {
|
||
console.error('loadProcessorCounts failed:', fileUuid, e)
|
||
} finally {
|
||
loadingProcessorCounts.delete(fileUuid)
|
||
}
|
||
}
|
||
|
||
export function getProcessorCounts(fileUuid: string): Record<string, ProcessorOutputInfo> {
|
||
return processorCountsCache.value[fileUuid] || {}
|
||
}
|
||
|
||
export type ProcessingLevel = 'unprocessed' | 'partially_processed' | 'fully_processed'
|
||
|
||
export interface ProcessingStatus {
|
||
level: ProcessingLevel
|
||
hasTraces: boolean
|
||
hasIdentities: boolean
|
||
hasTranscription: boolean
|
||
hasOcr: boolean
|
||
hasFrameExtraction: boolean
|
||
details?: Record<string, ProcessorOutputInfo>
|
||
}
|
||
|
||
export async function getFileProcessingStatus(fileUuid: string): Promise<ProcessingStatus> {
|
||
try {
|
||
const traces: any = await apiCall('get_traces', { fileUuid, pageSize: 1 })
|
||
const hasTraces = traces?.total > 0
|
||
|
||
await loadProcessorCounts(fileUuid)
|
||
const outputs = getProcessorCounts(fileUuid)
|
||
|
||
const hasTranscription = outputs.transcription?.has_json || false
|
||
const hasOcr = outputs.ocr?.has_json || false
|
||
const hasFrameExtraction = outputs.frame_extraction?.frame_count > 0 || false
|
||
|
||
let identities: any = null
|
||
let hasIdentities = false
|
||
try {
|
||
identities = await apiCall('get_file_identities', { fileUuid, pageSize: 1 })
|
||
hasIdentities = identities?.total > 0
|
||
} catch (e) {
|
||
}
|
||
|
||
let level: ProcessingLevel
|
||
if (!hasTraces && !hasTranscription && !hasOcr) {
|
||
level = 'unprocessed'
|
||
} else if (hasTraces && hasIdentities && hasTranscription && hasOcr && hasFrameExtraction) {
|
||
level = 'fully_processed'
|
||
} else {
|
||
level = 'partially_processed'
|
||
}
|
||
|
||
return {
|
||
level,
|
||
hasTraces,
|
||
hasIdentities,
|
||
hasTranscription,
|
||
hasOcr,
|
||
hasFrameExtraction,
|
||
details: outputs
|
||
}
|
||
} catch (e) {
|
||
console.error('getFileProcessingStatus failed:', fileUuid, e)
|
||
return {
|
||
level: 'unprocessed',
|
||
hasTraces: false,
|
||
hasIdentities: false,
|
||
hasTranscription: false,
|
||
hasOcr: false,
|
||
hasFrameExtraction: false
|
||
}
|
||
}
|
||
}
|
||
|
||
export async function pollProgress(fileUuid: string) {
|
||
if (!fileUuid || pollingProgress.has(fileUuid)) return
|
||
pollingProgress.add(fileUuid)
|
||
try {
|
||
while (pollingProgress.has(fileUuid)) {
|
||
const data: any = await apiCall('get_progress', { fileUuid })
|
||
fileProgressCache.value[fileUuid] = {
|
||
file_uuid: data.file_uuid || fileUuid,
|
||
overall_progress: data.overall_progress || 0,
|
||
processors: (data.processors || []).map((p: any) => ({
|
||
processor_type: p.processor_type,
|
||
status: p.status,
|
||
progress: p.progress || 0,
|
||
current: p.current,
|
||
total: p.total,
|
||
eta_seconds: p.eta_seconds,
|
||
})),
|
||
cpu_percent: data.cpu_percent,
|
||
gpu_percent: data.gpu_percent,
|
||
memory_percent: data.memory_percent,
|
||
}
|
||
// 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)
|
||
saveProgressCacheToStorage()
|
||
await loadProcessorCounts(fileUuid)
|
||
break
|
||
}
|
||
await new Promise(r => setTimeout(r, 2000))
|
||
}
|
||
} catch (e) {
|
||
console.error('pollProgress failed:', fileUuid, e)
|
||
} finally {
|
||
pollingProgress.delete(fileUuid)
|
||
}
|
||
}
|
||
|
||
export function stopPolling(fileUuid: string) {
|
||
pollingProgress.delete(fileUuid)
|
||
}
|
||
|
||
export function getProgress(fileUuid: string): FileProgress | null {
|
||
return fileProgressCache.value[fileUuid] || null
|
||
}
|
||
|
||
export function getFileStatus(fileUuid: string): string {
|
||
return fileStatusCache.value[fileUuid] || ''
|
||
}
|
||
|
||
export function clearFileStatusCache(fileUuid: string) {
|
||
delete fileStatusCache.value[fileUuid]
|
||
saveProgressCacheToStorage()
|
||
}
|
||
|
||
export async function refreshFileStatus(fileUuid: string) {
|
||
try {
|
||
const pipeline = await loadPipelineStats(fileUuid, true)
|
||
if (pipeline) {
|
||
// No local status promotion - status comes from Core API only
|
||
await loadFileStats(fileUuid, true)
|
||
}
|
||
} catch (e) {
|
||
console.error('refreshFileStatus failed:', fileUuid, e)
|
||
}
|
||
}
|
||
|
||
export async function refreshAllFilesStatus() {
|
||
const tasks = filesCache.value
|
||
.filter((f: any) => f.isRegistered && f.file_uuid)
|
||
.map((f: any) => refreshFileStatus(f.file_uuid))
|
||
await Promise.all(tasks)
|
||
}
|
||
|
||
export async function syncFileStatus(fileUuid: string): Promise<any> {
|
||
try {
|
||
const result = await apiCall('sync_file_status', { fileUuid })
|
||
|
||
if (result?.status) {
|
||
const newStatus = result.status
|
||
const oldStatus = fileStatusCache.value[fileUuid]
|
||
|
||
fileStatusCache.value[fileUuid] = newStatus
|
||
|
||
const f = filesCache.value.find((x: any) => x.file_uuid === fileUuid)
|
||
if (f) {
|
||
// If status changed from processing to completed, clear pipeline cache
|
||
if (oldStatus === 'processing' && newStatus === 'completed') {
|
||
delete pipelineStatsCache.value[fileUuid]
|
||
delete fileProgressCache.value[fileUuid]
|
||
}
|
||
f.status = newStatus
|
||
|
||
if (newStatus === 'processing') {
|
||
await loadPipelineStats(fileUuid, true)
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
} catch (e) {
|
||
console.error('syncFileStatus failed:', fileUuid, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function syncAllProcessingFiles(): Promise<void> {
|
||
const processingFiles = filesCache.value.filter((f: any) => f.status === 'processing' && f.file_uuid)
|
||
for (const f of processingFiles) {
|
||
await syncFileStatus(f.file_uuid)
|
||
}
|
||
}
|
||
|
||
export async function loadPipelineStats(fileUuid: string, force = false): Promise<PipelineStats | null> {
|
||
if (!fileUuid) return null
|
||
if (!force && pipelineStatsCache.value[fileUuid]) return pipelineStatsCache.value[fileUuid]
|
||
if (loadingPipelineStats.has(fileUuid)) return null
|
||
|
||
loadingPipelineStats.add(fileUuid)
|
||
try {
|
||
const data: any = await apiCall('get_pipeline_stats', { fileUuid })
|
||
|
||
const stages = data.stages || []
|
||
|
||
const WEIGHT_MAP: Record<string, number> = {
|
||
processors: 0.30,
|
||
rule1_ingestion: 0.05,
|
||
face_tracing: 0.05,
|
||
face_dedup: 0.10,
|
||
tkg_nodes: 0.20,
|
||
tkg_edges: 0.15,
|
||
rule2_ingestion: 0.10,
|
||
vlm_summary: 0.03,
|
||
scene_vlm: 0.03,
|
||
moment_chunks: 0.01
|
||
}
|
||
|
||
const mappedStages = stages.map((s: any) => ({
|
||
name: s.name,
|
||
weight: WEIGHT_MAP[s.name] ?? s.weight ?? 0,
|
||
progress: s.progress || 0,
|
||
status: s.status || 'pending'
|
||
}))
|
||
|
||
const calculatedProgress = mappedStages.reduce((sum, s) => sum + s.weight * s.progress, 0)
|
||
|
||
const pipelineStats: PipelineStats = {
|
||
overall_progress: calculatedProgress,
|
||
stages: mappedStages
|
||
}
|
||
|
||
console.log('[loadPipelineStats]', fileUuid, pipelineStats)
|
||
|
||
pipelineStatsCache.value = { ...pipelineStatsCache.value, [fileUuid]: pipelineStats }
|
||
|
||
// No local status promotion - status comes from Core API only
|
||
|
||
return pipelineStats
|
||
} catch (e) {
|
||
console.warn('get_pipeline_stats failed, falling back to get_progress:', fileUuid, e)
|
||
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')
|
||
|
||
const progress = allComplete ? 1.0 : (oldData.overall_progress || 0) / 100
|
||
const status = allComplete ? 'completed' : (hasRunning ? 'running' : 'pending')
|
||
|
||
console.log('[loadPipelineStats fallback]', fileUuid, { allComplete, hasRunning, progress, status, processorCount: oldData.processors?.length })
|
||
|
||
const fallbackStats: PipelineStats = {
|
||
overall_progress: progress,
|
||
stages: [
|
||
{ name: 'processors', weight: 1.0, progress, status }
|
||
]
|
||
}
|
||
pipelineStatsCache.value = { ...pipelineStatsCache.value, [fileUuid]: fallbackStats }
|
||
return fallbackStats
|
||
} catch (fallbackError) {
|
||
console.error('Both pipeline APIs failed:', fileUuid, fallbackError)
|
||
return null
|
||
}
|
||
} finally {
|
||
loadingPipelineStats.delete(fileUuid)
|
||
}
|
||
}
|
||
|
||
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]
|
||
if (loadingFileStats.has(fileUuid)) return null
|
||
|
||
loadingFileStats.add(fileUuid)
|
||
try {
|
||
const data: any = await apiCall('get_file_stats', { fileUuid })
|
||
|
||
// 转换新API格式为前端格式
|
||
const processors: Record<string, JsonProcessorInfo> = {}
|
||
for (const p of data.processors || []) {
|
||
const name = p.name.toLowerCase()
|
||
processors[name] = {
|
||
status: p.status,
|
||
segment_count: p.name === 'ASR' || p.name === 'ASRX' ? null : null,
|
||
frame_count: p.name === 'FACE' || p.name === 'OCR' || p.name === 'POSE' || p.name === 'APPEARANCE' || p.name === 'CUT' ? null : null,
|
||
chunk_count: null
|
||
}
|
||
}
|
||
|
||
const fileStats: FileStats = {
|
||
json: {
|
||
file_name: data.file_name || '',
|
||
status: data.status || '',
|
||
processors
|
||
},
|
||
postgresql: {
|
||
sentence_chunks: data.postgres?.sentence_chunks || 0,
|
||
trace_chunks: data.postgres?.trace_chunks || 0,
|
||
relationship_chunks: data.postgres?.relationship_chunks || 0,
|
||
identities: data.postgres?.identities || 0,
|
||
file_identities: data.postgres?.file_identities || 0
|
||
},
|
||
qdrant: {
|
||
faces: data.qdrant?.faces || 0,
|
||
face_traces: data.qdrant?.face_traces || 0,
|
||
face_identities: data.qdrant?.face_identities || 0,
|
||
text_chunks: data.qdrant?.text_chunks || 0,
|
||
speakers: data.qdrant?.speakers || 0
|
||
},
|
||
tkg: {
|
||
nodes: {
|
||
character: data.tkg?.face_track_nodes || 0,
|
||
object: data.tkg?.object_nodes || 0,
|
||
scene: 0,
|
||
action: 0,
|
||
location: 0,
|
||
event: 0,
|
||
dialogue: data.tkg?.text_region_nodes || 0,
|
||
emotion: 0,
|
||
relation: 0
|
||
},
|
||
edges: {
|
||
appear_in: data.tkg?.face_face_edges || 0,
|
||
interact_with: data.tkg?.co_occurrence_edges || 0,
|
||
located_at: 0,
|
||
perform_action: 0,
|
||
cause_effect: 0,
|
||
temporal_link: 0,
|
||
speak_to: data.tkg?.speaker_face_edges || 0,
|
||
feel: 0
|
||
},
|
||
total_nodes: data.tkg?.total_nodes || 0,
|
||
total_edges: data.tkg?.total_edges || 0
|
||
},
|
||
identity_agent: {
|
||
clusters: data.identity_agent?.clusters || 0,
|
||
identities_created: data.identity_agent?.identities_created || 0,
|
||
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
|
||
}
|
||
}
|
||
|
||
fileStatsCache.value = { ...fileStatsCache.value, [fileUuid]: fileStats }
|
||
|
||
if (data.status === 'completed') {
|
||
pipelineStatsCache.value[fileUuid] = {
|
||
overall_progress: 1.0,
|
||
stages: [{ name: 'processors', weight: 1.0, progress: 1.0, status: 'completed' }]
|
||
}
|
||
}
|
||
|
||
return fileStats
|
||
} catch (e) {
|
||
console.warn('get_file_stats failed, falling back to get_processor_counts:', fileUuid, e)
|
||
try {
|
||
// 降级到processor-counts API
|
||
await loadProcessorCounts(fileUuid, true)
|
||
const procCounts = getProcessorCounts(fileUuid)
|
||
|
||
// 构建降级版FileStats
|
||
const fallbackStats: FileStats = {
|
||
json: {
|
||
file_name: '',
|
||
status: 'completed',
|
||
processors: {}
|
||
},
|
||
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 },
|
||
vlm_summary: { total: 0, done: 0 },
|
||
scene_vlm: { total_scenes: 0, status: '', vlm_done: 0, vlm_total: 0 }
|
||
}
|
||
|
||
// 从processorCounts填充json.processors
|
||
for (const [proc, info] of Object.entries(procCounts)) {
|
||
fallbackStats.json.processors[proc] = {
|
||
status: info.has_json ? 'completed' : 'pending',
|
||
segment_count: info.segment_count,
|
||
frame_count: info.frame_count,
|
||
chunk_count: info.chunk_count
|
||
}
|
||
}
|
||
|
||
fileStatsCache.value = { ...fileStatsCache.value, [fileUuid]: fallbackStats }
|
||
return fallbackStats
|
||
} catch (fallbackError) {
|
||
console.error('Both file stats APIs failed:', fileUuid, fallbackError)
|
||
return null
|
||
}
|
||
} finally {
|
||
loadingFileStats.delete(fileUuid)
|
||
}
|
||
}
|
||
|
||
export function getPipelineStats(fileUuid: string): PipelineStats | null {
|
||
return pipelineStatsCache.value[fileUuid] || null
|
||
}
|
||
|
||
export function getFileStats(fileUuid: string): FileStats | null {
|
||
return fileStatsCache.value[fileUuid] || null
|
||
}
|
||
|
||
export async function pollPipelineProgress(fileUuid: string) {
|
||
if (!fileUuid || pollingProgress.has(fileUuid)) return
|
||
pollingProgress.add(fileUuid)
|
||
try {
|
||
while (pollingProgress.has(fileUuid)) {
|
||
const stats = await loadPipelineStats(fileUuid, true)
|
||
if (stats) {
|
||
const idx = filesCache.value.findIndex((x: any) => x.file_uuid === fileUuid)
|
||
if (idx === -1) continue
|
||
|
||
// 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)
|
||
saveProgressCacheToStorage()
|
||
await loadFileStats(fileUuid, true)
|
||
break
|
||
}
|
||
}
|
||
await new Promise(r => setTimeout(r, 2000))
|
||
}
|
||
} catch (e) {
|
||
console.error('pollPipelineProgress failed:', fileUuid, e)
|
||
} finally {
|
||
pollingProgress.delete(fileUuid)
|
||
}
|
||
}
|
||
|
||
export function invalidateFiles() {
|
||
filesLoaded.value = false
|
||
filesCache.value = []
|
||
}
|
||
|
||
export function invalidatePeople() {
|
||
peopleLoaded.value = false
|
||
peopleCache.value = []
|
||
faceCandidatesLoaded.value = false
|
||
faceCandidatesCache.value = []
|
||
}
|
||
|
||
export function invalidateProfile(uuid: string) {
|
||
delete profilesCache.value[uuid]
|
||
loadingProfiles.delete(uuid)
|
||
}
|
||
|
||
export async function getPose(fileUuid: string, frame: number, bbox?: { x: number; y: number; width: number; height: number } | null): Promise<PoseResponse | null> {
|
||
const key = bbox ? `${fileUuid}:${frame}:${bbox.x}:${bbox.y}` : `${fileUuid}:${frame}`
|
||
if (poseCache.value[key]) return poseCache.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_pose', args)
|
||
if (data) poseCache.value[key] = data
|
||
return data
|
||
} catch (e) {
|
||
console.error('getPose failed:', fileUuid, frame, e)
|
||
return null
|
||
}
|
||
}
|
||
|
||
export async function getAppearance(fileUuid: string, frame: number, bbox?: { x: number; y: number; width: number; height: number } | null): Promise<AppearanceResponse | null> {
|
||
const key = bbox ? `${fileUuid}:${frame}:${bbox.x}:${bbox.y}` : `${fileUuid}:${frame}`
|
||
if (appearanceCache.value[key]) return appearanceCache.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_appearance', args)
|
||
if (data) appearanceCache.value[key] = data
|
||
return data
|
||
} catch (e) {
|
||
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
|
||
}
|
||
} |