zye0616 Claude Opus 4.6 (1M context) commited on
Commit
fec2ee8
·
1 Parent(s): 0b26519

refactor: anonymize LLM labels, merge fragmented tracks, and tune defaults

Browse files

Replace GPT-4o/Claude/Gemini branding with generic Analyst 1/2/3 labels
in both frontend and backend. Add track merging to deduplicate fragmented
IDs for the same physical object. Filter non-mission-relevant objects from
timeline events. Bump frame store budget to 24 GiB, increase default
segmentation step to 12, auto-detect API base URL, and fix video playback
reliability (blob MIME type, canplay wait, timeout increase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

.gitignore CHANGED
@@ -10,4 +10,5 @@ checkpoints/
10
  *.md
11
  .agent
12
  .claude/
13
- .superpowers/
 
 
10
  *.md
11
  .agent
12
  .claude/
13
+ .superpowers/
14
+ videos/
app.py CHANGED
@@ -409,7 +409,7 @@ async def detect_async_endpoint(
409
  depth_estimator: str = Form("depth"),
410
  depth_scale: float = Form(25.0),
411
  enable_depth: bool = Form(False),
412
- step: int = Form(7),
413
  mission: str = Form(None),
414
  ):
415
  _ttfs_t0 = time.perf_counter()
 
409
  depth_estimator: str = Form("depth"),
410
  depth_scale: float = Form(25.0),
411
  enable_depth: bool = Form(False),
412
+ step: int = Form(12),
413
  mission: str = Form(None),
414
  ):
415
  _ttfs_t0 = time.perf_counter()
demo/js/api.js CHANGED
@@ -1,8 +1,10 @@
1
  window.ISR = window.ISR || {};
2
 
3
  // ── API Base URL ──────────────────────────────────────────────────
4
- // Set to your HuggingFace Space URL for GPU access, or '' for same-origin
5
- const API_BASE = 'https://biaslab2025-isr.hf.space';
 
 
6
 
7
  // ── Async Data Functions (real backend API) ───────────────────────
8
 
 
1
  window.ISR = window.ISR || {};
2
 
3
  // ── API Base URL ──────────────────────────────────────────────────
4
+ // Auto-detect: same-origin in dev, HuggingFace Space in production demo
5
+ const API_BASE = window.location.hostname === 'biaslab2025-isr.hf.space'
6
+ ? ''
7
+ : (localStorage.getItem('ISR_API_BASE') || '');
8
 
9
  // ── Async Data Functions (real backend API) ───────────────────────
10
 
demo/js/explain.js CHANGED
@@ -44,7 +44,7 @@ async function loadExplainability(jobId, trackId) {
44
  if (_explainAbort) _explainAbort.abort();
45
  _explainAbort = new AbortController();
46
 
47
- panel.innerHTML = '<div class="explain-loading"><div class="explain-spinner"></div><span>Analyzing with GPT-4o, Claude, and Gemini...</span></div>';
48
 
49
  try {
50
  const resp = await fetch(`${ISR.API_BASE}/inspect/explain/${jobId}/${encodeURIComponent(trackId)}`, { signal: _explainAbort.signal });
@@ -97,9 +97,9 @@ function renderExplainGraph(data, container) {
97
  + '</div></div>';
98
 
99
  var llmHtml = '<div class="ex-llm-strip">'
100
- + '<span class="ex-llm-tag active">GPT-4o</span>'
101
- + '<span class="ex-llm-tag' + (data.consensus_bar && data.consensus_bar.validators_available >= 1 ? ' active' : '') + '">Claude</span>'
102
- + '<span class="ex-llm-tag' + (data.consensus_bar && data.consensus_bar.validators_available >= 2 ? ' active' : '') + '">Gemini</span>'
103
  + '</div>';
104
 
105
  // Trunk line
@@ -239,11 +239,11 @@ function render3DGraph(data, wrap) {
239
  cats.forEach(function(c) { allFeatures = allFeatures.concat(c.features || []); });
240
 
241
  // Aggregate validator counts for tooltips
242
- var claudeAgreed = 0, claudeTotal = 0, geminiAgreed = 0, geminiTotal = 0;
243
  allFeatures.forEach(function(f) {
244
  var vals = f.validators || {};
245
- if (vals.claude) { claudeTotal++; if (vals.claude.agree) claudeAgreed++; }
246
- if (vals.gemini) { geminiTotal++; if (vals.gemini.agree) geminiAgreed++; }
247
  });
248
  var validatorsAvailable = (data.consensus_bar && data.consensus_bar.validators_available) || 0;
249
 
@@ -348,20 +348,21 @@ function render3DGraph(data, wrap) {
348
  html += '<strong style="color:' + node.color + ';font-size:12px">' + ISR.escHtml(node.label) + '</strong>';
349
  if (node.nodeType === 'feature' && node.nodeData.feat) {
350
  var f = node.nodeData.feat;
351
- if (f.reasoning) html += '<div style="margin-top:5px;padding-top:5px;border-top:1px solid rgba(255,255,255,0.1);color:#cbd5e1"><span style="color:#10b981;font-weight:600">GPT-4o:</span> ' + ISR.escHtml(f.reasoning) + '</div>';
352
  var vals = f.validators || {};
353
  Object.keys(vals).forEach(function(name) {
354
  var v = vals[name];
355
  var ic = v.agree ? '#4ade80' : '#f87171';
356
  var icon = v.agree ? '\u2713' : '\u2717';
357
- html += '<div style="margin-top:3px;color:' + ic + '"><span style="font-weight:600">' + icon + ' ' + ISR.escHtml(name) + ':</span> ' + ISR.escHtml(v.note || '') + '</div>';
 
358
  });
359
  } else if (node.nodeType === 'category' && node.nodeData.features) {
360
  html += '<div style="margin-top:4px;color:#94a3b8">' + node.nodeData.features.length + ' features analyzed</div>';
361
- if (validatorsAvailable > 0) html += '<div style="color:#94a3b8">Validated by Claude + Gemini</div>';
362
  } else if (node.nodeType === 'verdict') {
363
  if (node.nodeData.reasoning_summary) html += '<div style="margin-top:4px;color:#cbd5e1">' + ISR.escHtml(node.nodeData.reasoning_summary) + '</div>';
364
- if (validatorsAvailable > 0) html += '<div style="margin-top:3px;color:#94a3b8">Claude: ' + claudeAgreed + '/' + claudeTotal + ' \u00b7 Gemini: ' + geminiAgreed + '/' + geminiTotal + '</div>';
365
  } else if (node.nodeType === 'input') {
366
  if (data.reasoning_summary) html += '<div style="margin-top:4px;color:#cbd5e1">' + ISR.escHtml(data.reasoning_summary) + '</div>';
367
  html += '<div style="margin-top:3px;color:#94a3b8">Mission: ' + ISR.escHtml((ISR.STATE.mission || 'N/A').slice(0, 50)) + '</div>';
@@ -420,18 +421,27 @@ function render3DGraph(data, wrap) {
420
  graph.d3Force('charge').strength(-180);
421
  graph.d3Force('link').distance(75);
422
 
423
- // Auto-fit camera: fire multiple times to catch layout as it stabilizes
424
- // The graph simulation runs warmupTicks synchronously then cooldownTicks async,
425
- // so we need progressive fits to ensure the final settled layout is fully visible.
426
- [300, 800, 1500, 2500].forEach(function(ms) {
 
 
 
 
 
 
 
 
 
427
  setTimeout(function() {
428
- graph.zoomToFit(600, 80);
429
  }, ms);
430
  });
431
 
432
- // Also fit when simulation finishes (cooldown complete)
433
  graph.onEngineStop(function() {
434
- graph.zoomToFit(600, 80);
435
  });
436
 
437
  // Handle container resize
 
44
  if (_explainAbort) _explainAbort.abort();
45
  _explainAbort = new AbortController();
46
 
47
+ panel.innerHTML = '<div class="explain-loading"><div class="explain-spinner"></div><span>Running multi-analyst consensus pipeline...</span></div>';
48
 
49
  try {
50
  const resp = await fetch(`${ISR.API_BASE}/inspect/explain/${jobId}/${encodeURIComponent(trackId)}`, { signal: _explainAbort.signal });
 
97
  + '</div></div>';
98
 
99
  var llmHtml = '<div class="ex-llm-strip">'
100
+ + '<span class="ex-llm-tag active">Analyst 1</span>'
101
+ + '<span class="ex-llm-tag' + (data.consensus_bar && data.consensus_bar.validators_available >= 1 ? ' active' : '') + '">Analyst 2</span>'
102
+ + '<span class="ex-llm-tag' + (data.consensus_bar && data.consensus_bar.validators_available >= 2 ? ' active' : '') + '">Analyst 3</span>'
103
  + '</div>';
104
 
105
  // Trunk line
 
239
  cats.forEach(function(c) { allFeatures = allFeatures.concat(c.features || []); });
240
 
241
  // Aggregate validator counts for tooltips
242
+ var analyst2Agreed = 0, analyst2Total = 0, analyst3Agreed = 0, analyst3Total = 0;
243
  allFeatures.forEach(function(f) {
244
  var vals = f.validators || {};
245
+ if (vals.analyst_2) { analyst2Total++; if (vals.analyst_2.agree) analyst2Agreed++; }
246
+ if (vals.analyst_3) { analyst3Total++; if (vals.analyst_3.agree) analyst3Agreed++; }
247
  });
248
  var validatorsAvailable = (data.consensus_bar && data.consensus_bar.validators_available) || 0;
249
 
 
348
  html += '<strong style="color:' + node.color + ';font-size:12px">' + ISR.escHtml(node.label) + '</strong>';
349
  if (node.nodeType === 'feature' && node.nodeData.feat) {
350
  var f = node.nodeData.feat;
351
+ if (f.reasoning) html += '<div style="margin-top:5px;padding-top:5px;border-top:1px solid rgba(255,255,255,0.1);color:#cbd5e1"><span style="color:#10b981;font-weight:600">Analyst 1:</span> ' + ISR.escHtml(f.reasoning) + '</div>';
352
  var vals = f.validators || {};
353
  Object.keys(vals).forEach(function(name) {
354
  var v = vals[name];
355
  var ic = v.agree ? '#4ade80' : '#f87171';
356
  var icon = v.agree ? '\u2713' : '\u2717';
357
+ var displayName = {analyst_2: 'Analyst 2', analyst_3: 'Analyst 3'}[name] || name;
358
+ html += '<div style="margin-top:3px;color:' + ic + '"><span style="font-weight:600">' + icon + ' ' + ISR.escHtml(displayName) + ':</span> ' + ISR.escHtml(v.note || '') + '</div>';
359
  });
360
  } else if (node.nodeType === 'category' && node.nodeData.features) {
361
  html += '<div style="margin-top:4px;color:#94a3b8">' + node.nodeData.features.length + ' features analyzed</div>';
362
+ if (validatorsAvailable > 0) html += '<div style="color:#94a3b8">Validated by Analyst 2 + Analyst 3</div>';
363
  } else if (node.nodeType === 'verdict') {
364
  if (node.nodeData.reasoning_summary) html += '<div style="margin-top:4px;color:#cbd5e1">' + ISR.escHtml(node.nodeData.reasoning_summary) + '</div>';
365
+ if (validatorsAvailable > 0) html += '<div style="margin-top:3px;color:#94a3b8">Analyst 2: ' + analyst2Agreed + '/' + analyst2Total + ' \u00b7 Analyst 3: ' + analyst3Agreed + '/' + analyst3Total + '</div>';
366
  } else if (node.nodeType === 'input') {
367
  if (data.reasoning_summary) html += '<div style="margin-top:4px;color:#cbd5e1">' + ISR.escHtml(data.reasoning_summary) + '</div>';
368
  html += '<div style="margin-top:3px;color:#94a3b8">Mission: ' + ISR.escHtml((ISR.STATE.mission || 'N/A').slice(0, 50)) + '</div>';
 
421
  graph.d3Force('charge').strength(-180);
422
  graph.d3Force('link').distance(75);
423
 
424
+ // Set initial camera to look down at the top-down DAG from a centered position.
425
+ // The DAG grows downward (td mode), so offset Y slightly negative to center vertically.
426
+ var nLevels = 4; // object, categories, features, verdict
427
+ var dagSpanY = (nLevels - 1) * 100; // dagLevelDistance=100
428
+ var centerY = dagSpanY / 2;
429
+ graph.cameraPosition({ x: 0, y: -centerY, z: dagSpanY * 1.2 }, { x: 0, y: -centerY, z: 0 });
430
+
431
+ // Immediate fit after warmup ticks (run synchronously), then progressive fits
432
+ // as the async cooldown ticks settle the layout.
433
+ requestAnimationFrame(function() {
434
+ graph.zoomToFit(0, 40);
435
+ });
436
+ [400, 1000, 2000].forEach(function(ms) {
437
  setTimeout(function() {
438
+ graph.zoomToFit(400, 40);
439
  }, ms);
440
  });
441
 
442
+ // Final fit when simulation finishes (cooldown complete)
443
  graph.onEngineStop(function() {
444
+ graph.zoomToFit(400, 40);
445
  });
446
 
447
  // Handle container resize
demo/js/inspect.js CHANGED
@@ -156,8 +156,8 @@ function enterInspectState(trackId) {
156
  if (hasRealData && ISR.STATE.jobId) {
157
  const tid = realTrack ? realTrack.track_id : trackId;
158
  ISR.loadExplainability(ISR.STATE.jobId, tid);
159
- // Auto-switch to EXPLAIN tab after a brief delay so user sees it loading
160
- setTimeout(() => ISR.switchDrawerTab('explain'), 300);
161
  }
162
 
163
  // Wire back button
 
156
  if (hasRealData && ISR.STATE.jobId) {
157
  const tid = realTrack ? realTrack.track_id : trackId;
158
  ISR.loadExplainability(ISR.STATE.jobId, tid);
159
+ // Auto-switch to REASONING tab after a brief delay so user sees it loading
160
+ setTimeout(() => ISR.switchDrawerTab('reasoning'), 300);
161
  }
162
 
163
  // Wire back button
demo/js/real-backend.js CHANGED
@@ -277,7 +277,8 @@ async function enterRealAnalysis(jobId, status) {
277
  if (res.status === 410) { ISR.showToast('Job was cancelled.', 6000); resetToReady(); return; }
278
  if (res.status === 202) { await new Promise(r => setTimeout(r, 1000)); continue; }
279
  if (res.ok) {
280
- const blob = await res.blob();
 
281
  videoBlobUrl = URL.createObjectURL(blob);
282
  ISR.STATE._videoBlobUrl = videoBlobUrl;
283
  break;
@@ -300,7 +301,7 @@ async function enterRealAnalysis(jobId, status) {
300
  processedVideo.classList.remove('hidden');
301
  processedVideo.style.display = 'block';
302
  await new Promise(resolve => {
303
- const timeout = setTimeout(resolve, 5000); // safety timeout
304
  processedVideo.addEventListener('canplay', () => { clearTimeout(timeout); resolve(); }, { once: true });
305
  processedVideo.load();
306
  });
@@ -693,6 +694,52 @@ function _deduplicateTracksByBbox(tracks) {
693
  return kept;
694
  }
695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  function renderTrackListFromData(tracks) {
697
  const panel = document.getElementById('tracksPanel');
698
  panel.innerHTML = '';
@@ -707,7 +754,8 @@ function renderTrackListFromData(tracks) {
707
  const relevant = hasVerdicts
708
  ? enriched.filter(t => t.satisfies === true)
709
  : enriched;
710
- const deduped = _deduplicateTracksByBbox(relevant);
 
711
  const filtered = ISR.STATE.trackFilter ? deduped.filter(t => t.type === ISR.STATE.trackFilter) : deduped;
712
 
713
  if (filtered.length === 0) {
@@ -753,7 +801,8 @@ function renderTrackListFromData(tracks) {
753
 
754
  const speedStr = t.speed_kph ? `${t.speed_kph.toFixed(1)} kph` : '--';
755
  const dirStr = t.direction_clock ? `${t.direction_clock}h` : '';
756
- const idStr = `ID:${t.track_id}`;
 
757
 
758
  // Status badge
759
  const badge = ISR.getStatusBadgeHTML(t);
@@ -1146,17 +1195,24 @@ async function deriveTimelineEvents(jobId, summary, frameCounts) {
1146
  const label = t.label_name || t.label || 'object';
1147
  const time = ISR.formatTime(frameIdx / fps);
1148
 
1149
- // First detection of this track ID (emit once)
 
 
 
 
 
1150
  if (!seenIds.has(tid)) {
1151
  seenIds.add(tid);
1152
- events.push({
1153
- frame: frameIdx, time, type: 'detection',
1154
- label: `New ${label}`, description: `Track ${tid} first detected.`,
1155
- priority: 'normal',
1156
- });
 
 
1157
  }
1158
 
1159
- // Track reappeared after being lost
1160
  if (lostTracks.has(tid)) {
1161
  lostTracks.delete(tid);
1162
  events.push({
@@ -1177,8 +1233,8 @@ async function deriveTimelineEvents(jobId, summary, frameCounts) {
1177
  });
1178
  }
1179
 
1180
- // Assessed as not relevant — emit ONCE per track
1181
- if (t.satisfies === false && !assessedIds.has(tid)) {
1182
  assessedIds.add(tid);
1183
  events.push({
1184
  frame: frameIdx, time, type: 'system',
 
277
  if (res.status === 410) { ISR.showToast('Job was cancelled.', 6000); resetToReady(); return; }
278
  if (res.status === 202) { await new Promise(r => setTimeout(r, 1000)); continue; }
279
  if (res.ok) {
280
+ const rawBlob = await res.blob();
281
+ const blob = new Blob([rawBlob], { type: 'video/mp4' });
282
  videoBlobUrl = URL.createObjectURL(blob);
283
  ISR.STATE._videoBlobUrl = videoBlobUrl;
284
  break;
 
301
  processedVideo.classList.remove('hidden');
302
  processedVideo.style.display = 'block';
303
  await new Promise(resolve => {
304
+ const timeout = setTimeout(resolve, 30000); // safety timeout — large videos need time
305
  processedVideo.addEventListener('canplay', () => { clearTimeout(timeout); resolve(); }, { once: true });
306
  processedVideo.load();
307
  });
 
694
  return kept;
695
  }
696
 
697
+ /**
698
+ * Merge fragmented track IDs that represent the same physical object.
699
+ *
700
+ * When the tracker loses and re-acquires an object (common in aerial/SAR footage
701
+ * with low-confidence detections), multiple track IDs get created for the same
702
+ * physical entity. This groups tracks by label + satisfies verdict and keeps
703
+ * only the best representative per group, merging subsidiary track IDs so
704
+ * inspect/selection still works.
705
+ *
706
+ * Grouping key: (label, satisfies). Within each group, keep the track with the
707
+ * highest score as the representative. Attach merged track IDs so the SVG overlay
708
+ * and click handlers can still resolve any constituent ID to the representative.
709
+ */
710
+ function _mergeFragmentedTracks(tracks) {
711
+ if (tracks.length <= 1) return tracks;
712
+
713
+ // Group by (label, satisfies) — only merge assessed tracks with same verdict
714
+ const groups = new Map(); // key -> [tracks]
715
+ const ungrouped = [];
716
+
717
+ for (const t of tracks) {
718
+ // Only merge tracks that have been assessed with a definite verdict
719
+ if (t.assessment_status === 'ASSESSED' && t.satisfies !== undefined && t.satisfies !== null) {
720
+ const key = `${(t.label || '').toLowerCase()}::${t.satisfies}`;
721
+ if (!groups.has(key)) groups.set(key, []);
722
+ groups.get(key).push(t);
723
+ } else {
724
+ ungrouped.push(t);
725
+ }
726
+ }
727
+
728
+ const merged = [];
729
+ for (const [, group] of groups) {
730
+ // Sort by score descending — best representative first
731
+ group.sort((a, b) => (b.score || 0) - (a.score || 0));
732
+ const best = { ...group[0] };
733
+ // Collect all subsidiary track IDs
734
+ if (group.length > 1) {
735
+ best._merged_track_ids = group.map(t => t.track_id);
736
+ }
737
+ merged.push(best);
738
+ }
739
+
740
+ return merged.concat(ungrouped);
741
+ }
742
+
743
  function renderTrackListFromData(tracks) {
744
  const panel = document.getElementById('tracksPanel');
745
  panel.innerHTML = '';
 
754
  const relevant = hasVerdicts
755
  ? enriched.filter(t => t.satisfies === true)
756
  : enriched;
757
+ const spatialDeduped = _deduplicateTracksByBbox(relevant);
758
+ const deduped = _mergeFragmentedTracks(spatialDeduped);
759
  const filtered = ISR.STATE.trackFilter ? deduped.filter(t => t.type === ISR.STATE.trackFilter) : deduped;
760
 
761
  if (filtered.length === 0) {
 
801
 
802
  const speedStr = t.speed_kph ? `${t.speed_kph.toFixed(1)} kph` : '--';
803
  const dirStr = t.direction_clock ? `${t.direction_clock}h` : '';
804
+ const mergedIds = t._merged_track_ids;
805
+ const idStr = mergedIds ? `IDs:${mergedIds.join(',')}` : `ID:${t.track_id}`;
806
 
807
  // Status badge
808
  const badge = ISR.getStatusBadgeHTML(t);
 
1195
  const label = t.label_name || t.label || 'object';
1196
  const time = ISR.formatTime(frameIdx / fps);
1197
 
1198
+ // Skip non-mission-relevant tracks for event generation.
1199
+ // Objects like "dog", "cat", "mouse" clutter the timeline when they
1200
+ // have no bearing on the mission objective.
1201
+ const isMissionRelevant = t.mission_relevant !== false;
1202
+
1203
+ // First detection of this track ID (emit once, only for mission-relevant)
1204
  if (!seenIds.has(tid)) {
1205
  seenIds.add(tid);
1206
+ if (isMissionRelevant) {
1207
+ events.push({
1208
+ frame: frameIdx, time, type: 'detection',
1209
+ label: `New ${label}`, description: `Track ${tid} first detected.`,
1210
+ priority: 'normal',
1211
+ });
1212
+ }
1213
  }
1214
 
1215
+ // Track reappeared after being lost (only mission-relevant tracks are in lostTracks)
1216
  if (lostTracks.has(tid)) {
1217
  lostTracks.delete(tid);
1218
  events.push({
 
1233
  });
1234
  }
1235
 
1236
+ // Assessed as mission-relevant but does not satisfy — emit ONCE per track
1237
+ if (t.satisfies === false && isMissionRelevant && !assessedIds.has(tid)) {
1238
  assessedIds.add(tid);
1239
  events.push({
1240
  frame: frameIdx, time, type: 'system',
demo/js/ui.js CHANGED
@@ -17,10 +17,26 @@ function togglePlayPause() {
17
  const processedVideo = document.getElementById('processedVideo');
18
  if (processedVideo) {
19
  if (ISR.STATE.isPlaying) {
20
- // Check video is ready
21
- if (processedVideo.readyState < 2) {
22
- console.warn('[ISR] Video not ready (readyState:', processedVideo.readyState, ')');
23
- ISR.showToast('Video still loading, please wait...', 3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ISR.STATE.isPlaying = false;
25
  btn.innerHTML = '&#9654;';
26
  return;
 
17
  const processedVideo = document.getElementById('processedVideo');
18
  if (processedVideo) {
19
  if (ISR.STATE.isPlaying) {
20
+ // If video has a source but isn't ready yet, wait for it
21
+ if (processedVideo.readyState < 2 && processedVideo.src) {
22
+ ISR.showToast('Video loading...', 3000);
23
+ processedVideo.addEventListener('canplay', function onReady() {
24
+ processedVideo.removeEventListener('canplay', onReady);
25
+ if (!ISR.STATE.isPlaying) return; // user may have toggled off while waiting
26
+ processedVideo.playbackRate = ISR.playbackSpeed;
27
+ if (processedVideo.currentTime >= processedVideo.duration - 0.1) {
28
+ processedVideo.currentTime = 0;
29
+ }
30
+ processedVideo.play().catch(err => {
31
+ console.error('[ISR] Video play failed:', err);
32
+ ISR.showToast('Video playback failed: ' + err.message, 5000);
33
+ ISR.STATE.isPlaying = false;
34
+ btn.innerHTML = '&#9654;';
35
+ });
36
+ });
37
+ return;
38
+ } else if (processedVideo.readyState < 2) {
39
+ console.warn('[ISR] Video not ready and no source set');
40
  ISR.STATE.isPlaying = false;
41
  btn.innerHTML = '&#9654;';
42
  return;
models/isr/explainer.py CHANGED
@@ -250,9 +250,9 @@ class ISRExplainer:
250
  model="gemini-2.0-flash",
251
  contents=[
252
  types.Content(role="user", parts=[
253
- types.Part.from_text(_VALIDATOR_SYSTEM_PROMPT + "\n\n" + user_text),
254
  types.Part.from_bytes(data=crop_bytes, mime_type="image/jpeg"),
255
- types.Part.from_text("\n[Full frame context]:"),
256
  types.Part.from_bytes(data=frame_bytes, mime_type="image/jpeg"),
257
  ]),
258
  ],
@@ -309,14 +309,14 @@ class ISRExplainer:
309
  if claude and "feature_validations" in claude:
310
  cv = self._find_validation(claude["feature_validations"], feat_key, feat["name"])
311
  if cv:
312
- validators["claude"] = cv
313
  if cv.get("agree"):
314
  feat_agreed += 1
315
 
316
  if gemini and "feature_validations" in gemini:
317
  gv = self._find_validation(gemini["feature_validations"], feat_key, feat["name"])
318
  if gv:
319
- validators["gemini"] = gv
320
  if gv.get("agree"):
321
  feat_agreed += 1
322
 
 
250
  model="gemini-2.0-flash",
251
  contents=[
252
  types.Content(role="user", parts=[
253
+ types.Part.from_text(text=_VALIDATOR_SYSTEM_PROMPT + "\n\n" + user_text),
254
  types.Part.from_bytes(data=crop_bytes, mime_type="image/jpeg"),
255
+ types.Part.from_text(text="\n[Full frame context]:"),
256
  types.Part.from_bytes(data=frame_bytes, mime_type="image/jpeg"),
257
  ]),
258
  ],
 
309
  if claude and "feature_validations" in claude:
310
  cv = self._find_validation(claude["feature_validations"], feat_key, feat["name"])
311
  if cv:
312
+ validators["analyst_2"] = cv
313
  if cv.get("agree"):
314
  feat_agreed += 1
315
 
316
  if gemini and "feature_validations" in gemini:
317
  gv = self._find_validation(gemini["feature_validations"], feat_key, feat["name"])
318
  if gv:
319
+ validators["analyst_3"] = gv
320
  if gv.get("agree"):
321
  feat_agreed += 1
322
 
utils/frame_store.py CHANGED
@@ -35,7 +35,7 @@ class SharedFrameStore:
35
  the budget ceiling, giving callers a chance to fall back to JPEG path.
36
  """
37
 
38
- MAX_BUDGET_BYTES = 12 * 1024**3 # 12 GiB ceiling
39
 
40
  def __init__(self, video_path: str, max_frames: Optional[int] = None):
41
  cap = cv2.VideoCapture(video_path)
 
35
  the budget ceiling, giving callers a chance to fall back to JPEG path.
36
  """
37
 
38
+ MAX_BUDGET_BYTES = 24 * 1024**3 # 24 GiB ceiling
39
 
40
  def __init__(self, video_path: str, max_frames: Optional[int] = None):
41
  cap = cv2.VideoCapture(video_path)