Mike0021 commited on
Commit
d2e7054
·
verified ·
1 Parent(s): 624f114

Switch to gr.Server with custom semantic studio frontend

Browse files
Files changed (5) hide show
  1. README.md +12 -4
  2. app.py +167 -712
  3. frontend/app.css +271 -0
  4. frontend/app.js +497 -0
  5. frontend/index.html +245 -0
README.md CHANGED
@@ -22,7 +22,7 @@ preload_from_hub:
22
 
23
  # WeMM Semantic Universe
24
 
25
- An immersive, retrieval-first showcase for [Tencent WeMM-Embedding-9B](https://huggingface.co/tencent/WeMM-Embedding-9B), a universal multimodal embedding model built on Qwen3.5.
26
 
27
  The Space demonstrates the model as a shared semantic geometry rather than a single similarity score:
28
 
@@ -31,13 +31,14 @@ The Space demonstrates the model as a shared semantic geometry rather than a sin
31
  - Add a custom mixed-media candidate collection alongside the curated universe.
32
  - Inspect rankings at every native Matryoshka size: 64, 128, 256, 512, 1,024, 2,048, and 4,096 dimensions.
33
  - Compare any two supported inputs in the Vector Microscope.
34
- - Explore compressed vector fingerprints and a structured API payload.
 
35
 
36
  ## Runtime design
37
 
38
  The model loads once on CPU at startup. A ZeroGPU allocation moves it to GPU only for inference, then returns it to CPU. The built-in candidate universe is embedded on the first search and cached as normalized 4,096-dimensional CPU tensors; all later dimension choices use lossless prefix truncation followed by re-normalization, without re-encoding the corpus.
39
 
40
- The first curated search is therefore slower than warm searches. The five `gr.Examples` are cached lazily so opening the Space does not consume GPU quota.
41
 
42
  Live ZeroGPU validation measured 17.7 seconds for the cold 10-item multimodal corpus pass and 8.7 seconds for a video-plus-text to text retrieval pass. The callable GPU budgets use a 30-second heavy-path allowance and a 15-second light-path allowance; actual latency still varies with media length and queue conditions.
43
 
@@ -45,13 +46,20 @@ This repository targets `zero-a10g` (the current 48 GB ZeroGPU allocation). `sug
45
 
46
  ## API
47
 
48
- Once deployed, open **Use via API** in the Gradio footer to inspect the generated client signatures.
49
 
50
  - `/search` ranks a mixed candidate collection from a text/image/video query.
51
  - `/compare` compares a query and candidate across every native Matryoshka dimension.
52
 
53
  Always call `Client.view_api()` before invoking either endpoint so the client uses the deployed schema.
54
 
 
 
 
 
 
 
 
55
  ## Score semantics
56
 
57
  Every output is an L2-normalized embedding. The displayed dot products are therefore cosine similarities. They are ranking signals within a candidate set—not calibrated probabilities or universal relevance grades.
 
22
 
23
  # WeMM Semantic Universe
24
 
25
+ An immersive, retrieval-first showcase for [Tencent WeMM-Embedding-9B](https://huggingface.co/tencent/WeMM-Embedding-9B), a universal multimodal embedding model built on Qwen3.5. The interface is a fully custom HTML/CSS/JavaScript application served by `gr.Server`; Gradio provides the queued API engine and ZeroGPU integration without rendering the UI.
26
 
27
  The Space demonstrates the model as a shared semantic geometry rather than a single similarity score:
28
 
 
31
  - Add a custom mixed-media candidate collection alongside the curated universe.
32
  - Inspect rankings at every native Matryoshka size: 64, 128, 256, 512, 1,024, 2,048, and 4,096 dimensions.
33
  - Compare any two supported inputs in the Vector Microscope.
34
+ - Explore ranked media cards, Matryoshka stability curves, compressed vector fingerprints, and structured API telemetry.
35
+ - Launch five one-click curated expeditions or drag in your own visual query and candidate set.
36
 
37
  ## Runtime design
38
 
39
  The model loads once on CPU at startup. A ZeroGPU allocation moves it to GPU only for inference, then returns it to CPU. The built-in candidate universe is embedded on the first search and cached as normalized 4,096-dimensional CPU tensors; all later dimension choices use lossless prefix truncation followed by re-normalization, without re-encoding the corpus.
40
 
41
+ The first curated search is therefore slower than warm searches. The five curated expeditions are implemented in the custom frontend and only invoke the model when selected, so opening the Space does not consume GPU quota.
42
 
43
  Live ZeroGPU validation measured 17.7 seconds for the cold 10-item multimodal corpus pass and 8.7 seconds for a video-plus-text to text retrieval pass. The callable GPU budgets use a 30-second heavy-path allowance and a 15-second light-path allowance; actual latency still varies with media length and queue conditions.
44
 
 
46
 
47
  ## API
48
 
49
+ The custom frontend calls the queued endpoints through `@gradio/client`, which preserves Hugging Face iframe authentication and ZeroGPU quota handling. The generated schema is available at `/gradio_api/info` (also linked as **API** in the top bar).
50
 
51
  - `/search` ranks a mixed candidate collection from a text/image/video query.
52
  - `/compare` compares a query and candidate across every native Matryoshka dimension.
53
 
54
  Always call `Client.view_api()` before invoking either endpoint so the client uses the deployed schema.
55
 
56
+ ## Interface architecture
57
+
58
+ - `app.py` owns model loading, ZeroGPU functions, structured `/search` and `/compare` endpoints, and static routes.
59
+ - `frontend/index.html` contains the semantic-search and vector-microscope application shell.
60
+ - `frontend/app.css` provides the responsive visual system and motion.
61
+ - `frontend/app.js` manages uploads, curated expeditions, queued client calls, and data visualizations.
62
+
63
  ## Score semantics
64
 
65
  Every output is an L2-normalized embedding. The displayed dot products are therefore cosine similarities. They are ranking signals within a candidate set—not calibrated probabilities or universal relevance grades.
app.py CHANGED
@@ -1,20 +1,16 @@
1
  import os
2
 
3
  # ZeroGPU and library caches must be configured before importing spaces/torch.
4
- EXAMPLE_CACHE_VERSION = "2026-08-26-a"
5
  os.environ.setdefault("HF_HOME", os.path.expanduser("~/.cache/huggingface"))
6
  os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
7
  os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
8
- os.environ.setdefault("GRADIO_EXAMPLES_CACHE", f"/tmp/gradio_cached_examples/{EXAMPLE_CACHE_VERSION}")
9
  os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
10
  os.environ.setdefault("GRADIO_SSR_MODE", "false")
11
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
12
 
13
  import spaces
14
 
15
- import html
16
  import logging
17
- import math
18
  import threading
19
  import time
20
  from dataclasses import dataclass
@@ -24,12 +20,16 @@ from typing import Any
24
  import gradio as gr
25
  import torch
26
  import torch.nn.functional as F
 
 
 
27
  from sentence_transformers import SentenceTransformer
28
 
29
 
30
  MODEL_ID = "tencent/WeMM-Embedding-9B"
31
  ROOT = Path(__file__).resolve().parent
32
  ASSET_DIR = ROOT / "assets"
 
33
  MATRYOSHKA_DIMS = (64, 128, 256, 512, 1024, 2048, 4096)
34
  MAX_CUSTOM_TEXTS = 6
35
  MAX_CUSTOM_MEDIA = 6
@@ -147,7 +147,7 @@ _SHOWCASE_EMBEDDINGS: torch.Tensor | None = None
147
 
148
 
149
  def _file_path(value: Any) -> str | None:
150
- """Normalize Gradio file values across UI and API representations."""
151
  if value is None:
152
  return None
153
  if isinstance(value, (str, Path)):
@@ -161,13 +161,7 @@ def _file_path(value: Any) -> str | None:
161
 
162
  def _is_video(path: str) -> bool:
163
  return Path(path.split("?", 1)[0]).suffix.lower() in {
164
- ".mp4",
165
- ".webm",
166
- ".mov",
167
- ".mkv",
168
- ".avi",
169
- ".mpeg",
170
- ".mpg",
171
  }
172
 
173
 
@@ -178,13 +172,9 @@ def _multimodal_payload(text: str | None, image: Any, video: Any) -> tuple[Any,
178
  if image_path and video_path:
179
  raise gr.Error("Choose one visual query: an image or a video, not both.")
180
  if image_path:
181
- if text:
182
- return {"image": image_path, "text": text}, "image + text"
183
- return image_path, "image"
184
  if video_path:
185
- if text:
186
- return {"video": video_path, "text": text}, "video + text"
187
- return video_path, "video"
188
  if text:
189
  return text, "text"
190
  raise gr.Error("Add a text, image, or video query to begin.")
@@ -201,17 +191,8 @@ def _parse_text_candidates(raw: str | None) -> list[Candidate]:
201
  title = title or f"Custom text {index}"
202
  body = body or title
203
  else:
204
- title = f"Custom text {index}"
205
- body = line
206
- candidates.append(
207
- Candidate(
208
- f"custom-text-{index}",
209
- title[:80],
210
- "text",
211
- body[:240],
212
- body,
213
- )
214
- )
215
  return candidates
216
 
217
 
@@ -221,21 +202,14 @@ def _parse_media_candidates(raw: Any) -> list[Candidate]:
221
  if len(items) > MAX_CUSTOM_MEDIA:
222
  raise gr.Error(f"Upload at most {MAX_CUSTOM_MEDIA} candidate media files.")
223
  for index, item in enumerate(items, start=1):
224
- media = item[0] if isinstance(item, (tuple, list)) else item
225
- caption = item[1] if isinstance(item, (tuple, list)) and len(item) > 1 else None
226
- path = _file_path(media)
227
  if not path:
228
  continue
229
  kind = "video" if _is_video(path) else "image"
230
- title = (caption or f"Uploaded {kind} {index}").strip()
231
  candidates.append(
232
  Candidate(
233
- f"custom-media-{index}",
234
- title[:80],
235
- kind,
236
- f"User-supplied {kind} candidate.",
237
- path,
238
- path,
239
  )
240
  )
241
  return candidates
@@ -245,11 +219,8 @@ def _encode(items: list[Any], *, query: bool) -> torch.Tensor:
245
  method = MODEL.encode_query if query else MODEL.encode_document
246
  with torch.inference_mode():
247
  embeddings = method(
248
- items,
249
- batch_size=1,
250
- convert_to_tensor=True,
251
- normalize_embeddings=True,
252
- show_progress_bar=False,
253
  )
254
  if embeddings.ndim == 1:
255
  embeddings = embeddings.unsqueeze(0)
@@ -269,6 +240,22 @@ def _dimension_scores(query: torch.Tensor, documents: torch.Tensor) -> dict[int,
269
  return scores
270
 
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  def _search_duration(*args: Any, **kwargs: Any) -> int:
273
  """Budget more time for the cold corpus pass and video queries."""
274
  query_payload = args[0] if args else None
@@ -301,9 +288,9 @@ def _run_search(
301
  try:
302
  progress(0.16, desc=f"Encoding the {query_kind} query")
303
  query_embedding = _encode([query_payload], query=True)
304
-
305
  document_blocks: list[torch.Tensor] = []
306
  candidates: list[Candidate] = []
 
307
  if include_showcase:
308
  with _CACHE_LOCK:
309
  cached = _SHOWCASE_EMBEDDINGS
@@ -335,230 +322,11 @@ def _run_search(
335
  torch.cuda.empty_cache()
336
 
337
 
338
- def _score_tone(score: float) -> str:
339
- if score >= 0.70:
340
- return "high"
341
- if score >= 0.40:
342
- return "mid"
343
- return "low"
344
-
345
-
346
- def _render_summary(
347
- query_kind: str,
348
- dimension: int,
349
- candidate_count: int,
350
- elapsed: float,
351
- was_cold: bool,
352
- top_title: str,
353
- top_score: float,
354
- ) -> str:
355
- cache_note = "cold corpus map" if was_cold else "warm corpus cache"
356
- return f"""
357
- <section class="run-summary">
358
- <div class="run-kicker"><span class="live-dot"></span> semantic field resolved</div>
359
- <div class="run-main">
360
- <div><span class="run-label">Top match</span><strong>{html.escape(top_title)}</strong></div>
361
- <div class="hero-score"><span>{top_score:+.3f}</span><small>cosine</small></div>
362
- </div>
363
- <div class="run-meta">
364
- <span>{html.escape(query_kind)}</span><i></i>
365
- <span>{dimension:,}D</span><i></i>
366
- <span>{candidate_count} candidates</span><i></i>
367
- <span>{elapsed:.1f}s</span><i></i>
368
- <span>{cache_note}</span>
369
- </div>
370
- </section>
371
- """
372
-
373
-
374
- def _render_rankings(ranked: list[tuple[Candidate, float]]) -> str:
375
- rows: list[str] = []
376
- for rank, (candidate, score) in enumerate(ranked, start=1):
377
- fill = min(100.0, max(2.0, max(0.0, score) * 100.0))
378
- rows.append(
379
- f"""
380
- <article class="rank-row {'winner' if rank == 1 else ''}">
381
- <div class="rank-number">{rank:02d}</div>
382
- <div class="rank-copy">
383
- <div class="rank-title-line">
384
- <strong>{html.escape(candidate.title)}</strong>
385
- <span class="kind-pill">{html.escape(candidate.kind)}</span>
386
- </div>
387
- <p>{html.escape(candidate.description)}</p>
388
- <div class="score-track"><span style="width:{fill:.1f}%"></span></div>
389
- </div>
390
- <div class="rank-score { _score_tone(score) }">{score:+.3f}<small>cos</small></div>
391
- </article>
392
- """
393
- )
394
- return '<div class="ranking-stack">' + "".join(rows) + "</div>"
395
-
396
-
397
- def _render_curve(series: dict[str, list[float]], title: str) -> str:
398
- width, height = 820, 300
399
- left, right, top, bottom = 58, 20, 28, 48
400
- plot_w, plot_h = width - left - right, height - top - bottom
401
- all_values = [value for values in series.values() for value in values]
402
- low = max(-1.0, min(all_values) - 0.08)
403
- high = min(1.0, max(all_values) + 0.08)
404
- if high - low < 0.2:
405
- midpoint = (high + low) / 2
406
- low, high = max(-1.0, midpoint - 0.1), min(1.0, midpoint + 0.1)
407
-
408
- def x_at(index: int) -> float:
409
- return left + index * plot_w / (len(MATRYOSHKA_DIMS) - 1)
410
-
411
- def y_at(value: float) -> float:
412
- return top + (high - value) * plot_h / max(1e-8, high - low)
413
-
414
- grid: list[str] = []
415
- for tick in range(5):
416
- value = high - tick * (high - low) / 4
417
- y = y_at(value)
418
- grid.append(
419
- f'<line x1="{left}" y1="{y:.1f}" x2="{width-right}" y2="{y:.1f}" class="chart-grid" />'
420
- f'<text x="{left-10}" y="{y+4:.1f}" text-anchor="end" class="chart-axis">{value:+.2f}</text>'
421
- )
422
- for index, dimension in enumerate(MATRYOSHKA_DIMS):
423
- x = x_at(index)
424
- grid.append(f'<text x="{x:.1f}" y="{height-18}" text-anchor="middle" class="chart-axis">{dimension}</text>')
425
-
426
- colors = ("#ffb86b", "#78e8df", "#a994ff", "#ff7aa2", "#9ad45b")
427
- paths: list[str] = []
428
- legend: list[str] = []
429
- for series_index, (name, values) in enumerate(series.items()):
430
- color = colors[series_index % len(colors)]
431
- points = " ".join(f"{x_at(i):.1f},{y_at(value):.1f}" for i, value in enumerate(values))
432
- circles = "".join(
433
- f'<circle cx="{x_at(i):.1f}" cy="{y_at(value):.1f}" r="3.5" fill="{color}" />'
434
- for i, value in enumerate(values)
435
- )
436
- paths.append(f'<polyline points="{points}" fill="none" stroke="{color}" stroke-width="3" />{circles}')
437
- legend.append(
438
- f'<span><b style="background:{color}"></b>{html.escape(name[:38])}</span>'
439
- )
440
-
441
- return f"""
442
- <section class="viz-card">
443
- <div class="viz-heading"><div><small>MATRYOSHKA SCOPE</small><h3>{html.escape(title)}</h3></div><span>64 → 4096 dimensions</span></div>
444
- <svg class="dimension-chart" viewBox="0 0 {width} {height}" role="img" aria-label="Similarity by embedding dimension">
445
- {''.join(grid)}{''.join(paths)}
446
- </svg>
447
- <div class="chart-legend">{''.join(legend)}</div>
448
- <p class="viz-note">Cosine similarity at every native truncation size. Compare trends, not universal thresholds.</p>
449
- </section>
450
- """
451
-
452
-
453
- def _fingerprint_svg(vector: torch.Tensor, label: str) -> str:
454
- values = vector.detach().float().flatten()
455
- bar_count = min(96, values.numel())
456
- chunks = torch.tensor_split(values, bar_count)
457
- samples = [float(chunk.mean()) for chunk in chunks]
458
- scale = max(max(abs(value) for value in samples), 1e-6)
459
- width, height = 800, 210
460
- center = 104
461
- bar_w = (width - 24) / bar_count
462
- bars: list[str] = []
463
- for index, value in enumerate(samples):
464
- magnitude = min(84.0, abs(value) / scale * 84.0)
465
- x = 12 + index * bar_w
466
- y = center - magnitude if value >= 0 else center
467
- color = "#70e4da" if value >= 0 else "#ff9d57"
468
- bars.append(
469
- f'<rect x="{x:.1f}" y="{y:.1f}" width="{max(1.2, bar_w-1.5):.1f}" height="{magnitude:.1f}" rx="1.5" fill="{color}" opacity=".9" />'
470
- )
471
- return f"""
472
- <section class="viz-card fingerprint-card">
473
- <div class="viz-heading"><div><small>VECTOR FINGERPRINT</small><h3>{html.escape(label)}</h3></div><span>{values.numel():,} values</span></div>
474
- <svg class="fingerprint" viewBox="0 0 {width} {height}" role="img" aria-label="Compressed signed embedding fingerprint">
475
- <line x1="12" y1="{center}" x2="{width-12}" y2="{center}" class="zero-line" />
476
- {''.join(bars)}
477
- </svg>
478
- <div class="fingerprint-key"><span><b class="positive"></b>positive</span><span><b class="negative"></b>negative</span><em>96 pooled slices · shape, not magnitude</em></div>
479
- </section>
480
- """
481
-
482
-
483
- def search_experience(
484
- query_text: str,
485
- query_image: Any,
486
- query_video: Any,
487
- custom_texts: str,
488
- candidate_media: Any,
489
- include_showcase: bool,
490
- dimension: int,
491
- progress: gr.Progress = gr.Progress(track_tqdm=True),
492
- ) -> tuple[str, str, list[tuple[str, str]], str, str, dict[str, Any]]:
493
- """Search a mixed text/image/video collection with a multimodal query."""
494
- dimension = int(dimension)
495
- if dimension not in MATRYOSHKA_DIMS:
496
- raise gr.Error("Choose one of the model's native Matryoshka dimensions.")
497
- query_payload, query_kind = _multimodal_payload(query_text, query_image, query_video)
498
- custom_candidates = _parse_text_candidates(custom_texts) + _parse_media_candidates(candidate_media)
499
-
500
- with _INFERENCE_LOCK:
501
- query, documents, candidates, elapsed, was_cold = _run_search(
502
- query_payload,
503
- query_kind,
504
- custom_candidates,
505
- bool(include_showcase),
506
- dimension,
507
- progress,
508
- )
509
- dimension_map = _dimension_scores(query, documents)
510
- chosen_scores = dimension_map[dimension]
511
- order = sorted(range(len(candidates)), key=lambda index: chosen_scores[index], reverse=True)
512
- ranked = [(candidates[index], float(chosen_scores[index])) for index in order]
513
-
514
- top_candidate, top_score = ranked[0]
515
- summary = _render_summary(
516
- query_kind,
517
- dimension,
518
- len(candidates),
519
- elapsed,
520
- was_cold,
521
- top_candidate.title,
522
- top_score,
523
- )
524
- rankings = _render_rankings(ranked[:8])
525
- gallery = [
526
- (candidate.media_path, f"#{rank} · {candidate.title} · cosine {score:+.3f}")
527
- for rank, (candidate, score) in enumerate(ranked, start=1)
528
- if candidate.media_path
529
- ][:8]
530
-
531
- curve_series: dict[str, list[float]] = {}
532
- for index in order[:4]:
533
- curve_series[candidates[index].title] = [dimension_map[dim][index] for dim in MATRYOSHKA_DIMS]
534
- curve = _render_curve(curve_series, "Does the ranking survive compression?")
535
- fingerprint = _fingerprint_svg(query[0, :dimension], f"Query · {query_kind} · {dimension:,}D")
536
-
537
- diagnostics = {
538
- "model": MODEL_ID,
539
- "query_modality": query_kind,
540
- "selected_dimension": dimension,
541
- "full_embedding_dimension": int(query.shape[-1]),
542
- "l2_norm_after_truncation": round(float(_truncate_normalize(query, dimension).norm()), 6),
543
- "candidate_count": len(candidates),
544
- "gpu_pass_seconds": round(elapsed, 3),
545
- "showcase_cache": "created" if was_cold else "reused" if include_showcase else "not_requested",
546
- "top_matches": [
547
- {"rank": rank, "title": item.title, "modality": item.kind, "cosine": round(score, 6)}
548
- for rank, (item, score) in enumerate(ranked[:5], start=1)
549
- ],
550
- "query_vector_preview": [round(float(value), 6) for value in query[0, :12]],
551
- "note": "Cosine similarity is a ranking signal, not a calibrated probability.",
552
- }
553
- return summary, rankings, gallery, curve, fingerprint, diagnostics
554
-
555
-
556
  def _pair_duration(*args: Any, **kwargs: Any) -> int:
557
- payloads = args[:2]
558
  has_video = any(
559
  (isinstance(value, dict) and bool(value.get("video")))
560
  or (isinstance(value, str) and _is_video(value))
561
- for value in payloads
562
  )
563
  return 30 if has_video else 15
564
 
@@ -594,467 +362,154 @@ def _interpret_score(score: float) -> tuple[str, str]:
594
  return "low alignment", "The model places these inputs relatively far apart."
595
 
596
 
597
- def _render_pair_score(score: float, dimension: int, query_kind: str, candidate_kind: str, elapsed: float) -> str:
598
- label, explanation = _interpret_score(score)
599
- ring = min(100.0, max(0.0, (score + 1.0) * 50.0))
600
- return f"""
601
- <section class="pair-score-card">
602
- <div class="score-orbit" style="--score:{ring:.2f}">
603
- <div><strong>{score:+.3f}</strong><span>cosine</span></div>
604
- </div>
605
- <div class="pair-score-copy">
606
- <small>PAIRWISE READOUT</small>
607
- <h2>{html.escape(label)}</h2>
608
- <p>{html.escape(explanation)}</p>
609
- <div class="run-meta"><span>{html.escape(query_kind)}</span><i></i><span>{html.escape(candidate_kind)}</span><i></i><span>{dimension:,}D</span><i></i><span>{elapsed:.1f}s</span></div>
610
- </div>
611
- </section>
612
- """
613
-
614
-
615
- def compare_experience(
616
- query_text: str,
617
- query_image: Any,
618
- query_video: Any,
619
- candidate_text: str,
620
- candidate_image: Any,
621
- candidate_video: Any,
622
- dimension: int,
623
  progress: gr.Progress = gr.Progress(track_tqdm=True),
624
- ) -> tuple[str, str, str, dict[str, Any]]:
625
- """Compare any two supported inputs across all Matryoshka dimensions."""
626
  dimension = int(dimension)
 
 
 
627
  query_payload, query_kind = _multimodal_payload(query_text, query_image, query_video)
628
- candidate_payload, candidate_kind = _multimodal_payload(candidate_text, candidate_image, candidate_video)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
  with _INFERENCE_LOCK:
630
  query, candidate, elapsed = _run_pair(query_payload, candidate_payload, progress)
631
- scores_by_dimension = {
 
632
  dim: float((_truncate_normalize(query, dim) @ _truncate_normalize(candidate, dim).T).item())
633
  for dim in MATRYOSHKA_DIMS
634
  }
635
- selected_score = scores_by_dimension[dimension]
636
- score_card = _render_pair_score(selected_score, dimension, query_kind, candidate_kind, elapsed)
637
- curve = _render_curve(
638
- {f"{query_kind} → {candidate_kind}": [scores_by_dimension[dim] for dim in MATRYOSHKA_DIMS]},
639
- "Semantic alignment under compression",
640
- )
641
- fingerprints = (
642
- '<div class="fingerprint-pair">'
643
- + _fingerprint_svg(query[0, :dimension], f"Query · {query_kind}")
644
- + _fingerprint_svg(candidate[0, :dimension], f"Candidate · {candidate_kind}")
645
- + "</div>"
646
- )
647
- diagnostics = {
648
  "model": MODEL_ID,
649
- "query_modality": query_kind,
650
- "candidate_modality": candidate_kind,
651
- "selected_dimension": dimension,
652
- "selected_cosine": round(selected_score, 6),
653
- "cosine_by_dimension": {str(dim): round(score, 6) for dim, score in scores_by_dimension.items()},
654
- "gpu_pass_seconds": round(elapsed, 3),
 
 
 
 
 
655
  "note": "Interpret thresholds relative to a task-specific candidate set.",
656
  }
657
- return score_card, curve, fingerprints, diagnostics
658
-
659
-
660
- CSS = """
661
- :root {
662
- --ink: #f6f3ec;
663
- --muted: #a9abb5;
664
- --panel: rgba(17, 20, 26, .82);
665
- --line: rgba(255, 255, 255, .10);
666
- --warm: #ffad66;
667
- --cool: #71e2da;
668
- --violet: #a994ff;
669
- }
670
-
671
- body, .gradio-container {
672
- background:
673
- radial-gradient(circle at 13% 0%, rgba(255, 143, 68, .16), transparent 30rem),
674
- radial-gradient(circle at 92% 13%, rgba(88, 218, 211, .11), transparent 34rem),
675
- #090b10 !important;
676
- color: var(--ink) !important;
677
- }
678
- .gradio-container { max-width: 1380px !important; padding: 0 28px 60px !important; }
679
- .gradio-container * { box-sizing: border-box; }
680
- .gradio-container .prose { color: var(--ink); }
681
- .gradio-container label, .gradio-container .label-wrap { color: #d7d7dc !important; }
682
- .gradio-container input, .gradio-container textarea {
683
- background: rgba(7, 9, 13, .72) !important;
684
- border-color: rgba(255,255,255,.12) !important;
685
- color: #f8f6f1 !important;
686
- }
687
- .gradio-container .block, .gradio-container .form {
688
- border-color: var(--line) !important;
689
- }
690
-
691
- #hero { padding: 76px 4px 38px; }
692
- .hero-shell { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); padding-bottom: 44px; }
693
- .eyebrow { display:flex; align-items:center; gap:10px; color:var(--cool); font-size:12px; font-weight:700; letter-spacing:.18em; text-transform:uppercase; }
694
- .eyebrow:before { content:""; width:26px; height:1px; background:var(--cool); box-shadow:0 0 14px var(--cool); }
695
- .hero-title { margin: 18px 0 8px; font-size: clamp(52px, 8vw, 112px); line-height:.88; letter-spacing:-.075em; font-weight:760; }
696
- .hero-title .accent { color:transparent; -webkit-text-stroke:1px rgba(255,255,255,.62); }
697
- .hero-title .dot { color:var(--warm); text-shadow:0 0 42px rgba(255,173,102,.6); }
698
- .hero-sub { max-width:760px; margin:24px 0 0; font-size:clamp(17px,2vw,23px); line-height:1.55; color:#c0c1c8; }
699
- .hero-grid { display:grid; grid-template-columns:1fr auto; align-items:end; gap:30px; }
700
- .hero-stats { display:grid; grid-template-columns:repeat(2,minmax(110px,1fr)); gap:1px; background:var(--line); border:1px solid var(--line); min-width:350px; }
701
- .hero-stats div { background:rgba(9,11,16,.88); padding:18px 20px; }
702
- .hero-stats strong { display:block; font-size:28px; line-height:1; color:#fff; letter-spacing:-.04em; }
703
- .hero-stats span { display:block; margin-top:8px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.12em; }
704
- .capability-rail { display:flex; gap:8px; flex-wrap:wrap; margin-top:26px; }
705
- .capability-rail span { border:1px solid var(--line); border-radius:99px; padding:7px 12px; color:#c9c9cf; font-size:12px; background:rgba(255,255,255,.025); }
706
-
707
- .section-intro { margin:34px 0 18px; }
708
- .section-intro small, .viz-heading small, .pair-score-copy small { color:var(--warm); letter-spacing:.16em; font-weight:750; font-size:11px; }
709
- .section-intro h2 { font-size:30px; letter-spacing:-.035em; margin:6px 0; }
710
- .section-intro p { color:var(--muted); margin:0; max-width:760px; }
711
-
712
- .input-panel, .output-panel { background:linear-gradient(145deg,rgba(22,25,32,.9),rgba(12,14,19,.86)) !important; border:1px solid var(--line) !important; border-radius:18px !important; padding:18px !important; box-shadow:0 22px 70px rgba(0,0,0,.24); }
713
- .primary-action { min-height:52px !important; border:0 !important; color:#16110d !important; font-weight:800 !important; letter-spacing:.01em; background:linear-gradient(105deg,#ff8e54,#ffd187) !important; box-shadow:0 10px 30px rgba(255,142,84,.2) !important; }
714
- .primary-action:hover { transform:translateY(-1px); filter:brightness(1.04); }
715
-
716
- .run-summary { border:1px solid rgba(112,228,218,.22); border-radius:18px; padding:22px 24px; background:linear-gradient(120deg,rgba(31,48,48,.54),rgba(17,19,25,.94)); margin-bottom:16px; }
717
- .run-kicker { color:var(--cool); font-size:11px; text-transform:uppercase; letter-spacing:.16em; font-weight:750; }
718
- .live-dot { display:inline-block; width:7px; height:7px; border-radius:99px; background:var(--cool); box-shadow:0 0 15px var(--cool); margin-right:8px; }
719
- .run-main { display:flex; align-items:flex-end; justify-content:space-between; gap:20px; margin:14px 0 18px; }
720
- .run-label { display:block; color:var(--muted); font-size:12px; margin-bottom:5px; }
721
- .run-main strong { font-size:clamp(23px,3vw,38px); letter-spacing:-.04em; }
722
- .hero-score { text-align:right; }
723
- .hero-score span { display:block; font-size:40px; color:var(--cool); font-variant-numeric:tabular-nums; letter-spacing:-.05em; }
724
- .hero-score small { color:var(--muted); text-transform:uppercase; letter-spacing:.14em; }
725
- .run-meta { display:flex; align-items:center; gap:10px; flex-wrap:wrap; color:#aeb0b8; font-size:12px; }
726
- .run-meta i { display:block; width:3px; height:3px; border-radius:99px; background:#555963; }
727
-
728
- .ranking-stack { display:grid; gap:9px; }
729
- .rank-row { display:grid; grid-template-columns:48px 1fr 80px; gap:16px; align-items:center; padding:15px 17px; border:1px solid var(--line); border-radius:14px; background:rgba(255,255,255,.025); transition:.2s ease; }
730
- .rank-row:hover { transform:translateX(3px); border-color:rgba(255,173,102,.28); background:rgba(255,255,255,.04); }
731
- .rank-row.winner { border-color:rgba(255,173,102,.35); background:linear-gradient(100deg,rgba(255,150,85,.11),rgba(255,255,255,.025)); }
732
- .rank-number { color:#6d7079; font-size:14px; font-variant-numeric:tabular-nums; }
733
- .rank-title-line { display:flex; gap:9px; align-items:center; flex-wrap:wrap; }
734
- .rank-title-line strong { font-size:16px; color:#f7f3ec; }
735
- .kind-pill { font-size:9px; letter-spacing:.09em; text-transform:uppercase; color:#bfc1c8; border:1px solid var(--line); border-radius:99px; padding:4px 7px; }
736
- .rank-copy p { margin:4px 0 9px; color:#91949e; font-size:12px; line-height:1.45; }
737
- .score-track { height:2px; background:rgba(255,255,255,.06); overflow:hidden; }
738
- .score-track span { display:block; height:100%; background:linear-gradient(90deg,var(--warm),var(--cool)); }
739
- .rank-score { text-align:right; font-size:18px; font-variant-numeric:tabular-nums; color:#b9bbc2; }
740
- .rank-score.high { color:var(--cool); }.rank-score.mid { color:var(--warm); }
741
- .rank-score small { display:block; font-size:8px; color:#747780; letter-spacing:.14em; margin-top:3px; text-transform:uppercase; }
742
-
743
- .viz-card { border:1px solid var(--line); border-radius:18px; padding:20px; background:rgba(15,17,23,.78); overflow:hidden; }
744
- .viz-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
745
- .viz-heading h3 { margin:5px 0 0; font-size:20px; letter-spacing:-.025em; }
746
- .viz-heading > span { color:#8f929d; font-size:11px; border:1px solid var(--line); border-radius:99px; padding:6px 9px; }
747
- .dimension-chart, .fingerprint { width:100%; height:auto; overflow:visible; }
748
- .chart-grid { stroke:rgba(255,255,255,.07); stroke-width:1; }
749
- .chart-axis { fill:#777b85; font-size:10px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
750
- .chart-legend { display:flex; flex-wrap:wrap; gap:8px 16px; }
751
- .chart-legend span { color:#aeb0b8; font-size:11px; }
752
- .chart-legend b { display:inline-block; width:7px; height:7px; border-radius:99px; margin-right:6px; }
753
- .viz-note { margin:14px 0 0; color:#6f727c; font-size:11px; }
754
- .zero-line { stroke:rgba(255,255,255,.18); stroke-width:1; }
755
- .fingerprint-key { display:flex; gap:14px; align-items:center; flex-wrap:wrap; color:#848791; font-size:10px; text-transform:uppercase; letter-spacing:.08em; }
756
- .fingerprint-key b { display:inline-block; width:7px; height:7px; border-radius:2px; margin-right:5px; }.fingerprint-key .positive{background:var(--cool)}.fingerprint-key .negative{background:var(--warm)}
757
- .fingerprint-key em { margin-left:auto; text-transform:none; letter-spacing:0; color:#6e717a; }
758
- .fingerprint-pair { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
759
-
760
- .pair-score-card { display:grid; grid-template-columns:190px 1fr; gap:30px; align-items:center; border:1px solid rgba(169,148,255,.24); border-radius:20px; padding:26px; background:linear-gradient(125deg,rgba(50,40,80,.35),rgba(14,17,23,.92)); }
761
- .score-orbit { width:170px; aspect-ratio:1; border-radius:50%; padding:2px; background:conic-gradient(var(--violet) calc(var(--score)*1%),rgba(255,255,255,.07) 0); box-shadow:0 0 55px rgba(169,148,255,.12); }
762
- .score-orbit > div { width:100%; height:100%; border-radius:50%; background:#0d0f15; display:flex; flex-direction:column; align-items:center; justify-content:center; }
763
- .score-orbit strong { font-size:38px; letter-spacing:-.05em; font-variant-numeric:tabular-nums; }.score-orbit span{color:#777b85;font-size:10px;text-transform:uppercase;letter-spacing:.14em}
764
- .pair-score-copy h2 { margin:6px 0 8px; font-size:34px; letter-spacing:-.045em; }.pair-score-copy p{color:#aeb0b8;max-width:580px}
765
-
766
- .model-note { margin:28px 0 0; padding:18px 20px; border-left:2px solid var(--warm); background:rgba(255,173,102,.05); color:#9ea0aa; font-size:12px; line-height:1.6; }
767
- .space-footer { display:flex; justify-content:space-between; align-items:center; gap:20px; flex-wrap:wrap; border-top:1px solid var(--line); padding:28px 4px 0; margin-top:44px; color:#767984; font-size:12px; }
768
- .space-footer a { color:#c4c6cd !important; text-decoration:none; }.space-footer a:hover{color:var(--warm)!important}
769
-
770
- @media (max-width: 900px) {
771
- .gradio-container { padding:0 14px 40px !important; }
772
- #hero { padding-top:44px; }
773
- .hero-grid { grid-template-columns:1fr; }
774
- .hero-stats { min-width:0; width:100%; }
775
- .fingerprint-pair { grid-template-columns:1fr; }
776
- }
777
- @media (max-width: 620px) {
778
- .hero-title { font-size:50px; }
779
- .hero-stats { grid-template-columns:1fr 1fr; }
780
- .rank-row { grid-template-columns:34px 1fr 64px; gap:8px; padding:12px 10px; }
781
- .rank-copy p { display:none; }
782
- .pair-score-card { grid-template-columns:1fr; text-align:center; }
783
- .score-orbit { margin:auto; }.pair-score-copy .run-meta{justify-content:center}
784
- }
785
- """
786
-
787
-
788
- HERO = """
789
- <div id="hero" class="hero-shell">
790
- <div class="eyebrow">Tencent WeMM · multimodal embedding</div>
791
- <div class="hero-grid">
792
- <div>
793
- <h1 class="hero-title">One space<span class="accent">.<br>Every medium</span><span class="dot">.</span></h1>
794
- <p class="hero-sub">Search meaning—not file types—across text, images, video, charts, and visual documents in one shared semantic geometry.</p>
795
- <div class="capability-rail"><span>text ↔ image</span><span>text ↔ video</span><span>visual documents</span><span>interleaved inputs</span><span>Matryoshka embeddings</span></div>
796
- </div>
797
- <div class="hero-stats">
798
- <div><strong>4096</strong><span>native dimensions</span></div>
799
- <div><strong>9B</strong><span>parameters</span></div>
800
- <div><strong>80.6</strong><span>MMEB-v2 avg</span></div>
801
- <div><strong>190</strong><span>MMEB-v3 tasks</span></div>
802
- </div>
803
- </div>
804
- </div>
805
- """
806
-
807
-
808
- INTRO_SEARCH = """
809
- <div class="section-intro">
810
- <small>01 / RETRIEVAL UNIVERSE</small>
811
- <h2>Ask in one modality. Discover in another.</h2>
812
- <p>Search the built-in field of screenshots, figures, dense documents, video, and multilingual text—or bring your own candidates.</p>
813
- </div>
814
- """
815
-
816
-
817
- INTRO_PAIR = """
818
- <div class="section-intro">
819
- <small>02 / VECTOR MICROSCOPE</small>
820
- <h2>Put any two ideas under the lens.</h2>
821
- <p>Use a query and candidate as text, image, video, or visual-plus-text. Then watch their alignment change as the embedding compresses.</p>
822
- </div>
823
- """
824
-
825
-
826
- INITIAL_SUMMARY = """
827
- <section class="run-summary">
828
- <div class="run-kicker"><span class="live-dot"></span> model ready</div>
829
- <div class="run-main"><div><span class="run-label">Awaiting a query</span><strong>Search across media</strong></div><div class="hero-score"><span>—</span><small>cosine</small></div></div>
830
- <div class="run-meta"><span>text</span><i></i><span>image</span><i></i><span>video</span><i></i><span>visual documents</span></div>
831
- </section>
832
- """
833
-
834
-
835
- INITIAL_RANKING = """
836
- <div class="model-note">Choose a curated example below or compose a multimodal query. The first run maps the showcase corpus once; later searches reuse its CPU-cached 4,096D vectors.</div>
837
- """
838
-
839
-
840
- with gr.Blocks(css=CSS, title="WeMM · Multimodal Embedding Universe", fill_width=True) as demo:
841
- gr.HTML(HERO)
842
-
843
- with gr.Tabs():
844
- with gr.Tab("Search the universe", id="search"):
845
- gr.HTML(INTRO_SEARCH)
846
- with gr.Row(equal_height=False):
847
- with gr.Column(scale=5, elem_classes="input-panel"):
848
- query_text = gr.Textbox(
849
- label="Query · text",
850
- placeholder="Try: Which document explains temporary road closures?",
851
- lines=3,
852
- max_lines=7,
853
- )
854
- with gr.Row():
855
- query_image = gr.Image(
856
- label="Query · image (optional)",
857
- type="filepath",
858
- sources=["upload", "clipboard", "webcam"],
859
- height=220,
860
- )
861
- query_video = gr.Video(
862
- label="Query · video (optional)",
863
- format="mp4",
864
- height=220,
865
- )
866
- gr.Markdown("Add text to an image or video to create a joint multimodal query.")
867
- with gr.Accordion("Build your own candidate collection", open=False):
868
- custom_texts = gr.Textbox(
869
- label="Text candidates",
870
- placeholder="Title :: Candidate text\nAnother title :: Another candidate",
871
- lines=5,
872
- info=f"One candidate per line, up to {MAX_CUSTOM_TEXTS}.",
873
- )
874
- candidate_media = gr.Gallery(
875
- label="Image + video candidates",
876
- type="filepath",
877
- file_types=["image", "video"],
878
- sources=["upload"],
879
- columns=3,
880
- height=250,
881
- )
882
- include_showcase = gr.Checkbox(
883
- value=True,
884
- label="Include the curated multimodal universe",
885
- info="10 candidates spanning text, images, video, figures, and visual documents.",
886
- )
887
- dimension = gr.Radio(
888
- choices=list(MATRYOSHKA_DIMS),
889
- value=1024,
890
- label="Embedding budget",
891
- info="Native Matryoshka dimensions; smaller vectors trade storage for fidelity.",
892
- )
893
- search_button = gr.Button("Map the semantic field →", variant="primary", elem_classes="primary-action")
894
-
895
- with gr.Column(scale=7, elem_classes="output-panel"):
896
- search_summary = gr.HTML(INITIAL_SUMMARY)
897
- ranking_output = gr.HTML(INITIAL_RANKING)
898
-
899
- with gr.Row(equal_height=False):
900
- result_gallery = gr.Gallery(
901
- value=[
902
- (str(ASSET_DIR / "llama4_hgf.png"), "Visual document · Llama 4 model card"),
903
- (str(ASSET_DIR / "doc2.jpg"), "Visual document · 1971 budget infographic"),
904
- (str(ASSET_DIR / "mapo_tofu.mp4"), "Video · Mapo tofu in motion"),
905
- (str(ASSET_DIR / "doc4.jpg"), "Visual document · road-safety assessment"),
906
- ],
907
- label="Ranked visual field",
908
- columns=4,
909
- rows=2,
910
- height=430,
911
- object_fit="contain",
912
- interactive=False,
913
- buttons=["fullscreen", "download_all"],
914
- )
915
- with gr.Row(equal_height=False):
916
- dimension_output = gr.HTML('<div class="viz-card"><div class="viz-heading"><div><small>MATRYOSHKA SCOPE</small><h3>Dimension stability appears here</h3></div></div></div>')
917
- fingerprint_output = gr.HTML('<div class="viz-card"><div class="viz-heading"><div><small>VECTOR FINGERPRINT</small><h3>Your query vector appears here</h3></div></div></div>')
918
- with gr.Accordion("Embedding telemetry · inspect the API payload", open=False):
919
- diagnostics_output = gr.JSON(label="Diagnostics")
920
-
921
- example_inputs = [
922
- query_text,
923
- query_image,
924
- query_video,
925
- custom_texts,
926
- candidate_media,
927
- include_showcase,
928
- dimension,
929
- ]
930
- example_outputs = [
931
- search_summary,
932
- ranking_output,
933
- result_gallery,
934
- dimension_output,
935
- fingerprint_output,
936
- diagnostics_output,
937
- ]
938
- gr.Examples(
939
- examples=[
940
- ["Which Llama 4 model variants are available?", None, None, "", None, True, 512],
941
- ["How is mapo tofu prepared?", None, None, "", None, True, 1024],
942
- ["Find the environmental assessment page about driver training and temporary road closures.", None, None, "", None, True, 256],
943
- [
944
- "Match this screenshot to the most relevant description.",
945
- str(ASSET_DIR / "llama4_hgf.png"),
946
- None,
947
- "Llama family :: Scout and Maverick are multimodal mixture-of-experts model variants.\nRecipe :: Soft tofu simmered in spicy chili-bean sauce.",
948
- None,
949
- False,
950
- 256,
951
- ],
952
- [
953
- "What dish is being prepared in this clip?",
954
- None,
955
- str(ASSET_DIR / "mapo_tofu.mp4"),
956
- "Sichuan classic :: Mapo tofu combines soft tofu with a spicy, numbing bean-paste sauce.\nSpaceflight :: A launch vehicle carries a satellite into orbit.",
957
- None,
958
- False,
959
- 512,
960
- ],
961
- ],
962
- inputs=example_inputs,
963
- outputs=example_outputs,
964
- fn=search_experience,
965
- cache_examples=True,
966
- cache_mode="lazy",
967
- label="Curated expeditions",
968
- example_labels=[
969
- "Find Llama 4 across a screenshot",
970
- "Search a cooking video with text",
971
- "Retrieve a dense safety document",
972
- "Match an image to text candidates",
973
- "Match a video to text candidates",
974
- ],
975
- )
976
 
977
- search_event = search_button.click(
978
- fn=search_experience,
979
- inputs=example_inputs,
980
- outputs=example_outputs,
981
- api_name="search",
982
- api_description="Rank a mixed text/image/video collection using WeMM-Embedding-9B.",
983
- concurrency_limit=1,
984
- concurrency_id="wemm_gpu",
985
- time_limit=300,
986
- scroll_to_output=True,
987
- )
988
- query_text.submit(
989
- fn=search_experience,
990
- inputs=example_inputs,
991
- outputs=example_outputs,
992
- api_name=None,
993
- api_visibility="private",
994
- concurrency_limit=1,
995
- concurrency_id="wemm_gpu",
996
- time_limit=300,
997
- scroll_to_output=True,
998
- )
999
 
1000
- with gr.Tab("Compare two ideas", id="compare"):
1001
- gr.HTML(INTRO_PAIR)
1002
- with gr.Row(equal_height=False):
1003
- with gr.Column(elem_classes="input-panel"):
1004
- gr.Markdown("### A · Query")
1005
- pair_query_text = gr.Textbox(label="Text", placeholder="Describe or contextualize the query", lines=3)
1006
- with gr.Row():
1007
- pair_query_image = gr.Image(label="Image", type="filepath", height=210)
1008
- pair_query_video = gr.Video(label="Video", format="mp4", height=210)
1009
- with gr.Column(elem_classes="input-panel"):
1010
- gr.Markdown("### B · Candidate")
1011
- pair_candidate_text = gr.Textbox(label="Text", placeholder="Describe or contextualize the candidate", lines=3)
1012
- with gr.Row():
1013
- pair_candidate_image = gr.Image(label="Image", type="filepath", height=210)
1014
- pair_candidate_video = gr.Video(label="Video", format="mp4", height=210)
1015
- pair_dimension = gr.Radio(
1016
- choices=list(MATRYOSHKA_DIMS),
1017
- value=1024,
1018
- label="Embedding budget",
1019
- )
1020
- compare_button = gr.Button("Measure semantic alignment →", variant="primary", elem_classes="primary-action")
1021
- pair_score_output = gr.HTML('<div class="model-note">Add one modality on each side. You may pair visual media with text context.</div>')
1022
- pair_curve_output = gr.HTML('<div class="viz-card"><div class="viz-heading"><div><small>MATRYOSHKA SCOPE</small><h3>Alignment by dimension appears here</h3></div></div></div>')
1023
- pair_fingerprints_output = gr.HTML()
1024
- with gr.Accordion("Pairwise telemetry", open=False):
1025
- pair_diagnostics_output = gr.JSON(label="Diagnostics")
1026
-
1027
- pair_inputs = [
1028
- pair_query_text,
1029
- pair_query_image,
1030
- pair_query_video,
1031
- pair_candidate_text,
1032
- pair_candidate_image,
1033
- pair_candidate_video,
1034
- pair_dimension,
1035
- ]
1036
- compare_button.click(
1037
- fn=compare_experience,
1038
- inputs=pair_inputs,
1039
- outputs=[pair_score_output, pair_curve_output, pair_fingerprints_output, pair_diagnostics_output],
1040
- api_name="compare",
1041
- api_description="Compare a multimodal query and candidate across every native Matryoshka dimension.",
1042
- concurrency_limit=1,
1043
- concurrency_id="wemm_gpu",
1044
- time_limit=240,
1045
- scroll_to_output=True,
1046
- )
1047
 
1048
- gr.HTML(
1049
- """
1050
- <div class="model-note"><strong>Read scores comparatively.</strong> Cosine similarity is useful for ranking candidates within a collection; it is not a calibrated confidence or a universal relevance grade. Audio is not supported by WeMM-Embedding-9B.</div>
1051
- <footer class="space-footer"><span>WeMM-Embedding-9B · Apache-2.0 · built on Qwen3.5</span><span><a href="https://huggingface.co/tencent/WeMM-Embedding-9B" target="_blank">Model card ↗</a>&nbsp;&nbsp;&nbsp;<a href="https://arxiv.org/abs/2608.24053" target="_blank">Technical report ↗</a></span></footer>
1052
- """
1053
- )
 
 
 
 
 
1054
 
1055
 
1056
  if __name__ == "__main__":
1057
- demo.queue(default_concurrency_limit=1, max_size=24).launch(
1058
- allowed_paths=[str(ASSET_DIR)],
1059
- show_error=True,
1060
- )
 
1
  import os
2
 
3
  # ZeroGPU and library caches must be configured before importing spaces/torch.
 
4
  os.environ.setdefault("HF_HOME", os.path.expanduser("~/.cache/huggingface"))
5
  os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
6
  os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
 
7
  os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
8
  os.environ.setdefault("GRADIO_SSR_MODE", "false")
9
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
10
 
11
  import spaces
12
 
 
13
  import logging
 
14
  import threading
15
  import time
16
  from dataclasses import dataclass
 
20
  import gradio as gr
21
  import torch
22
  import torch.nn.functional as F
23
+ from fastapi.responses import HTMLResponse
24
+ from fastapi.staticfiles import StaticFiles
25
+ from gradio.data_classes import FileData
26
  from sentence_transformers import SentenceTransformer
27
 
28
 
29
  MODEL_ID = "tencent/WeMM-Embedding-9B"
30
  ROOT = Path(__file__).resolve().parent
31
  ASSET_DIR = ROOT / "assets"
32
+ FRONTEND_DIR = ROOT / "frontend"
33
  MATRYOSHKA_DIMS = (64, 128, 256, 512, 1024, 2048, 4096)
34
  MAX_CUSTOM_TEXTS = 6
35
  MAX_CUSTOM_MEDIA = 6
 
147
 
148
 
149
  def _file_path(value: Any) -> str | None:
150
+ """Normalize Gradio file values across browser and programmatic clients."""
151
  if value is None:
152
  return None
153
  if isinstance(value, (str, Path)):
 
161
 
162
  def _is_video(path: str) -> bool:
163
  return Path(path.split("?", 1)[0]).suffix.lower() in {
164
+ ".mp4", ".webm", ".mov", ".mkv", ".avi", ".mpeg", ".mpg"
 
 
 
 
 
 
165
  }
166
 
167
 
 
172
  if image_path and video_path:
173
  raise gr.Error("Choose one visual query: an image or a video, not both.")
174
  if image_path:
175
+ return ({"image": image_path, "text": text}, "image + text") if text else (image_path, "image")
 
 
176
  if video_path:
177
+ return ({"video": video_path, "text": text}, "video + text") if text else (video_path, "video")
 
 
178
  if text:
179
  return text, "text"
180
  raise gr.Error("Add a text, image, or video query to begin.")
 
191
  title = title or f"Custom text {index}"
192
  body = body or title
193
  else:
194
+ title, body = f"Custom text {index}", line
195
+ candidates.append(Candidate(f"custom-text-{index}", title[:80], "text", body[:240], body))
 
 
 
 
 
 
 
 
 
196
  return candidates
197
 
198
 
 
202
  if len(items) > MAX_CUSTOM_MEDIA:
203
  raise gr.Error(f"Upload at most {MAX_CUSTOM_MEDIA} candidate media files.")
204
  for index, item in enumerate(items, start=1):
205
+ path = _file_path(item)
 
 
206
  if not path:
207
  continue
208
  kind = "video" if _is_video(path) else "image"
 
209
  candidates.append(
210
  Candidate(
211
+ f"custom-media-{index}", f"Uploaded {kind} {index}", kind,
212
+ f"User-supplied {kind} candidate.", path, path,
 
 
 
 
213
  )
214
  )
215
  return candidates
 
219
  method = MODEL.encode_query if query else MODEL.encode_document
220
  with torch.inference_mode():
221
  embeddings = method(
222
+ items, batch_size=1, convert_to_tensor=True,
223
+ normalize_embeddings=True, show_progress_bar=False,
 
 
 
224
  )
225
  if embeddings.ndim == 1:
226
  embeddings = embeddings.unsqueeze(0)
 
240
  return scores
241
 
242
 
243
+ def _fingerprint(vector: torch.Tensor, bar_count: int = 96) -> list[float]:
244
+ values = vector.detach().float().flatten()
245
+ chunks = torch.tensor_split(values, min(bar_count, values.numel()))
246
+ return [round(float(chunk.mean()), 7) for chunk in chunks]
247
+
248
+
249
+ def _public_media(candidate: Candidate) -> str | None:
250
+ if not candidate.media_path:
251
+ return None
252
+ try:
253
+ relative = Path(candidate.media_path).resolve().relative_to(ASSET_DIR.resolve())
254
+ except ValueError:
255
+ return None
256
+ return f"/assets/{relative.as_posix()}"
257
+
258
+
259
  def _search_duration(*args: Any, **kwargs: Any) -> int:
260
  """Budget more time for the cold corpus pass and video queries."""
261
  query_payload = args[0] if args else None
 
288
  try:
289
  progress(0.16, desc=f"Encoding the {query_kind} query")
290
  query_embedding = _encode([query_payload], query=True)
 
291
  document_blocks: list[torch.Tensor] = []
292
  candidates: list[Candidate] = []
293
+
294
  if include_showcase:
295
  with _CACHE_LOCK:
296
  cached = _SHOWCASE_EMBEDDINGS
 
322
  torch.cuda.empty_cache()
323
 
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  def _pair_duration(*args: Any, **kwargs: Any) -> int:
 
326
  has_video = any(
327
  (isinstance(value, dict) and bool(value.get("video")))
328
  or (isinstance(value, str) and _is_video(value))
329
+ for value in args[:2]
330
  )
331
  return 30 if has_video else 15
332
 
 
362
  return "low alignment", "The model places these inputs relatively far apart."
363
 
364
 
365
+ app = gr.Server(
366
+ title="WeMM Semantic Universe API",
367
+ summary="Custom multimodal retrieval studio powered by tencent/WeMM-Embedding-9B",
368
+ version="2.0.0",
369
+ )
370
+
371
+
372
+ @app.api(
373
+ name="search",
374
+ description="Rank a mixed text/image/video collection using WeMM-Embedding-9B.",
375
+ concurrency_limit=1,
376
+ concurrency_id="wemm_gpu",
377
+ time_limit=300,
378
+ )
379
+ def search_api(
380
+ query_text: str = "",
381
+ query_image: FileData | None = None,
382
+ query_video: FileData | None = None,
383
+ custom_texts: str = "",
384
+ candidate_media: list[FileData] | None = None,
385
+ include_showcase: bool = True,
386
+ dimension: int = 1024,
 
 
 
 
387
  progress: gr.Progress = gr.Progress(track_tqdm=True),
388
+ ) -> dict[str, Any]:
389
+ """Search a mixed media collection and return structured visualization data."""
390
  dimension = int(dimension)
391
+ if dimension not in MATRYOSHKA_DIMS:
392
+ raise gr.Error("Choose one of the model's native Matryoshka dimensions.")
393
+
394
  query_payload, query_kind = _multimodal_payload(query_text, query_image, query_video)
395
+ custom_candidates = _parse_text_candidates(custom_texts) + _parse_media_candidates(candidate_media)
396
+ with _INFERENCE_LOCK:
397
+ query, documents, candidates, elapsed, was_cold = _run_search(
398
+ query_payload, query_kind, custom_candidates, bool(include_showcase), dimension, progress,
399
+ )
400
+
401
+ dimension_map = _dimension_scores(query, documents)
402
+ chosen_scores = dimension_map[dimension]
403
+ order = sorted(range(len(candidates)), key=lambda index: chosen_scores[index], reverse=True)
404
+ ranked = [
405
+ {
406
+ "rank": rank,
407
+ "key": candidates[index].key,
408
+ "title": candidates[index].title,
409
+ "kind": candidates[index].kind,
410
+ "description": candidates[index].description,
411
+ "score": round(float(chosen_scores[index]), 7),
412
+ "media_url": _public_media(candidates[index]),
413
+ }
414
+ for rank, index in enumerate(order, start=1)
415
+ ]
416
+ curve_series = [
417
+ {
418
+ "key": candidates[index].key,
419
+ "title": candidates[index].title,
420
+ "values": [round(float(dimension_map[dim][index]), 7) for dim in MATRYOSHKA_DIMS],
421
+ }
422
+ for index in order[:4]
423
+ ]
424
+ return {
425
+ "mode": "search",
426
+ "model": MODEL_ID,
427
+ "query_kind": query_kind,
428
+ "dimension": dimension,
429
+ "dimensions": list(MATRYOSHKA_DIMS),
430
+ "candidate_count": len(candidates),
431
+ "elapsed_seconds": round(elapsed, 3),
432
+ "cache_state": "created" if was_cold else "reused" if include_showcase else "not requested",
433
+ "top_match": ranked[0],
434
+ "rankings": ranked,
435
+ "dimension_series": curve_series,
436
+ "query_fingerprint": _fingerprint(query[0, :dimension]),
437
+ "full_embedding_dimension": int(query.shape[-1]),
438
+ "query_vector_preview": [round(float(value), 7) for value in query[0, :12]],
439
+ "l2_norm_after_truncation": round(float(_truncate_normalize(query, dimension).norm()), 7),
440
+ "note": "Cosine similarity is a ranking signal, not a calibrated probability.",
441
+ }
442
+
443
+
444
+ @app.api(
445
+ name="compare",
446
+ description="Compare a multimodal query and candidate across every native Matryoshka dimension.",
447
+ concurrency_limit=1,
448
+ concurrency_id="wemm_gpu",
449
+ time_limit=240,
450
+ )
451
+ def compare_api(
452
+ query_text: str = "",
453
+ query_image: FileData | None = None,
454
+ query_video: FileData | None = None,
455
+ candidate_text: str = "",
456
+ candidate_image: FileData | None = None,
457
+ candidate_video: FileData | None = None,
458
+ dimension: int = 1024,
459
+ progress: gr.Progress = gr.Progress(track_tqdm=True),
460
+ ) -> dict[str, Any]:
461
+ """Compare two supported inputs and return structured visualization data."""
462
+ dimension = int(dimension)
463
+ if dimension not in MATRYOSHKA_DIMS:
464
+ raise gr.Error("Choose one of the model's native Matryoshka dimensions.")
465
+
466
+ query_payload, query_kind = _multimodal_payload(query_text, query_image, query_video)
467
+ candidate_payload, candidate_kind = _multimodal_payload(
468
+ candidate_text, candidate_image, candidate_video
469
+ )
470
  with _INFERENCE_LOCK:
471
  query, candidate, elapsed = _run_pair(query_payload, candidate_payload, progress)
472
+
473
+ scores = {
474
  dim: float((_truncate_normalize(query, dim) @ _truncate_normalize(candidate, dim).T).item())
475
  for dim in MATRYOSHKA_DIMS
476
  }
477
+ selected_score = scores[dimension]
478
+ label, explanation = _interpret_score(selected_score)
479
+ return {
480
+ "mode": "compare",
 
 
 
 
 
 
 
 
 
481
  "model": MODEL_ID,
482
+ "query_kind": query_kind,
483
+ "candidate_kind": candidate_kind,
484
+ "dimension": dimension,
485
+ "dimensions": list(MATRYOSHKA_DIMS),
486
+ "selected_score": round(selected_score, 7),
487
+ "label": label,
488
+ "explanation": explanation,
489
+ "scores": [round(scores[dim], 7) for dim in MATRYOSHKA_DIMS],
490
+ "query_fingerprint": _fingerprint(query[0, :dimension]),
491
+ "candidate_fingerprint": _fingerprint(candidate[0, :dimension]),
492
+ "elapsed_seconds": round(elapsed, 3),
493
  "note": "Interpret thresholds relative to a task-specific candidate set.",
494
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
 
497
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
498
+ async def homepage() -> HTMLResponse:
499
+ return HTMLResponse((FRONTEND_DIR / "index.html").read_text(encoding="utf-8"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
+
502
+ @app.get("/health", include_in_schema=False)
503
+ async def health() -> dict[str, str]:
504
+ return {"status": "ok", "model": MODEL_ID, "interface": "gradio-server"}
505
+
506
+
507
+ app.mount("/assets", StaticFiles(directory=str(ASSET_DIR)), name="assets")
508
+ app.mount("/ui", StaticFiles(directory=str(FRONTEND_DIR)), name="ui")
509
+
510
+ # Hugging Face Spaces expects the application object under this conventional name.
511
+ demo = app
512
 
513
 
514
  if __name__ == "__main__":
515
+ app.launch(show_error=True)
 
 
 
frontend/app.css ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #080908;
3
+ --ink: #f1efe7;
4
+ --paper: #d9d4c8;
5
+ --muted: #8d908b;
6
+ --line: rgba(241, 239, 231, 0.12);
7
+ --line-strong: rgba(241, 239, 231, 0.23);
8
+ --panel: rgba(15, 17, 16, 0.88);
9
+ --acid: #d9ff63;
10
+ --acid-soft: #bdda54;
11
+ --teal: #66e3cf;
12
+ --orange: #ff9c62;
13
+ --violet: #a993ff;
14
+ --serif: Iowan Old Style, Baskerville, Times New Roman, serif;
15
+ --sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
16
+ --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
17
+ }
18
+
19
+ * { box-sizing: border-box; }
20
+ html { scroll-behavior: smooth; background: var(--bg); }
21
+ body {
22
+ margin: 0;
23
+ min-width: 320px;
24
+ min-height: 100vh;
25
+ color: var(--ink);
26
+ font-family: var(--sans);
27
+ background:
28
+ radial-gradient(circle at 71% 1%, rgba(110, 150, 74, 0.11), transparent 35rem),
29
+ radial-gradient(circle at 10% 32%, rgba(142, 91, 58, 0.08), transparent 34rem),
30
+ var(--bg);
31
+ overflow-x: hidden;
32
+ }
33
+ button, input, textarea { font: inherit; }
34
+ button, a { -webkit-tap-highlight-color: transparent; }
35
+ button { color: inherit; }
36
+ a { color: inherit; }
37
+ [hidden] { display: none !important; }
38
+ ::selection { color: #111; background: var(--acid); }
39
+
40
+ .noise {
41
+ position: fixed;
42
+ inset: 0;
43
+ z-index: 20;
44
+ pointer-events: none;
45
+ opacity: 0.035;
46
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.7'/%3E%3C/svg%3E");
47
+ }
48
+ .aurora { position: fixed; border-radius: 50%; filter: blur(120px); pointer-events: none; opacity: 0.08; }
49
+ .aurora-a { width: 30rem; height: 30rem; background: var(--acid); top: -18rem; left: 30%; }
50
+ .aurora-b { width: 24rem; height: 24rem; background: var(--teal); right: -14rem; top: 35rem; }
51
+
52
+ .site-header {
53
+ width: min(1460px, calc(100% - 64px));
54
+ height: 78px;
55
+ margin: 0 auto;
56
+ display: grid;
57
+ grid-template-columns: 1fr auto 1fr;
58
+ align-items: center;
59
+ gap: 24px;
60
+ border-bottom: 1px solid var(--line);
61
+ position: relative;
62
+ z-index: 10;
63
+ }
64
+ .brand { display: inline-flex; align-items: center; gap: 12px; width: max-content; text-decoration: none; }
65
+ .brand > span:last-child { display: flex; flex-direction: column; gap: 1px; }
66
+ .brand b { font-size: 17px; letter-spacing: -0.04em; }
67
+ .brand small { font: 8px/1.2 var(--mono); letter-spacing: 0.17em; color: var(--muted); }
68
+ .brand-mark { width: 33px; height: 33px; display: inline-grid; grid-template-columns: repeat(3, 1fr); gap: 2px; transform: skew(-6deg); }
69
+ .brand-mark i { display: block; background: var(--acid); }
70
+ .brand-mark i:nth-child(1) { height: 55%; align-self: end; }
71
+ .brand-mark i:nth-child(2) { height: 78%; align-self: end; opacity: 0.78; }
72
+ .brand-mark i:nth-child(3) { height: 100%; opacity: 0.48; }
73
+ .mode-nav { display: flex; padding: 4px; background: rgba(255,255,255,.035); border: 1px solid var(--line); border-radius: 99px; }
74
+ .nav-pill { border: 0; background: transparent; color: #7e817c; cursor: pointer; padding: 9px 16px; border-radius: 99px; font-size: 11px; font-weight: 700; letter-spacing: .02em; transition: .25s ease; }
75
+ .nav-pill.active { background: var(--paper); color: #101210; box-shadow: 0 3px 14px rgba(0,0,0,.3); }
76
+ .header-actions { justify-self: end; display: flex; align-items: center; gap: 9px; }
77
+ .model-status { display: flex; align-items: center; gap: 7px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 99px; color: var(--muted); font: 9px var(--mono); letter-spacing: .08em; }
78
+ .model-status i { width: 6px; height: 6px; background: #b9a64d; border-radius: 50%; box-shadow: 0 0 9px #b9a64d; }
79
+ .model-status.ready i { background: var(--acid); box-shadow: 0 0 10px var(--acid); }
80
+ .model-status.error i { background: #ff766e; box-shadow: 0 0 9px #ff766e; }
81
+ .icon-link { min-width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 50%; display: grid; place-items: center; text-decoration: none; color: #b5b6b0; font: 9px var(--mono); transition: .2s; }
82
+ .icon-link:hover { border-color: var(--acid); color: var(--acid); }
83
+ .arrow-link { font: 15px var(--sans); }
84
+
85
+ main { width: min(1460px, calc(100% - 64px)); margin: 0 auto; position: relative; z-index: 1; }
86
+ .hero { min-height: 660px; display: grid; grid-template-columns: 1.08fr .92fr; align-items: center; gap: 60px; padding: 72px 3vw 76px; border-bottom: 1px solid var(--line); }
87
+ .overline { display: flex; align-items: center; gap: 17px; font: 9px var(--mono); letter-spacing: .17em; color: var(--acid); }
88
+ .overline span { display: flex; align-items: center; gap: 12px; }
89
+ .overline span::before { content: ""; width: 28px; height: 1px; background: var(--acid); box-shadow: 0 0 9px rgba(217,255,99,.45); }
90
+ .overline b { color: #656961; font-weight: 500; border-left: 1px solid var(--line); padding-left: 17px; }
91
+ .hero h1 { font-size: clamp(68px, 7.7vw, 124px); line-height: .83; letter-spacing: -.075em; font-weight: 620; margin: 30px 0 35px; max-width: 920px; }
92
+ .hero h1 em { font-family: var(--serif); color: transparent; -webkit-text-stroke: 1px rgba(241,239,231,.58); font-weight: 400; }
93
+ .hero-copy > p { max-width: 650px; color: #a5a69f; font-size: clamp(16px, 1.6vw, 21px); line-height: 1.55; letter-spacing: -.015em; }
94
+ .hero-meta { display: flex; gap: 0; margin-top: 47px; }
95
+ .hero-meta div { padding: 0 30px; border-right: 1px solid var(--line); }
96
+ .hero-meta div:first-child { padding-left: 0; }
97
+ .hero-meta div:last-child { border: 0; }
98
+ .hero-meta b { display: block; font-size: 25px; letter-spacing: -.05em; }
99
+ .hero-meta span { display: block; color: #6d706a; font: 8px var(--mono); letter-spacing: .1em; text-transform: uppercase; margin-top: 6px; }
100
+
101
+ .semantic-orbit { width: min(100%, 550px); aspect-ratio: 1; justify-self: center; position: relative; }
102
+ .semantic-orbit::before { content: ""; position: absolute; inset: 13%; border-radius: 50%; background: radial-gradient(circle, rgba(217,255,99,.1), transparent 66%); filter: blur(8px); }
103
+ .orbit-ring { position: absolute; border: 1px solid rgba(217,255,99,.12); border-radius: 50%; animation: orbit-spin 34s linear infinite; }
104
+ .ring-one { inset: 10%; border-style: dashed; }
105
+ .ring-two { inset: 22%; animation-direction: reverse; animation-duration: 21s; }
106
+ .ring-three { inset: 35%; border-color: rgba(102,227,207,.16); }
107
+ .orbit-ring::before { content: ""; width: 6px; height: 6px; position: absolute; top: 11%; left: 18%; border-radius: 50%; background: var(--acid); box-shadow: 0 0 15px var(--acid); }
108
+ .orbit-core { position: absolute; inset: 39%; border-radius: 50%; display: grid; place-content: center; text-align: center; background: linear-gradient(145deg,#e8ff9c,#b9da55); color: #11130e; box-shadow: 0 0 80px rgba(217,255,99,.15); z-index: 2; }
109
+ .orbit-core span { font: 700 45px/.9 var(--serif); }
110
+ .orbit-core small { margin-top: 7px; font: 7px var(--mono); letter-spacing: .13em; }
111
+ .semantic-orbit svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
112
+ .semantic-orbit path { stroke: rgba(241,239,231,.14); fill: none; stroke-dasharray: 2 5; }
113
+ .orbit-node { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 6px; color: #747871; font: 7px var(--mono); letter-spacing: .12em; z-index: 3; }
114
+ .orbit-node i { width: 46px; height: 46px; border: 1px solid rgba(241,239,231,.17); border-radius: 50%; background: rgba(13,15,13,.92); display: grid; place-items: center; color: #c7c9c0; font-style: normal; font-size: 12px; box-shadow: 0 13px 40px rgba(0,0,0,.3); }
115
+ .node-text { left: 11%; top: 31%; }.node-image { right: 7%; top: 25%; }.node-video { right: 8%; bottom: 19%; }.node-doc { left: 10%; bottom: 18%; }
116
+ @keyframes orbit-spin { to { transform: rotate(360deg); } }
117
+
118
+ .expedition-strip { min-height: 155px; display: grid; grid-template-columns: 220px 1fr; align-items: stretch; border-bottom: 1px solid var(--line); }
119
+ .strip-label { padding: 32px 22px 28px 4px; display: flex; flex-direction: column; justify-content: center; border-right: 1px solid var(--line); }
120
+ .strip-label span { color: var(--acid); font: 9px var(--mono); letter-spacing: .15em; }
121
+ .strip-label small { color: #696c67; margin-top: 8px; font-size: 10px; }
122
+ .expedition-list { display: grid; grid-template-columns: repeat(5,1fr); }
123
+ .expedition { position: relative; text-align: left; cursor: pointer; border: 0; border-right: 1px solid var(--line); background: transparent; padding: 28px 20px; transition: .25s ease; overflow: hidden; }
124
+ .expedition:last-child { border-right: 0; }
125
+ .expedition::before { content: ""; position: absolute; inset: auto 0 0; height: 2px; background: var(--acid); transform: scaleX(0); transform-origin: left; transition: .25s; }
126
+ .expedition:hover { background: rgba(217,255,99,.04); }.expedition:hover::before { transform: scaleX(1); }
127
+ .expedition > span { display: block; color: #565a54; font: 8px var(--mono); margin-bottom: 21px; }
128
+ .expedition b { display: block; font-size: 12px; }.expedition small { display: block; color: #73766f; margin-top: 5px; font: 8px var(--mono); }
129
+
130
+ .studio { padding: 108px 0 112px; border-bottom: 1px solid var(--line); scroll-margin-top: 30px; }
131
+ .section-heading { display: grid; grid-template-columns: 1fr 430px; align-items: end; gap: 60px; margin-bottom: 47px; }
132
+ .section-heading > div > span, .story-copy > span { color: var(--acid); font: 9px var(--mono); letter-spacing: .17em; }
133
+ .section-heading h2, .story-copy h2 { margin: 14px 0 0; font-size: clamp(43px,5vw,76px); line-height: .92; letter-spacing: -.06em; font-weight: 570; }
134
+ .section-heading > p { color: #8b8e88; line-height: 1.65; font-size: 13px; margin: 0 0 7px; }
135
+ .studio-grid { display: grid; grid-template-columns: minmax(370px, .8fr) minmax(520px, 1.2fr); border: 1px solid var(--line); background: rgba(9,11,9,.54); min-height: 790px; }
136
+ .control-deck { padding: 30px; border-right: 1px solid var(--line); background: rgba(20,22,19,.56); }
137
+ .deck-heading { display: flex; justify-content: space-between; align-items: center; padding-bottom: 25px; border-bottom: 1px solid var(--line); margin-bottom: 25px; }
138
+ .deck-heading > span, .field-label, .dimension-field legend > span { font: 9px var(--mono); letter-spacing: .13em; color: #9da098; }
139
+ .text-button { background: none; border: 0; cursor: pointer; color: #656862; font-size: 9px; text-decoration: underline; text-underline-offset: 4px; }
140
+ .field-label { display: block; margin-bottom: 9px; }
141
+ .textarea-wrap { position: relative; }
142
+ textarea { width: 100%; resize: vertical; border: 1px solid var(--line); outline: none; color: var(--ink); background: rgba(7,8,7,.7); padding: 15px; border-radius: 2px; line-height: 1.5; font-size: 13px; transition: .2s; }
143
+ textarea:focus { border-color: rgba(217,255,99,.46); box-shadow: 0 0 0 3px rgba(217,255,99,.035); }
144
+ textarea::placeholder { color: #4f524d; }
145
+ .key-hint { position: absolute; right: 11px; bottom: 10px; color: #4f524c; font: 8px var(--mono); padding: 3px 5px; border: 1px solid var(--line); }
146
+ .visual-query-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 11px; }
147
+ .drop-field { min-height: 105px; position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; border: 1px dashed rgba(241,239,231,.16); background: rgba(255,255,255,.015); transition: .2s; overflow: hidden; }
148
+ .drop-field:hover, .drop-field.dragover { border-color: var(--acid); background: rgba(217,255,99,.035); }
149
+ .drop-field input, .mini-upload input, .pair-uploads input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
150
+ .drop-icon { color: var(--acid); font: 24px var(--serif); line-height: .7; }.drop-icon.play { font: 15px var(--sans); }
151
+ .drop-field b { font-size: 10px; margin-top: 8px; }.drop-field small { color: #5e615b; font: 7px var(--mono); margin-top: 5px; letter-spacing: .08em; }
152
+ .file-chip { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 7px; padding: 10px; background: #161916; }
153
+ .file-chip span { max-width: 100%; font: 9px var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
154
+ .file-chip button { border: 1px solid var(--line); background: transparent; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; }
155
+ .context-note { color: #626660; font-size: 9px; margin: 10px 0 23px; }.context-note span { color: var(--acid); margin-right: 5px; }
156
+ .collection-builder { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); margin: 0 -2px 23px; }
157
+ .collection-builder summary { list-style: none; display: flex; align-items: center; justify-content: space-between; padding: 15px 2px; cursor: pointer; font-size: 10px; }
158
+ .collection-builder summary::-webkit-details-marker { display:none; }.collection-builder summary i { color: var(--acid); font-style: normal; margin-right: 7px; }
159
+ .collection-builder summary small { color: #585b56; font: 8px var(--mono); text-transform: uppercase; }
160
+ .collection-builder[open] summary { border-bottom: 1px solid var(--line); }
161
+ .details-body { padding: 18px 1px; }.details-body textarea { font-size: 11px; }
162
+ .mini-upload { position: relative; min-height: 44px; margin-top: 9px; padding: 0 12px; display: flex; align-items: center; gap: 9px; border: 1px dashed var(--line); cursor: pointer; }
163
+ .mini-upload span { color: var(--acid); }.mini-upload b { font-size: 9px; }.mini-upload small { margin-left: auto; color: #5a5d57; font: 7px var(--mono); }
164
+ .candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
165
+ .candidate-chip { display: flex; align-items: center; gap: 6px; max-width: 100%; padding: 5px 7px; background: rgba(255,255,255,.04); border: 1px solid var(--line); font: 8px var(--mono); color: #9b9e97; }
166
+ .candidate-chip span { max-width: 150px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
167
+ .candidate-chip button { border: 0; padding: 0; background: none; color: #747771; cursor: pointer; }
168
+ .switch-line { display: flex; align-items: center; justify-content: space-between; margin: 0 0 26px; }
169
+ .switch-line b { display: block; font-size: 11px; }.switch-line small { display: block; color: #60635d; font: 8px var(--mono); margin-top: 4px; }
170
+ .switch { cursor: pointer; }.switch input { position: absolute; opacity: 0; }.switch span { width: 37px; height: 20px; padding: 2px; display: block; border-radius: 99px; background: #373a35; transition: .2s; }
171
+ .switch span::after { content: ""; display: block; width: 16px; height: 16px; border-radius: 50%; background: #858880; transition: .2s; }
172
+ .switch input:checked + span { background: var(--acid); }.switch input:checked + span::after { background: #12140f; transform: translateX(17px); }
173
+ .dimension-field { padding: 0; margin: 0 0 22px; border: 0; }
174
+ .dimension-field legend { width: 100%; display: flex; justify-content: space-between; margin-bottom: 10px; }
175
+ .dimension-field output { color: var(--acid); font: 10px var(--mono); }
176
+ .dimension-pills { display: grid; grid-template-columns: repeat(7,1fr); border: 1px solid var(--line); }
177
+ .dimension-pills button { border: 0; border-right: 1px solid var(--line); padding: 9px 2px; background: transparent; color: #656961; font: 8px var(--mono); cursor: pointer; transition: .2s; }
178
+ .dimension-pills button:last-child { border: 0; }.dimension-pills button:hover { color: var(--paper); }.dimension-pills button.active { background: var(--acid); color: #12130f; }
179
+ .dimension-field p { color: #5a5d57; font-size: 8px; margin: 8px 0 0; }
180
+ .primary-button { width: 100%; height: 55px; display: flex; align-items: center; justify-content: space-between; padding: 0 19px; border: 1px solid var(--acid); color: #12140f; background: var(--acid); cursor: pointer; font-weight: 800; font-size: 11px; transition: .22s; position: relative; overflow: hidden; }
181
+ .primary-button::before { content: ""; position: absolute; inset: 0; background: #efffb8; transform: translateX(-102%); transition: .25s; }.primary-button:hover::before { transform: translateX(0); }.primary-button span, .primary-button i { position: relative; z-index: 1; }.primary-button i { font-style: normal; font-size: 20px; }
182
+ .primary-button:disabled { cursor: wait; filter: grayscale(.55); opacity: .65; }
183
+
184
+ .result-deck { min-width: 0; padding: 30px; position: relative; background: radial-gradient(circle at 50% 27%, rgba(217,255,99,.035), transparent 21rem); }
185
+ .result-topbar { display: flex; justify-content: space-between; gap: 20px; padding-bottom: 24px; border-bottom: 1px solid var(--line); }
186
+ .result-topbar > div { display: flex; align-items: center; gap: 13px; }.result-topbar small, .result-count { color: #5e625b; font: 8px var(--mono); letter-spacing: .06em; }
187
+ .signal { color: var(--acid); font: 8px var(--mono); letter-spacing: .1em; }.signal i { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--acid); box-shadow: 0 0 10px var(--acid); margin-right: 6px; }
188
+ .empty-universe { min-height: 685px; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; }
189
+ .empty-field { width: 230px; height: 230px; position: relative; display: grid; place-items: center; margin-bottom: 27px; border: 1px solid rgba(217,255,99,.1); border-radius: 50%; box-shadow: inset 0 0 70px rgba(217,255,99,.025); }
190
+ .empty-field::before, .empty-field::after { content: ""; position: absolute; border-radius: 50%; border: 1px dashed rgba(241,239,231,.08); }.empty-field::before { inset: 18%; }.empty-field::after { inset: 35%; }
191
+ .empty-field span { width: 48px; height: 48px; display: grid; place-items: center; border-radius: 50%; background: rgba(217,255,99,.12); color: var(--acid); font: 25px var(--serif); box-shadow: 0 0 35px rgba(217,255,99,.09); }
192
+ .empty-field i { position: absolute; width: 5px; height: 5px; border-radius: 50%; background: #767c70; animation: pulse 2.5s ease-in-out infinite; }.empty-field i:nth-child(1){top:12%;left:46%}.empty-field i:nth-child(2){top:30%;right:12%;animation-delay:.4s}.empty-field i:nth-child(3){bottom:20%;right:21%;animation-delay:.8s}.empty-field i:nth-child(4){bottom:10%;left:37%;animation-delay:1.2s}.empty-field i:nth-child(5){bottom:30%;left:10%;animation-delay:1.6s}.empty-field i:nth-child(6){top:22%;left:18%;animation-delay:2s}
193
+ @keyframes pulse { 50% { background: var(--acid); box-shadow: 0 0 12px var(--acid); transform: scale(1.5); } }
194
+ .empty-universe h3 { margin: 0; font-size: 20px; letter-spacing: -.035em; }.empty-universe > p { max-width: 490px; color: #6c7069; font-size: 11px; line-height: 1.65; }
195
+ .media-preview-rail { display: grid; grid-template-columns: repeat(4,1fr); gap: 5px; width: min(100%,530px); height: 95px; margin-top: 23px; opacity: .64; }
196
+ .media-preview-rail img, .media-preview-rail video { width: 100%; height: 100%; object-fit: cover; filter: saturate(.65); }
197
+
198
+ .summary-card { display: grid; grid-template-columns: 1fr auto; gap: 20px; align-items: end; padding: 24px 0 28px; border-bottom: 1px solid var(--line); }
199
+ .summary-card > div > span { display: block; color: var(--acid); font: 8px var(--mono); letter-spacing: .13em; margin-bottom: 8px; }.summary-card h3 { margin: 0; font-size: clamp(25px,3vw,42px); letter-spacing: -.055em; line-height: 1; }.summary-card p { margin: 8px 0 0; color: #73776f; font-size: 10px; }
200
+ .top-score { text-align: right; }.top-score b { display: block; color: var(--acid); font: 400 clamp(36px,4vw,55px)/.9 var(--mono); letter-spacing: -.08em; }.top-score small { color: #656962; font: 7px var(--mono); letter-spacing: .13em; }
201
+ .result-tabs { display: flex; align-items: center; gap: 18px; padding: 16px 0; }
202
+ .result-tabs button { padding: 0 0 6px; border: 0; border-bottom: 1px solid transparent; background: none; color: #5f625c; font: 8px var(--mono); letter-spacing: .1em; cursor: pointer; }.result-tabs button.active { color: var(--paper); border-color: var(--acid); }
203
+ .ranking-list { display: grid; gap: 7px; }
204
+ .ranking-card { display: grid; grid-template-columns: 38px 64px minmax(0,1fr) 66px; gap: 13px; align-items: center; min-height: 75px; padding: 9px 12px 9px 5px; border: 1px solid var(--line); background: rgba(255,255,255,.014); transition: .2s; opacity: 0; transform: translateY(8px); animation: row-in .35s forwards; animation-delay: calc(var(--rank) * 35ms); }
205
+ @keyframes row-in { to { opacity: 1; transform: translateY(0); } }
206
+ .ranking-card:first-child { border-color: rgba(217,255,99,.3); background: linear-gradient(90deg,rgba(217,255,99,.07),transparent); }
207
+ .ranking-card:hover { transform: translateX(3px); border-color: var(--line-strong); }
208
+ .rank-index { color: #5e625b; font: 9px var(--mono); text-align: center; }.rank-media { width: 64px; height: 54px; object-fit: cover; background: #151715; border: 1px solid var(--line); }.rank-media.text-tile { display: grid; place-items: center; color: #73776f; font: 15px var(--serif); }
209
+ .rank-copy { min-width: 0; }.rank-title { display: flex; align-items: center; gap: 7px; }.rank-title b { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 11px; }.kind-tag { flex: none; padding: 3px 5px; border: 1px solid var(--line); color: #666a63; font: 6px var(--mono); text-transform: uppercase; letter-spacing: .08em; }.rank-copy p { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin: 6px 0 8px; color: #656961; font-size: 8px; }
210
+ .score-line { height: 2px; background: rgba(255,255,255,.05); }.score-line i { display: block; height: 100%; background: linear-gradient(90deg,var(--acid),var(--teal)); }
211
+ .rank-value { text-align: right; font: 14px var(--mono); color: #a8aaa3; }.ranking-card:first-child .rank-value { color: var(--acid); }.rank-value small { display: block; margin-top: 4px; color: #555852; font-size: 6px; letter-spacing: .1em; }
212
+ .result-view[hidden] { display: none; }
213
+ .visual-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 8px; }.visual-card { min-height: 185px; position: relative; overflow: hidden; border: 1px solid var(--line); background: #111311; }.visual-card img,.visual-card video { width: 100%; height: 185px; object-fit: cover; }.visual-card figcaption { position: absolute; inset: auto 0 0; display: flex; justify-content: space-between; padding: 30px 10px 9px; background: linear-gradient(transparent,rgba(7,8,7,.92)); font-size: 9px; }.visual-card figcaption span:last-child { color: var(--acid); font-family: var(--mono); }
214
+ .viz-stack { display: grid; gap: 12px; }.viz-panel { border: 1px solid var(--line); padding: 15px; background: rgba(255,255,255,.012); }.viz-head { display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px; }.viz-head span { color: var(--acid); font: 7px var(--mono); letter-spacing: .14em; }.viz-head b { display: block; margin-top: 4px; font-size: 13px; }.viz-head small { color: #5f625d; font: 7px var(--mono); }
215
+ .line-chart { width: 100%; height: auto; overflow: visible; }.chart-grid-line { stroke: rgba(241,239,231,.07); }.chart-label { fill: #60645d; font: 7px var(--mono); }.chart-line { fill: none; stroke-width: 2; }.chart-dot { stroke: #101210; stroke-width: 1.5; }
216
+ .chart-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 7px; }.chart-legend span { color: #777b74; font-size: 7px; }.chart-legend i { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px; }
217
+ .fingerprint-svg { width: 100%; height: auto; }.fingerprint-zero { stroke: rgba(241,239,231,.13); }.fingerprint-key { display: flex; gap: 15px; color: #5e625b; font: 7px var(--mono); }.fingerprint-key i { display: inline-block; width: 6px; height: 6px; margin-right: 4px; }
218
+ .telemetry { border: 1px solid var(--line); }.telemetry summary { list-style: none; cursor: pointer; padding: 12px 14px; display: flex; justify-content: space-between; font: 8px var(--mono); color: #8f928b; }.telemetry summary::-webkit-details-marker { display:none; }.telemetry pre { overflow: auto; margin: 0; padding: 15px; border-top: 1px solid var(--line); color: #82867e; background: #090a09; font: 8px/1.6 var(--mono); }
219
+
220
+ .compare-studio { padding-bottom: 100px; }.compare-inputs { display: grid; grid-template-columns: 1fr 80px 1fr; align-items: stretch; }.compare-side { padding: 30px; border: 1px solid var(--line); background: rgba(18,20,18,.58); }.compare-side header { display: flex; gap: 13px; align-items: center; margin-bottom: 24px; }.compare-side header > span { width: 35px; height: 35px; display: grid; place-items: center; border-radius: 50%; color: #111; background: var(--acid); font: 12px var(--mono); }.compare-side header b { display: block; font: 9px var(--mono); letter-spacing: .15em; }.compare-side header small { display: block; margin-top: 5px; color: #63665f; font-size: 8px; }.compare-side textarea { min-height: 150px; resize: vertical; }
221
+ .pair-uploads { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 8px; }.pair-uploads label { position: relative; display: flex; align-items: center; justify-content: center; gap: 7px; height: 42px; border: 1px dashed var(--line); cursor: pointer; color: #7b7e77; font: 7px var(--mono); }.pair-uploads label:hover { color: var(--acid); border-color: var(--acid); }.pair-uploads span { font-size: 15px; color: var(--acid); }
222
+ .pair-file { margin-top: 8px; padding: 8px 10px; border: 1px solid var(--line); color: #8c9088; font: 8px var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.versus { display: grid; place-content: center; text-align: center; color: var(--acid); }.versus span { font-size: 21px; }.versus small { margin-top: 6px; color: #565953; font: 6px var(--mono); letter-spacing: .1em; writing-mode: vertical-rl; justify-self: center; }
223
+ .compare-controls { display: grid; grid-template-columns: minmax(350px,.8fr) minmax(280px,.45fr); gap: 12px; margin: 14px auto 0; max-width: 850px; }.compare-controls .dimension-pills { align-self: center; }.compare-results { margin-top: 32px; }.compare-empty { min-height: 160px; border: 1px dashed var(--line); display: grid; place-content: center; text-align: center; color: #666a63; }.compare-empty span { color: var(--acid); font-size: 23px; }.compare-empty p { font-size: 10px; }
224
+ .alignment-card { display: grid; grid-template-columns: 210px 1fr; gap: 40px; align-items: center; border: 1px solid rgba(169,147,255,.25); padding: 34px; background: radial-gradient(circle at 8% 50%,rgba(169,147,255,.12),transparent 25rem),rgba(13,14,14,.5); }
225
+ .score-ring { --score: 50; width: 190px; height: 190px; padding: 2px; border-radius: 50%; background: conic-gradient(var(--violet) calc(var(--score) * 1%), rgba(255,255,255,.07) 0); transform: rotate(-90deg); box-shadow: 0 0 70px rgba(169,147,255,.1); }.score-ring > div { width: 100%; height: 100%; border-radius: 50%; display: grid; place-content: center; text-align: center; background: #0c0e0c; transform: rotate(90deg); }.score-ring b { font: 400 36px var(--mono); letter-spacing: -.08em; }.score-ring small { color: #686b65; font: 7px var(--mono); letter-spacing: .12em; margin-top: 6px; }
226
+ .alignment-copy > span { color: var(--violet); font: 8px var(--mono); letter-spacing: .14em; }.alignment-copy h3 { margin: 8px 0 9px; text-transform: capitalize; font-size: 37px; letter-spacing: -.05em; }.alignment-copy p { max-width: 580px; color: #858881; font-size: 12px; line-height: 1.6; }.alignment-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 18px; }.alignment-meta span { padding: 5px 8px; border: 1px solid var(--line); color: #777a74; font: 7px var(--mono); text-transform: uppercase; }
227
+ .compare-viz-grid { display: grid; grid-template-columns: 1.25fr .75fr; gap: 12px; margin-top: 12px; }.fingerprint-pair { display: grid; gap: 10px; }
228
+
229
+ .model-story { min-height: 570px; position: relative; display: grid; grid-template-columns: .58fr .9fr 1fr; gap: 60px; align-items: center; padding: 100px 3vw; overflow: hidden; border-bottom: 1px solid var(--line); }.story-number { align-self: stretch; display: flex; align-items: center; color: rgba(241,239,231,.032); font: 700 clamp(90px,12vw,190px)/1 var(--mono); writing-mode: vertical-rl; transform: rotate(180deg); letter-spacing: -.12em; }.story-copy h2 em { color: var(--acid); font-family: var(--serif); font-weight: 400; }.story-copy p { max-width: 480px; color: #878a83; font-size: 12px; line-height: 1.7; margin-top: 25px; }
230
+ .dimension-tower { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }.dimension-tower i { width: var(--w); height: 33px; min-width: 54px; display: flex; justify-content: flex-end; align-items: center; padding-right: 9px; background: linear-gradient(90deg,rgba(217,255,99,.04),rgba(217,255,99,.35)); border-right: 2px solid var(--acid); font-style: normal; animation: tower 4s ease-in-out infinite alternate; animation-delay: calc(var(--w) * -20ms); }.dimension-tower span { color: #afb2aa; font: 7px var(--mono); }
231
+ @keyframes tower { to { filter: brightness(1.35); transform: translateX(6px); } }
232
+
233
+ footer { width: min(1460px,calc(100% - 64px)); min-height: 145px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1.5fr 1fr; gap: 30px; align-items: center; color: #666962; }.footer-brand { display: flex; align-items: center; gap: 10px; }.footer-brand .brand-mark { width: 26px; height: 26px; }.footer-brand b { color: #b9bbb4; }.footer-brand + p { font-size: 9px; line-height: 1.6; text-align: center; }.footer-brand ~ div { justify-self: end; display: flex; gap: 18px; }.footer-brand ~ div a { font: 8px var(--mono); text-decoration: none; }.footer-brand ~ div a:hover { color: var(--acid); }
234
+
235
+ .job-toast { position: fixed; z-index: 50; right: 24px; bottom: 24px; width: min(430px,calc(100% - 48px)); min-height: 104px; display: grid; grid-template-columns: 65px 1fr 28px; gap: 14px; align-items: center; padding: 17px 18px 21px; border: 1px solid rgba(217,255,99,.28); background: rgba(12,14,12,.96); box-shadow: 0 25px 80px rgba(0,0,0,.5); backdrop-filter: blur(18px); }.job-orbit { width: 55px; height: 55px; position: relative; display: grid; place-items: center; border: 1px solid rgba(217,255,99,.2); border-radius: 50%; }.job-orbit::before { content:""; position:absolute; inset:7px; border:1px dashed rgba(217,255,99,.2); border-radius:50%; animation:orbit-spin 4s linear infinite; }.job-orbit i { position:absolute; width:4px; height:4px; border-radius:50%; background:var(--acid); box-shadow:0 0 8px var(--acid); }.job-orbit i:first-child{top:5px}.job-orbit i:nth-child(2){bottom:9px;right:6px}.job-orbit span{color:var(--acid);font:17px var(--serif)}
236
+ .job-toast > div:nth-child(2) span { color: var(--acid); font: 7px var(--mono); letter-spacing: .14em; }.job-toast > div:nth-child(2) b { display: block; margin-top: 5px; font-size: 11px; }.job-toast > div:nth-child(2) small { display: block; color: #666a63; font: 7px var(--mono); margin-top: 5px; }.job-toast > button { align-self: start; border: 0; background: none; color: #666a63; cursor: pointer; font-size: 18px; }.job-progress { position: absolute; inset: auto 0 0; height: 2px; background: rgba(255,255,255,.06); }.job-progress i { display: block; width: 3%; height: 100%; background: var(--acid); box-shadow: 0 0 8px rgba(217,255,99,.7); transition: width 1s ease; }
237
+ .error-toast { position: fixed; z-index: 60; left: 50%; bottom: 25px; transform: translateX(-50%); width: min(520px,calc(100% - 36px)); display: grid; grid-template-columns: 1fr auto; gap: 5px 16px; padding: 15px 17px; color: #ffd0cb; background: rgba(42,18,17,.96); border: 1px solid rgba(255,118,110,.35); box-shadow: 0 18px 60px rgba(0,0,0,.5); }.error-toast b { font-size: 10px; }.error-toast span { grid-column: 1; color: #bf8e89; font-size: 9px; line-height: 1.45; }.error-toast button { grid-column: 2; grid-row: 1/3; border: 0; background: none; color: #d69d98; cursor: pointer; font-size: 18px; }
238
+
239
+ @media (prefers-reduced-motion: reduce) {
240
+ *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
241
+ }
242
+ @media (max-width: 1100px) {
243
+ .site-header, main, footer { width: min(100% - 36px,1460px); }
244
+ .hero { min-height: 590px; gap: 20px; padding-inline: 0; }.semantic-orbit { width: 440px; }
245
+ .studio-grid { grid-template-columns: 400px 1fr; }.result-deck,.control-deck { padding: 24px; }
246
+ .expedition-strip { grid-template-columns: 170px 1fr; }.expedition { padding-inline: 12px; }
247
+ .section-heading { grid-template-columns: 1fr 350px; }
248
+ }
249
+ @media (max-width: 900px) {
250
+ .site-header { grid-template-columns: 1fr auto; }.mode-nav { position: fixed; z-index: 40; left: 50%; bottom: 14px; transform: translateX(-50%); background: rgba(22,24,21,.96); box-shadow: 0 9px 35px rgba(0,0,0,.5); }.model-status { display: none; }
251
+ .hero { grid-template-columns: 1fr; padding-top: 70px; }.semantic-orbit { width: min(80vw,490px); margin-top: -40px; }.hero-copy { position: relative; z-index: 4; }.hero h1 { font-size: clamp(69px,13vw,108px); }
252
+ .expedition-strip { grid-template-columns: 1fr; }.strip-label { border-right: 0; border-bottom: 1px solid var(--line); padding-left: 0; }.expedition-list { overflow-x: auto; grid-template-columns: repeat(5,170px); }.expedition { min-height: 125px; }
253
+ .section-heading { grid-template-columns: 1fr; gap: 20px; }.section-heading > p { max-width: 590px; }
254
+ .studio-grid { grid-template-columns: 1fr; }.control-deck { border-right: 0; border-bottom: 1px solid var(--line); }.result-deck { min-height: 650px; }
255
+ .compare-inputs { grid-template-columns: 1fr; gap: 9px; }.versus { min-height: 45px; }.versus small { writing-mode: initial; }.compare-controls { grid-template-columns: 1fr; }
256
+ .model-story { grid-template-columns: .7fr 1.3fr; }.story-number { display:none; }.dimension-tower { grid-column: 1/3; width: 80%; }
257
+ footer { margin-bottom: 75px; grid-template-columns: 1fr 1fr; }.footer-brand + p { grid-column: 1/3; grid-row: 2; text-align: left; }.footer-brand ~ div { grid-column: 2; grid-row: 1; }
258
+ }
259
+ @media (max-width: 620px) {
260
+ .site-header, main, footer { width: calc(100% - 24px); }.site-header { height: 65px; }.brand small { display:none; }.header-actions .icon-link:first-of-type { display:none; }
261
+ .hero { min-height: 640px; padding-top: 58px; }.hero h1 { font-size: clamp(57px,17vw,88px); margin-top: 23px; }.overline b { display:none; }.hero-copy > p { font-size: 15px; }.hero-meta { margin-top: 35px; }.hero-meta div { padding-inline: 16px; }.hero-meta b { font-size: 20px; }.hero-meta span { font-size: 6px; }.semantic-orbit { width: 105vw; margin-left: -10vw; margin-top: -70px; opacity: .8; }
262
+ .studio { padding-block: 75px; }.section-heading h2,.story-copy h2 { font-size: 46px; }.studio-grid { border-left: 0; border-right: 0; margin-inline: -12px; }.control-deck,.result-deck { padding: 20px 15px; }.visual-query-grid { grid-template-columns: 1fr 1fr; }.drop-field { min-height: 92px; }
263
+ .result-topbar small { display:none; }.empty-universe { min-height: 580px; }.media-preview-rail { height: 70px; }
264
+ .ranking-card { grid-template-columns: 28px 48px minmax(0,1fr) 53px; gap: 8px; padding-right: 8px; }.rank-media { width: 48px; height: 48px; }.rank-title b { font-size: 9px; }.kind-tag { display:none; }.rank-copy p { display:none; }.score-line { margin-top: 9px; }.rank-value { font-size: 11px; }
265
+ .summary-card { grid-template-columns: 1fr auto; }.summary-card h3 { font-size: 24px; }.top-score b { font-size: 34px; }
266
+ .visual-grid,.compare-viz-grid { grid-template-columns: 1fr; }.visual-card,.visual-card img,.visual-card video { height: 165px; min-height: 165px; }
267
+ .compare-side { padding: 20px 15px; }.compare-controls { min-width: 0; }.alignment-card { grid-template-columns: 1fr; padding: 25px 18px; text-align: center; }.score-ring { margin: auto; }.alignment-meta { justify-content: center; }
268
+ .model-story { grid-template-columns: 1fr; padding: 76px 0; gap: 40px; }.dimension-tower { grid-column: 1; width: 100%; }
269
+ footer { grid-template-columns: 1fr; gap: 15px; padding-block: 35px; }.footer-brand + p,.footer-brand ~ div { grid-column: 1; grid-row:auto; justify-self:start; text-align:left; }
270
+ .job-toast { right: 12px; bottom: 75px; width: calc(100% - 24px); grid-template-columns: 51px 1fr 20px; }.job-orbit { width: 46px; height: 46px; }
271
+ }
frontend/app.js ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Client,
3
+ handle_file,
4
+ } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.15.0/dist/index.min.js";
5
+
6
+ const $ = (selector, root = document) => root.querySelector(selector);
7
+ const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
8
+ const dims = [64, 128, 256, 512, 1024, 2048, 4096];
9
+ const colors = ["#d9ff63", "#66e3cf", "#ff9c62", "#a993ff"];
10
+
11
+ const state = {
12
+ client: null,
13
+ connecting: null,
14
+ searchImage: null,
15
+ searchVideo: null,
16
+ candidateMedia: [],
17
+ pairQueryImage: null,
18
+ pairQueryVideo: null,
19
+ pairCandidateImage: null,
20
+ pairCandidateVideo: null,
21
+ searchDimension: 1024,
22
+ compareDimension: 1024,
23
+ objectUrls: new Map(),
24
+ jobTimer: null,
25
+ jobStarted: 0,
26
+ };
27
+
28
+ const examples = {
29
+ llama: {
30
+ text: "Which Llama 4 model variants are available?",
31
+ dimension: 512,
32
+ },
33
+ tofu: {
34
+ text: "How is mapo tofu prepared?",
35
+ dimension: 1024,
36
+ },
37
+ safety: {
38
+ text: "Find the environmental assessment page about driver training and temporary road closures.",
39
+ dimension: 256,
40
+ },
41
+ image: {
42
+ text: "Match this screenshot to the most relevant description.",
43
+ asset: "/assets/llama4_hgf.png",
44
+ filename: "llama4_hgf.png",
45
+ type: "image/png",
46
+ custom:
47
+ "Llama family :: Scout and Maverick are multimodal mixture-of-experts model variants.\nRecipe :: Soft tofu simmered in spicy chili-bean sauce.",
48
+ include: false,
49
+ dimension: 256,
50
+ },
51
+ video: {
52
+ text: "What dish is being prepared in this clip?",
53
+ asset: "/assets/mapo_tofu.mp4",
54
+ filename: "mapo_tofu.mp4",
55
+ type: "video/mp4",
56
+ custom:
57
+ "Sichuan classic :: Mapo tofu combines soft tofu with a spicy, numbing bean-paste sauce.\nSpaceflight :: A launch vehicle carries a satellite into orbit.",
58
+ include: false,
59
+ dimension: 512,
60
+ },
61
+ };
62
+
63
+ function escapeHtml(value) {
64
+ return String(value ?? "")
65
+ .replaceAll("&", "&amp;")
66
+ .replaceAll("<", "&lt;")
67
+ .replaceAll(">", "&gt;")
68
+ .replaceAll('"', "&quot;")
69
+ .replaceAll("'", "&#039;");
70
+ }
71
+
72
+ function formatScore(value) {
73
+ const number = Number(value);
74
+ return `${number >= 0 ? "+" : ""}${number.toFixed(3)}`;
75
+ }
76
+
77
+ function formatSeconds(value) {
78
+ return `${Number(value).toFixed(1)}s`;
79
+ }
80
+
81
+ function clearObjectUrls() {
82
+ for (const value of state.objectUrls.values()) URL.revokeObjectURL(value);
83
+ state.objectUrls.clear();
84
+ }
85
+
86
+ async function connectClient() {
87
+ if (state.client) return state.client;
88
+ if (state.connecting) return state.connecting;
89
+ const status = $("#model-status");
90
+ state.connecting = (async () => {
91
+ try {
92
+ const client = await Client.connect(window.location.origin);
93
+ await client.view_api();
94
+ state.client = client;
95
+ status.className = "model-status ready";
96
+ $("span", status).textContent = "Model ready";
97
+ return client;
98
+ } catch (error) {
99
+ status.className = "model-status error";
100
+ $("span", status).textContent = "Connection failed";
101
+ state.connecting = null;
102
+ throw error;
103
+ }
104
+ })();
105
+ return state.connecting;
106
+ }
107
+
108
+ function switchMode(mode) {
109
+ $$(".nav-pill").forEach((button) => button.classList.toggle("active", button.dataset.mode === mode));
110
+ $$("[data-panel]").forEach((panel) => { panel.hidden = panel.dataset.panel !== mode; });
111
+ $(mode === "search" ? '[data-panel="search"]' : '[data-panel="compare"]').scrollIntoView({ behavior: "smooth", block: "start" });
112
+ }
113
+
114
+ function setDimension(group, value) {
115
+ const dimension = Number(value);
116
+ state[`${group}Dimension`] = dimension;
117
+ $(`[data-dimension-group="${group}"]`).querySelectorAll("button").forEach((button) => {
118
+ button.classList.toggle("active", Number(button.dataset.value) === dimension);
119
+ });
120
+ $(`#${group}-dimension`).value = String(dimension);
121
+ const output = $(`#${group}-dimension-output`);
122
+ if (output) output.textContent = `${dimension.toLocaleString()}D`;
123
+ }
124
+
125
+ function fileLabel(drop, file) {
126
+ const chip = $(".file-chip", drop);
127
+ if (!chip) return;
128
+ chip.hidden = !file;
129
+ if (file) $("span", chip).textContent = file.name;
130
+ }
131
+
132
+ function configureDropzone(dropSelector, inputSelector, stateKey, otherStateKey, otherDropSelector) {
133
+ const drop = $(dropSelector);
134
+ const input = $(inputSelector);
135
+ const acceptFile = (file) => {
136
+ if (!file) return;
137
+ state[stateKey] = file;
138
+ fileLabel(drop, file);
139
+ if (otherStateKey) {
140
+ state[otherStateKey] = null;
141
+ fileLabel($(otherDropSelector), null);
142
+ const otherInput = $(`${otherDropSelector} input`);
143
+ if (otherInput) otherInput.value = "";
144
+ }
145
+ };
146
+ input.addEventListener("change", () => acceptFile(input.files[0]));
147
+ ["dragenter", "dragover"].forEach((eventName) => drop.addEventListener(eventName, (event) => {
148
+ event.preventDefault();
149
+ drop.classList.add("dragover");
150
+ }));
151
+ ["dragleave", "drop"].forEach((eventName) => drop.addEventListener(eventName, (event) => {
152
+ event.preventDefault();
153
+ drop.classList.remove("dragover");
154
+ }));
155
+ drop.addEventListener("drop", (event) => acceptFile(event.dataTransfer.files[0]));
156
+ const remove = $(".file-chip button", drop);
157
+ if (remove) remove.addEventListener("click", (event) => {
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ input.value = "";
161
+ state[stateKey] = null;
162
+ fileLabel(drop, null);
163
+ });
164
+ }
165
+
166
+ function renderCandidateChips() {
167
+ const root = $("#candidate-chips");
168
+ root.innerHTML = state.candidateMedia.map((file, index) => `
169
+ <span class="candidate-chip"><span>${escapeHtml(file.name)}</span><button type="button" data-remove-media="${index}" aria-label="Remove ${escapeHtml(file.name)}">×</button></span>
170
+ `).join("");
171
+ $$('[data-remove-media]', root).forEach((button) => button.addEventListener("click", () => {
172
+ state.candidateMedia.splice(Number(button.dataset.removeMedia), 1);
173
+ renderCandidateChips();
174
+ }));
175
+ }
176
+
177
+ function bindPairFile(inputSelector, stateKey, otherKey, labelSelector) {
178
+ const input = $(inputSelector);
179
+ input.addEventListener("change", () => {
180
+ const file = input.files[0] ?? null;
181
+ state[stateKey] = file;
182
+ if (file && otherKey) {
183
+ state[otherKey] = null;
184
+ const side = input.closest(".compare-side");
185
+ const otherInput = input.accept.includes("image") ? $('input[accept="video/*"]', side) : $('input[accept="image/*"]', side);
186
+ if (otherInput) otherInput.value = "";
187
+ }
188
+ const label = $(labelSelector);
189
+ label.hidden = !file;
190
+ label.textContent = file ? file.name : "";
191
+ });
192
+ }
193
+
194
+ function startJob(mode, heavy = false) {
195
+ const toast = $("#job-toast");
196
+ const phase = $("#job-phase");
197
+ const elapsed = $("#job-elapsed");
198
+ const progress = $("#job-progress-bar");
199
+ $("#job-label").textContent = mode === "search" ? "RESOLVING SEMANTIC FIELD" : "MEASURING VECTOR ALIGNMENT";
200
+ phase.textContent = "Waiting for the ZeroGPU allocator…";
201
+ progress.style.width = "3%";
202
+ toast.hidden = false;
203
+ state.jobStarted = Date.now();
204
+ clearInterval(state.jobTimer);
205
+ state.jobTimer = setInterval(() => {
206
+ const seconds = Math.floor((Date.now() - state.jobStarted) / 1000);
207
+ elapsed.textContent = `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")} elapsed`;
208
+ const first = heavy ? 13 : 7;
209
+ if (seconds < 3) {
210
+ phase.textContent = "Waiting for the ZeroGPU allocator…";
211
+ progress.style.width = "8%";
212
+ } else if (seconds < first) {
213
+ phase.textContent = "Encoding inputs into 4,096 dimensions…";
214
+ progress.style.width = `${Math.min(52, 18 + seconds * 4)}%`;
215
+ } else {
216
+ phase.textContent = mode === "search" ? "Ranking the multimodal universe…" : "Tracing the Matryoshka curve…";
217
+ progress.style.width = `${Math.min(92, 55 + seconds)}%`;
218
+ }
219
+ }, 500);
220
+ }
221
+
222
+ function finishJob() {
223
+ clearInterval(state.jobTimer);
224
+ $("#job-phase").textContent = "Semantic field resolved.";
225
+ $("#job-progress-bar").style.width = "100%";
226
+ setTimeout(() => { $("#job-toast").hidden = true; }, 900);
227
+ }
228
+
229
+ function failJob(error) {
230
+ clearInterval(state.jobTimer);
231
+ $("#job-toast").hidden = true;
232
+ const toast = $("#error-toast");
233
+ $("span", toast).textContent = error?.message || String(error) || "The request could not be completed.";
234
+ toast.hidden = false;
235
+ }
236
+
237
+ function unwrapResult(result) {
238
+ let data = result?.data ?? result;
239
+ if (Array.isArray(data) && data.length === 1) data = data[0];
240
+ if (typeof data === "string") {
241
+ try { return JSON.parse(data); } catch { return data; }
242
+ }
243
+ return data;
244
+ }
245
+
246
+ function mediaUrl(item) {
247
+ if (item.media_url) return item.media_url;
248
+ return state.objectUrls.get(item.key) ?? null;
249
+ }
250
+
251
+ function mediaMarkup(item, className = "rank-media") {
252
+ const url = mediaUrl(item);
253
+ if (!url) return `<span class="${className} text-tile">Aa</span>`;
254
+ if (item.kind === "video") return `<video class="${className}" src="${escapeHtml(url)}" muted playsinline preload="metadata"></video>`;
255
+ return `<img class="${className}" src="${escapeHtml(url)}" alt="" loading="lazy" />`;
256
+ }
257
+
258
+ function rankingMarkup(items) {
259
+ return `<div class="ranking-list">${items.map((item) => {
260
+ const fill = Math.max(2, Math.min(100, Math.max(0, Number(item.score)) * 100));
261
+ return `<article class="ranking-card" style="--rank:${item.rank}">
262
+ <span class="rank-index">${String(item.rank).padStart(2, "0")}</span>
263
+ ${mediaMarkup(item)}
264
+ <div class="rank-copy"><div class="rank-title"><b>${escapeHtml(item.title)}</b><span class="kind-tag">${escapeHtml(item.kind)}</span></div><p>${escapeHtml(item.description)}</p><div class="score-line"><i style="width:${fill}%"></i></div></div>
265
+ <div class="rank-value">${formatScore(item.score)}<small>COSINE</small></div>
266
+ </article>`;
267
+ }).join("")}</div>`;
268
+ }
269
+
270
+ function visualMarkup(items) {
271
+ const visual = items.filter((item) => mediaUrl(item));
272
+ if (!visual.length) return `<div class="compare-empty"><p>No visual candidates in this result set.</p></div>`;
273
+ return `<div class="visual-grid">${visual.map((item) => `<figure class="visual-card">
274
+ ${item.kind === "video" ? `<video src="${escapeHtml(mediaUrl(item))}" controls muted playsinline preload="metadata"></video>` : `<img src="${escapeHtml(mediaUrl(item))}" alt="${escapeHtml(item.title)}" loading="lazy" />`}
275
+ <figcaption><span>#${item.rank} · ${escapeHtml(item.title)}</span><span>${formatScore(item.score)}</span></figcaption>
276
+ </figure>`).join("")}</div>`;
277
+ }
278
+
279
+ function lineChart(dimensions, series, title) {
280
+ const width = 720;
281
+ const height = 260;
282
+ const pad = { l: 46, r: 15, t: 20, b: 36 };
283
+ const values = series.flatMap((item) => item.values);
284
+ let low = Math.max(-1, Math.min(...values) - 0.08);
285
+ let high = Math.min(1, Math.max(...values) + 0.08);
286
+ if (high - low < 0.2) { const mid = (high + low) / 2; low = Math.max(-1, mid - 0.1); high = Math.min(1, mid + 0.1); }
287
+ const x = (index) => pad.l + (index * (width - pad.l - pad.r)) / (dimensions.length - 1);
288
+ const y = (value) => pad.t + ((high - value) * (height - pad.t - pad.b)) / Math.max(0.0001, high - low);
289
+ const grid = Array.from({ length: 5 }, (_, index) => {
290
+ const value = high - (index * (high - low)) / 4;
291
+ return `<line class="chart-grid-line" x1="${pad.l}" y1="${y(value)}" x2="${width-pad.r}" y2="${y(value)}"/><text class="chart-label" x="${pad.l-8}" y="${y(value)+3}" text-anchor="end">${formatScore(value).slice(0,-1)}</text>`;
292
+ }).join("");
293
+ const labels = dimensions.map((value, index) => `<text class="chart-label" x="${x(index)}" y="${height-12}" text-anchor="middle">${value}</text>`).join("");
294
+ const paths = series.map((item, seriesIndex) => {
295
+ const color = colors[seriesIndex % colors.length];
296
+ const points = item.values.map((value, index) => `${x(index)},${y(value)}`).join(" ");
297
+ const dots = item.values.map((value, index) => `<circle class="chart-dot" cx="${x(index)}" cy="${y(value)}" r="3" fill="${color}"/>`).join("");
298
+ return `<polyline class="chart-line" points="${points}" stroke="${color}"/>${dots}`;
299
+ }).join("");
300
+ const legend = series.map((item, index) => `<span><i style="background:${colors[index % colors.length]}"></i>${escapeHtml(item.title.slice(0, 40))}</span>`).join("");
301
+ return `<section class="viz-panel"><div class="viz-head"><div><span>MATRYOSHKA SCOPE</span><b>${escapeHtml(title)}</b></div><small>64 → 4096D</small></div><svg class="line-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="Cosine similarity across embedding dimensions">${grid}${labels}${paths}</svg><div class="chart-legend">${legend}</div></section>`;
302
+ }
303
+
304
+ function fingerprintChart(values, title, dimension) {
305
+ const width = 720;
306
+ const height = 155;
307
+ const center = 78;
308
+ const scale = Math.max(...values.map((value) => Math.abs(value)), 0.000001);
309
+ const barWidth = (width - 16) / values.length;
310
+ const bars = values.map((value, index) => {
311
+ const magnitude = Math.min(64, (Math.abs(value) / scale) * 64);
312
+ const y = value >= 0 ? center - magnitude : center;
313
+ return `<rect x="${8 + index * barWidth}" y="${y}" width="${Math.max(1.2,barWidth-1.4)}" height="${magnitude}" rx="1" fill="${value >= 0 ? "#66e3cf" : "#ff9c62"}" opacity=".86"/>`;
314
+ }).join("");
315
+ return `<section class="viz-panel"><div class="viz-head"><div><span>VECTOR FINGERPRINT</span><b>${escapeHtml(title)}</b></div><small>${Number(dimension).toLocaleString()} values</small></div><svg class="fingerprint-svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="Compressed signed vector fingerprint"><line class="fingerprint-zero" x1="8" y1="${center}" x2="${width-8}" y2="${center}"/>${bars}</svg><div class="fingerprint-key"><span><i style="background:#66e3cf"></i>POSITIVE</span><span><i style="background:#ff9c62"></i>NEGATIVE</span><span>96 POOLED SLICES</span></div></section>`;
316
+ }
317
+
318
+ function telemetryMarkup(data) {
319
+ const compact = { ...data };
320
+ delete compact.rankings;
321
+ delete compact.dimension_series;
322
+ delete compact.query_fingerprint;
323
+ delete compact.candidate_fingerprint;
324
+ return `<details class="telemetry"><summary><span>EMBEDDING TELEMETRY</span><span>INSPECT JSON +</span></summary><pre>${escapeHtml(JSON.stringify(compact, null, 2))}</pre></details>`;
325
+ }
326
+
327
+ function analysisMarkup(data) {
328
+ return `<div class="viz-stack">${lineChart(data.dimensions, data.dimension_series, "Does the ranking survive compression?")}${fingerprintChart(data.query_fingerprint, `Query · ${data.query_kind}`, data.dimension)}${telemetryMarkup(data)}</div>`;
329
+ }
330
+
331
+ function bindResultTabs(root) {
332
+ $$('[data-result-tab]', root).forEach((button) => button.addEventListener("click", () => {
333
+ $$('[data-result-tab]', root).forEach((item) => item.classList.toggle("active", item === button));
334
+ $$('[data-result-view]', root).forEach((view) => { view.hidden = view.dataset.resultView !== button.dataset.resultTab; });
335
+ }));
336
+ }
337
+
338
+ function renderSearchResults(data) {
339
+ const root = $("#search-results");
340
+ root.innerHTML = `
341
+ <div class="result-topbar"><div><span class="signal"><i></i>FIELD RESOLVED</span><small>tencent / WeMM-Embedding-9B</small></div><span class="result-count">${data.candidate_count} CANDIDATES · ${Number(data.dimension).toLocaleString()}D</span></div>
342
+ <section class="summary-card"><div><span>TOP SEMANTIC MATCH</span><h3>${escapeHtml(data.top_match.title)}</h3><p>${escapeHtml(data.query_kind)} query · ${formatSeconds(data.elapsed_seconds)} GPU pass · ${escapeHtml(data.cache_state)} cache</p></div><div class="top-score"><b>${formatScore(data.top_match.score)}</b><small>COSINE</small></div></section>
343
+ <nav class="result-tabs" aria-label="Result view"><button class="active" data-result-tab="rank">RANKING</button><button data-result-tab="visual">VISUAL FIELD</button><button data-result-tab="analysis">ANALYSIS</button></nav>
344
+ <div class="result-view" data-result-view="rank">${rankingMarkup(data.rankings)}</div>
345
+ <div class="result-view" data-result-view="visual" hidden>${visualMarkup(data.rankings)}</div>
346
+ <div class="result-view" data-result-view="analysis" hidden>${analysisMarkup(data)}</div>`;
347
+ bindResultTabs(root);
348
+ root.scrollIntoView({ behavior: "smooth", block: "start" });
349
+ }
350
+
351
+ function renderCompareResults(data) {
352
+ const ring = Math.max(0, Math.min(100, (Number(data.selected_score) + 1) * 50));
353
+ const series = [{ title: `${data.query_kind} → ${data.candidate_kind}`, values: data.scores }];
354
+ $("#compare-results").innerHTML = `
355
+ <section class="alignment-card"><div class="score-ring" style="--score:${ring}"><div><b>${formatScore(data.selected_score)}</b><small>COSINE</small></div></div><div class="alignment-copy"><span>PAIRWISE READOUT</span><h3>${escapeHtml(data.label)}</h3><p>${escapeHtml(data.explanation)}</p><div class="alignment-meta"><span>${escapeHtml(data.query_kind)}</span><span>→</span><span>${escapeHtml(data.candidate_kind)}</span><span>${Number(data.dimension).toLocaleString()}D</span><span>${formatSeconds(data.elapsed_seconds)}</span></div></div></section>
356
+ <div class="compare-viz-grid">${lineChart(data.dimensions, series, "Semantic alignment under compression?")}<div class="fingerprint-pair">${fingerprintChart(data.query_fingerprint, `A · ${data.query_kind}`, data.dimension)}${fingerprintChart(data.candidate_fingerprint, `B · ${data.candidate_kind}`, data.dimension)}</div></div>
357
+ <div style="margin-top:12px">${telemetryMarkup(data)}</div>`;
358
+ $("#compare-results").scrollIntoView({ behavior: "smooth", block: "start" });
359
+ }
360
+
361
+ async function submitSearch() {
362
+ const button = $("#search-button");
363
+ clearObjectUrls();
364
+ state.candidateMedia.forEach((file, index) => state.objectUrls.set(`custom-media-${index + 1}`, URL.createObjectURL(file)));
365
+ const payload = {
366
+ query_text: $("#query-text").value,
367
+ query_image: state.searchImage ? handle_file(state.searchImage) : null,
368
+ query_video: state.searchVideo ? handle_file(state.searchVideo) : null,
369
+ custom_texts: $("#custom-texts").value,
370
+ candidate_media: state.candidateMedia.map((file) => handle_file(file)),
371
+ include_showcase: $("#include-showcase").checked,
372
+ dimension: state.searchDimension,
373
+ };
374
+ button.disabled = true;
375
+ startJob("search", Boolean(state.searchVideo || state.candidateMedia.some((file) => file.type.startsWith("video/")) || payload.include_showcase));
376
+ try {
377
+ const client = await connectClient();
378
+ const result = await client.predict("/search", payload);
379
+ const data = unwrapResult(result);
380
+ if (!data || typeof data !== "object") throw new Error("The server returned an unexpected search result.");
381
+ renderSearchResults(data);
382
+ finishJob();
383
+ } catch (error) {
384
+ failJob(error);
385
+ } finally {
386
+ button.disabled = false;
387
+ }
388
+ }
389
+
390
+ async function submitCompare() {
391
+ const button = $("#compare-button");
392
+ const payload = {
393
+ query_text: $("#pair-query-text").value,
394
+ query_image: state.pairQueryImage ? handle_file(state.pairQueryImage) : null,
395
+ query_video: state.pairQueryVideo ? handle_file(state.pairQueryVideo) : null,
396
+ candidate_text: $("#pair-candidate-text").value,
397
+ candidate_image: state.pairCandidateImage ? handle_file(state.pairCandidateImage) : null,
398
+ candidate_video: state.pairCandidateVideo ? handle_file(state.pairCandidateVideo) : null,
399
+ dimension: state.compareDimension,
400
+ };
401
+ button.disabled = true;
402
+ startJob("compare", Boolean(state.pairQueryVideo || state.pairCandidateVideo));
403
+ try {
404
+ const client = await connectClient();
405
+ const result = await client.predict("/compare", payload);
406
+ const data = unwrapResult(result);
407
+ if (!data || typeof data !== "object") throw new Error("The server returned an unexpected comparison result.");
408
+ renderCompareResults(data);
409
+ finishJob();
410
+ } catch (error) {
411
+ failJob(error);
412
+ } finally {
413
+ button.disabled = false;
414
+ }
415
+ }
416
+
417
+ async function loadAssetAsFile(url, filename, type) {
418
+ const response = await fetch(url);
419
+ if (!response.ok) throw new Error(`Could not load the curated asset ${filename}.`);
420
+ return new File([await response.blob()], filename, { type });
421
+ }
422
+
423
+ async function runExample(name) {
424
+ const example = examples[name];
425
+ switchMode("search");
426
+ $("#query-text").value = example.text;
427
+ $("#custom-texts").value = example.custom ?? "";
428
+ $("#include-showcase").checked = example.include ?? true;
429
+ state.searchImage = null;
430
+ state.searchVideo = null;
431
+ fileLabel($("#query-image-drop"), null);
432
+ fileLabel($("#query-video-drop"), null);
433
+ $("#query-image").value = "";
434
+ $("#query-video").value = "";
435
+ setDimension("search", example.dimension);
436
+ if (example.asset) {
437
+ try {
438
+ const file = await loadAssetAsFile(example.asset, example.filename, example.type);
439
+ if (example.type.startsWith("image")) {
440
+ state.searchImage = file;
441
+ fileLabel($("#query-image-drop"), file);
442
+ } else {
443
+ state.searchVideo = file;
444
+ fileLabel($("#query-video-drop"), file);
445
+ }
446
+ } catch (error) {
447
+ failJob(error);
448
+ return;
449
+ }
450
+ }
451
+ setTimeout(submitSearch, 350);
452
+ }
453
+
454
+ function resetSearch() {
455
+ state.searchImage = null;
456
+ state.searchVideo = null;
457
+ state.candidateMedia = [];
458
+ setDimension("search", 1024);
459
+ setTimeout(() => {
460
+ fileLabel($("#query-image-drop"), null);
461
+ fileLabel($("#query-video-drop"), null);
462
+ renderCandidateChips();
463
+ });
464
+ }
465
+
466
+ function bindEvents() {
467
+ $$(".nav-pill").forEach((button) => button.addEventListener("click", () => switchMode(button.dataset.mode)));
468
+ $$("[data-dimension-group]").forEach((group) => $$('button', group).forEach((button) => button.addEventListener("click", () => setDimension(group.dataset.dimensionGroup, button.dataset.value))));
469
+ $$(".expedition").forEach((button) => button.addEventListener("click", () => runExample(button.dataset.example)));
470
+
471
+ configureDropzone("#query-image-drop", "#query-image", "searchImage", "searchVideo", "#query-video-drop");
472
+ configureDropzone("#query-video-drop", "#query-video", "searchVideo", "searchImage", "#query-image-drop");
473
+ $("#candidate-media").addEventListener("change", (event) => {
474
+ state.candidateMedia = [...event.target.files].slice(0, 6);
475
+ renderCandidateChips();
476
+ });
477
+
478
+ bindPairFile("#pair-query-image", "pairQueryImage", "pairQueryVideo", "#pair-query-file");
479
+ bindPairFile("#pair-query-video", "pairQueryVideo", "pairQueryImage", "#pair-query-file");
480
+ bindPairFile("#pair-candidate-image", "pairCandidateImage", "pairCandidateVideo", "#pair-candidate-file");
481
+ bindPairFile("#pair-candidate-video", "pairCandidateVideo", "pairCandidateImage", "#pair-candidate-file");
482
+
483
+ $("#search-form").addEventListener("submit", (event) => { event.preventDefault(); submitSearch(); });
484
+ $("#search-form").addEventListener("reset", resetSearch);
485
+ $("#compare-form").addEventListener("submit", (event) => { event.preventDefault(); submitCompare(); });
486
+ $("#query-text").addEventListener("keydown", (event) => {
487
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
488
+ event.preventDefault();
489
+ submitSearch();
490
+ }
491
+ });
492
+ $("#job-close").addEventListener("click", () => { $("#job-toast").hidden = true; });
493
+ $("#error-toast button").addEventListener("click", () => { $("#error-toast").hidden = true; });
494
+ }
495
+
496
+ bindEvents();
497
+ connectClient().catch(() => {});
frontend/index.html ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#080908" />
7
+ <meta
8
+ name="description"
9
+ content="Explore WeMM-Embedding-9B across text, images, video, figures, and visual documents."
10
+ />
11
+ <title>WeMM — Semantic Universe</title>
12
+ <link rel="stylesheet" href="/ui/app.css?v=2" />
13
+ <script type="module" src="/ui/app.js?v=2"></script>
14
+ </head>
15
+ <body>
16
+ <div class="noise" aria-hidden="true"></div>
17
+ <div class="aurora aurora-a" aria-hidden="true"></div>
18
+ <div class="aurora aurora-b" aria-hidden="true"></div>
19
+
20
+ <header class="site-header">
21
+ <a class="brand" href="#top" aria-label="WeMM home">
22
+ <span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span>
23
+ <span><b>WeMM</b><small>SEMANTIC UNIVERSE</small></span>
24
+ </a>
25
+ <nav class="mode-nav" aria-label="Studio mode">
26
+ <button class="nav-pill active" data-mode="search">Semantic search</button>
27
+ <button class="nav-pill" data-mode="compare">Vector microscope</button>
28
+ </nav>
29
+ <div class="header-actions">
30
+ <span class="model-status" id="model-status"><i></i><span>Connecting</span></span>
31
+ <a class="icon-link" href="/gradio_api/info" target="_blank" title="API schema" aria-label="Open API schema">API</a>
32
+ <a class="icon-link arrow-link" href="https://huggingface.co/tencent/WeMM-Embedding-9B" target="_blank" rel="noreferrer" aria-label="Open model card">↗</a>
33
+ </div>
34
+ </header>
35
+
36
+ <main id="top">
37
+ <section class="hero" aria-labelledby="hero-title">
38
+ <div class="hero-copy">
39
+ <div class="overline"><span>UNIVERSAL MULTIMODAL EMBEDDING</span><b>QWEN3.5 · 9B</b></div>
40
+ <h1 id="hero-title">Meaning has<br /><em>no file type.</em></h1>
41
+ <p>
42
+ Project language, imagery, video, charts, and visual documents into one shared
43
+ geometry—then watch meaning arrange itself.
44
+ </p>
45
+ <div class="hero-meta">
46
+ <div><b>4,096</b><span>native dimensions</span></div>
47
+ <div><b>80.6</b><span>MMEB-v2 average</span></div>
48
+ <div><b>190</b><span>MMEB-v3 tasks</span></div>
49
+ </div>
50
+ </div>
51
+ <div class="semantic-orbit" aria-hidden="true">
52
+ <div class="orbit-ring ring-one"></div>
53
+ <div class="orbit-ring ring-two"></div>
54
+ <div class="orbit-ring ring-three"></div>
55
+ <div class="orbit-core"><span>W</span><small>4096D</small></div>
56
+ <span class="orbit-node node-text"><i>Tx</i>TEXT</span>
57
+ <span class="orbit-node node-image"><i>Im</i>IMAGE</span>
58
+ <span class="orbit-node node-video"><i>Vd</i>VIDEO</span>
59
+ <span class="orbit-node node-doc"><i>Dc</i>DOCUMENT</span>
60
+ <svg viewBox="0 0 500 500">
61
+ <path d="M110 190 C190 220 204 235 250 250" />
62
+ <path d="M390 154 C335 192 302 220 250 250" />
63
+ <path d="M390 355 C328 317 302 283 250 250" />
64
+ <path d="M110 355 C164 315 198 285 250 250" />
65
+ </svg>
66
+ </div>
67
+ </section>
68
+
69
+ <section class="expedition-strip" id="expeditions">
70
+ <div class="strip-label"><span>CURATED EXPEDITIONS</span><small>Choose a path into the model</small></div>
71
+ <div class="expedition-list">
72
+ <button class="expedition" data-example="llama"><span>01</span><b>Find Llama 4</b><small>text → screenshot</small></button>
73
+ <button class="expedition" data-example="tofu"><span>02</span><b>Find mapo tofu</b><small>text → video</small></button>
74
+ <button class="expedition" data-example="safety"><span>03</span><b>Read road safety</b><small>text → document</small></button>
75
+ <button class="expedition" data-example="image"><span>04</span><b>Match a screenshot</b><small>image + text</small></button>
76
+ <button class="expedition" data-example="video"><span>05</span><b>Understand a clip</b><small>video → text</small></button>
77
+ </div>
78
+ </section>
79
+
80
+ <section class="studio" data-panel="search">
81
+ <div class="section-heading">
82
+ <div><span>01 / RETRIEVAL UNIVERSE</span><h2>Ask in one medium.<br />Discover in another.</h2></div>
83
+ <p>Search the curated field or add a small collection of your own. Every result is scored in the same normalized vector space.</p>
84
+ </div>
85
+
86
+ <div class="studio-grid">
87
+ <form class="control-deck" id="search-form">
88
+ <div class="deck-heading">
89
+ <span>QUERY COMPOSER</span>
90
+ <button class="text-button" type="reset">Clear all</button>
91
+ </div>
92
+
93
+ <label class="field-label" for="query-text">Describe what you mean</label>
94
+ <div class="textarea-wrap">
95
+ <textarea id="query-text" rows="4" maxlength="1200" placeholder="Which document explains temporary road closures?"></textarea>
96
+ <span class="key-hint">⌘ ↵</span>
97
+ </div>
98
+
99
+ <div class="visual-query-grid">
100
+ <label class="drop-field" id="query-image-drop">
101
+ <input id="query-image" type="file" accept="image/*" />
102
+ <span class="drop-icon">⌁</span><b>Add image</b><small>PNG, JPG, WEBP</small>
103
+ <div class="file-chip" hidden><span></span><button type="button" aria-label="Remove image">×</button></div>
104
+ </label>
105
+ <label class="drop-field" id="query-video-drop">
106
+ <input id="query-video" type="file" accept="video/*" />
107
+ <span class="drop-icon play">▷</span><b>Add video</b><small>MP4, WEBM, MOV</small>
108
+ <div class="file-chip" hidden><span></span><button type="button" aria-label="Remove video">×</button></div>
109
+ </label>
110
+ </div>
111
+ <p class="context-note"><span>+</span> Visual media and text become one interleaved query.</p>
112
+
113
+ <details class="collection-builder">
114
+ <summary><span><i>+</i> Build your candidate collection</span><small>optional</small></summary>
115
+ <div class="details-body">
116
+ <label class="field-label" for="custom-texts">Text candidates · one per line</label>
117
+ <textarea id="custom-texts" rows="5" placeholder="Title :: Candidate text&#10;Another title :: Another candidate"></textarea>
118
+ <label class="mini-upload">
119
+ <input id="candidate-media" type="file" accept="image/*,video/*" multiple />
120
+ <span>+</span><b>Add candidate media</b><small>up to 6 files</small>
121
+ </label>
122
+ <div class="candidate-chips" id="candidate-chips"></div>
123
+ </div>
124
+ </details>
125
+
126
+ <div class="switch-line">
127
+ <div><b>Curated universe</b><small>10 mixed-media candidates</small></div>
128
+ <label class="switch"><input id="include-showcase" type="checkbox" checked /><span></span></label>
129
+ </div>
130
+
131
+ <fieldset class="dimension-field">
132
+ <legend><span>EMBEDDING BUDGET</span><output id="search-dimension-output">1,024D</output></legend>
133
+ <div class="dimension-pills" data-dimension-group="search">
134
+ <button type="button" data-value="64">64</button><button type="button" data-value="128">128</button>
135
+ <button type="button" data-value="256">256</button><button type="button" data-value="512">512</button>
136
+ <button type="button" data-value="1024" class="active">1K</button><button type="button" data-value="2048">2K</button>
137
+ <button type="button" data-value="4096">4K</button>
138
+ </div>
139
+ <input id="search-dimension" type="hidden" value="1024" />
140
+ <p>Smaller vectors trade storage for retrieval fidelity.</p>
141
+ </fieldset>
142
+
143
+ <button class="primary-button" id="search-button" type="submit">
144
+ <span>Map the semantic field</span><i>→</i>
145
+ </button>
146
+ </form>
147
+
148
+ <div class="result-deck" id="search-results" aria-live="polite">
149
+ <div class="result-topbar">
150
+ <div><span class="signal"><i></i>MODEL READY</span><small>tencent / WeMM-Embedding-9B</small></div>
151
+ <span class="result-count">AWAITING QUERY</span>
152
+ </div>
153
+ <div class="empty-universe">
154
+ <div class="empty-field" aria-hidden="true">
155
+ <i></i><i></i><i></i><i></i><i></i><i></i><span>W</span>
156
+ </div>
157
+ <h3>Your semantic field is quiet.</h3>
158
+ <p>Compose a query or choose a curated expedition. The first run maps the multimodal corpus; later runs reuse its cached vectors.</p>
159
+ <div class="media-preview-rail">
160
+ <img src="/assets/llama4_hgf.png" alt="Llama 4 model card screenshot" />
161
+ <img src="/assets/doc2.jpg" alt="Historical budget infographic" />
162
+ <video src="/assets/mapo_tofu.mp4" muted playsinline aria-label="Mapo tofu cooking clip"></video>
163
+ <img src="/assets/doc4.jpg" alt="Road safety assessment page" />
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </section>
169
+
170
+ <section class="studio compare-studio" data-panel="compare" hidden>
171
+ <div class="section-heading">
172
+ <div><span>02 / VECTOR MICROSCOPE</span><h2>Put any two ideas<br />under the lens.</h2></div>
173
+ <p>Compare any supported pair, then observe whether their relationship survives Matryoshka compression from 4,096 to 64 dimensions.</p>
174
+ </div>
175
+
176
+ <form id="compare-form">
177
+ <div class="compare-inputs">
178
+ <article class="compare-side" data-side="a">
179
+ <header><span>A</span><div><b>QUERY</b><small>What are you looking for?</small></div></header>
180
+ <textarea id="pair-query-text" rows="5" placeholder="A picture or description of an idea…"></textarea>
181
+ <div class="pair-uploads">
182
+ <label><input id="pair-query-image" type="file" accept="image/*" /><span>⌁</span>IMAGE</label>
183
+ <label><input id="pair-query-video" type="file" accept="video/*" /><span>▷</span>VIDEO</label>
184
+ </div>
185
+ <div class="pair-file" id="pair-query-file" hidden></div>
186
+ </article>
187
+ <div class="versus" aria-hidden="true"><span>↔</span><small>COSINE</small></div>
188
+ <article class="compare-side" data-side="b">
189
+ <header><span>B</span><div><b>CANDIDATE</b><small>What should it align with?</small></div></header>
190
+ <textarea id="pair-candidate-text" rows="5" placeholder="A candidate in text, image, or video…"></textarea>
191
+ <div class="pair-uploads">
192
+ <label><input id="pair-candidate-image" type="file" accept="image/*" /><span>⌁</span>IMAGE</label>
193
+ <label><input id="pair-candidate-video" type="file" accept="video/*" /><span>▷</span>VIDEO</label>
194
+ </div>
195
+ <div class="pair-file" id="pair-candidate-file" hidden></div>
196
+ </article>
197
+ </div>
198
+
199
+ <div class="compare-controls">
200
+ <div class="dimension-pills" data-dimension-group="compare">
201
+ <button type="button" data-value="64">64</button><button type="button" data-value="128">128</button>
202
+ <button type="button" data-value="256">256</button><button type="button" data-value="512">512</button>
203
+ <button type="button" data-value="1024" class="active">1K</button><button type="button" data-value="2048">2K</button>
204
+ <button type="button" data-value="4096">4K</button>
205
+ </div>
206
+ <input id="compare-dimension" type="hidden" value="1024" />
207
+ <button class="primary-button" id="compare-button" type="submit"><span>Measure alignment</span><i>→</i></button>
208
+ </div>
209
+ </form>
210
+ <div class="compare-results" id="compare-results" aria-live="polite">
211
+ <div class="compare-empty"><span>+</span><p>Add one input on each side to activate the microscope.</p></div>
212
+ </div>
213
+ </section>
214
+
215
+ <section class="model-story">
216
+ <div class="story-number">4096</div>
217
+ <div class="story-copy">
218
+ <span>ONE MODEL · MANY RESOLUTIONS</span>
219
+ <h2>Fold the vector.<br /><em>Keep the meaning.</em></h2>
220
+ <p>WeMM emits a 4,096-dimensional embedding with Matryoshka structure. Store fewer leading dimensions when latency or memory matters, and inspect the tradeoff here before choosing.</p>
221
+ </div>
222
+ <div class="dimension-tower" aria-hidden="true">
223
+ <i style="--w:100%"><span>4096</span></i><i style="--w:72%"><span>2048</span></i>
224
+ <i style="--w:50%"><span>1024</span></i><i style="--w:36%"><span>512</span></i>
225
+ <i style="--w:24%"><span>256</span></i><i style="--w:16%"><span>128</span></i>
226
+ <i style="--w:10%"><span>64</span></i>
227
+ </div>
228
+ </section>
229
+ </main>
230
+
231
+ <footer>
232
+ <div class="footer-brand"><span class="brand-mark"><i></i><i></i><i></i></span><b>WeMM</b></div>
233
+ <p>Cosine similarity ranks candidates within a collection. It is not a calibrated probability. Audio is not supported.</p>
234
+ <div><a href="https://huggingface.co/tencent/WeMM-Embedding-9B" target="_blank" rel="noreferrer">MODEL CARD ↗</a><a href="https://arxiv.org/abs/2608.24053" target="_blank" rel="noreferrer">PAPER ↗</a></div>
235
+ </footer>
236
+
237
+ <div class="job-toast" id="job-toast" hidden>
238
+ <div class="job-orbit"><i></i><i></i><span>W</span></div>
239
+ <div><span id="job-label">ALLOCATING ZERO GPU</span><b id="job-phase">Entering the semantic field…</b><small id="job-elapsed">00:00 elapsed</small></div>
240
+ <button id="job-close" type="button" aria-label="Dismiss">×</button>
241
+ <div class="job-progress"><i id="job-progress-bar"></i></div>
242
+ </div>
243
+ <div class="error-toast" id="error-toast" hidden><b>Something interrupted the field.</b><span></span><button type="button">×</button></div>
244
+ </body>
245
+ </html>