M4: trace API, portal embed client, EmbeddingGemma sync, release plan
This commit is contained in:
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user