ISR / demo /js /api.js
Zhen Ye
feat: auto-suggest ISR missions from video preview frame
7e09916
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,
});