Use actual scatter layout composition
Browse files- .hyperview/extensions/fashion-search-readout/panel.js +66 -10
- Dockerfile +2 -1
- README.md +1 -1
- demo.py +35 -53
.hyperview/extensions/fashion-search-readout/panel.js
CHANGED
|
@@ -67,6 +67,7 @@ function normalizeModels(value) {
|
|
| 67 |
model.buttonLabel || model.button_label || `${model.displayName || model.key || "Model"} query`,
|
| 68 |
),
|
| 69 |
layoutKey: model.layoutKey || model.layout_key || null,
|
|
|
|
| 70 |
}));
|
| 71 |
}
|
| 72 |
|
|
@@ -316,6 +317,7 @@ function RankedResultStrip({ item, model, rows, targetRank, loadingKey, onSelect
|
|
| 316 |
|
| 317 |
function SearchResultComparison({ item, models, loadingKey, onSelectResult }) {
|
| 318 |
const proof = rankedResults[item.id];
|
|
|
|
| 319 |
if (!proof) {
|
| 320 |
return React.createElement(
|
| 321 |
"div",
|
|
@@ -345,7 +347,9 @@ function SearchResultComparison({ item, models, loadingKey, onSelectResult }) {
|
|
| 345 |
React.createElement(
|
| 346 |
"div",
|
| 347 |
{ style: { color: colors.mutedText, fontSize: 10, marginTop: 3 } },
|
| 348 |
-
|
|
|
|
|
|
|
| 349 |
),
|
| 350 |
),
|
| 351 |
models.map((model) =>
|
|
@@ -601,8 +605,11 @@ export default function FashionSearchComparisonPanel() {
|
|
| 601 |
const panelProps = usePanelProps();
|
| 602 |
const [panelError, setPanelError] = React.useState(null);
|
| 603 |
const [loadingKey, setLoadingKey] = React.useState(null);
|
| 604 |
-
const [selectedExampleId, setSelectedExampleId] = React.useState(
|
|
|
|
|
|
|
| 605 |
const didApplyDemoLayout = React.useRef(false);
|
|
|
|
| 606 |
const mode = panelProps.mode === "summary" ? "summary" : panelProps.mode === "results" ? "results" : "full";
|
| 607 |
|
| 608 |
const models = React.useMemo(() => normalizeModels(panelProps.models), [panelProps.models]);
|
|
@@ -636,10 +643,42 @@ export default function FashionSearchComparisonPanel() {
|
|
| 636 |
return () => window.removeEventListener("fashion-search-example-change", handleExampleChange);
|
| 637 |
}, []);
|
| 638 |
|
| 639 |
-
const
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
React.useEffect(() => {
|
| 645 |
if (mode === "summary") return;
|
|
@@ -663,6 +702,23 @@ export default function FashionSearchComparisonPanel() {
|
|
| 663 |
return () => window.clearInterval(timer);
|
| 664 |
}, [commands, mode]);
|
| 665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
const clearSelection = async () => {
|
| 667 |
if (commands.setLabelFilter) commands.setLabelFilter(null);
|
| 668 |
setPanelError(null);
|
|
@@ -684,12 +740,12 @@ export default function FashionSearchComparisonPanel() {
|
|
| 684 |
await commands.showSimilar({
|
| 685 |
sampleId: item.queryId,
|
| 686 |
layoutKey: model.layoutKey,
|
|
|
|
| 687 |
k: 10,
|
| 688 |
source: `fashion-demo:${model.key}`,
|
| 689 |
-
focus:
|
| 690 |
persist: false,
|
| 691 |
});
|
| 692 |
-
commands.focusPanel?.(model.key === "candidate" ? "fashion-map-hyper3" : "fashion-map-clip");
|
| 693 |
} catch (error) {
|
| 694 |
const message = error instanceof Error ? error.message : String(error);
|
| 695 |
setPanelError(`Could not select target item: ${message}`);
|
|
@@ -715,12 +771,12 @@ export default function FashionSearchComparisonPanel() {
|
|
| 715 |
await commands.showSimilar({
|
| 716 |
sampleId: result.sampleId,
|
| 717 |
layoutKey: model.layoutKey,
|
|
|
|
| 718 |
k: 10,
|
| 719 |
source: `fashion-ranked-result:${model.key}`,
|
| 720 |
-
focus:
|
| 721 |
persist: false,
|
| 722 |
});
|
| 723 |
-
commands.focusPanel?.(model.key === "candidate" ? "fashion-map-hyper3" : "fashion-map-clip");
|
| 724 |
} catch (error) {
|
| 725 |
const message = error instanceof Error ? error.message : String(error);
|
| 726 |
setPanelError(`Could not select ranked result: ${message}`);
|
|
|
|
| 67 |
model.buttonLabel || model.button_label || `${model.displayName || model.key || "Model"} query`,
|
| 68 |
),
|
| 69 |
layoutKey: model.layoutKey || model.layout_key || null,
|
| 70 |
+
spaceKey: model.spaceKey || model.space_key || null,
|
| 71 |
}));
|
| 72 |
}
|
| 73 |
|
|
|
|
| 317 |
|
| 318 |
function SearchResultComparison({ item, models, loadingKey, onSelectResult }) {
|
| 319 |
const proof = rankedResults[item.id];
|
| 320 |
+
const hasInspectableLayouts = models.some((model) => model.layoutKey);
|
| 321 |
if (!proof) {
|
| 322 |
return React.createElement(
|
| 323 |
"div",
|
|
|
|
| 347 |
React.createElement(
|
| 348 |
"div",
|
| 349 |
{ style: { color: colors.mutedText, fontSize: 10, marginTop: 3 } },
|
| 350 |
+
hasInspectableLayouts
|
| 351 |
+
? "Green card = exact product. Click a card to inspect it in Samples and the model neighborhood."
|
| 352 |
+
: "Green card = exact product. Click a card to select it in Samples.",
|
| 353 |
),
|
| 354 |
),
|
| 355 |
models.map((model) =>
|
|
|
|
| 605 |
const panelProps = usePanelProps();
|
| 606 |
const [panelError, setPanelError] = React.useState(null);
|
| 607 |
const [loadingKey, setLoadingKey] = React.useState(null);
|
| 608 |
+
const [selectedExampleId, setSelectedExampleId] = React.useState(() =>
|
| 609 |
+
typeof panelProps.initialExampleId === "string" ? panelProps.initialExampleId : null,
|
| 610 |
+
);
|
| 611 |
const didApplyDemoLayout = React.useRef(false);
|
| 612 |
+
const didPrimeEvidence = React.useRef(false);
|
| 613 |
const mode = panelProps.mode === "summary" ? "summary" : panelProps.mode === "results" ? "results" : "full";
|
| 614 |
|
| 615 |
const models = React.useMemo(() => normalizeModels(panelProps.models), [panelProps.models]);
|
|
|
|
| 643 |
return () => window.removeEventListener("fashion-search-example-change", handleExampleChange);
|
| 644 |
}, []);
|
| 645 |
|
| 646 |
+
const showExampleEvidence = React.useCallback(
|
| 647 |
+
async (item, model, source = "fashion-demo:query") => {
|
| 648 |
+
if (!item || !model?.layoutKey) return;
|
| 649 |
+
try {
|
| 650 |
+
await commands.setActiveLayout(model.layoutKey, { persist: false });
|
| 651 |
+
await commands.showSimilar({
|
| 652 |
+
sampleId: item.queryId,
|
| 653 |
+
layoutKey: model.layoutKey,
|
| 654 |
+
spaceKey: model.spaceKey,
|
| 655 |
+
k: 10,
|
| 656 |
+
source,
|
| 657 |
+
focus: "samples",
|
| 658 |
+
persist: false,
|
| 659 |
+
});
|
| 660 |
+
} catch (error) {
|
| 661 |
+
const message = error instanceof Error ? error.message : String(error);
|
| 662 |
+
setPanelError(`Could not load the sample evidence panel: ${message}`);
|
| 663 |
+
}
|
| 664 |
+
},
|
| 665 |
+
[commands],
|
| 666 |
+
);
|
| 667 |
+
|
| 668 |
+
const chooseExample = React.useCallback(
|
| 669 |
+
(exampleId) => {
|
| 670 |
+
setSelectedExampleId(exampleId);
|
| 671 |
+
window.dispatchEvent(new CustomEvent("fashion-search-example-change", { detail: exampleId }));
|
| 672 |
+
const item = examples.find((entry) => entry.id === exampleId);
|
| 673 |
+
const evidenceModel =
|
| 674 |
+
models.find((model) => model.key === "clip" && model.layoutKey) ||
|
| 675 |
+
models.find((model) => model.layoutKey);
|
| 676 |
+
if (item && evidenceModel) {
|
| 677 |
+
void showExampleEvidence(item, evidenceModel, "fashion-demo:query-picker");
|
| 678 |
+
}
|
| 679 |
+
},
|
| 680 |
+
[examples, models, showExampleEvidence],
|
| 681 |
+
);
|
| 682 |
|
| 683 |
React.useEffect(() => {
|
| 684 |
if (mode === "summary") return;
|
|
|
|
| 702 |
return () => window.clearInterval(timer);
|
| 703 |
}, [commands, mode]);
|
| 704 |
|
| 705 |
+
React.useEffect(() => {
|
| 706 |
+
if (mode === "summary") return;
|
| 707 |
+
if (didPrimeEvidence.current) return;
|
| 708 |
+
if (!selectedExample) return;
|
| 709 |
+
const evidenceModel =
|
| 710 |
+
models.find((model) => model.key === "clip" && model.layoutKey) ||
|
| 711 |
+
models.find((model) => model.layoutKey);
|
| 712 |
+
if (!evidenceModel) return;
|
| 713 |
+
didPrimeEvidence.current = true;
|
| 714 |
+
|
| 715 |
+
const timer = window.setTimeout(() => {
|
| 716 |
+
void showExampleEvidence(selectedExample, evidenceModel, "fashion-demo:initial");
|
| 717 |
+
}, 700);
|
| 718 |
+
|
| 719 |
+
return () => window.clearTimeout(timer);
|
| 720 |
+
}, [mode, models, selectedExample, showExampleEvidence]);
|
| 721 |
+
|
| 722 |
const clearSelection = async () => {
|
| 723 |
if (commands.setLabelFilter) commands.setLabelFilter(null);
|
| 724 |
setPanelError(null);
|
|
|
|
| 740 |
await commands.showSimilar({
|
| 741 |
sampleId: item.queryId,
|
| 742 |
layoutKey: model.layoutKey,
|
| 743 |
+
spaceKey: model.spaceKey,
|
| 744 |
k: 10,
|
| 745 |
source: `fashion-demo:${model.key}`,
|
| 746 |
+
focus: "samples",
|
| 747 |
persist: false,
|
| 748 |
});
|
|
|
|
| 749 |
} catch (error) {
|
| 750 |
const message = error instanceof Error ? error.message : String(error);
|
| 751 |
setPanelError(`Could not select target item: ${message}`);
|
|
|
|
| 771 |
await commands.showSimilar({
|
| 772 |
sampleId: result.sampleId,
|
| 773 |
layoutKey: model.layoutKey,
|
| 774 |
+
spaceKey: model.spaceKey,
|
| 775 |
k: 10,
|
| 776 |
source: `fashion-ranked-result:${model.key}`,
|
| 777 |
+
focus: "samples",
|
| 778 |
persist: false,
|
| 779 |
});
|
|
|
|
| 780 |
} catch (error) {
|
| 781 |
const message = error instanceof Error ? error.message : String(error);
|
| 782 |
setPanelError(`Could not select ranked result: ${message}`);
|
Dockerfile
CHANGED
|
@@ -42,9 +42,10 @@ COPY --chown=user . .
|
|
| 42 |
|
| 43 |
ENV HYPERVIEW_HOST=0.0.0.0 \
|
| 44 |
HYPERVIEW_PORT=7860 \
|
| 45 |
-
HYPERVIEW_WORKSPACE_ID=fashion-retail-search-
|
| 46 |
HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
|
| 47 |
HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
|
|
|
|
| 48 |
DEEPFASHION_SAMPLES_PER_CATEGORY=10 \
|
| 49 |
DEEPFASHION_MAX_SAMPLES=180 \
|
| 50 |
HF_HUB_ETAG_TIMEOUT=30 \
|
|
|
|
| 42 |
|
| 43 |
ENV HYPERVIEW_HOST=0.0.0.0 \
|
| 44 |
HYPERVIEW_PORT=7860 \
|
| 45 |
+
HYPERVIEW_WORKSPACE_ID=fashion-retail-search-actual-scatter \
|
| 46 |
HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
|
| 47 |
HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
|
| 48 |
+
FASHION_ENABLE_CONTEXT_MAPS=1 \
|
| 49 |
DEEPFASHION_SAMPLES_PER_CATEGORY=10 \
|
| 50 |
DEEPFASHION_MAX_SAMPLES=180 \
|
| 51 |
HF_HUB_ETAG_TIMEOUT=30 \
|
README.md
CHANGED
|
@@ -26,7 +26,7 @@ The demo is not "another embedding map." The buyer-facing story is:
|
|
| 26 |
- In the DeepFashion text-to-image probe, Hyper3-CLIP has a small aggregate edge and concrete examples where the exact item appears on the first screen while CLIP buries it.
|
| 27 |
- The best example is a query for light denim leggings with skinny fit, zipper details, five-pocket construction, and pockets: Hyper3 ranks the exact target first; CLIP first surfaces it at rank 32.
|
| 28 |
|
| 29 |
-
Use the ranked result strips first.
|
| 30 |
|
| 31 |
## What Is In The Demo
|
| 32 |
|
|
|
|
| 26 |
- In the DeepFashion text-to-image probe, Hyper3-CLIP has a small aggregate edge and concrete examples where the exact item appears on the first screen while CLIP buries it.
|
| 27 |
- The best example is a query for light denim leggings with skinny fit, zipper details, five-pocket construction, and pockets: Hyper3 ranks the exact target first; CLIP first surfaces it at rank 32.
|
| 28 |
|
| 29 |
+
Use the ranked result strips first. The Samples panel then shows the selected catalog item and nearest neighbors, while the smaller context maps show the same item in the actual CLIP and Hyper3-CLIP embedding layouts. The text-search ranks are precomputed from the bounded DeepFashion probe; the maps provide visual neighborhood context for the same fashion items.
|
| 30 |
|
| 31 |
## What Is In The Demo
|
| 32 |
|
demo.py
CHANGED
|
@@ -6,7 +6,6 @@ from __future__ import annotations
|
|
| 6 |
import os
|
| 7 |
import re
|
| 8 |
import time
|
| 9 |
-
import math
|
| 10 |
from collections import Counter
|
| 11 |
from pathlib import Path
|
| 12 |
from typing import Any
|
|
@@ -19,7 +18,7 @@ import hyperview as hv
|
|
| 19 |
SPACE_DIR = Path(__file__).resolve().parent
|
| 20 |
SPACE_HOST = os.environ.get("HYPERVIEW_HOST", "127.0.0.1")
|
| 21 |
SPACE_PORT = int(os.environ.get("HYPERVIEW_PORT", "6262"))
|
| 22 |
-
WORKSPACE_ID = os.environ.get("HYPERVIEW_WORKSPACE_ID", "fashion-retail-search-
|
| 23 |
DATASET_NAME = os.environ.get("HYPERVIEW_DATASET_NAME", "deepfashion_text_search_clip_hyper3clip")
|
| 24 |
EXTENSION_DIR = SPACE_DIR / ".hyperview" / "extensions" / "fashion-search-readout"
|
| 25 |
|
|
@@ -38,7 +37,7 @@ ALLOW_CANDIDATE_FALLBACK = os.environ.get("HYPERVIEW_ALLOW_CANDIDATE_FALLBACK",
|
|
| 38 |
"true",
|
| 39 |
"yes",
|
| 40 |
}
|
| 41 |
-
ENABLE_CONTEXT_MAPS = os.environ.get("FASHION_ENABLE_CONTEXT_MAPS", "").lower() in {
|
| 42 |
"1",
|
| 43 |
"true",
|
| 44 |
"yes",
|
|
@@ -46,6 +45,7 @@ ENABLE_CONTEXT_MAPS = os.environ.get("FASHION_ENABLE_CONTEXT_MAPS", "").lower()
|
|
| 46 |
EMBEDDING_MAX_ATTEMPTS = max(1, int(os.environ.get("HYPERVIEW_EMBEDDING_MAX_ATTEMPTS", "4")))
|
| 47 |
EMBEDDING_RETRY_DELAY_SECONDS = float(os.environ.get("HYPERVIEW_EMBEDDING_RETRY_DELAY_SECONDS", "15"))
|
| 48 |
RUNTIME_WARNINGS: list[str] = []
|
|
|
|
| 49 |
|
| 50 |
MODEL_SPECS = [
|
| 51 |
{
|
|
@@ -335,45 +335,12 @@ def ensure_layouts(dataset: hv.Dataset) -> dict[str, str]:
|
|
| 335 |
return layouts
|
| 336 |
|
| 337 |
|
| 338 |
-
def ensure_catalog_layout(dataset: hv.Dataset) -> str:
|
| 339 |
-
samples = dataset.samples
|
| 340 |
-
if not samples:
|
| 341 |
-
raise RuntimeError("DeepFashion demo has no samples to lay out.")
|
| 342 |
-
|
| 343 |
-
categories = sorted({str(sample.metadata.get("category") or sample.label or "unknown") for sample in samples})
|
| 344 |
-
category_index = {category: index for index, category in enumerate(categories)}
|
| 345 |
-
category_totals = Counter(str(sample.metadata.get("category") or sample.label or "unknown") for sample in samples)
|
| 346 |
-
category_seen: Counter[str] = Counter()
|
| 347 |
-
ids: list[str] = []
|
| 348 |
-
coords: list[list[float]] = []
|
| 349 |
-
|
| 350 |
-
for sample in samples:
|
| 351 |
-
category = str(sample.metadata.get("category") or sample.label or "unknown")
|
| 352 |
-
center_angle = (2.0 * math.pi * category_index[category]) / max(1, len(categories))
|
| 353 |
-
slot = category_seen[category]
|
| 354 |
-
category_seen[category] += 1
|
| 355 |
-
|
| 356 |
-
local_angle = (2.0 * math.pi * slot) / max(1, category_totals[category])
|
| 357 |
-
local_radius = 0.14 + 0.035 * (slot % 5)
|
| 358 |
-
center_radius = 4.0
|
| 359 |
-
coords.append(
|
| 360 |
-
[
|
| 361 |
-
center_radius * math.cos(center_angle) + local_radius * math.cos(local_angle),
|
| 362 |
-
center_radius * math.sin(center_angle) + local_radius * math.sin(local_angle),
|
| 363 |
-
]
|
| 364 |
-
)
|
| 365 |
-
ids.append(sample.id)
|
| 366 |
-
|
| 367 |
-
return dataset.set_coords("euclidean", ids, coords)
|
| 368 |
-
|
| 369 |
-
|
| 370 |
def build_dataset() -> tuple[hv.Dataset, dict[str, str]]:
|
| 371 |
dataset = hv.Dataset(DATASET_NAME)
|
| 372 |
add_deepfashion_samples(dataset)
|
| 373 |
if ENABLE_CONTEXT_MAPS:
|
| 374 |
layouts = ensure_layouts(dataset)
|
| 375 |
else:
|
| 376 |
-
ensure_catalog_layout(dataset)
|
| 377 |
layouts = {}
|
| 378 |
return dataset, layouts
|
| 379 |
|
|
@@ -388,6 +355,7 @@ def model_panel_props(layouts: dict[str, str]) -> list[dict[str, Any]]:
|
|
| 388 |
"displayName": spec["display_name"],
|
| 389 |
"buttonLabel": spec["button_label"],
|
| 390 |
"layoutKey": layout_key,
|
|
|
|
| 391 |
}
|
| 392 |
)
|
| 393 |
return props
|
|
@@ -450,6 +418,7 @@ def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.ui.View:
|
|
| 450 |
shared_props = {
|
| 451 |
"models": model_panel_props(layouts),
|
| 452 |
"examples": build_examples(dataset),
|
|
|
|
| 453 |
"warnings": RUNTIME_WARNINGS,
|
| 454 |
"metrics": {
|
| 455 |
"typedQueryCount": 180,
|
|
@@ -475,16 +444,17 @@ def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.ui.View:
|
|
| 475 |
title="Ranked Search Results",
|
| 476 |
extension="fashion-search-readout",
|
| 477 |
panel="fashion-comparison",
|
| 478 |
-
position="
|
|
|
|
|
|
|
| 479 |
props={
|
| 480 |
**shared_props,
|
| 481 |
"mode": "results",
|
| 482 |
"dockview": {
|
| 483 |
"closeSamplesPanel": False,
|
| 484 |
"hideExplorerPanel": True,
|
| 485 |
-
"initialWidth": int(os.environ.get("FASHION_RESULTS_WIDTH", "
|
| 486 |
"minimumWidth": 620,
|
| 487 |
-
"maximumWidth": int(os.environ.get("FASHION_RESULTS_MAX_WIDTH", "920")),
|
| 488 |
},
|
| 489 |
},
|
| 490 |
)
|
|
@@ -494,36 +464,45 @@ def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.ui.View:
|
|
| 494 |
|
| 495 |
clip_spec = MODEL_SPECS[0]
|
| 496 |
candidate_spec = MODEL_SPECS[1]
|
| 497 |
-
|
| 498 |
-
"
|
| 499 |
-
"
|
| 500 |
-
"maximumWidth": int(os.environ.get("FASHION_CONTEXT_MAX_WIDTH", "440")),
|
| 501 |
}
|
| 502 |
clip_map = hv.ui.Scatter(
|
| 503 |
id="fashion-map-clip",
|
| 504 |
title="Context Map: CLIP",
|
| 505 |
layout_key=layouts["clip"],
|
| 506 |
-
position="
|
| 507 |
-
reference_panel_id="
|
| 508 |
-
direction="
|
| 509 |
geometry=clip_spec["geometry"],
|
| 510 |
layout_dimension=clip_spec["layout_dimension"],
|
| 511 |
-
props={"dockview":
|
| 512 |
)
|
| 513 |
candidate_map = hv.ui.Scatter(
|
| 514 |
id="fashion-map-hyper3",
|
| 515 |
title="Context Map: Hyper3",
|
| 516 |
layout_key=layouts["candidate"],
|
| 517 |
-
position="
|
| 518 |
reference_panel_id="fashion-map-clip",
|
| 519 |
-
direction="
|
| 520 |
geometry=candidate_spec["geometry"],
|
| 521 |
layout_dimension=candidate_spec["layout_dimension"],
|
| 522 |
-
props={"dockview":
|
| 523 |
)
|
| 524 |
return hv.ui.View(results_panel, clip_map, candidate_map)
|
| 525 |
|
| 526 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
|
| 528 |
session = hv.launch(
|
| 529 |
dataset,
|
|
@@ -537,11 +516,14 @@ def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
|
|
| 537 |
session.ui.add_extension(EXTENSION_DIR, workspace_id=WORKSPACE_ID)
|
| 538 |
print("Applying DeepFashion retail search demo view...", flush=True)
|
| 539 |
session.ui.apply_view(build_demo_view(dataset, layouts), workspace_id=WORKSPACE_ID)
|
| 540 |
-
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
| 542 |
print(f"\nHyperView DeepFashion text-search demo is running at {session.url}", flush=True)
|
| 543 |
if ENABLE_CONTEXT_MAPS:
|
| 544 |
-
print(" Samples stay visible; scatter maps
|
| 545 |
else:
|
| 546 |
print(" Samples stay visible; ranked text-search results are the main demo.", flush=True)
|
| 547 |
return session
|
|
|
|
| 6 |
import os
|
| 7 |
import re
|
| 8 |
import time
|
|
|
|
| 9 |
from collections import Counter
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Any
|
|
|
|
| 18 |
SPACE_DIR = Path(__file__).resolve().parent
|
| 19 |
SPACE_HOST = os.environ.get("HYPERVIEW_HOST", "127.0.0.1")
|
| 20 |
SPACE_PORT = int(os.environ.get("HYPERVIEW_PORT", "6262"))
|
| 21 |
+
WORKSPACE_ID = os.environ.get("HYPERVIEW_WORKSPACE_ID", "fashion-retail-search-actual-scatter")
|
| 22 |
DATASET_NAME = os.environ.get("HYPERVIEW_DATASET_NAME", "deepfashion_text_search_clip_hyper3clip")
|
| 23 |
EXTENSION_DIR = SPACE_DIR / ".hyperview" / "extensions" / "fashion-search-readout"
|
| 24 |
|
|
|
|
| 37 |
"true",
|
| 38 |
"yes",
|
| 39 |
}
|
| 40 |
+
ENABLE_CONTEXT_MAPS = os.environ.get("FASHION_ENABLE_CONTEXT_MAPS", "1").lower() in {
|
| 41 |
"1",
|
| 42 |
"true",
|
| 43 |
"yes",
|
|
|
|
| 45 |
EMBEDDING_MAX_ATTEMPTS = max(1, int(os.environ.get("HYPERVIEW_EMBEDDING_MAX_ATTEMPTS", "4")))
|
| 46 |
EMBEDDING_RETRY_DELAY_SECONDS = float(os.environ.get("HYPERVIEW_EMBEDDING_RETRY_DELAY_SECONDS", "15"))
|
| 47 |
RUNTIME_WARNINGS: list[str] = []
|
| 48 |
+
DEFAULT_EXAMPLE_ID = os.environ.get("FASHION_DEFAULT_EXAMPLE_ID", "light-denim-leggings")
|
| 49 |
|
| 50 |
MODEL_SPECS = [
|
| 51 |
{
|
|
|
|
| 335 |
return layouts
|
| 336 |
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
def build_dataset() -> tuple[hv.Dataset, dict[str, str]]:
|
| 339 |
dataset = hv.Dataset(DATASET_NAME)
|
| 340 |
add_deepfashion_samples(dataset)
|
| 341 |
if ENABLE_CONTEXT_MAPS:
|
| 342 |
layouts = ensure_layouts(dataset)
|
| 343 |
else:
|
|
|
|
| 344 |
layouts = {}
|
| 345 |
return dataset, layouts
|
| 346 |
|
|
|
|
| 355 |
"displayName": spec["display_name"],
|
| 356 |
"buttonLabel": spec["button_label"],
|
| 357 |
"layoutKey": layout_key,
|
| 358 |
+
"spaceKey": spec.get("space_key"),
|
| 359 |
}
|
| 360 |
)
|
| 361 |
return props
|
|
|
|
| 418 |
shared_props = {
|
| 419 |
"models": model_panel_props(layouts),
|
| 420 |
"examples": build_examples(dataset),
|
| 421 |
+
"initialExampleId": DEFAULT_EXAMPLE_ID,
|
| 422 |
"warnings": RUNTIME_WARNINGS,
|
| 423 |
"metrics": {
|
| 424 |
"typedQueryCount": 180,
|
|
|
|
| 444 |
title="Ranked Search Results",
|
| 445 |
extension="fashion-search-readout",
|
| 446 |
panel="fashion-comparison",
|
| 447 |
+
position="center",
|
| 448 |
+
reference_panel_id="grid",
|
| 449 |
+
direction="left",
|
| 450 |
props={
|
| 451 |
**shared_props,
|
| 452 |
"mode": "results",
|
| 453 |
"dockview": {
|
| 454 |
"closeSamplesPanel": False,
|
| 455 |
"hideExplorerPanel": True,
|
| 456 |
+
"initialWidth": int(os.environ.get("FASHION_RESULTS_WIDTH", "760")),
|
| 457 |
"minimumWidth": 620,
|
|
|
|
| 458 |
},
|
| 459 |
},
|
| 460 |
)
|
|
|
|
| 464 |
|
| 465 |
clip_spec = MODEL_SPECS[0]
|
| 466 |
candidate_spec = MODEL_SPECS[1]
|
| 467 |
+
map_dockview = {
|
| 468 |
+
"initialHeight": int(os.environ.get("FASHION_MAP_HEIGHT", "280")),
|
| 469 |
+
"minimumHeight": 220,
|
|
|
|
| 470 |
}
|
| 471 |
clip_map = hv.ui.Scatter(
|
| 472 |
id="fashion-map-clip",
|
| 473 |
title="Context Map: CLIP",
|
| 474 |
layout_key=layouts["clip"],
|
| 475 |
+
position="bottom",
|
| 476 |
+
reference_panel_id="grid",
|
| 477 |
+
direction="below",
|
| 478 |
geometry=clip_spec["geometry"],
|
| 479 |
layout_dimension=clip_spec["layout_dimension"],
|
| 480 |
+
props={"dockview": map_dockview},
|
| 481 |
)
|
| 482 |
candidate_map = hv.ui.Scatter(
|
| 483 |
id="fashion-map-hyper3",
|
| 484 |
title="Context Map: Hyper3",
|
| 485 |
layout_key=layouts["candidate"],
|
| 486 |
+
position="bottom",
|
| 487 |
reference_panel_id="fashion-map-clip",
|
| 488 |
+
direction="right",
|
| 489 |
geometry=candidate_spec["geometry"],
|
| 490 |
layout_dimension=candidate_spec["layout_dimension"],
|
| 491 |
+
props={"dockview": map_dockview},
|
| 492 |
)
|
| 493 |
return hv.ui.View(results_panel, clip_map, candidate_map)
|
| 494 |
|
| 495 |
|
| 496 |
+
def initial_target_sample_id() -> str | None:
|
| 497 |
+
example = next(
|
| 498 |
+
(item for item in TEXT_SEARCH_EXAMPLES if item["id"] == DEFAULT_EXAMPLE_ID),
|
| 499 |
+
TEXT_SEARCH_EXAMPLES[0] if TEXT_SEARCH_EXAMPLES else None,
|
| 500 |
+
)
|
| 501 |
+
if example is None:
|
| 502 |
+
return None
|
| 503 |
+
return safe_sample_id(example["targetItemId"])
|
| 504 |
+
|
| 505 |
+
|
| 506 |
def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
|
| 507 |
session = hv.launch(
|
| 508 |
dataset,
|
|
|
|
| 516 |
session.ui.add_extension(EXTENSION_DIR, workspace_id=WORKSPACE_ID)
|
| 517 |
print("Applying DeepFashion retail search demo view...", flush=True)
|
| 518 |
session.ui.apply_view(build_demo_view(dataset, layouts), workspace_id=WORKSPACE_ID)
|
| 519 |
+
if ENABLE_CONTEXT_MAPS and layouts:
|
| 520 |
+
session.ui.set_active_layout(layouts["clip"], workspace_id=WORKSPACE_ID)
|
| 521 |
+
sample_id = initial_target_sample_id()
|
| 522 |
+
if sample_id:
|
| 523 |
+
session.ui.set_selection([sample_id], workspace_id=WORKSPACE_ID)
|
| 524 |
print(f"\nHyperView DeepFashion text-search demo is running at {session.url}", flush=True)
|
| 525 |
if ENABLE_CONTEXT_MAPS:
|
| 526 |
+
print(" Samples and nearest neighbors stay visible; scatter maps use the actual CLIP/Hyper3 layouts.", flush=True)
|
| 527 |
else:
|
| 528 |
print(" Samples stay visible; ranked text-search results are the main demo.", flush=True)
|
| 529 |
return session
|