Gayanukaa commited on
Commit
c621c10
Β·
1 Parent(s): 5f2339c

add examples and update readme

Browse files
Files changed (4) hide show
  1. README.md +55 -2
  2. app.py +42 -1
  3. example_input_larger.json +0 -0
  4. static/index.html +566 -34
README.md CHANGED
@@ -5,8 +5,61 @@ colorFrom: indigo
5
  colorTo: gray
6
  sdk: docker
7
  pinned: true
8
- license: apache-2.0
9
  short_description: SPICE metric evaluation and visualization
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  colorTo: gray
6
  sdk: docker
7
  pinned: true
8
+ license: agpl-3.0
9
  short_description: SPICE metric evaluation and visualization
10
  ---
11
 
12
+ # SPICE Evaluator
13
+
14
+ Evaluate image captions using the [SPICE metric](https://panderson.me/spice/) with interactive scene graph visualization.
15
+
16
+ ## Local Setup
17
+
18
+ ### Prerequisites
19
+
20
+ - **Python 3.10+**
21
+ - **Java 11** (SPICE requires the Nashorn JS engine, removed in Java 15+)
22
+ - **SPICE-1.0 JAR files** (~416MB)
23
+
24
+ ### 1. Download SPICE-1.0
25
+
26
+ ```bash
27
+ pip install huggingface_hub
28
+ python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='Gayanukaa/spice-1.0-jar', repo_type='dataset', local_dir='SPICE-1.0')"
29
+ ```
30
+
31
+ Or manually download from [huggingface.co/datasets/Gayanukaa/spice-1.0-jar](https://huggingface.co/datasets/Gayanukaa/spice-1.0-jar) and place files in a `SPICE-1.0/` directory.
32
+
33
+ ### 2. Install dependencies
34
+
35
+ ```bash
36
+ pip install -r requirements.txt
37
+ ```
38
+
39
+ ### 3. Run the app
40
+
41
+ ```bash
42
+ uvicorn app:app --host 0.0.0.0 --port 7860 --reload
43
+ ```
44
+
45
+ Open [http://localhost:7860](http://localhost:7860) in your browser.
46
+
47
+ ### Docker
48
+
49
+ ```bash
50
+ docker build -t spice-evaluator .
51
+ docker run -p 7860:7860 spice-evaluator
52
+ ```
53
+
54
+ ## Verify Java version
55
+
56
+ ```bash
57
+ java -version
58
+ # Should show: openjdk version "11.x.x"
59
+ ```
60
+
61
+ If you have multiple Java versions, set `JAVA_HOME` to your Java 11 installation.
62
+
63
+ ## License
64
+
65
+ AGPL-3.0 (SPICE and Stanford CoreNLP are GPL v3+ / AGPL v3 compatible)
app.py CHANGED
@@ -1,13 +1,16 @@
1
  import asyncio
2
  import json
3
  import logging
 
4
  import queue
 
 
5
  from contextlib import asynccontextmanager
6
  from typing import Optional
7
 
8
  from fastapi import FastAPI, HTTPException, Request
9
  from fastapi.middleware.cors import CORSMiddleware
10
- from fastapi.responses import StreamingResponse
11
  from fastapi.staticfiles import StaticFiles
12
  from pydantic import BaseModel
13
 
@@ -52,6 +55,44 @@ def health():
52
  return {"status": "ready" if evaluator else "loading"}
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  @app.post("/api/evaluate")
56
  async def evaluate(req: EvaluateRequest):
57
  if not evaluator:
 
1
  import asyncio
2
  import json
3
  import logging
4
+ import os
5
  import queue
6
+ import ssl
7
+ import urllib.request
8
  from contextlib import asynccontextmanager
9
  from typing import Optional
10
 
11
  from fastapi import FastAPI, HTTPException, Request
12
  from fastapi.middleware.cors import CORSMiddleware
13
+ from fastapi.responses import Response, StreamingResponse
14
  from fastapi.staticfiles import StaticFiles
15
  from pydantic import BaseModel
16
 
 
55
  return {"status": "ready" if evaluator else "loading"}
56
 
57
 
58
+ _examples_cache = None
59
+
60
+
61
+ @app.get("/api/examples")
62
+ def get_examples():
63
+ global _examples_cache
64
+ if _examples_cache is None:
65
+ examples_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_input_larger.json")
66
+ if not os.path.exists(examples_path):
67
+ raise HTTPException(404, "Examples data file not found. Place example_input_larger.json next to app.py.")
68
+ with open(examples_path) as f:
69
+ _examples_cache = json.load(f)
70
+ return _examples_cache
71
+
72
+
73
+ @app.get("/api/coco-image/{image_id}")
74
+ async def coco_image(image_id: int):
75
+ """Proxy a COCO image through the server to work around the SSL cert mismatch
76
+ on images.cocodataset.org (cert is issued to s3.amazonaws.com)."""
77
+ padded = str(image_id).zfill(12)
78
+ url = f"https://images.cocodataset.org/train2017/{padded}.jpg"
79
+
80
+ def _fetch():
81
+ ctx = ssl.create_default_context()
82
+ ctx.check_hostname = False
83
+ ctx.verify_mode = ssl.CERT_NONE
84
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
85
+ with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
86
+ return resp.read()
87
+
88
+ loop = asyncio.get_event_loop()
89
+ try:
90
+ data = await loop.run_in_executor(None, _fetch)
91
+ except Exception:
92
+ raise HTTPException(404, f"COCO image {image_id} not found")
93
+ return Response(content=data, media_type="image/jpeg")
94
+
95
+
96
  @app.post("/api/evaluate")
97
  async def evaluate(req: EvaluateRequest):
98
  if not evaluator:
example_input_larger.json ADDED
The diff for this file is too large to render. See raw diff
 
static/index.html CHANGED
@@ -416,7 +416,9 @@
416
  font-size: 0.82rem;
417
  font-weight: 500;
418
  color: var(--primary);
419
- transition: background 0.2s, border-color 0.2s;
 
 
420
  }
421
 
422
  .about-links a:hover {
@@ -439,6 +441,181 @@
439
  display: block;
440
  }
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  footer {
443
  text-align: center;
444
  padding: 16px;
@@ -463,6 +640,101 @@
463
  </header>
464
 
465
  <div class="container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  <!-- Input Section -->
467
  <div class="input-section">
468
  <h2>Caption Inputs</h2>
@@ -483,11 +755,13 @@
483
  </div>
484
  <div class="input-group">
485
  <label for="references">Reference Captions (one per line)</label>
486
- <textarea id="references">a couple of giraffes that are walking around
 
487
  a herd of giraffe standing on top of a dirt field.
488
  Several smaller giraffes that are in an enclosure.
489
  The giraffes are walking in different directions outside.
490
- A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
 
491
  </div>
492
  </div>
493
  <button class="btn-evaluate" id="evalBtn" onclick="runEvaluation()">
@@ -500,8 +774,11 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
500
  <div class="error-banner" id="errorBanner"></div>
501
 
502
  <!-- Logs (above results, shown during evaluation) -->
503
- <div class="collapsible" id="logsSection" style="display: none;">
504
- <div class="collapsible-header" onclick="toggleCollapsible('logsSection')">
 
 
 
505
  Evaluation Logs
506
  <span class="arrow">&#9654;</span>
507
  </div>
@@ -541,14 +818,35 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
541
 
542
  <!-- Tuples -->
543
  <div class="collapsible" id="tuplesSection">
544
- <div class="collapsible-header" onclick="toggleCollapsible('tuplesSection')">
 
 
 
545
  Extracted Tuples
546
  <span class="arrow">&#9654;</span>
547
  </div>
548
  <div class="collapsible-body">
549
- <label style="font-size: 0.82rem; font-weight: 600; color: var(--text-dim); display: block; margin-bottom: 6px;">Candidate Tuples</label>
 
 
 
 
 
 
 
 
 
550
  <div class="tuple-list" id="candidateTuples"></div>
551
- <label style="font-size: 0.82rem; font-weight: 600; color: var(--text-dim); display: block; margin: 12px 0 6px;">Reference Tuples</label>
 
 
 
 
 
 
 
 
 
552
  <div class="tuple-list" id="referenceTuples"></div>
553
  </div>
554
  </div>
@@ -556,31 +854,53 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
556
 
557
  <!-- About (always visible, collapsed) -->
558
  <div class="collapsible open" id="aboutSection">
559
- <div class="collapsible-header" onclick="toggleCollapsible('aboutSection')">
 
 
 
560
  About This Project
561
  <span class="arrow">&#9654;</span>
562
  </div>
563
  <div class="collapsible-body about-body">
564
  <p>
565
- <strong>SPICE Evaluator</strong> is an interactive tool for evaluating image captions using the
566
- <a href="https://panderson.me/spice/" target="_blank">SPICE</a> (Semantic Propositional Image Caption Evaluation) metric.
567
- It computes precision, recall, and F1 scores by comparing semantic scene graphs extracted from candidate and reference captions.
 
 
 
568
  </p>
569
  <p>
570
- The evaluation pipeline invokes the official SPICE-1.0 Java implementation, which uses Stanford CoreNLP for dependency parsing
571
- and scene graph extraction. Extracted tuples are matched using WordNet-based synonym matching.
 
 
572
  </p>
573
  <div class="about-links">
574
- <a href="https://gayanukaa.com/projects/spice-evaluator" target="_blank">Project Page</a>
575
- <a href="https://gayanukaa.com/blog/2025-05-29-understanding-spice-metric" target="_blank">Understanding SPICE - Blog Post</a>
576
- <a href="https://github.com/Gayanukaa/SPICE-Evaluator" target="_blank">Source Code</a>
 
 
 
 
 
 
 
 
 
 
 
 
577
  </div>
578
  </div>
579
  </div>
580
 
581
  <footer>
582
- Developed by <a href="https://gayanukaa.com" target="_blank">Gayanukaa</a>
583
- &middot; Powered by <a href="https://panderson.me/spice/" target="_blank">SPICE</a>
 
 
584
  </footer>
585
  </div>
586
 
@@ -724,13 +1044,83 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
724
  document.getElementById(id).classList.toggle("open");
725
  }
726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  /* ── Graph rendering ───────────────────────────────── */
728
  function renderGraph(containerId, tuples, nodeColor, edgeColor) {
729
  const container = document.getElementById(containerId);
730
  container.innerHTML = "";
731
 
 
 
 
 
 
 
732
  if (!tuples || tuples.length === 0) {
733
- container.innerHTML = '<div class="graph-empty">No tuples to visualize</div>';
 
734
  return;
735
  }
736
 
@@ -752,7 +1142,12 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
752
  for (let i = 0; i < tpl.length - 1; i++) {
753
  const fromId = getNodeId(tpl[i]);
754
  const toId = getNodeId(tpl[i + 1]);
755
- edgesArr.push({ from: fromId, to: toId, color: { color: edgeColor }, arrows: "to" });
 
 
 
 
 
756
  }
757
  }
758
  });
@@ -765,12 +1160,12 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
765
  font: { size: 13, color: "#2c3e50" },
766
  size: 18,
767
  borderWidth: 2,
768
- }))
769
  );
770
 
771
  const edges = new vis.DataSet(edgesArr);
772
 
773
- new vis.Network(
774
  container,
775
  { nodes, edges },
776
  {
@@ -793,8 +1188,12 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
793
  zoomView: true,
794
  dragView: true,
795
  },
796
- }
797
  );
 
 
 
 
798
  }
799
 
800
  /* ── Format tuples for display ─────────────────────── */
@@ -872,7 +1271,8 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
872
  } else if (line.startsWith("data: ")) {
873
  const data = line.slice(6);
874
  if (eventType === "log") {
875
- logOutput.textContent += (logOutput.textContent ? "\n" : "") + data;
 
876
  logOutput.scrollTop = logOutput.scrollHeight;
877
  } else if (eventType === "result") {
878
  resultData = JSON.parse(data);
@@ -892,15 +1292,32 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
892
  logsSection.classList.remove("open");
893
  document.getElementById("aboutSection").classList.remove("open");
894
 
895
- document.getElementById("metricF1").textContent = resultData.spice_f1.toFixed(3);
896
- document.getElementById("metricPrec").textContent = resultData.spice_precision.toFixed(3);
897
- document.getElementById("metricRec").textContent = resultData.spice_recall.toFixed(3);
898
-
899
- renderGraph("candidateGraph", resultData.test_tuples, "#a8d5f2", "#4a6fa5");
900
- renderGraph("referenceGraph", resultData.ref_tuples, "#a8f0c6", "#27ae60");
901
-
902
- document.getElementById("candidateTuples").textContent = formatTuples(resultData.test_tuples);
903
- document.getElementById("referenceTuples").textContent = formatTuples(resultData.ref_tuples);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
 
905
  resultsSection.classList.add("visible");
906
  } catch (e) {
@@ -916,6 +1333,121 @@ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea>
916
  banner.textContent = msg;
917
  banner.classList.add("visible");
918
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  </script>
920
  </body>
921
  </html>
 
416
  font-size: 0.82rem;
417
  font-weight: 500;
418
  color: var(--primary);
419
+ transition:
420
+ background 0.2s,
421
+ border-color 0.2s;
422
  }
423
 
424
  .about-links a:hover {
 
441
  display: block;
442
  }
443
 
444
+ /* ── Gallery ─────────────────────────────────────────── */
445
+ .gallery-card {
446
+ display: flex;
447
+ min-height: 280px;
448
+ }
449
+
450
+ .gallery-img-wrap {
451
+ flex: 0 0 220px;
452
+ background: var(--bg);
453
+ border-right: 1px solid var(--border);
454
+ display: flex;
455
+ flex-direction: column;
456
+ align-items: center;
457
+ justify-content: center;
458
+ overflow: hidden;
459
+ }
460
+
461
+ .gallery-img-wrap img {
462
+ width: 100%;
463
+ height: 240px;
464
+ object-fit: cover;
465
+ display: block;
466
+ }
467
+
468
+ .gallery-img-id {
469
+ font-size: 0.7rem;
470
+ color: var(--text-dim);
471
+ padding: 4px 8px;
472
+ align-self: flex-start;
473
+ }
474
+
475
+ .gallery-img-placeholder {
476
+ display: flex;
477
+ flex-direction: column;
478
+ align-items: center;
479
+ justify-content: center;
480
+ height: 240px;
481
+ width: 100%;
482
+ color: var(--text-dim);
483
+ font-size: 0.8rem;
484
+ gap: 6px;
485
+ }
486
+
487
+ .gallery-captions {
488
+ flex: 1;
489
+ padding: 14px 16px;
490
+ display: flex;
491
+ flex-direction: column;
492
+ gap: 8px;
493
+ overflow: hidden;
494
+ min-width: 0;
495
+ }
496
+
497
+ .gallery-section-label {
498
+ font-size: 0.7rem;
499
+ font-weight: 700;
500
+ text-transform: uppercase;
501
+ letter-spacing: 0.5px;
502
+ color: var(--text-dim);
503
+ margin-bottom: 3px;
504
+ }
505
+
506
+ .gallery-candidate-text {
507
+ font-size: 0.88rem;
508
+ color: var(--text);
509
+ font-style: italic;
510
+ line-height: 1.45;
511
+ background: var(--bg);
512
+ border-radius: 6px;
513
+ padding: 7px 10px;
514
+ border-left: 3px solid var(--accent);
515
+ }
516
+
517
+ .gallery-refs-list {
518
+ list-style: none;
519
+ font-size: 0.78rem;
520
+ color: var(--text-dim);
521
+ line-height: 1.7;
522
+ max-height: 160px;
523
+ overflow-y: auto;
524
+ padding: 0;
525
+ }
526
+
527
+ .gallery-refs-list li::before {
528
+ content: "Β· ";
529
+ color: var(--primary-light);
530
+ font-weight: 700;
531
+ }
532
+
533
+ .gallery-nav {
534
+ display: flex;
535
+ align-items: center;
536
+ gap: 8px;
537
+ padding: 10px 14px;
538
+ border-top: 1px solid var(--border);
539
+ background: var(--bg);
540
+ border-radius: 0 0 var(--radius) var(--radius);
541
+ flex-wrap: wrap;
542
+ }
543
+
544
+ .gallery-btn {
545
+ padding: 5px 13px;
546
+ border: 1px solid var(--border);
547
+ border-radius: 6px;
548
+ background: var(--card-bg);
549
+ color: var(--text);
550
+ font-size: 0.82rem;
551
+ font-weight: 500;
552
+ cursor: pointer;
553
+ transition:
554
+ background 0.15s,
555
+ border-color 0.15s;
556
+ white-space: nowrap;
557
+ }
558
+
559
+ .gallery-btn:hover {
560
+ background: #edf2f8;
561
+ border-color: var(--primary-light);
562
+ }
563
+
564
+ .gallery-btn-load {
565
+ margin-left: auto;
566
+ background: var(--primary);
567
+ color: #fff;
568
+ border-color: var(--primary);
569
+ font-weight: 600;
570
+ }
571
+
572
+ .gallery-btn-load:hover {
573
+ background: var(--primary-light);
574
+ border-color: var(--primary-light);
575
+ }
576
+
577
+ .gallery-counter {
578
+ font-size: 0.82rem;
579
+ color: var(--text-dim);
580
+ white-space: nowrap;
581
+ min-width: 70px;
582
+ text-align: center;
583
+ }
584
+
585
+ .gallery-jump input {
586
+ width: 54px;
587
+ padding: 4px 7px;
588
+ border: 1px solid var(--border);
589
+ border-radius: 6px;
590
+ font-size: 0.82rem;
591
+ color: var(--text);
592
+ background: var(--card-bg);
593
+ text-align: center;
594
+ }
595
+
596
+ .gallery-jump input:focus {
597
+ outline: none;
598
+ border-color: var(--primary-light);
599
+ box-shadow: 0 0 0 2px rgba(74, 111, 165, 0.1);
600
+ }
601
+
602
+ @media (max-width: 600px) {
603
+ .gallery-card {
604
+ flex-direction: column;
605
+ }
606
+ .gallery-img-wrap {
607
+ flex: none;
608
+ border-right: none;
609
+ border-bottom: 1px solid var(--border);
610
+ }
611
+ .gallery-img-wrap img {
612
+ height: 200px;
613
+ }
614
+ .gallery-btn-load {
615
+ margin-left: 0;
616
+ }
617
+ }
618
+
619
  footer {
620
  text-align: center;
621
  padding: 16px;
 
640
  </header>
641
 
642
  <div class="container">
643
+ <!-- MS-COCO Examples Gallery -->
644
+ <div class="collapsible" id="gallerySection">
645
+ <div
646
+ class="collapsible-header"
647
+ onclick="toggleCollapsible('gallerySection')"
648
+ >
649
+ Browse MS-COCO Examples
650
+ <span
651
+ style="
652
+ font-weight: 400;
653
+ color: var(--text-dim);
654
+ font-size: 0.8rem;
655
+ margin-left: 6px;
656
+ "
657
+ id="galleryHeaderCount"
658
+ ></span>
659
+ <span class="arrow">&#9654;</span>
660
+ </div>
661
+ <div class="collapsible-body" style="padding: 0">
662
+ <div
663
+ id="galleryLoading"
664
+ style="padding: 16px; color: var(--text-dim); font-size: 0.85rem"
665
+ >
666
+ Loading examples…
667
+ </div>
668
+ <div id="galleryContent" style="display: none">
669
+ <div class="gallery-card">
670
+ <div class="gallery-img-wrap">
671
+ <div id="galleryImgPlaceholder" class="gallery-img-placeholder">
672
+ <span style="font-size: 1.8rem">πŸ–Ό</span>
673
+ <span>Loading image…</span>
674
+ </div>
675
+ <img id="galleryImg" src="" alt="" style="display: none" />
676
+ <div class="gallery-img-id">
677
+ COCO ID: <span id="galleryImgId">β€”</span>
678
+ </div>
679
+ </div>
680
+ <div class="gallery-captions">
681
+ <div>
682
+ <div class="gallery-section-label">Candidate Caption</div>
683
+ <div id="galleryCandText" class="gallery-candidate-text">
684
+ β€”
685
+ </div>
686
+ </div>
687
+ <div style="flex: 1; min-height: 0">
688
+ <div class="gallery-section-label">Reference Captions</div>
689
+ <ul id="galleryRefsList" class="gallery-refs-list"></ul>
690
+ </div>
691
+ </div>
692
+ </div>
693
+ <div class="gallery-nav">
694
+ <button
695
+ class="gallery-btn"
696
+ onclick="galleryNav(-1)"
697
+ title="Previous (←)"
698
+ >
699
+ β—€ Prev
700
+ </button>
701
+ <button
702
+ class="gallery-btn"
703
+ onclick="galleryRandom()"
704
+ title="Random example"
705
+ >
706
+ βš„ Random
707
+ </button>
708
+ <div class="gallery-counter">
709
+ <span id="galleryCurr">β€”</span> / <span id="galleryTot">β€”</span>
710
+ </div>
711
+ <div class="gallery-jump">
712
+ <input
713
+ type="number"
714
+ id="galleryJumpInput"
715
+ min="1"
716
+ placeholder="#"
717
+ title="Jump to example number"
718
+ />
719
+ </div>
720
+ <button
721
+ class="gallery-btn"
722
+ onclick="galleryNav(1)"
723
+ title="Next (β†’)"
724
+ >
725
+ Next β–Ά
726
+ </button>
727
+ <button
728
+ class="gallery-btn gallery-btn-load"
729
+ onclick="galleryLoad()"
730
+ >
731
+ Use this Example β†’
732
+ </button>
733
+ </div>
734
+ </div>
735
+ </div>
736
+ </div>
737
+
738
  <!-- Input Section -->
739
  <div class="input-section">
740
  <h2>Caption Inputs</h2>
 
755
  </div>
756
  <div class="input-group">
757
  <label for="references">Reference Captions (one per line)</label>
758
+ <textarea id="references">
759
+ a couple of giraffes that are walking around
760
  a herd of giraffe standing on top of a dirt field.
761
  Several smaller giraffes that are in an enclosure.
762
  The giraffes are walking in different directions outside.
763
+ A giraffe standing next to three baby giraffes in a zoo exhibit.</textarea
764
+ >
765
  </div>
766
  </div>
767
  <button class="btn-evaluate" id="evalBtn" onclick="runEvaluation()">
 
774
  <div class="error-banner" id="errorBanner"></div>
775
 
776
  <!-- Logs (above results, shown during evaluation) -->
777
+ <div class="collapsible" id="logsSection" style="display: none">
778
+ <div
779
+ class="collapsible-header"
780
+ onclick="toggleCollapsible('logsSection')"
781
+ >
782
  Evaluation Logs
783
  <span class="arrow">&#9654;</span>
784
  </div>
 
818
 
819
  <!-- Tuples -->
820
  <div class="collapsible" id="tuplesSection">
821
+ <div
822
+ class="collapsible-header"
823
+ onclick="toggleCollapsible('tuplesSection')"
824
+ >
825
  Extracted Tuples
826
  <span class="arrow">&#9654;</span>
827
  </div>
828
  <div class="collapsible-body">
829
+ <label
830
+ style="
831
+ font-size: 0.82rem;
832
+ font-weight: 600;
833
+ color: var(--text-dim);
834
+ display: block;
835
+ margin-bottom: 6px;
836
+ "
837
+ >Candidate Tuples</label
838
+ >
839
  <div class="tuple-list" id="candidateTuples"></div>
840
+ <label
841
+ style="
842
+ font-size: 0.82rem;
843
+ font-weight: 600;
844
+ color: var(--text-dim);
845
+ display: block;
846
+ margin: 12px 0 6px;
847
+ "
848
+ >Reference Tuples</label
849
+ >
850
  <div class="tuple-list" id="referenceTuples"></div>
851
  </div>
852
  </div>
 
854
 
855
  <!-- About (always visible, collapsed) -->
856
  <div class="collapsible open" id="aboutSection">
857
+ <div
858
+ class="collapsible-header"
859
+ onclick="toggleCollapsible('aboutSection')"
860
+ >
861
  About This Project
862
  <span class="arrow">&#9654;</span>
863
  </div>
864
  <div class="collapsible-body about-body">
865
  <p>
866
+ <strong>SPICE Evaluator</strong> is an interactive tool for
867
+ evaluating image captions using the
868
+ <a href="https://panderson.me/spice/" target="_blank">SPICE</a>
869
+ (Semantic Propositional Image Caption Evaluation) metric. It
870
+ computes precision, recall, and F1 scores by comparing semantic
871
+ scene graphs extracted from candidate and reference captions.
872
  </p>
873
  <p>
874
+ The evaluation pipeline invokes the official SPICE-1.0 Java
875
+ implementation, which uses Stanford CoreNLP for dependency parsing
876
+ and scene graph extraction. Extracted tuples are matched using
877
+ WordNet-based synonym matching.
878
  </p>
879
  <div class="about-links">
880
+ <a
881
+ href="https://gayanukaa.com/projects/spice-evaluator"
882
+ target="_blank"
883
+ >Project Page</a
884
+ >
885
+ <a
886
+ href="https://gayanukaa.com/blog/2025-05-29-understanding-spice-metric"
887
+ target="_blank"
888
+ >Understanding SPICE - Blog Post</a
889
+ >
890
+ <a
891
+ href="https://github.com/Gayanukaa/SPICE-Evaluator"
892
+ target="_blank"
893
+ >Source Code</a
894
+ >
895
  </div>
896
  </div>
897
  </div>
898
 
899
  <footer>
900
+ Developed by
901
+ <a href="https://gayanukaa.com" target="_blank">Gayanukaa</a> &middot;
902
+ Powered by
903
+ <a href="https://panderson.me/spice/" target="_blank">SPICE</a>
904
  </footer>
905
  </div>
906
 
 
1044
  document.getElementById(id).classList.toggle("open");
1045
  }
1046
 
1047
+ /* ── Graph auto-reset management ───────────────────── */
1048
+ const graphResetTimers = {};
1049
+ const graphInstances = {};
1050
+ const graphInitialStates = {};
1051
+
1052
+ function setupAutoReset(network, containerId) {
1053
+ let resetTimer = null;
1054
+
1055
+ const resetToInitial = () => {
1056
+ if (graphInitialStates[containerId]) {
1057
+ const { positions, scale, viewPosition } =
1058
+ graphInitialStates[containerId];
1059
+
1060
+ // Restore node positions
1061
+ network.body.data.nodes.update(
1062
+ Object.keys(positions).map((nodeId) => ({
1063
+ id: parseInt(nodeId),
1064
+ x: positions[nodeId].x,
1065
+ y: positions[nodeId].y,
1066
+ })),
1067
+ );
1068
+
1069
+ // Restore view with animation
1070
+ network.moveTo({
1071
+ position: viewPosition,
1072
+ scale: scale,
1073
+ animation: {
1074
+ duration: 800,
1075
+ easingFunction: "easeInOutQuad",
1076
+ },
1077
+ });
1078
+ }
1079
+ };
1080
+
1081
+ const scheduleReset = () => {
1082
+ clearTimeout(resetTimer);
1083
+ resetTimer = setTimeout(resetToInitial, 3000);
1084
+ };
1085
+
1086
+ // Capture initial state once physics stabilizes
1087
+ network.once("stabilized", () => {
1088
+ const positions = network.getPositions();
1089
+ const viewPosition = network.getViewPosition();
1090
+ const scale = network.getScale();
1091
+
1092
+ graphInitialStates[containerId] = {
1093
+ positions,
1094
+ viewPosition,
1095
+ scale,
1096
+ };
1097
+ });
1098
+
1099
+ // Listen for user interactions
1100
+ network.on("zoom", scheduleReset);
1101
+ network.on("dragStart", scheduleReset);
1102
+ network.on("dragging", () => {
1103
+ clearTimeout(resetTimer);
1104
+ });
1105
+ network.on("dragEnd", scheduleReset);
1106
+
1107
+ graphResetTimers[containerId] = resetTimer;
1108
+ }
1109
+
1110
  /* ── Graph rendering ───────────────────────────────── */
1111
  function renderGraph(containerId, tuples, nodeColor, edgeColor) {
1112
  const container = document.getElementById(containerId);
1113
  container.innerHTML = "";
1114
 
1115
+ // Clear any existing timer
1116
+ if (graphResetTimers[containerId]) {
1117
+ clearTimeout(graphResetTimers[containerId]);
1118
+ delete graphResetTimers[containerId];
1119
+ }
1120
+
1121
  if (!tuples || tuples.length === 0) {
1122
+ container.innerHTML =
1123
+ '<div class="graph-empty">No tuples to visualize</div>';
1124
  return;
1125
  }
1126
 
 
1142
  for (let i = 0; i < tpl.length - 1; i++) {
1143
  const fromId = getNodeId(tpl[i]);
1144
  const toId = getNodeId(tpl[i + 1]);
1145
+ edgesArr.push({
1146
+ from: fromId,
1147
+ to: toId,
1148
+ color: { color: edgeColor },
1149
+ arrows: "to",
1150
+ });
1151
  }
1152
  }
1153
  });
 
1160
  font: { size: 13, color: "#2c3e50" },
1161
  size: 18,
1162
  borderWidth: 2,
1163
+ })),
1164
  );
1165
 
1166
  const edges = new vis.DataSet(edgesArr);
1167
 
1168
+ const network = new vis.Network(
1169
  container,
1170
  { nodes, edges },
1171
  {
 
1188
  zoomView: true,
1189
  dragView: true,
1190
  },
1191
+ },
1192
  );
1193
+
1194
+ // Store network instance and setup auto-reset
1195
+ graphInstances[containerId] = network;
1196
+ setupAutoReset(network, containerId);
1197
  }
1198
 
1199
  /* ── Format tuples for display ─────────────────────── */
 
1271
  } else if (line.startsWith("data: ")) {
1272
  const data = line.slice(6);
1273
  if (eventType === "log") {
1274
+ logOutput.textContent +=
1275
+ (logOutput.textContent ? "\n" : "") + data;
1276
  logOutput.scrollTop = logOutput.scrollHeight;
1277
  } else if (eventType === "result") {
1278
  resultData = JSON.parse(data);
 
1292
  logsSection.classList.remove("open");
1293
  document.getElementById("aboutSection").classList.remove("open");
1294
 
1295
+ document.getElementById("metricF1").textContent =
1296
+ resultData.spice_f1.toFixed(3);
1297
+ document.getElementById("metricPrec").textContent =
1298
+ resultData.spice_precision.toFixed(3);
1299
+ document.getElementById("metricRec").textContent =
1300
+ resultData.spice_recall.toFixed(3);
1301
+
1302
+ renderGraph(
1303
+ "candidateGraph",
1304
+ resultData.test_tuples,
1305
+ "#a8d5f2",
1306
+ "#4a6fa5",
1307
+ );
1308
+ renderGraph(
1309
+ "referenceGraph",
1310
+ resultData.ref_tuples,
1311
+ "#a8f0c6",
1312
+ "#27ae60",
1313
+ );
1314
+
1315
+ document.getElementById("candidateTuples").textContent = formatTuples(
1316
+ resultData.test_tuples,
1317
+ );
1318
+ document.getElementById("referenceTuples").textContent = formatTuples(
1319
+ resultData.ref_tuples,
1320
+ );
1321
 
1322
  resultsSection.classList.add("visible");
1323
  } catch (e) {
 
1333
  banner.textContent = msg;
1334
  banner.classList.add("visible");
1335
  }
1336
+
1337
+ /* ── MS-COCO Examples Gallery ───────────────────────── */
1338
+ let galleryExamples = [];
1339
+ let galleryIdx = 0;
1340
+
1341
+ function cocoImageUrl(imageId) {
1342
+ // Proxy through the backend: images.cocodataset.org cert is for s3.amazonaws.com
1343
+ // and the images live under train2017/, not val2017/
1344
+ return `/api/coco-image/${imageId}`;
1345
+ }
1346
+
1347
+ async function loadGallery() {
1348
+ try {
1349
+ const res = await fetch("/api/examples");
1350
+ if (!res.ok) throw new Error("HTTP " + res.status);
1351
+ galleryExamples = await res.json();
1352
+
1353
+ document.getElementById("galleryLoading").style.display = "none";
1354
+ document.getElementById("galleryContent").style.display = "block";
1355
+ document.getElementById("galleryTot").textContent =
1356
+ galleryExamples.length;
1357
+ document.getElementById("galleryHeaderCount").textContent = "";
1358
+
1359
+ const jumpInput = document.getElementById("galleryJumpInput");
1360
+ jumpInput.max = galleryExamples.length;
1361
+ jumpInput.addEventListener("keydown", (e) => {
1362
+ if (e.key === "Enter") {
1363
+ const n = parseInt(jumpInput.value, 10);
1364
+ if (n >= 1 && n <= galleryExamples.length) galleryShow(n - 1);
1365
+ }
1366
+ });
1367
+ jumpInput.addEventListener("change", () => {
1368
+ const n = parseInt(jumpInput.value, 10);
1369
+ if (n >= 1 && n <= galleryExamples.length) galleryShow(n - 1);
1370
+ });
1371
+
1372
+ // Start at a random example for variety
1373
+ galleryShow(Math.floor(Math.random() * galleryExamples.length));
1374
+ } catch (e) {
1375
+ document.getElementById("galleryLoading").textContent =
1376
+ "Could not load examples (" + e.message + ").";
1377
+ }
1378
+ }
1379
+
1380
+ function galleryShow(idx) {
1381
+ if (!galleryExamples.length) return;
1382
+ idx =
1383
+ ((idx % galleryExamples.length) + galleryExamples.length) %
1384
+ galleryExamples.length;
1385
+ galleryIdx = idx;
1386
+
1387
+ const ex = galleryExamples[idx];
1388
+ document.getElementById("galleryCurr").textContent = idx + 1;
1389
+ document.getElementById("galleryImgId").textContent = ex.image_id;
1390
+ document.getElementById("galleryCandText").textContent = ex.test;
1391
+ document.getElementById("galleryJumpInput").value = idx + 1;
1392
+
1393
+ const refsList = document.getElementById("galleryRefsList");
1394
+ refsList.innerHTML = ex.refs.map((r) => `<li>${r}</li>`).join("");
1395
+
1396
+ // Image with placeholder while loading
1397
+ const img = document.getElementById("galleryImg");
1398
+ const placeholder = document.getElementById("galleryImgPlaceholder");
1399
+ img.style.display = "none";
1400
+ placeholder.style.display = "flex";
1401
+ placeholder.innerHTML =
1402
+ '<span style="font-size:1.8rem;">⏳</span><span>Loading…</span>';
1403
+
1404
+ const url = cocoImageUrl(ex.image_id);
1405
+ const probe = new Image();
1406
+ probe.onload = () => {
1407
+ img.src = url;
1408
+ img.style.display = "block";
1409
+ placeholder.style.display = "none";
1410
+ };
1411
+ probe.onerror = () => {
1412
+ placeholder.innerHTML =
1413
+ '<span style="font-size:1.5rem;">🚫</span><span>Image unavailable</span>';
1414
+ };
1415
+ probe.src = url;
1416
+ }
1417
+
1418
+ function galleryNav(dir) {
1419
+ galleryShow(galleryIdx + dir);
1420
+ }
1421
+
1422
+ function galleryRandom() {
1423
+ galleryShow(Math.floor(Math.random() * galleryExamples.length));
1424
+ }
1425
+
1426
+ function galleryLoad() {
1427
+ if (!galleryExamples.length) return;
1428
+ const ex = galleryExamples[galleryIdx];
1429
+ document.getElementById("candidate").value = ex.test;
1430
+ document.getElementById("references").value = ex.refs.join("\n");
1431
+ document
1432
+ .querySelector(".input-section")
1433
+ .scrollIntoView({ behavior: "smooth" });
1434
+ }
1435
+
1436
+ // Keyboard arrow navigation (when not typing in an input)
1437
+ document.addEventListener("keydown", (e) => {
1438
+ const tag = document.activeElement.tagName;
1439
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1440
+ if (e.key === "ArrowLeft") {
1441
+ e.preventDefault();
1442
+ galleryNav(-1);
1443
+ }
1444
+ if (e.key === "ArrowRight") {
1445
+ e.preventDefault();
1446
+ galleryNav(1);
1447
+ }
1448
+ });
1449
+
1450
+ loadGallery();
1451
  </script>
1452
  </body>
1453
  </html>