Files
momentry_studio/src/store.ts
Momentry Studio af60ea118a feat: 添加 Scene VLM 阶段到 Pipeline Progress 显示
问题:
- 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)
✅ 阶段显示名称正确
2026-10-10 14:57:22 +08:00

1333 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}