Spaces:
Paused
Paused
Switch to gr.Server with custom semantic studio frontend
Browse files- README.md +12 -4
- app.py +167 -712
- frontend/app.css +271 -0
- frontend/app.js +497 -0
- 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
|
|
|
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 182 |
-
return {"image": image_path, "text": text}, "image + text"
|
| 183 |
-
return image_path, "image"
|
| 184 |
if video_path:
|
| 185 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
""
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 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 |
-
) ->
|
| 625 |
-
"""
|
| 626 |
dimension = int(dimension)
|
|
|
|
|
|
|
|
|
|
| 627 |
query_payload, query_kind = _multimodal_payload(query_text, query_image, query_video)
|
| 628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
with _INFERENCE_LOCK:
|
| 630 |
query, candidate, elapsed = _run_pair(query_payload, candidate_payload, progress)
|
| 631 |
-
|
|
|
|
| 632 |
dim: float((_truncate_normalize(query, dim) @ _truncate_normalize(candidate, dim).T).item())
|
| 633 |
for dim in MATRYOSHKA_DIMS
|
| 634 |
}
|
| 635 |
-
selected_score =
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 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 |
-
"
|
| 650 |
-
"
|
| 651 |
-
"
|
| 652 |
-
"
|
| 653 |
-
"
|
| 654 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 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 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
|
| 1055 |
|
| 1056 |
if __name__ == "__main__":
|
| 1057 |
-
|
| 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("&", "&")
|
| 66 |
+
.replaceAll("<", "<")
|
| 67 |
+
.replaceAll(">", ">")
|
| 68 |
+
.replaceAll('"', """)
|
| 69 |
+
.replaceAll("'", "'");
|
| 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 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>
|