diff --git a/src/composables/useVLM.ts b/src/composables/useVLM.ts new file mode 100644 index 0000000..f0c57b2 --- /dev/null +++ b/src/composables/useVLM.ts @@ -0,0 +1,156 @@ +import { ref } from 'vue' +import { getApiBase } from '@/api/config' + +export interface VLMTag { + en: string + local: string +} + +export interface VLMScene { + file_uuid: string + file_name: string + scene_index: number + timestamp_sec: number + frame_number: number + thumbnail_url: string + vlm_description: string + vlm_description_localized?: string + vlm_objects: string[] + vlm_tags: VLMTag[] + score: number + source: string +} + +export interface VLMTrace { + file_uuid: string + file_name: string + trace_id: number + start_frame: number + end_frame: number + start_time: number + end_time: number + timestamp_sec: number + thumbnail_url: string + vlm_description: string + vlm_description_localized?: string + vlm_objects: string[] + vlm_tags: VLMTag[] + score: number + identity_name?: string +} + +export interface FileScene { + index: number + filename: string + timestamp_sec: number + vlm_description: string + vlm_description_localized?: string + vlm_objects: string[] + vlm_tags: VLMTag[] + vlm_status: string +} + +export function useVLMScenes(fileUuid: string) { + const scenes = ref([]) + const loading = ref(false) + const error = ref(null) + + async function loadScenes() { + loading.value = true + error.value = null + try { + const base = getApiBase() + const resp = await fetch(`${base}/api/v1/file/${fileUuid}/scenes`) + if (!resp.ok) throw new Error(`HTTP ${resp.status}`) + const data = await resp.json() + scenes.value = Array.isArray(data) ? data : (data.scenes || []) + } catch (e: any) { + error.value = e.message || 'Failed to load scenes' + scenes.value = [] + } finally { + loading.value = false + } + } + + function getThumbnailUrl(sceneIndex: number): string { + const base = getApiBase() + const paddedIndex = String(sceneIndex).padStart(3, '0') + return `${base}/api/v1/file/${fileUuid}/scenes/scene_${paddedIndex}.jpg` + } + + return { scenes, loading, error, loadScenes, getThumbnailUrl } +} + +export function useVLMSearch() { + const scenes = ref([]) + const traces = ref([]) + const loading = ref(false) + const error = ref(null) + const total = ref(0) + + async function search(query: string, options?: { + page_size?: number + min_score?: number + output_language?: string + offset?: number + }) { + loading.value = true + error.value = null + const base = getApiBase() + const params = { + query, + page_size: options?.page_size || 20, + min_score: options?.min_score ?? 0.0, + output_language: options?.output_language || 'en', + ...(options?.offset != null ? { offset: options.offset } : {}) + } + + try { + const [scenesRes, tracesRes] = await Promise.all([ + fetch(`${base}/api/v1/search/vlm/scenes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params) + }).then(r => r.json()), + fetch(`${base}/api/v1/search/vlm/traces`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params) + }).then(r => r.json()) + ]) + + scenes.value = scenesRes?.results || [] + traces.value = tracesRes?.results || [] + total.value = scenesRes?.total || 0 + } catch (e: any) { + error.value = e.message || 'Search failed' + scenes.value = [] + traces.value = [] + } finally { + loading.value = false + } + } + + function clearResults() { + scenes.value = [] + traces.value = [] + total.value = 0 + } + + return { scenes, traces, loading, error, total, search, clearResults } +} + +export function useSceneThumbnail(fileUuid: string, sceneIndex: number) { + const url = ref(null) + const loading = ref(false) + + function loadThumbnail() { + loading.value = true + const base = getApiBase() + const paddedIndex = String(sceneIndex).padStart(3, '0') + url.value = `${base}/api/v1/file/${fileUuid}/scenes/scene_${paddedIndex}.jpg` + loading.value = false + } + + return { url, loading, loadThumbnail } +}