Spaces:
Runtime error
Runtime error
| window.ISR = window.ISR || {}; | |
| // ββ API Base URL ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Auto-detect: use same origin when served by the backend, HF Space otherwise | |
| const API_BASE = window.location.hostname.includes('hf.space') | |
| ? window.location.origin | |
| : ''; | |
| // ββ Async Data Functions (real backend API) βββββββββββββββββββββββ | |
| async function suggestMissions(frameBlob) { | |
| const form = new FormData(); | |
| form.append('frame', frameBlob, 'frame.jpg'); | |
| try { | |
| const res = await fetch(`${API_BASE}/suggest-missions`, { method: 'POST', body: form }); | |
| if (!res.ok) return []; | |
| return res.json(); | |
| } catch (err) { console.warn('[ISR] suggestMissions failed:', err); return []; } | |
| } | |
| async function startDetection(videoFile, config) { | |
| const form = new FormData(); | |
| form.append('video', videoFile); | |
| form.append('mode', config.mode); | |
| if (config.queries) form.append('queries', config.queries); | |
| form.append('detector', config.detector); | |
| form.append('segmenter', config.segmenter); | |
| if (config.mission) form.append('mission', config.mission); | |
| if (config.ai_postprocessing !== undefined) form.append('ai_postprocessing', config.ai_postprocessing); | |
| const res = await fetch(`${API_BASE}/detect/async`, { method: 'POST', body: form }); | |
| if (!res.ok) throw new Error(`Detection failed: ${res.status}`); | |
| return res.json(); | |
| } | |
| async function pollStatus(jobId) { | |
| const url = ISR.STATE._statusUrl || `${API_BASE}/detect/status/${jobId}`; | |
| const res = await fetch(url, { cache: 'no-store' }); | |
| if (!res.ok) throw new Error(`Status poll failed: ${res.status}`); | |
| return res.json(); | |
| } | |
| async function fetchTracks(jobId, frameIdx) { | |
| const cached = ISR.getCachedTracks(frameIdx); | |
| if (cached) return cached; | |
| try { | |
| const res = await fetch(`${API_BASE}/detect/tracks/${jobId}/${frameIdx}`); | |
| if (!res.ok) { | |
| if (res.status === 404 && ISR.STATE._analysisStartTime && Date.now() - ISR.STATE._analysisStartTime > 3600000) { | |
| ISR.showToast('Job expired β results are no longer available'); | |
| ISR.STATE.jobId = null; | |
| } | |
| return []; | |
| } | |
| const data = await res.json(); | |
| ISR.cacheTrackData(frameIdx, data); | |
| return data; | |
| } catch (err) { console.warn('[ISR] fetchTracks failed:', err); return []; } | |
| } | |
| async function fetchVerdicts(jobId) { | |
| try { | |
| const res = await fetch(`${API_BASE}/detect/verdicts/${jobId}`); | |
| if (!res.ok) return {}; | |
| const verdicts = await res.json(); | |
| for (const [trackId, v] of Object.entries(verdicts)) { | |
| ISR.cacheAssessment(trackId, v); | |
| } | |
| return verdicts; | |
| } catch (err) { console.warn('[ISR] fetchVerdicts failed:', err); return {}; } | |
| } | |
| async function fetchTimelineSummary(jobId) { | |
| const res = await fetch(`${API_BASE}/detect/tracks/${jobId}/summary`); | |
| if (!res.ok) throw new Error('Summary fetch failed'); | |
| return res.json(); | |
| } | |
| async function fetchFrame(jobId, frameIdx, trackId) { | |
| if (trackId === undefined) trackId = null; | |
| try { | |
| let url = `${API_BASE}/inspect/frame/${jobId}/${frameIdx}`; | |
| if (trackId) url += `?track_id=${encodeURIComponent(trackId)}&padding=0.20`; | |
| const res = await fetch(url); | |
| if (!res.ok) return null; | |
| return res.blob(); | |
| } catch (err) { console.warn('[ISR] fetchFrame failed:', err); return null; } | |
| } | |
| async function fetchMask(jobId, frameIdx, trackId) { | |
| try { | |
| const url = `${API_BASE}/inspect/mask/${jobId}/${frameIdx}/${encodeURIComponent(trackId)}`; | |
| const res = await fetch(url); | |
| if (res.status === 404) { | |
| // No pre-computed mask β generate one on-demand via SAM2 | |
| try { | |
| const gen = await fetch(`${API_BASE}/inspect/generate-mask/${jobId}/${frameIdx}/${encodeURIComponent(trackId)}`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ sam2_size: 'large' }) | |
| }); | |
| if (!gen.ok) return null; | |
| return await gen.json(); | |
| } catch (err) { console.warn('[ISR] fetchMask generate failed:', err); return null; } | |
| } | |
| if (!res.ok) return null; | |
| return await res.json(); | |
| } catch (err) { console.warn('[ISR] fetchMask failed:', err); return null; } | |
| } | |
| async function fetchPointCloud(jobId, frameIdx, trackId, useGenerative) { | |
| try { | |
| const res = await fetch(`${API_BASE}/inspect/pointcloud/${jobId}/${frameIdx}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ track_id: String(trackId), max_points: 50000, render_mode: 'mesh', use_generative: !!useGenerative }) | |
| }); | |
| if (!res.ok) return null; | |
| const contentType = res.headers.get('content-type') || ''; | |
| if (contentType.includes('model/gltf-binary')) { | |
| return { type: 'glb', buffer: await res.arrayBuffer() }; | |
| } | |
| const json = await res.json(); | |
| json.type = 'legacy'; | |
| return json; | |
| } catch (err) { console.warn('[ISR] fetchPointCloud failed:', err); return null; } | |
| } | |
| async function askAI(question, trackContext) { | |
| if (trackContext === undefined) trackContext = null; | |
| // Prepend reasoning trace context if a node is selected | |
| var message = question; | |
| if (ISR.STATE.selectedExplainNode) { | |
| message = '[REASONING CONTEXT: ' + JSON.stringify(ISR.STATE.selectedExplainNode) + ']\n' + question; | |
| } | |
| const body = { | |
| message: message, | |
| mission: ISR.STATE.mission || '', | |
| active_objects: Array.isArray(trackContext) ? trackContext : (trackContext ? [trackContext] : []), | |
| history: (ISR.STATE.chatHistory && ISR.STATE.chatHistory.length > 0) ? ISR.STATE.chatHistory.slice(-20) : [] | |
| }; | |
| try { | |
| const res = await fetch(`${API_BASE}/chat`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body) | |
| }); | |
| if (!res.ok) throw new Error(); | |
| const data = await res.json(); | |
| return data; | |
| } catch (err) { | |
| console.warn('[ISR] askAI failed, using fallback:', err); | |
| const fallback = ISR.matchAICommand(question); | |
| return { response: fallback.text, action: fallback.action }; | |
| } | |
| } | |
| async function cancelJob(jobId) { | |
| try { await fetch(`${API_BASE}/detect/job/${jobId}`, { method: 'DELETE' }); } catch (err) { console.warn('[ISR] cancelJob failed:', err); } | |
| } | |
| async function fetchTracksBatch(jobId, frameIndices) { | |
| try { | |
| const res = await fetch(`${API_BASE}/detect/tracks/${jobId}/batch?frames=${frameIndices.join(',')}`); | |
| if (!res.ok) return {}; | |
| return await res.json(); | |
| } catch (err) { console.warn('[ISR] fetchTracksBatch failed:', err); return {}; } | |
| } | |
| function resolveUrl(path) { | |
| // If the path is already absolute, use it; otherwise prepend API_BASE | |
| if (!path) return null; | |
| return path.startsWith('http') ? path : `${API_BASE}${path}`; | |
| } | |
| // ββ Export to namespace βββββββββββββββββββββββββββββββββββββββββ | |
| Object.assign(window.ISR, { | |
| API_BASE, | |
| suggestMissions, | |
| startDetection, | |
| pollStatus, | |
| fetchTracks, | |
| fetchVerdicts, | |
| fetchTimelineSummary, | |
| fetchFrame, | |
| fetchMask, | |
| fetchPointCloud, | |
| askAI, | |
| cancelJob, | |
| fetchTracksBatch, | |
| resolveUrl, | |
| }); | |