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, });