M4: trace API, portal embed client, EmbeddingGemma sync, release plan

This commit is contained in:
Warren
2026-05-08 01:04:23 +08:00
parent 26d9c33419
commit 6d82131589
4 changed files with 506 additions and 31 deletions

View File

@@ -251,30 +251,27 @@ export async function searchVideos(query: string, limit = 10, mode = 'vector'):
}
const config = getConfig()
const url = mode === 'smart' || mode === 'bm25'
? `${config.api_base_url}/api/v1/search`
: `${config.api_base_url}/api/v1/search`
const url = `${config.api_base_url}/api/v1/search/universal`
const response: any = await httpFetch<any>(url, {
method: 'POST',
body: JSON.stringify({ query, limit }),
})
// Map backend response ({ results: [...], query: string }) to frontend SearchResult ({ hits: [...], query: string, count: number })
return {
query: response.query,
query: response.query || query,
count: response.results?.length || 0,
hits: (response.results || []).map((r: any) => ({
id: r.chunk_id || r.id,
vid: r.uuid || r.vid,
start_frame: Math.floor((r.start_time || 0) * 30),
end_frame: Math.floor((r.end_time || 0) * 30),
fps: 30,
vid: r.uuid || r.vid || r.file_uuid || '',
start_frame: Math.floor((r.start_time || 0) * (r.fps || 30)),
end_frame: Math.floor((r.end_time || 0) * (r.fps || 30)),
fps: r.fps || 30,
start: r.start_time || r.start || 0,
end: r.end_time || r.end || 0,
text: r.text || '',
text: r.text || r.text_content || '',
score: r.score || 0,
title: r.title || r.file_name,
title: r.title || r.file_name || '',
file_path: r.file_path,
has_visual_stats: !!r.visual_stats,
parent_id: r.parent_chunk_id,
@@ -288,29 +285,27 @@ export async function searchChunks(query: string, uuid?: string): Promise<Search
}
const config = getConfig()
const url = uuid
? `${config.api_base_url}/api/v1/search?uuid=${encodeURIComponent(uuid)}`
: `${config.api_base_url}/api/v1/search`
const url = `${config.api_base_url}/api/v1/search/universal`
const response: any = await httpFetch<any>(url, {
method: 'POST',
body: JSON.stringify({ query, limit: 10 }),
body: JSON.stringify({ query, uuid, limit: 20 }),
})
return {
query: response.query,
query: response.query || query,
count: response.results?.length || 0,
hits: (response.results || []).map((r: any) => ({
id: r.chunk_id || r.id,
vid: r.uuid || r.vid,
start_frame: Math.floor((r.start_time || 0) * 30),
end_frame: Math.floor((r.end_time || 0) * 30),
fps: 30,
vid: r.uuid || r.vid || r.file_uuid || '',
start_frame: Math.floor((r.start_time || 0) * (r.fps || 30)),
end_frame: Math.floor((r.end_time || 0) * (r.fps || 30)),
fps: r.fps || 30,
start: r.start_time || r.start || 0,
end: r.end_time || r.end || 0,
text: r.text || '',
text: r.text || r.text_content || '',
score: r.score || 0,
title: r.title || r.file_name,
title: r.title || r.file_name || '',
file_path: r.file_path,
has_visual_stats: !!r.visual_stats,
parent_id: r.parent_chunk_id,
@@ -415,12 +410,15 @@ export async function translateText(text: string, targetLang: string = 'zh-TW'):
return text
}
export async function getPersonThumbnail(personId: string): Promise<string> {
export async function getPersonThumbnail(fileUuid: string, traceId?: number): Promise<string> {
if (isTauri()) {
return tauriInvoke<string>('get_person_thumbnail_b64', { person_id: personId })
return tauriInvoke<string>('get_person_thumbnail_b64', { file_uuid: fileUuid, trace_id: traceId })
}
const config = getConfig()
return `${config.api_base_url}/api/v1/file/:file_uuid/faces/:face_id/thumbnail`
if (traceId !== undefined) {
return `${config.api_base_url}/api/v1/file/${fileUuid}/trace/${traceId}/video`
}
return `${config.api_base_url}/api/v1/file/${fileUuid}/thumbnail`
}
export async function registerIdentity(name: string, images: string[]): Promise<any> {
@@ -462,7 +460,7 @@ export async function unregisterVideo(fileUuid: string): Promise<UnregisterRespo
return tauriInvoke<UnregisterResponse>('unregister_video', { file_uuid: fileUuid })
}
const config = getConfig()
return httpFetch<UnregisterResponse>(`${config.api_base_url}/api/v1/files/unregister`, {
return httpFetch<UnregisterResponse>(`${config.api_base_url}/api/v1/unregister`, {
method: 'POST',
body: JSON.stringify({ file_uuid: fileUuid }),
})
@@ -483,17 +481,62 @@ export async function listFaceCandidates(fileUuid?: string, minConfidence = 0.5,
return httpFetch(`${config.api_base_url}/api/v1/faces/candidates?${params.toString()}`)
}
export async function listTracesSorted(
fileUuid: string,
sortBy = 'face_count',
limit = 100,
minFaces = 1
): Promise<any> {
if (isTauri()) {
return tauriInvoke('list_traces_sorted', { file_uuid: fileUuid, sort_by: sortBy, limit, min_faces: minFaces })
}
const config = getConfig()
return httpFetch(`${config.api_base_url}/api/v1/file/${fileUuid}/face_trace/sortby`, {
method: 'POST',
body: JSON.stringify({ sort_by: sortBy, limit, min_faces: minFaces }),
})
}
/**
* Embed query text using EmbeddingGemma with fallback.
* Tries M5 (192.168.110.201:11436) first, falls back to M4 localhost.
*/
export async function embedQuery(text: string): Promise<number[]> {
const servers = [
'http://192.168.110.201:11436/v1/embeddings',
'http://localhost:11436/v1/embeddings',
]
for (const url of servers) {
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: text, model: 'embeddinggemma-300m' }),
signal: AbortSignal.timeout(5000),
})
if (!res.ok) continue
const data = await res.json()
if (data?.data?.[0]?.embedding) return data.data[0].embedding
} catch { continue }
}
throw new Error('Embedding servers unreachable')
}
export async function listTraceFaces(fileUuid: string, traceId: number, limit = 200, offset = 0): Promise<any> {
if (isTauri()) {
return tauriInvoke('list_trace_faces', { file_uuid: fileUuid, trace_id: traceId, limit, offset })
}
const config = getConfig()
return httpFetch(`${config.api_base_url}/api/v1/file/${fileUuid}/trace/${traceId}/faces?limit=${limit}&offset=${offset}`)
}
export async function getIdentityFaces(identityId: number, page = 1, pageSize = 100): Promise<any> {
if (isTauri()) {
return tauriInvoke('get_identity_faces', { identity_id: identityId, page, page_size: pageSize })
}
const config = getConfig()
const params = new URLSearchParams()
params.append('page', String(page))
params.append('page_size', String(pageSize))
return httpFetch(`${config.api_base_url}/api/v1/identities/${identityId}/faces?${params.toString()}`)
return httpFetch(`${config.api_base_url}/api/v1/identity/${identityId}/files?page_size=${pageSize}&offset=${(page-1)*pageSize}`)
}
// ── Config helpers ──────────────────────────────────────────────────────