Files
momentry_studio/src/api/index.ts

899 lines
33 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 { invoke } from '@tauri-apps/api/core'
import { isTauri, getApiBase } from './config'
// Detect base URL: use current origin when remote, fallback to localStorage or localhost
function detectBaseUrl(): string {
const hostname = typeof window !== 'undefined' ? window.location.hostname : ''
const isLocal = hostname === 'localhost' || hostname === '127.0.0.1'
if (!isLocal && typeof window !== 'undefined') {
return window.location.origin
}
return localStorage.getItem('markbase_url') || 'http://localhost:11438'
}
// API proxy: Tauri IPC or HTTP fetch
export async function apiCall(cmd: string, args: Record<string, any>): Promise<any> {
if (cmd === 'get_video_stream') {
const base = isTauri ? 'http://localhost:8888' : getApiBase()
const { url } = buildHttpRequest(cmd, args)
return `${base}${url}`
}
if (cmd === 'upload_profile_image') {
if (isTauri) {
return invoke('upload_profile_image', { uuid: args.uuid, filePath: args.filePath })
}
const base = getApiBase()
const { url } = buildHttpRequest(cmd, args)
const formData = new FormData()
formData.append('image', args.file)
const fullUrl = `${base}${url}`
const response = await fetch(fullUrl, { method: 'POST', body: formData })
if (!response.ok) throw new Error(`Upload failed: ${response.status}`)
return response.json()
}
if (!isTauri && (cmd === 'update_identity_starred' || cmd === 'update_identity_status')) {
const current: any = await httpCall('get_identity', { uuid: args.uuid })
const metadata = { ...(current.metadata || {}), ...(current.metadataJson ? JSON.parse(current.metadataJson) : {}) }
if (cmd === 'update_identity_starred') {
metadata.starred = args.starred
} else {
metadata.status = args.status
}
return httpCall('update_identity', { uuid: args.uuid, metadataJson: JSON.stringify(metadata) })
}
// run_identity_agent 沒有 Tauri command直接走 HTTP 避免 invoke error
if (cmd === 'run_identity_agent') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// run_identity_for_seed 也沒有 Tauri command
if (cmd === 'run_identity_for_seed') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// run_cluster_agent 也沒有 Tauri command
if (cmd === 'run_cluster_agent') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// get_cluster_results 也沒有 Tauri command
if (cmd === 'get_cluster_results') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// create_pending_identity 也沒有 Tauri command
if (cmd === 'create_pending_identity') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// bind_speakers 也沒有 Tauri command
if (cmd === 'bind_speakers') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
// get_processor_json 也沒有 Tauri command
if (cmd === 'get_processor_json') {
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
if (isTauri) {
try {
const data = await invoke(cmd, args)
return transformResponse(cmd, data)
} catch (e: any) {
console.error('[apiCall] invoke error:', typeof e, e)
const errMsg = typeof e === 'string' ? e : (e?.message || String(e))
console.error('[apiCall] falling back to HTTP for', cmd, '- error:', errMsg)
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
}
const data = await httpCall(cmd, args)
return transformResponse(cmd, data)
}
const DEFAULT_TIMEOUT = 30000
export enum ApiErrorType {
NETWORK = 'network',
TIMEOUT = 'timeout',
SERVER = 'server',
VALIDATION = 'validation',
NOT_FOUND = 'not_found',
}
export class ApiError extends Error {
type: ApiErrorType
status?: number
constructor(type: ApiErrorType, message: string, status?: number) {
super(message)
this.type = type
this.status = status
}
}
async function httpCall(cmd: string, args: Record<string, any>, retries = 3): Promise<any> {
const base = getApiBase()
const { url, method, body } = buildHttpRequest(cmd, args)
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? url : `${base}${url}`
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT)
let response: Response | null = null
try {
for (let i = 0; i < retries; i++) {
try {
response = await fetch(fullUrl, {
method,
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
})
if (response.ok) break
if (response.status >= 500 && i < retries - 1) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)))
response = null
continue
}
break
} catch (e: any) {
if (e.name === 'AbortError') {
throw new ApiError(ApiErrorType.TIMEOUT, `Request timeout: ${cmd}`)
}
if (i < retries - 1) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)))
continue
}
throw new ApiError(ApiErrorType.NETWORK, `Network error: ${e.message}`)
}
}
} finally {
clearTimeout(timeout)
}
if (!response) throw new ApiError(ApiErrorType.NETWORK, 'No response')
if (!response.ok) {
const status = response.status
const errorText = await response.text().catch(() => 'Unknown error')
if (status === 404) {
throw new ApiError(ApiErrorType.NOT_FOUND, errorText, status)
}
if (status >= 400 && status < 500) {
throw new ApiError(ApiErrorType.VALIDATION, errorText, status)
}
throw new ApiError(ApiErrorType.SERVER, errorText, status)
}
if (response.status === 204) {
return { success: true }
}
const contentType = response.headers.get('content-type') || ''
if (contentType.includes('image/') || contentType.includes('video/') || contentType.includes('octet-stream')) {
const buffer = await response.arrayBuffer()
const bytes = new Uint8Array(buffer)
if (cmd === 'get_identity_profile' || cmd === 'get_thumbnail' || cmd === 'get_face_thumbnail' || cmd === 'get_file_thumbnail_by_path') {
const ext = contentType.includes('png') ? 'png' : 'jpeg'
const blob = new Blob([bytes], { type: `image/${ext}` })
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject(new Error('FileReader failed'))
reader.readAsDataURL(blob)
})
}
const chunks: string[] = []
const chunkSize = 8192
for (let i = 0; i < bytes.length; i += chunkSize) {
const slice = bytes.subarray(i, Math.min(i + chunkSize, bytes.length))
chunks.push(String.fromCharCode(...slice))
}
return btoa(chunks.join(''))
}
const text = await response.text()
try {
return JSON.parse(text)
} catch {
return text
}
}
function buildHttpRequest(cmd: string, args: Record<string, any>): { url: string; method: string; body?: any } {
const a = args
switch (cmd) {
// --- Data APIs ---
case 'get_files': {
const ps = a.args?.pageSize || 500
return { url: `/api/v1/files/scan?page_size=${ps}`, method: 'GET' }
}
case 'get_people': {
return { url: `/api/v1/identities?page=${a.page || 1}&per_page=${a.perPage || 100}`, method: 'GET' }
}
case 'get_file_identities': {
return { url: `/api/v1/file/${a.fileUuid}/identities?page=${a.page || 1}&page_size=${a.pageSize || 50}`, method: 'GET' }
}
case 'get_identity_files': {
return { url: `/api/v1/identity/${a.uuid}/files?page=${a.page || 1}&page_size=${a.pageSize || 20}`, method: 'GET' }
}
case 'get_faces': {
return { url: `/api/v1/identity/${a.uuid}/faces?page_size=${a.perPage || 100}`, method: 'GET' }
}
case 'get_traces': {
let url = `/api/v1/identity/${a.uuid}/traces?page_size=${a.perPage || 50}`
if (a.page) url += `&page=${a.page}`
return { url, method: 'GET' }
}
case 'get_face_candidates': {
let url = `/api/v1/faces/candidates?page=${a.page || 1}&page_size=${a.perPage || 100}`
if (a.fileUuid) url += `&file_uuid=${a.fileUuid}`
return { url, method: 'GET' }
}
case 'get_unassigned_traces': {
let url = `/api/v1/traces/unassigned?page=${a.page || 1}&page_size=${a.perPage || 20}`
if (a.fileUuid) url += `&file_uuid=${a.fileUuid}`
return { url, method: 'GET' }
}
case 'get_pending_persons': {
return { url: `/api/v1/file/${a.fileUuid}/pending-persons`, method: 'GET' }
}
case 'get_identity': {
return { url: `/api/v1/identity/${a.uuid}`, method: 'GET' }
}
// --- Search APIs ---
case 'search_keyword': {
return { url: '/api/v1/search/keyword', method: 'POST', body: { query: a.query, limit: a.limit || 20 } }
}
case 'search_semantic': {
return { url: '/api/v1/search/semantic', method: 'POST', body: { query: a.query, limit: a.limit || 20 } }
}
case 'search_llm_smart': {
return { url: '/api/v1/search/llm-smart', method: 'POST', body: { query: a.query, limit: a.limit || 20 } }
}
case 'search_people': {
const params = new URLSearchParams()
if (a.query) params.set('query', a.query)
if (a.fileUuid) params.set('file_uuid', a.fileUuid)
if (a.limit) params.set('limit', String(a.limit))
return { url: `/api/v1/search/people?${params.toString()}`, method: 'GET' }
}
case 'search_agents': {
const body: any = { query: a.query }
if (a.conversationId) body.conversation_id = a.conversationId
return { url: '/api/v1/agents/search', method: 'POST', body }
}
case 'search_identities': {
return { url: `/api/v1/identities/search?q=${encodeURIComponent(a.query)}&limit=${a.limit || 50}`, method: 'GET' }
}
case 'search_people': {
const params = new URLSearchParams()
if (a.query) params.set('query', a.query)
if (a.fileUuid) params.set('file_uuid', a.fileUuid)
if (a.limit) params.set('limit', String(a.limit))
return { url: `/api/v1/search/people?${params.toString()}`, method: 'GET' }
}
// --- Image APIs ---
case 'get_thumbnail': {
return { url: `/api/v1/file/${a.uuid}/thumbnail?frame=${a.frame || 30}`, method: 'GET' }
}
case 'get_identity_profile': {
return { url: `/api/v1/identity/${a.uuid}/profile`, method: 'GET' }
}
case 'get_face_thumbnail': {
let url = `/api/v1/face-thumbnail?uuid=${a.uuid}&frame=${a.frame || 0}`
if (a.bboxX != null) url += `&bbox_x=${a.bboxX}`
if (a.bboxY != null) url += `&bbox_y=${a.bboxY}`
if (a.bboxW != null) url += `&bbox_w=${a.bboxW}`
if (a.bboxH != null) url += `&bbox_h=${a.bboxH}`
return { url, method: 'GET' }
}
case 'get_file_thumbnail_by_path': {
return { url: `/api/v1/file/thumbnail?path=${encodeURIComponent(a.path)}&frame=${a.frame || 30}`, method: 'GET' }
}
// --- Video API ---
case 'get_video_stream': {
let url = `/api/v1/file/${a.uuid}/video`
const params: string[] = []
if (a.startFrame != null) params.push(`start_frame=${a.startFrame}`)
if (a.endFrame != null) params.push(`end_frame=${a.endFrame}`)
if (a.original === true) params.push('original=true')
if (params.length) url += '?' + params.join('&')
return { url, method: 'GET' }
}
// --- Identity Management ---
case 'update_identity': {
const meta: any = a.metadataJson ? JSON.parse(a.metadataJson) : {}
const body: any = {}
if (a.name) body.name = a.name
if (Object.keys(meta).length) body.metadata = meta
return { url: `/api/v1/identity/${a.uuid}`, method: 'PATCH', body }
}
case 'update_identity_name': {
return { url: `/api/v1/identity/${a.uuid}`, method: 'PATCH', body: { name: a.name } }
}
case 'update_identity_status': {
return { url: `/api/v1/identity/${a.uuid}`, method: 'PATCH', body: { metadata: { status: a.status } } }
}
case 'update_identity_starred': {
return { url: `/api/v1/identity/${a.uuid}`, method: 'PATCH', body: { metadata: { starred: a.starred } } }
}
case 'upload_profile_image': {
return { url: `/api/v1/identity/${a.uuid}/profile-image`, method: 'POST' }
}
case 'set_profile_from_face': {
const body: any = { file_uuid: a.fileUuid }
if (a.faceId) body.face_id = a.faceId
if (a.id) body.id = a.id
if (a.traceId) body.trace_id = a.traceId
if (a.frameNumber) body.frame_number = a.frameNumber
return { url: `/api/v1/identity/${a.uuid}/profile-image/from-face`, method: 'POST', body }
}
case 'delete_identity': {
return { url: `/api/v1/identity/${a.uuid}`, method: 'DELETE' }
}
case 'create_identity_from_face': {
const body: any = { name: a.name }
if (a.traceIds) body.trace_ids = a.traceIds
return { url: `/api/v1/file/${a.fileUuid}/pending-person`, method: 'POST', body }
}
// --- Identity Operations ---
case 'merge_identities': {
return { url: `/api/v1/identity/${a.uuid}/mergeinto`, method: 'POST', body: { into_uuid: a.intoUuid } }
}
// --- Trace Management ---
case 'list_traces': {
return { url: `/api/v1/file/${a.file_uuid}/traces`, method: 'POST' }
}
case 'get_trace_profile': {
const params = new URLSearchParams()
if (a.file_uuid) params.set('file_uuid', a.file_uuid)
if (a.trace_id) params.set('trace_id', String(a.trace_id))
return { url: `/api/v1/trace-profile?${params.toString()}`, method: 'GET' }
}
case 'update_trace_profile': {
return { url: '/api/v1/trace-profile', method: 'PUT', body: a.body }
}
case 'delete_trace': {
const body: any = { hard_delete: a.hard_delete ?? true }
return { url: `/api/v1/file/${a.file_uuid}/trace/${a.trace_id}`, method: 'DELETE', body }
}
case 'restore_trace': {
return { url: `/api/v1/file/${a.file_uuid}/trace/${a.trace_id}/restore`, method: 'POST' }
}
case 'merge_trace': {
return { url: `/api/v1/file/${a.file_uuid}/trace/${a.source_id}/merge/${a.target_id}`, method: 'POST' }
}
case 'merge_groups': {
return {
url: `/api/v1/file/${a.fileUuid}/groups/merge`,
method: 'POST',
body: {
file_uuid: a.fileUuid,
source_groups: a.sourceGroups,
target_group_name: a.targetGroupName
}
}
}
case 'bind_face': {
const bindBody: any = { file_uuid: a.fileUuid }
if (a.faceId) bindBody.face_id = a.faceId
if (a.faceRowId) bindBody.id = a.faceRowId
return { url: `/api/v1/identity/${a.uuid}/bind`, method: 'POST', body: bindBody }
}
case 'unbind_face': {
const unbindBody: any = { file_uuid: a.fileUuid }
if (a.faceId) unbindBody.face_id = a.faceId
if (a.faceRowId) unbindBody.id = a.faceRowId
if (a.frameNumber != null) unbindBody.frame_number = a.frameNumber
return { url: `/api/v1/identity/${a.uuid}/unbind`, method: 'POST', body: unbindBody }
}
// --- Trace Operations ---
case 'merge_traces': {
return { url: `/api/v1/file/${a.file_uuid}/trace/${a.source_trace_id}/merge/${a.target_trace_id}`, method: 'POST' }
}
// --- Identity Undo/Redo ---
case 'identity_undo': {
const body: any = {}
if (a.steps != null) body.steps = a.steps
return { url: `/api/v1/identity/${a.uuid}/undo`, method: 'POST', body }
}
case 'identity_redo': {
const body: any = {}
if (a.steps != null) body.steps = a.steps
return { url: `/api/v1/identity/${a.uuid}/redo`, method: 'POST', body }
}
case 'identity_history': {
let url = `/api/v1/identity/${a.uuid}/history`
const params: string[] = []
if (a.page != null) params.push(`page=${a.page}`)
if (a.pageSize != null) params.push(`page_size=${a.pageSize}`)
if (params.length) url += '?' + params.join('&')
return { url, method: 'GET' }
}
case 'identity_bind_undo': {
const body: any = {}
if (a.steps != null) body.steps = a.steps
return { url: `/api/v1/identity/${a.uuid}/bind/undo`, method: 'POST', body }
}
case 'identity_bind_redo': {
const body: any = {}
if (a.steps != null) body.steps = a.steps
return { url: `/api/v1/identity/${a.uuid}/bind/redo`, method: 'POST', body }
}
case 'identity_bind_history': {
let url = `/api/v1/identity/${a.uuid}/bind/history`
const params: string[] = []
if (a.page != null) params.push(`page=${a.page}`)
if (a.pageSize != null) params.push(`page_size=${a.pageSize}`)
if (params.length) url += '?' + params.join('&')
return { url, method: 'GET' }
}
case 'merge_undo': {
return { url: `/api/v1/identity/merge/${a.mergeId}/undo`, method: 'POST' }
}
case 'merge_redo': {
return { url: `/api/v1/identity/merge/${a.mergeId}/redo`, method: 'POST' }
}
case 'merge_history': {
let url = '/api/v1/identity/merge/history'
const params: string[] = []
if (a.sourceUuid) params.push(`source_uuid=${encodeURIComponent(a.sourceUuid)}`)
if (a.targetUuid) params.push(`target_uuid=${encodeURIComponent(a.targetUuid)}`)
if (a.page != null) params.push(`page=${a.page}`)
if (a.pageSize != null) params.push(`page_size=${a.pageSize}`)
if (params.length) url += '?' + params.join('&')
return { url, method: 'GET' }
}
// --- File Operations ---
case 'get_health': {
return { url: '/api/v1/health', method: 'GET' }
}
case 'list_jobs': {
return { url: '/api/v1/jobs', method: 'POST', body: {} }
}
case 'register_file': {
return { url: '/api/v1/files/register', method: 'POST', body: { file_path: a.filePath } }
}
case 'process_file': {
return { url: `/api/v1/file/${a.fileUuid}/process`, method: 'POST', body: { processors: a.processors } }
}
case 'run_identity_agent': {
return { url: `/api/v1/file/${a.fileUuid}/identity-agent`, method: 'POST' }
}
case 'run_identity_for_seed': {
return { url: '/api/v1/agents/identity/run-for-seed', method: 'POST', body: { file_uuid: a.fileUuid, identity_uuid: a.identityUuid } }
}
case 'get_identity_matches': {
return { url: `/api/v1/identity-matches?file_hash=${a.fileHash}`, method: 'GET' }
}
case 'run_cluster_agent': {
return { url: `/api/v1/file/${a.fileUuid}/cluster-agent`, method: 'POST' }
}
case 'get_cluster_results': {
return { url: `/api/v1/cluster-results?file_hash=${a.fileHash}`, method: 'GET' }
}
case 'update_trace_profile_group': {
return { url: '/api/v1/trace-profile/group', method: 'PUT', body: { file_uuid: a.fileUuid, trace_ids: a.traceIds, name: a.name, key_frame: a.keyFrame, key_face: a.keyFace } }
}
case 'get_file_profile': {
return { url: `/api/v1/file-profile?file_uuid=${a.fileUuid}`, method: 'GET' }
}
case 'update_file_profile': {
return { url: '/api/v1/file-profile', method: 'PUT', body: { file_uuid: a.fileUuid, file_path: a.filePath, file_name: a.fileName } }
}
case 'get_processor_json': {
return { url: `/api/v1/processor-json?file_hash=${a.fileHash}&processor=${a.processor}`, method: 'GET' }
}
case 'create_pending_identity': {
return { url: '/api/v1/identities/pending', method: 'POST', body: { file_uuid: a.fileUuid, trace_id: a.traceId, face_id: a.faceId, cluster_id: a.clusterId, trace_count: a.traceCount } }
}
case 'bind_speakers': {
return { url: `/api/v1/file/${a.fileUuid}/bind-speakers`, method: 'POST' }
}
case 'unregister_file': {
const body: any = { file_uuid: a.fileUuid }
if (a.deleteOutputFiles != null) body.delete_output_files = a.deleteOutputFiles
return { url: '/api/v1/unregister', method: 'POST', body }
}
case 'ingest_file': {
return { url: `/api/v1/file/${a.fileUuid}/checkin`, method: 'POST' }
}
case 'checkout_file': {
return { url: `/api/v1/file/${a.fileUuid}/checkout`, method: 'POST' }
}
// --- File Detail ---
case 'get_file_info': {
return { url: `/api/v1/file/${a.uuid}`, method: 'GET' }
}
case 'get_processor_counts': {
return { url: `/api/v1/file/${a.uuid}/processor-counts`, method: 'GET' }
}
case 'get_progress': {
return { url: `/api/v1/progress/${a.fileUuid}`, method: 'POST', body: {} }
}
case 'get_pipeline_stats': {
return { url: `/api/v1/stats/pipeline/${a.fileUuid}`, method: 'GET' }
}
case 'get_file_stats': {
return { url: `/api/v1/stats/file/${a.fileUuid}`, method: 'GET' }
}
// --- Search History ---
case 'get_search_history': {
const limit = a.limit ?? 30
return { url: `/api/v1/search-history?limit=${limit}`, method: 'GET' }
}
case 'save_search_history': {
return { url: '/api/v1/search-history', method: 'POST', body: { id: a.id, query: a.query, title: a.title, chat_state: a.chatState, mode: a.mode } }
}
case 'rename_search_history': {
return { url: `/api/v1/search-history/${a.id}/rename`, method: 'PATCH', body: { title: a.title } }
}
case 'pin_search_history': {
return { url: `/api/v1/search-history/${a.id}/pin`, method: 'PATCH', body: { pinned: a.pinned } }
}
case 'delete_search_history': {
return { url: `/api/v1/search-history/${a.id}`, method: 'DELETE' }
}
// --- Bookmarks ---
case 'get_bookmarks': {
return { url: '/api/v1/bookmarks', method: 'GET' }
}
case 'save_bookmark': {
return { url: '/api/v1/bookmarks', method: 'POST', body: { label: a.label, history_id: a.historyId } }
}
case 'delete_bookmark': {
return { url: `/api/v1/bookmarks/${a.id}`, method: 'DELETE' }
}
// --- Service Management ---
case 'stop_service': {
const mbUrl = detectBaseUrl()
return { url: `${mbUrl}/api/v2/service/stop`, method: 'POST', body: { name: a.name } }
}
case 'start_service': {
const mbUrl = detectBaseUrl()
return { url: `${mbUrl}/api/v2/service/start`, method: 'POST', body: { name: a.name } }
}
// --- Admin Panel ---
case 'get_system_stats':
return { url: '/api/v2/admin/system-stats', method: 'GET' }
case 'list_admin_users':
return { url: '/api/v2/admin/users', method: 'GET' }
case 'list_shares':
return { url: '/api/v2/admin/shares', method: 'GET' }
// --- Pose & Appearance ---
case 'get_pose': {
let url = `/api/v1/file/${a.fileUuid}/pose?frame=${a.frame}`
if (a.bboxX != null) url += `&bbox_x=${a.bboxX}`
if (a.bboxY != null) url += `&bbox_y=${a.bboxY}`
if (a.bboxW != null) url += `&bbox_w=${a.bboxW}`
if (a.bboxH != null) url += `&bbox_h=${a.bboxH}`
return { url, method: 'GET' }
}
case 'get_appearance': {
let url = `/api/v1/file/${a.fileUuid}/appearance?frame=${a.frame}`
if (a.bboxX != null) url += `&bbox_x=${a.bboxX}`
if (a.bboxY != null) url += `&bbox_y=${a.bboxY}`
if (a.bboxW != null) url += `&bbox_w=${a.bboxW}`
if (a.bboxH != null) url += `&bbox_h=${a.bboxH}`
return { url, method: 'GET' }
}
case 'sync_file_status': {
return { url: `/api/v1/file/${a.fileUuid}/sync-status`, method: 'POST' }
}
default:
throw new Error(`Unknown command: ${cmd}`)
}
}
// Transform HTTP response to match Tauri invoke format
// Some endpoints need data reshaping to match what the Rust commands return
export function transformResponse(cmd: string, data: any): any {
switch (cmd) {
case 'get_files': {
const files = data.files || data.data || data || []
return files.map((f: any) => ({
file_uuid: f.file_uuid || '',
file_name: f.file_name || '',
file_path: f.file_path || '',
file_size: f.file_size || 0,
modified_time: f.modified_time || '',
isRegistered: f.isRegistered ?? f.is_registered ?? false,
status: f.status || '',
registrationTime: f.registration_time || null,
ingested: f.ingested ?? (f.status === 'completed'),
}))
}
case 'get_people': {
const identities = data.identities || data.data || data || []
return identities.map((p: any) => ({
identity_uuid: p.identity_uuid || '',
name: p.name || '',
starred: p.metadata?.starred ?? p.starred ?? false,
status: p.status || p.metadata?.status || 'pending',
metadata: p.metadata || {},
file_uuids: p.file_uuids || [],
source: p.source || '',
tmdb_id: p.tmdb_id ?? null,
}))
}
case 'get_file_identities': {
return data
}
case 'get_faces': {
const faces = data.data || data.faces || data || []
return faces.map((f: any) => ({
id: f.id,
file_uuid: f.file_uuid || '',
frame_number: f.frame_number || 0,
timestamp_secs: f.timestamp_secs || 0,
face_id: f.face_id ?? null,
confidence: f.confidence || 0,
bbox: f.bbox ? { x: f.bbox.x, y: f.bbox.y, width: f.bbox.width, height: f.bbox.height } : null,
}))
}
case 'get_traces': {
const traces = data.traces || data.data || []
return {
total: data.total || traces.length || 0,
traces: traces.map((t: any) => ({
trace_id: t.trace_id,
file_uuid: t.file_uuid || '',
frame_count: t.frame_count || 0,
first_frame: t.first_frame || 0,
last_frame: t.last_frame || 0,
first_sec: t.first_sec || 0,
last_sec: t.last_sec || 0,
avg_confidence: t.avg_confidence || 0,
face_id: t.face_id ?? null,
})),
}
}
case 'get_face_candidates': {
const candidates = data.candidates || data.data || data || []
return candidates.map((c: any) => ({
id: c.id,
face_id: c.face_id ?? null,
file_uuid: c.file_uuid || '',
frame_number: c.frame_number || 0,
confidence: c.confidence || 0,
bbox: c.bbox ? { x: c.bbox.x, y: c.bbox.y, width: c.bbox.width, height: c.bbox.height } : null,
}))
}
case 'get_unassigned_traces': {
const traces = data.traces || data.data || []
return {
total: data.total || traces.length || 0,
traces: traces.map((t: any) => ({
trace_id: t.trace_id,
file_uuid: t.file_uuid || '',
frame_count: t.frame_count || 0,
start_frame: t.start_frame || 0,
end_frame: t.end_frame || 1,
best_face_id: t.best_face_id,
best_face_frame: t.best_face_frame || 1,
best_face_confidence: t.best_face_confidence || 1,
best_face_bbox: t.best_face_bbox ? { x: t.best_face_bbox.x, y: t.best_face_bbox.y, width: t.best_face_bbox.width, height: t.best_face_bbox.height } : null,
})),
}
}
case 'get_pending_persons': {
return {
pending_persons: (data.pending_persons || data.data || []).map((p: any) => ({
identity_uuid: p.identity_uuid || '',
name: p.name || '',
trace_count: p.trace_count || 0,
})),
}
}
case 'search_llm_smart': {
const results = data.results || data.data || data || []
return results.map((r: any) => {
const asr = r.asr || null
const asrSegments = asr?.segments || []
const asrLang = asr?.language || ''
const asrLangProb = asr?.language_probability || 0
let asrStatus: 'no_audio_track' | 'silent_audio' | 'has_transcript' | 'processing' = 'processing'
let asrMessage = '處理中'
if (asrSegments.length === 0) {
if (asrLang === '' && asrLangProb === 0) {
asrStatus = 'no_audio_track'
asrMessage = '無音軌'
} else {
asrStatus = 'silent_audio'
asrMessage = asrLang ? `無語音 (${asrLang})` : '無語音'
}
} else {
asrStatus = 'has_transcript'
asrMessage = `${asrSegments.length} 段語音 (${asrLang || 'unknown'})`
}
return {
file_uuid: r.file_uuid || '',
start_time: r.start_time ?? 0,
end_time: r.end_time ?? 0,
start_frame: r.start_frame ?? 0,
end_frame: r.end_frame ?? 0,
summary: r.summary || r.raw_text || '',
similarity: r.similarity || 0,
file_name: r.file_name || null,
asr_status: asrStatus,
asr_message: asrMessage,
asr_segment_count: asrSegments.length,
asr_language: asrLang,
}
})
}
case 'search_keyword': {
const results = data.results || data.data || data || []
return results.map((r: any) => ({
file_uuid: r.file_uuid || '',
start_time: r.start_time ?? 0,
end_time: r.end_time ?? 0,
start_frame: r.start_frame ?? 0,
end_frame: r.end_frame ?? 0,
summary: r.summary || r.raw_text || '',
similarity: r.similarity || 0,
file_name: r.file_name || null,
source_type: r.source_type || null,
}))
}
case 'search_semantic': {
const results = data.results || data.data || data || []
return results.map((r: any) => ({
file_uuid: r.file_uuid || '',
start_time: r.start_time ?? 0,
end_time: r.end_time ?? 0,
start_frame: r.start_frame ?? 0,
end_frame: r.end_frame ?? 0,
summary: r.summary || r.raw_text || '',
similarity: r.similarity || 0,
file_name: r.file_name || null,
source_type: r.source_type || null,
}))
}
case 'search_identities': {
const results = data.results || data.data || data || []
return results.map((r: any) => ({
identity_id: r.identity_id,
name: r.name,
source: r.source || '',
tmdb_id: r.tmdb_id ?? null,
file_uuid: r.file_uuid ?? null,
start_time: r.start_time ?? 0,
end_time: r.end_time ?? 0,
start_frame: r.start_frame ?? null,
end_frame: r.end_frame ?? null,
text_content: r.text_content ?? null,
}))
}
case 'register_file': {
return {
success: data.success ?? false,
file_uuid: data.file_uuid ?? '',
file_name: data.file_name ?? '',
message: data.message ?? '',
}
}
case 'process_file': {
return {
success: data.success ?? false,
file_uuid: data.file_uuid ?? '',
message: data.message ?? '',
}
}
case 'run_identity_agent': {
return {
success: data.success ?? false,
file_uuid: data.file_uuid ?? '',
message: data.message ?? '',
}
}
case 'run_cluster_agent': {
return {
success: data.success ?? false,
file_uuid: data.file_uuid ?? '',
message: data.message ?? '',
clusters: data.clusters ?? 0,
total_traces: data.total_traces ?? 0,
}
}
case 'get_cluster_results': {
return data || null
}
case 'get_trace_profile': { return data || null }
case 'update_trace_profile': { return data || { success: true } }
case 'update_trace_profile_group': { return data || { success: true } }
case 'get_file_profile': { return data || null }
case 'update_file_profile': { return data || { success: true } }
case 'get_processor_json': {
return data || null
}
case 'bind_speakers': {
return {
success: data.success ?? false,
bindings: data.bindings ?? 0,
message: data.message ?? '',
}
}
case 'create_pending_identity': {
return {
success: data.success ?? false,
identity_uuid: data.identity_uuid ?? '',
identity_id: data.identity_id ?? 0,
message: data.message ?? '',
}
}
case 'unregister_file':
case 'ingest_file':
case 'checkout_file': {
return {
success: data.success ?? false,
file_uuid: data.file_uuid ?? '',
message: data.message ?? '',
}
}
case 'update_identity':
case 'upload_profile_image':
case 'get_identity_profile':
case 'get_face_thumbnail':
case 'get_file_thumbnail_by_path':
case 'get_search_history':
case 'save_search_history':
case 'rename_search_history':
case 'pin_search_history':
case 'delete_search_history':
case 'get_bookmarks':
case 'save_bookmark':
case 'delete_bookmark':
case 'identity_undo':
case 'identity_redo':
case 'identity_history':
case 'identity_bind_undo':
case 'identity_bind_redo':
case 'identity_bind_history':
case 'merge_undo':
case 'merge_redo':
case 'merge_history':
return data
case 'create_identity_from_face': {
const respData = data.data || data
return {
success: data.success ?? false,
identity_uuid: respData.identity_uuid ?? respData.uuid ?? '',
name: respData.name ?? '',
}
}
case 'get_pipeline_stats':
case 'get_file_stats': {
return data
}
case 'get_pose':
case 'get_appearance':
return data
default:
return data
}
}