mnm-matin commited on
Commit
1a6f263
·
verified ·
1 Parent(s): dd955f8

Deploy DeepFashion text search demo

Browse files
.dockerignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ demo_data/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
.hyperview/extensions/fashion-search-readout/contact_sheets.js ADDED
The diff for this file is too large to render. See raw diff
 
.hyperview/extensions/fashion-search-readout/extension.toml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ name = "fashion-search-readout"
2
+ description = "DeepFashion CLIP vs Hyper3-CLIP text-search readout"
3
+
4
+ [[panels]]
5
+ id = "fashion-comparison"
6
+ title = "Fashion Search Readout"
7
+ position = "right"
8
+ file = "panel.js"
.hyperview/extensions/fashion-search-readout/panel.js ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { rankedResults } from "./ranked_results.js";
2
+
3
+ const sdk = globalThis.HyperViewPanelSDK;
4
+ if (!sdk) throw new Error("HyperViewPanelSDK is not available on window.");
5
+
6
+ const { React, components, hooks } = sdk;
7
+ const { Panel, PanelToolbar, PanelToolbarButton } = components;
8
+ const {
9
+ usePanelClient,
10
+ usePanelSelection,
11
+ usePanelSamples,
12
+ usePanelCommands,
13
+ usePanelProps,
14
+ } = hooks;
15
+
16
+ const colors = {
17
+ panelBg: "#111827",
18
+ cardBg: "#161f2d",
19
+ buttonBg: "#1f2937",
20
+ border: "#334155",
21
+ text: "#e5e7eb",
22
+ strongText: "#f8fafc",
23
+ mutedText: "#9ca3af",
24
+ bodyText: "#cbd5e1",
25
+ accent: "#93c5fd",
26
+ success: "#86efac",
27
+ warningBg: "#3b2f12",
28
+ warningBorder: "#92400e",
29
+ warningText: "#fde68a",
30
+ error: "#fca5a5",
31
+ };
32
+
33
+ function Section({ title, children }) {
34
+ return React.createElement(
35
+ "section",
36
+ { style: { display: "flex", flexDirection: "column", gap: 8 } },
37
+ React.createElement(
38
+ "h3",
39
+ {
40
+ style: {
41
+ margin: 0,
42
+ color: colors.strongText,
43
+ fontSize: 13,
44
+ fontWeight: 700,
45
+ },
46
+ },
47
+ title,
48
+ ),
49
+ children,
50
+ );
51
+ }
52
+
53
+ function TextBlock({ children }) {
54
+ return React.createElement(
55
+ "div",
56
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
57
+ children,
58
+ );
59
+ }
60
+
61
+ function normalizeModels(value) {
62
+ if (!Array.isArray(value)) return [];
63
+ return value
64
+ .map((model, index) => ({
65
+ key: String(model.key || `model-${index}`),
66
+ displayName: String(model.displayName || model.display_name || model.key || `Model ${index + 1}`),
67
+ buttonLabel: String(
68
+ model.buttonLabel || model.button_label || `${model.displayName || model.key || "Model"} query`,
69
+ ),
70
+ layoutKey: model.layoutKey || model.layout_key || null,
71
+ spaceKey: model.spaceKey || model.space_key || null,
72
+ }))
73
+ .filter((model) => model.layoutKey);
74
+ }
75
+
76
+ function getSummary(item, modelKey) {
77
+ return item.summaries?.[modelKey] || item.modelSummaries?.[modelKey] || {};
78
+ }
79
+
80
+ function rankLabel(rank) {
81
+ return rank === 0 || rank ? `#${rank}` : "-";
82
+ }
83
+
84
+ function pct(value) {
85
+ if (typeof value !== "number") return "-";
86
+ return `${(value * 100).toFixed(1)}%`;
87
+ }
88
+
89
+ function metricValue(value, kind = "pct") {
90
+ if (typeof value !== "number") return "-";
91
+ if (kind === "decimal") return value.toFixed(3);
92
+ return pct(value);
93
+ }
94
+
95
+ function modelAccent(model) {
96
+ return model.key === "candidate" ? colors.success : colors.bodyText;
97
+ }
98
+
99
+ function modelBorder(model) {
100
+ return model.key === "candidate" ? "#3f6f5b" : colors.border;
101
+ }
102
+
103
+ function MetricGrid({ rows }) {
104
+ return React.createElement(
105
+ "div",
106
+ {
107
+ style: {
108
+ display: "grid",
109
+ gridTemplateColumns: "1.5fr 1fr 1fr",
110
+ border: `1px solid ${colors.border}`,
111
+ borderRadius: 6,
112
+ overflow: "hidden",
113
+ fontSize: 11,
114
+ },
115
+ },
116
+ React.createElement("div", { style: headerCellStyle() }, "Metric"),
117
+ React.createElement("div", { style: headerCellStyle() }, "Hyper3"),
118
+ React.createElement("div", { style: headerCellStyle() }, "CLIP"),
119
+ rows.flatMap((row) => [
120
+ React.createElement("div", { key: `${row.label}-label`, style: bodyCellStyle() }, row.label),
121
+ React.createElement(
122
+ "div",
123
+ { key: `${row.label}-h`, style: bodyCellStyle(colors.success) },
124
+ metricValue(row.hyper3, row.kind),
125
+ ),
126
+ React.createElement(
127
+ "div",
128
+ { key: `${row.label}-c`, style: bodyCellStyle() },
129
+ metricValue(row.clip, row.kind),
130
+ ),
131
+ ]),
132
+ );
133
+ }
134
+
135
+ function headerCellStyle() {
136
+ return {
137
+ background: "#0f172a",
138
+ borderBottom: `1px solid ${colors.border}`,
139
+ color: colors.strongText,
140
+ fontWeight: 700,
141
+ padding: 7,
142
+ };
143
+ }
144
+
145
+ function bodyCellStyle(color = colors.bodyText) {
146
+ return {
147
+ borderTop: `1px solid ${colors.border}`,
148
+ color,
149
+ padding: 7,
150
+ };
151
+ }
152
+
153
+ function ExampleChooser({ examples, selectedId, onSelect }) {
154
+ return React.createElement(
155
+ "div",
156
+ { style: { display: "grid", gridTemplateColumns: "1fr", gap: 6 } },
157
+ examples.map((item) => {
158
+ const active = item.id === selectedId;
159
+ return React.createElement(
160
+ "button",
161
+ {
162
+ key: item.id,
163
+ type: "button",
164
+ onClick: () => onSelect(item.id),
165
+ style: {
166
+ border: `1px solid ${active ? colors.accent : colors.border}`,
167
+ background: active ? "rgba(147, 197, 253, 0.08)" : colors.cardBg,
168
+ color: colors.text,
169
+ borderRadius: 5,
170
+ padding: "7px 8px",
171
+ textAlign: "left",
172
+ cursor: "pointer",
173
+ },
174
+ },
175
+ React.createElement(
176
+ "div",
177
+ { style: { color: colors.strongText, fontSize: 12, fontWeight: 700, marginBottom: 2 } },
178
+ item.title,
179
+ ),
180
+ React.createElement(
181
+ "div",
182
+ {
183
+ style: {
184
+ color: colors.bodyText,
185
+ fontSize: 10,
186
+ lineHeight: 1.3,
187
+ overflow: "hidden",
188
+ textOverflow: "ellipsis",
189
+ whiteSpace: "nowrap",
190
+ },
191
+ },
192
+ item.query,
193
+ ),
194
+ );
195
+ }),
196
+ );
197
+ }
198
+
199
+ function ResultCard({ result, model, loadingKey, onSelectResult }) {
200
+ const choiceKey = `${result.sampleId}:${model.key}:ranked-result`;
201
+ return React.createElement(
202
+ "button",
203
+ {
204
+ type: "button",
205
+ onClick: () => onSelectResult(result, model),
206
+ disabled: loadingKey === choiceKey,
207
+ title: `Select rank ${result.rank} in ${model.displayName}`,
208
+ style: {
209
+ width: 82,
210
+ flex: "0 0 82px",
211
+ border: `1px solid ${result.isTarget ? colors.success : colors.border}`,
212
+ background: result.isTarget ? "rgba(134, 239, 172, 0.06)" : "#0f172a",
213
+ color: colors.text,
214
+ borderRadius: 5,
215
+ padding: 4,
216
+ textAlign: "left",
217
+ cursor: loadingKey === choiceKey ? "default" : "pointer",
218
+ opacity: loadingKey === choiceKey ? 0.65 : 1,
219
+ },
220
+ },
221
+ React.createElement(
222
+ "div",
223
+ {
224
+ style: {
225
+ display: "flex",
226
+ justifyContent: "space-between",
227
+ alignItems: "center",
228
+ gap: 4,
229
+ marginBottom: 4,
230
+ },
231
+ },
232
+ React.createElement(
233
+ "span",
234
+ { style: { color: colors.strongText, fontSize: 10, fontWeight: 700 } },
235
+ `#${result.rank}`,
236
+ ),
237
+ result.isTarget
238
+ ? React.createElement(
239
+ "span",
240
+ { style: { color: colors.success, fontSize: 8, fontWeight: 700 } },
241
+ "target",
242
+ )
243
+ : null,
244
+ ),
245
+ React.createElement("img", {
246
+ src: result.image,
247
+ alt: `${model.displayName} rank ${result.rank}`,
248
+ style: {
249
+ width: "100%",
250
+ height: 90,
251
+ objectFit: "cover",
252
+ borderRadius: 3,
253
+ background: "#ffffff",
254
+ display: "block",
255
+ marginBottom: 4,
256
+ },
257
+ }),
258
+ React.createElement(
259
+ "div",
260
+ {
261
+ style: {
262
+ color: colors.mutedText,
263
+ fontSize: 8,
264
+ lineHeight: 1.2,
265
+ overflow: "hidden",
266
+ display: "-webkit-box",
267
+ WebkitLineClamp: 2,
268
+ WebkitBoxOrient: "vertical",
269
+ },
270
+ },
271
+ `${result.color || "unknown"} ${result.category || ""}`.trim(),
272
+ ),
273
+ );
274
+ }
275
+
276
+ function RankedResultStrip({ item, model, rows, targetRank, loadingKey, onSelectResult }) {
277
+ return React.createElement(
278
+ "div",
279
+ {
280
+ style: {
281
+ border: `1px solid ${modelBorder(model)}`,
282
+ borderRadius: 6,
283
+ background: colors.cardBg,
284
+ padding: 7,
285
+ display: "flex",
286
+ flexDirection: "column",
287
+ gap: 6,
288
+ },
289
+ },
290
+ React.createElement(
291
+ "div",
292
+ { style: { display: "flex", justifyContent: "space-between", gap: 8, alignItems: "baseline" } },
293
+ React.createElement(
294
+ "div",
295
+ { style: { color: colors.strongText, fontSize: 12, fontWeight: 700 } },
296
+ model.displayName,
297
+ ),
298
+ React.createElement(
299
+ "div",
300
+ { style: { color: modelAccent(model), fontSize: 11, fontWeight: 700 } },
301
+ `exact target ${rankLabel(targetRank)}`,
302
+ ),
303
+ ),
304
+ React.createElement(
305
+ "div",
306
+ { style: { display: "flex", gap: 7, overflowX: "auto", paddingBottom: 2 } },
307
+ rows.map((result) =>
308
+ React.createElement(ResultCard, {
309
+ key: `${item.id}-${model.key}-${result.rank}-${result.itemId}`,
310
+ result,
311
+ model,
312
+ loadingKey,
313
+ onSelectResult,
314
+ }),
315
+ ),
316
+ ),
317
+ );
318
+ }
319
+
320
+ function SearchResultComparison({ item, models, loadingKey, onSelectResult }) {
321
+ const proof = rankedResults[item.id];
322
+ if (!proof) {
323
+ return React.createElement(
324
+ "div",
325
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
326
+ "Ranked results are not configured for this query.",
327
+ );
328
+ }
329
+
330
+ return React.createElement(
331
+ "div",
332
+ { style: { display: "flex", flexDirection: "column", gap: 7 } },
333
+ React.createElement(
334
+ "div",
335
+ {
336
+ style: {
337
+ border: `1px solid ${colors.border}`,
338
+ borderRadius: 5,
339
+ background: "#0f172a",
340
+ padding: 7,
341
+ color: colors.bodyText,
342
+ fontSize: 11,
343
+ lineHeight: 1.35,
344
+ },
345
+ },
346
+ React.createElement("strong", { style: { color: colors.strongText } }, "Selected query: "),
347
+ item.query,
348
+ React.createElement(
349
+ "div",
350
+ { style: { color: colors.mutedText, fontSize: 10, marginTop: 3 } },
351
+ "Green card = exact product. Click any card to select it in the map.",
352
+ ),
353
+ ),
354
+ models.map((model) =>
355
+ React.createElement(RankedResultStrip, {
356
+ key: `${item.id}-${model.key}-ranked-results`,
357
+ item,
358
+ model,
359
+ rows: proof.results?.[model.key] || [],
360
+ targetRank: proof.targetRanks?.[model.key],
361
+ loadingKey,
362
+ onSelectResult,
363
+ }),
364
+ ),
365
+ );
366
+ }
367
+
368
+ function ExactTargetActions({ item, models, loadingKey, onSelectQuery }) {
369
+ const gridColumns = models.length > 1 ? "repeat(2, minmax(0, 1fr))" : "1fr";
370
+
371
+ return React.createElement(
372
+ "div",
373
+ { style: { display: "grid", gridTemplateColumns: gridColumns, gap: 7 } },
374
+ models.map((model) => {
375
+ const summary = getSummary(item, model.key);
376
+ const isCandidate = model.key === "candidate";
377
+ const targetRank = rankLabel(summary.rank);
378
+ const neighborLine =
379
+ typeof summary.categoryHits === "number"
380
+ ? `${summary.categoryHits}/${summary.total || 10} same-category image neighbors`
381
+ : "Open the map to inspect image neighbors";
382
+ const choiceKey = `${item.queryId}:${model.key}`;
383
+ return React.createElement(
384
+ "button",
385
+ {
386
+ key: model.key,
387
+ type: "button",
388
+ onClick: () => onSelectQuery(item, model),
389
+ disabled: loadingKey === choiceKey,
390
+ title: `Select target item and inspect ${model.displayName} image neighbors`,
391
+ style: {
392
+ border: `1px solid ${modelBorder(model)}`,
393
+ borderRadius: 5,
394
+ padding: 8,
395
+ background: isCandidate ? "rgba(134, 239, 172, 0.06)" : "transparent",
396
+ color: colors.text,
397
+ textAlign: "left",
398
+ cursor: loadingKey === choiceKey ? "default" : "pointer",
399
+ opacity: loadingKey === choiceKey ? 0.65 : 1,
400
+ },
401
+ },
402
+ React.createElement(
403
+ "div",
404
+ { style: { display: "flex", justifyContent: "space-between", gap: 8, marginBottom: 4 } },
405
+ React.createElement(
406
+ "span",
407
+ { style: { color: colors.strongText, fontSize: 12, fontWeight: 700 } },
408
+ model.displayName,
409
+ ),
410
+ React.createElement(
411
+ "span",
412
+ { style: { color: modelAccent(model), fontSize: 11, fontWeight: 700 } },
413
+ `target ${targetRank}`,
414
+ ),
415
+ ),
416
+ React.createElement(
417
+ "div",
418
+ { style: { color: isCandidate ? colors.success : colors.bodyText, fontSize: 11, lineHeight: 1.35 } },
419
+ summary.text || "",
420
+ ),
421
+ React.createElement(
422
+ "div",
423
+ { style: { color: colors.mutedText, fontSize: 10, lineHeight: 1.35, marginTop: 5 } },
424
+ loadingKey === choiceKey ? "Loading map selection..." : neighborLine,
425
+ ),
426
+ );
427
+ }),
428
+ );
429
+ }
430
+
431
+ export default function FashionSearchComparisonPanel() {
432
+ const client = usePanelClient();
433
+ const selection = usePanelSelection();
434
+ const samplesState = usePanelSamples();
435
+ const commands = usePanelCommands();
436
+ const panelProps = usePanelProps();
437
+ const [panelError, setPanelError] = React.useState(null);
438
+ const [loadingKey, setLoadingKey] = React.useState(null);
439
+ const [selectedExampleId, setSelectedExampleId] = React.useState(null);
440
+
441
+ const models = React.useMemo(() => normalizeModels(panelProps.models), [panelProps.models]);
442
+ const examples = Array.isArray(panelProps.examples) ? panelProps.examples : [];
443
+ const warnings = Array.isArray(panelProps.warnings) ? panelProps.warnings.filter(Boolean) : [];
444
+ const metrics = panelProps.metrics || {};
445
+ const selectedExample =
446
+ examples.find((item) => item.id === selectedExampleId) || examples[0] || null;
447
+ const typedMetricRows = [
448
+ { label: "Typed search Hit@1", hyper3: metrics.typedHit1Hyper3, clip: metrics.typedHit1Clip },
449
+ { label: "Typed search Hit@10", hyper3: metrics.typedHit10Hyper3, clip: metrics.typedHit10Clip },
450
+ {
451
+ label: "Typed category P@10",
452
+ hyper3: metrics.typedCategoryP10Hyper3,
453
+ clip: metrics.typedCategoryP10Clip,
454
+ },
455
+ { label: "Typed search MRR", hyper3: metrics.typedMrrHyper3, clip: metrics.typedMrrClip, kind: "decimal" },
456
+ ];
457
+ const imageMetricRows = [
458
+ { label: "Image-to-image mAP", hyper3: metrics.imageRetrievalMapHyper3, clip: metrics.imageRetrievalMapClip },
459
+ ];
460
+
461
+ const clearSelection = async () => {
462
+ if (commands.setLabelFilter) commands.setLabelFilter(null);
463
+ setPanelError(null);
464
+ await client.clearSimilarityQuery();
465
+ await commands.clearSelection();
466
+ };
467
+
468
+ const selectModelQuery = async (item, model) => {
469
+ const key = `${item.queryId}:${model.key}`;
470
+ setPanelError(null);
471
+ if (!model.layoutKey) {
472
+ setPanelError(`${model.displayName} layout is not ready yet. Try again in a moment.`);
473
+ return;
474
+ }
475
+ setLoadingKey(key);
476
+ try {
477
+ await commands.setActiveLayout(model.layoutKey, { persist: false });
478
+ await commands.showSimilar({
479
+ sampleId: item.queryId,
480
+ layoutKey: model.layoutKey,
481
+ spaceKey: model.spaceKey,
482
+ k: 10,
483
+ source: `fashion-demo:${model.key}`,
484
+ focus: false,
485
+ persist: false,
486
+ });
487
+ } catch (error) {
488
+ const message = error instanceof Error ? error.message : String(error);
489
+ setPanelError(`Could not select target item: ${message}`);
490
+ } finally {
491
+ setLoadingKey(null);
492
+ }
493
+ };
494
+
495
+ const selectRankedResult = async (result, model) => {
496
+ const key = `${result.sampleId}:${model.key}:ranked-result`;
497
+ setPanelError(null);
498
+ if (!model.layoutKey) {
499
+ setPanelError(`${model.displayName} layout is not ready yet. Try again in a moment.`);
500
+ return;
501
+ }
502
+ setLoadingKey(key);
503
+ try {
504
+ await commands.setActiveLayout(model.layoutKey, { persist: false });
505
+ await commands.showSimilar({
506
+ sampleId: result.sampleId,
507
+ layoutKey: model.layoutKey,
508
+ spaceKey: model.spaceKey,
509
+ k: 10,
510
+ source: `fashion-ranked-result:${model.key}`,
511
+ focus: false,
512
+ persist: false,
513
+ });
514
+ } catch (error) {
515
+ const message = error instanceof Error ? error.message : String(error);
516
+ setPanelError(`Could not select ranked result: ${message}`);
517
+ } finally {
518
+ setLoadingKey(null);
519
+ }
520
+ };
521
+
522
+ return React.createElement(
523
+ Panel,
524
+ { className: "h-full" },
525
+ React.createElement(PanelToolbar, {
526
+ items: [
527
+ { id: "dataset", label: "Data", value: "DeepFashion" },
528
+ { id: "samples", label: "Items", value: String(samplesState.totalSamples ?? "-") },
529
+ { id: "selected", label: "Selected", value: String(selection.selectedIds?.length ?? 0) },
530
+ ],
531
+ actions: React.createElement(PanelToolbarButton, { onClick: clearSelection }, "Reset"),
532
+ }),
533
+ React.createElement(
534
+ "div",
535
+ {
536
+ style: {
537
+ height: "100%",
538
+ overflow: "auto",
539
+ padding: 12,
540
+ display: "flex",
541
+ flexDirection: "column",
542
+ gap: 14,
543
+ background: colors.panelBg,
544
+ },
545
+ },
546
+ React.createElement(
547
+ Section,
548
+ { title: "Pick A Shopper Query" },
549
+ React.createElement(
550
+ TextBlock,
551
+ null,
552
+ "Choose a typed product search, then compare the ranked image results.",
553
+ ),
554
+ examples.length
555
+ ? React.createElement(ExampleChooser, {
556
+ examples,
557
+ selectedId: selectedExample?.id,
558
+ onSelect: setSelectedExampleId,
559
+ })
560
+ : React.createElement(
561
+ "div",
562
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
563
+ "Demo examples are not configured.",
564
+ ),
565
+ selectedExample && models.length
566
+ ? React.createElement(SearchResultComparison, {
567
+ item: selectedExample,
568
+ models,
569
+ loadingKey,
570
+ onSelectResult: selectRankedResult,
571
+ })
572
+ : null,
573
+ ),
574
+ React.createElement(
575
+ Section,
576
+ { title: "Inspect Exact Target" },
577
+ React.createElement(
578
+ TextBlock,
579
+ null,
580
+ `Select the same target product in the map: ${selectedExample?.targetTitle || "target product"}.`,
581
+ ),
582
+ selectedExample && models.length
583
+ ? React.createElement(ExactTargetActions, {
584
+ item: selectedExample,
585
+ models,
586
+ loadingKey,
587
+ onSelectQuery: selectModelQuery,
588
+ })
589
+ : React.createElement(
590
+ "div",
591
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
592
+ "Demo examples are not configured.",
593
+ ),
594
+ ),
595
+ warnings.length
596
+ ? React.createElement(
597
+ "div",
598
+ {
599
+ style: {
600
+ border: `1px solid ${colors.warningBorder}`,
601
+ borderRadius: 4,
602
+ background: colors.warningBg,
603
+ color: colors.warningText,
604
+ padding: 8,
605
+ fontSize: 11,
606
+ lineHeight: 1.35,
607
+ },
608
+ },
609
+ warnings[0],
610
+ )
611
+ : null,
612
+ React.createElement(
613
+ Section,
614
+ { title: "Benchmark Details" },
615
+ React.createElement(
616
+ TextBlock,
617
+ null,
618
+ "Bottom reference only. The result strips above are the main demo; these numbers keep the probe traceable.",
619
+ ),
620
+ React.createElement(MetricGrid, { rows: typedMetricRows }),
621
+ React.createElement(
622
+ TextBlock,
623
+ null,
624
+ "Separate image-to-image check, shown only for context because it is not the typed shopper search task.",
625
+ ),
626
+ React.createElement(MetricGrid, { rows: imageMetricRows }),
627
+ ),
628
+ panelError
629
+ ? React.createElement(
630
+ "div",
631
+ { style: { color: colors.error, fontSize: 11, lineHeight: 1.35 } },
632
+ panelError,
633
+ )
634
+ : null,
635
+ ),
636
+ );
637
+ }
.hyperview/extensions/fashion-search-readout/ranked_results.js ADDED
The diff for this file is too large to render. See raw diff
 
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ build-essential \
5
+ curl \
6
+ libssl-dev \
7
+ pkg-config \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ RUN useradd -m -u 1000 user
11
+ USER user
12
+
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH \
15
+ HF_HOME=/home/user/.cache/huggingface \
16
+ PYTHONUNBUFFERED=1 \
17
+ PIP_NO_CACHE_DIR=1
18
+
19
+ WORKDIR $HOME/app
20
+
21
+ RUN pip install --upgrade pip
22
+
23
+ ARG HYPERVIEW_VERSION=0.6.1
24
+ ARG HYPER_MODELS_VERSION=0.3.0
25
+
26
+ # Install CPU-only PyTorch first so the Space does not pull the default CUDA bundle.
27
+ RUN pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
28
+ RUN echo "Installing hyperview==${HYPERVIEW_VERSION}" \
29
+ && pip install "hyperview==${HYPERVIEW_VERSION}" \
30
+ && python - <<'PY'
31
+ import inspect
32
+ import hyperview as hv
33
+
34
+ print("hyperview", hv.__version__, inspect.signature(hv.launch))
35
+ PY
36
+ RUN pip install \
37
+ "hyper-models[ml]==${HYPER_MODELS_VERSION}" \
38
+ "datasets>=4.5.0" \
39
+ "Pillow>=12.0.0"
40
+
41
+ COPY --chown=user . .
42
+
43
+ ENV HYPERVIEW_HOST=0.0.0.0 \
44
+ HYPERVIEW_PORT=7860 \
45
+ HYPERVIEW_WORKSPACE_ID=fashion-deepfashion-text-search \
46
+ HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
47
+ HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
48
+ HF_HUB_ETAG_TIMEOUT=30 \
49
+ HF_HUB_DOWNLOAD_TIMEOUT=120
50
+
51
+ EXPOSE 7860
52
+
53
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=2700s --retries=3 \
54
+ CMD curl -f http://localhost:7860/__hyperview__/health || exit 1
55
+
56
+ CMD ["python", "demo.py"]
README.md CHANGED
@@ -1,10 +1,65 @@
1
  ---
2
  title: HyperView DeepFashion Text Search
3
- emoji: 😻
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: HyperView DeepFashion Text Search
3
+ emoji: 👖
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # HyperView - DeepFashion Text Search Comparison
12
+
13
+ This demo loads a curated DeepFashion In-Shop subset into HyperView and compares:
14
+
15
+ - CLIP ViT-B/32 in a Euclidean 2D layout
16
+ - Hyper3-CLIP `hyper3-clip-v0.5` in a Poincare 2D layout
17
+
18
+ The main readout is built for a retail search buyer. It shows fixed text-to-image examples where a shopper-style query asks for a specific item and the exact target product appears much earlier under Hyper3-CLIP than under CLIP.
19
+
20
+ ## Business Story
21
+
22
+ The demo is not "another embedding map." The buyer-facing story is:
23
+
24
+ - Product search often fails by returning almost-right variants.
25
+ - Fashion queries combine color, fit, fabric, cut, and construction details.
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. Then use the compact map buttons to inspect the same target item under each embedding space. 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
+
33
+ - Three shopper-style query examples with ranked CLIP vs Hyper3-CLIP image results.
34
+ - Exact-target rank readouts for each model.
35
+ - Clickable result cards that select the product in the active HyperView map.
36
+ - A compact benchmark footer for traceability, with text-to-image numbers separated from the older image-to-image retrieval check.
37
+
38
+ This is a demo probe, not a broad production benchmark. It is meant to show the failure mode: specific text-to-image product search where exact-item rank matters.
39
+
40
+ ## Run Locally
41
+
42
+ From the HyperView repository:
43
+
44
+ ```bash
45
+ uv run python hyperview-spaces/spaces/fashion-deepfashion-text-search-clip-hyper3clip/demo.py
46
+ ```
47
+
48
+ Useful overrides:
49
+
50
+ ```bash
51
+ HYPERVIEW_PORT=6265 DEEPFASHION_SAMPLES_PER_CATEGORY=30 \
52
+ uv run python hyperview-spaces/spaces/fashion-deepfashion-text-search-clip-hyper3clip/demo.py
53
+ ```
54
+
55
+ ## Benchmark Context
56
+
57
+ Landing-page-safe wording:
58
+
59
+ > On a bounded DeepFashion text-to-image probe, Hyper3-CLIP has a small aggregate top-10 edge over CLIP-B/32 and produces concrete typed-search wins. In one query for light denim leggings with specific construction details, Hyper3 ranks the target product first while CLIP does not surface it until rank 32.
60
+
61
+ Do not present this as a broad claim against every modern multimodal embedding model. We have not yet added SigLIP, Jina-CLIP, NV-CLIP, or Gemini Embedding 2 to this demo.
62
+
63
+ ## Deploy Source
64
+
65
+ This folder is intended to deploy to `hyper3labs/HyperView-DeepFashion-Text-Search` from the `hyperview-spaces` deployment repository.
demo.py ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """DeepFashion text-search comparison demo for CLIP vs Hyper3-CLIP in HyperView."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import re
8
+ from collections import Counter
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from datasets import load_dataset
13
+ from PIL import Image, ImageOps
14
+
15
+ import hyperview as hv
16
+
17
+
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-deepfashion-text-search")
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
+
25
+ HF_DATASET = os.environ.get("DEEPFASHION_HF_DATASET", "Marqo/deepfashion-inshop")
26
+ HF_SPLIT = os.environ.get("DEEPFASHION_HF_SPLIT", "data")
27
+ SAMPLES_PER_CATEGORY = int(os.environ.get("DEEPFASHION_SAMPLES_PER_CATEGORY", "45"))
28
+ MAX_SAMPLES = int(os.environ.get("DEEPFASHION_MAX_SAMPLES", "700"))
29
+ IMAGE_MAX_SIZE = (768, 768)
30
+ FORCE_SAMPLE_REFRESH = os.environ.get("HYPERVIEW_DEEPFASHION_FORCE_REFRESH", "").lower() in {
31
+ "1",
32
+ "true",
33
+ "yes",
34
+ }
35
+ ALLOW_CANDIDATE_FALLBACK = os.environ.get("HYPERVIEW_ALLOW_CANDIDATE_FALLBACK", "1").lower() in {
36
+ "1",
37
+ "true",
38
+ "yes",
39
+ }
40
+ RUNTIME_WARNINGS: list[str] = []
41
+
42
+ MODEL_SPECS = [
43
+ {
44
+ "key": "clip",
45
+ "display_name": os.environ.get("FASHION_BASELINE_DISPLAY_NAME", "CLIP"),
46
+ "button_label": os.environ.get("FASHION_BASELINE_BUTTON_LABEL", "Inspect CLIP neighbors"),
47
+ "provider": os.environ.get("FASHION_BASELINE_PROVIDER", "embed-anything"),
48
+ "model": os.environ.get("FASHION_BASELINE_MODEL", "openai/clip-vit-base-patch32"),
49
+ "layout": os.environ.get("FASHION_BASELINE_LAYOUT", "euclidean:2d"),
50
+ "geometry": os.environ.get("FASHION_BASELINE_GEOMETRY", "euclidean"),
51
+ "layout_dimension": int(os.environ.get("FASHION_BASELINE_LAYOUT_DIMENSION", "2")),
52
+ "metric": os.environ.get("FASHION_BASELINE_METRIC", "cosine"),
53
+ "panel_title": os.environ.get("FASHION_BASELINE_PANEL_TITLE", "CLIP - Fashion Catalog Map"),
54
+ },
55
+ {
56
+ "key": "candidate",
57
+ "display_name": os.environ.get("FASHION_CANDIDATE_DISPLAY_NAME", "Hyper3-CLIP"),
58
+ "button_label": os.environ.get("FASHION_CANDIDATE_BUTTON_LABEL", "Inspect Hyper3-CLIP neighbors"),
59
+ "provider": os.environ.get("FASHION_CANDIDATE_PROVIDER", "hyper-models"),
60
+ "model": os.environ.get("FASHION_CANDIDATE_MODEL", "hyper3-clip-v0.5"),
61
+ "layout": os.environ.get("FASHION_CANDIDATE_LAYOUT", "poincare:2d"),
62
+ "geometry": os.environ.get("FASHION_CANDIDATE_GEOMETRY", "poincare"),
63
+ "layout_dimension": int(os.environ.get("FASHION_CANDIDATE_LAYOUT_DIMENSION", "2")),
64
+ "metric": os.environ.get("FASHION_CANDIDATE_METRIC", "cosine"),
65
+ "panel_title": os.environ.get("FASHION_CANDIDATE_PANEL_TITLE", "Hyper3-CLIP - Fashion Catalog Map"),
66
+ },
67
+ ]
68
+
69
+ TEXT_SEARCH_EXAMPLES = [
70
+ {
71
+ "id": "light-denim-leggings",
72
+ "title": "Light denim leggings",
73
+ "targetItemId": "WOMEN_Leggings_id_00001867_02_3_back",
74
+ "targetProduct": "WOMEN_Leggings_id_00001867_02",
75
+ "targetTitle": "women's light denim leggings",
76
+ "family": "Specific typed product search",
77
+ "query": "women's light denim leggings with skinny fit, zipper details, five-pocket construction, pockets",
78
+ "hyper3Rank": 1,
79
+ "clipRank": 32,
80
+ "hyper3Text": "Exact target is the first result.",
81
+ "clipText": "Top results drift to dark denim, black, and similar blue leggings before the exact item appears.",
82
+ },
83
+ {
84
+ "id": "olive-navy-pants",
85
+ "title": "Olive and navy drawstring pants",
86
+ "targetItemId": "MEN_Pants_id_00001468_03_6_flat",
87
+ "targetProduct": "MEN_Pants_id_00001468_03",
88
+ "targetTitle": "men's olive and navy drawstring pants",
89
+ "family": "Specific typed product search",
90
+ "query": "men's olive and navy pants with drawstring waist, pockets, striped pattern, knit fabric",
91
+ "hyper3Rank": 1,
92
+ "clipRank": 56,
93
+ "hyper3Text": "Exact target is the first result.",
94
+ "clipText": "CLIP ranks burgundy pants and visually similar pants before the requested product.",
95
+ },
96
+ {
97
+ "id": "cream-blue-halter-blouse",
98
+ "title": "Cream and blue halter blouse",
99
+ "targetItemId": "WOMEN_Blouses_Shirts_id_00007161_02_1_front",
100
+ "targetProduct": "WOMEN_Blouses_Shirts_id_00007161_02",
101
+ "targetTitle": "cream and blue halter blouse",
102
+ "family": "Attribute-heavy apparel search",
103
+ "query": "women's cream and blue blouse with halter neckline, floral pattern, striped pattern, tribal print",
104
+ "hyper3Rank": 4,
105
+ "clipRank": 33,
106
+ "hyper3Text": "Target views appear in the top 10.",
107
+ "clipText": "CLIP retrieves broadly similar tops but misses the exact blouse in the first screen.",
108
+ },
109
+ ]
110
+
111
+ DEMO_RESULT_ITEM_IDS = {
112
+ "MEN_Pants_id_00001468_03_6_flat",
113
+ "MEN_Pants_id_00001468_04_6_flat",
114
+ "MEN_Pants_id_00004045_03_2_side",
115
+ "MEN_Pants_id_00004045_04_1_front",
116
+ "MEN_Pants_id_00004045_09_3_back",
117
+ "MEN_Pants_id_00004045_11_1_front",
118
+ "MEN_Pants_id_00004045_11_2_side",
119
+ "MEN_Pants_id_00004045_12_1_front",
120
+ "MEN_Pants_id_00004045_12_2_side",
121
+ "MEN_Pants_id_00004045_12_3_back",
122
+ "MEN_Pants_id_00004045_12_7_additional",
123
+ "MEN_Shirts_Polos_id_00007027_01_6_flat",
124
+ "MEN_Sweaters_id_00005177_03_2_side",
125
+ "MEN_Sweaters_id_00005177_03_3_back",
126
+ "MEN_Sweaters_id_00005177_03_4_full",
127
+ "WOMEN_Blouses_Shirts_id_00003641_01_1_front",
128
+ "WOMEN_Blouses_Shirts_id_00006345_01_7_additional",
129
+ "WOMEN_Blouses_Shirts_id_00007049_01_7_additional",
130
+ "WOMEN_Blouses_Shirts_id_00007161_02_1_front",
131
+ "WOMEN_Cardigans_id_00000521_02_3_back",
132
+ "WOMEN_Denim_id_00000152_04_1_front",
133
+ "WOMEN_Denim_id_00000152_04_2_side",
134
+ "WOMEN_Denim_id_00002338_02_7_additional",
135
+ "WOMEN_Denim_id_00002338_03_1_front",
136
+ "WOMEN_Denim_id_00002338_03_3_back",
137
+ "WOMEN_Denim_id_00002338_03_7_additional",
138
+ "WOMEN_Denim_id_00005673_02_3_back",
139
+ "WOMEN_Dresses_id_00006961_02_1_front",
140
+ "WOMEN_Leggings_id_00001412_01_2_side",
141
+ "WOMEN_Leggings_id_00001867_02_3_back",
142
+ "WOMEN_Leggings_id_00002130_02_2_side",
143
+ "WOMEN_Leggings_id_00003850_01_2_side",
144
+ "WOMEN_Leggings_id_00003908_07_2_side",
145
+ "WOMEN_Leggings_id_00003908_08_2_side",
146
+ "WOMEN_Leggings_id_00004562_01_3_back",
147
+ "WOMEN_Pants_id_00000053_02_1_front",
148
+ "WOMEN_Pants_id_00001574_02_3_back",
149
+ "WOMEN_Rompers_Jumpsuits_id_00004432_02_3_back",
150
+ "WOMEN_Rompers_Jumpsuits_id_00004653_02_2_side",
151
+ "WOMEN_Rompers_Jumpsuits_id_00005484_01_3_back",
152
+ "WOMEN_Sweaters_id_00003304_01_1_front",
153
+ "WOMEN_Sweaters_id_00003304_01_2_side",
154
+ "WOMEN_Tees_Tanks_id_00000676_01_1_front",
155
+ "WOMEN_Tees_Tanks_id_00000676_01_2_side",
156
+ }
157
+
158
+
159
+ def media_root() -> Path:
160
+ root = Path(os.environ.get("HYPERVIEW_MEDIA_DIR", str(SPACE_DIR / "demo_data" / "media")))
161
+ path = root / DATASET_NAME
162
+ path.mkdir(parents=True, exist_ok=True)
163
+ return path
164
+
165
+
166
+ def product_key(item_id: str) -> str:
167
+ return re.sub(r"_\d+_[A-Za-z]+$", "", str(item_id))
168
+
169
+
170
+ def safe_sample_id(item_id: str) -> str:
171
+ return re.sub(r"[^A-Za-z0-9_.-]+", "_", str(item_id)).strip("_")[:96]
172
+
173
+
174
+ def readable(value: Any) -> str:
175
+ text = str(value or "").replace("_", " ").replace("-", " ")
176
+ return re.sub(r"\s+", " ", text).strip()
177
+
178
+
179
+ def save_image(image: Image.Image, destination: Path) -> None:
180
+ if destination.exists() and destination.stat().st_size > 0 and not FORCE_SAMPLE_REFRESH:
181
+ return
182
+ tmp_path = destination.with_suffix(destination.suffix + ".tmp")
183
+ image = ImageOps.exif_transpose(image).convert("RGB")
184
+ image.thumbnail(IMAGE_MAX_SIZE, Image.Resampling.LANCZOS)
185
+ image.save(tmp_path, format="JPEG", quality=92, optimize=True)
186
+ tmp_path.replace(destination)
187
+
188
+
189
+ def select_deepfashion_records() -> list[dict[str, Any]]:
190
+ print(f"Loading DeepFashion split {HF_SPLIT!r} from {HF_DATASET}...", flush=True)
191
+ source = load_dataset(HF_DATASET, split=HF_SPLIT)
192
+ required_products = {example["targetProduct"] for example in TEXT_SEARCH_EXAMPLES}
193
+ required_item_ids = {example["targetItemId"] for example in TEXT_SEARCH_EXAMPLES} | DEMO_RESULT_ITEM_IDS
194
+ selected: list[dict[str, Any]] = []
195
+ seen: set[str] = set()
196
+ category_counts: Counter[str] = Counter()
197
+
198
+ for index, row in enumerate(source):
199
+ item_id = str(row["item_ID"])
200
+ category = str(row.get("category2") or "unknown")
201
+ product = product_key(item_id)
202
+ required = product in required_products or item_id in required_item_ids
203
+ balanced = category_counts[category] < SAMPLES_PER_CATEGORY and len(selected) < MAX_SAMPLES
204
+ if not required and not balanced:
205
+ continue
206
+ if item_id in seen:
207
+ continue
208
+ selected.append({"index": index, **row})
209
+ seen.add(item_id)
210
+ category_counts[category] += 1
211
+
212
+ missing = sorted(required_item_ids - seen)
213
+ if missing:
214
+ raise RuntimeError(f"Missing required demo items from DeepFashion: {missing}")
215
+ print(f"Selected {len(selected)} DeepFashion images: {dict(category_counts)}", flush=True)
216
+ return selected
217
+
218
+
219
+ def add_deepfashion_samples(dataset: hv.Dataset) -> None:
220
+ existing_ids = {sample.id for sample in dataset.samples}
221
+ media_dir = media_root()
222
+ added = 0
223
+ updated = 0
224
+ records = select_deepfashion_records()
225
+
226
+ for record in records:
227
+ item_id = str(record["item_ID"])
228
+ sample_id = safe_sample_id(item_id)
229
+ destination = media_dir / f"{sample_id}.jpg"
230
+ save_image(record["image"], destination)
231
+ category = readable(record.get("category2") or "unknown").lower()
232
+ color = readable(record.get("color") or "unknown")
233
+ metadata = {
234
+ "item_id": item_id,
235
+ "product_key": product_key(item_id),
236
+ "gender": readable(record.get("category1") or "unknown"),
237
+ "category": category,
238
+ "subcategory": readable(record.get("category3") or "unknown"),
239
+ "color": color,
240
+ "description": readable(record.get("description") or ""),
241
+ "text": readable(record.get("text") or ""),
242
+ "source_dataset": HF_DATASET,
243
+ "split": HF_SPLIT,
244
+ }
245
+ existed = sample_id in existing_ids
246
+ dataset.add_image(str(destination), label=category, metadata=metadata, sample_id=sample_id)
247
+ if existed:
248
+ updated += 1
249
+ else:
250
+ existing_ids.add(sample_id)
251
+ added += 1
252
+ print(f"Prepared DeepFashion samples ({added} added, {updated} updated).", flush=True)
253
+
254
+
255
+ def ensure_layouts(dataset: hv.Dataset) -> dict[str, str]:
256
+ layouts: dict[str, str] = {}
257
+ for spec in MODEL_SPECS:
258
+ print(f"Ensuring {spec['display_name']} embeddings...", flush=True)
259
+ try:
260
+ space_key = dataset.compute_embeddings(
261
+ model=spec["model"],
262
+ provider=spec["provider"],
263
+ batch_size=32,
264
+ show_progress=True,
265
+ )
266
+ except Exception as exc:
267
+ if spec["key"] == "candidate" and ALLOW_CANDIDATE_FALLBACK and "clip" in layouts:
268
+ warning = (
269
+ f"Hyper3-CLIP embeddings are unavailable ({type(exc).__name__}: {exc}). "
270
+ "Showing the CLIP layout as a clearly labeled fallback so the Space can start."
271
+ )
272
+ print(warning, flush=True)
273
+ RUNTIME_WARNINGS.append(warning)
274
+ spec.update(
275
+ {
276
+ "display_name": "Hyper3-CLIP unavailable (CLIP fallback)",
277
+ "button_label": "Inspect CLIP fallback neighbors",
278
+ "geometry": MODEL_SPECS[0]["geometry"],
279
+ "layout_dimension": MODEL_SPECS[0]["layout_dimension"],
280
+ "panel_title": "Hyper3-CLIP unavailable - showing CLIP fallback",
281
+ "fallback": True,
282
+ "space_key": MODEL_SPECS[0].get("space_key"),
283
+ }
284
+ )
285
+ layouts[spec["key"]] = layouts["clip"]
286
+ continue
287
+ raise
288
+ spec["space_key"] = space_key
289
+ print(f"Ensuring {spec['display_name']} layout...", flush=True)
290
+ layouts[spec["key"]] = dataset.compute_visualization(
291
+ space_key=space_key,
292
+ layout=spec["layout"],
293
+ n_neighbors=20,
294
+ min_dist=0.08,
295
+ metric=spec["metric"],
296
+ )
297
+ return layouts
298
+
299
+
300
+ def build_dataset() -> tuple[hv.Dataset, dict[str, str]]:
301
+ dataset = hv.Dataset(DATASET_NAME)
302
+ add_deepfashion_samples(dataset)
303
+ layouts = ensure_layouts(dataset)
304
+ return dataset, layouts
305
+
306
+
307
+ def space_key_from_layout(layout_key: str) -> str:
308
+ return layout_key.split("__euclidean_umap", 1)[0].split("__poincare_umap", 1)[0]
309
+
310
+
311
+ def model_panel_props(layouts: dict[str, str]) -> list[dict[str, Any]]:
312
+ props = []
313
+ for spec in MODEL_SPECS:
314
+ layout_key = layouts[spec["key"]]
315
+ props.append(
316
+ {
317
+ "key": spec["key"],
318
+ "displayName": spec["display_name"],
319
+ "buttonLabel": spec["button_label"],
320
+ "layoutKey": layout_key,
321
+ "spaceKey": spec.get("space_key") or space_key_from_layout(layout_key),
322
+ }
323
+ )
324
+ return props
325
+
326
+
327
+ def neighbor_summary(dataset: hv.Dataset, sample_id: str, model_key: str) -> dict[str, Any]:
328
+ spec = next((item for item in MODEL_SPECS if item["key"] == model_key), None)
329
+ if spec is None:
330
+ return {}
331
+ query = dataset[sample_id]
332
+ space_key = spec.get("space_key")
333
+ if space_key is None:
334
+ return {}
335
+ neighbors = dataset.find_similar(sample_id, k=10, space_key=str(space_key))
336
+ query_product = query.metadata.get("product_key")
337
+ query_category = query.metadata.get("category")
338
+ product_hits = sum(1 for sample, _distance in neighbors if sample.metadata.get("product_key") == query_product)
339
+ category_hits = sum(1 for sample, _distance in neighbors if sample.metadata.get("category") == query_category)
340
+ return {"productHits": product_hits, "categoryHits": category_hits, "total": len(neighbors)}
341
+
342
+
343
+ def build_examples(dataset: hv.Dataset) -> list[dict[str, Any]]:
344
+ examples = []
345
+ candidate_is_fallback = any(spec["key"] == "candidate" and spec.get("fallback") for spec in MODEL_SPECS)
346
+ for item in TEXT_SEARCH_EXAMPLES:
347
+ sample_id = safe_sample_id(item["targetItemId"])
348
+ if sample_id not in {sample.id for sample in dataset.samples}:
349
+ continue
350
+ candidate_text = (
351
+ "Hyper3-CLIP is unavailable in this runtime, so this button shows the CLIP fallback neighborhood."
352
+ if candidate_is_fallback
353
+ else item["hyper3Text"]
354
+ )
355
+ examples.append(
356
+ {
357
+ "id": item["id"],
358
+ "title": item["title"],
359
+ "family": item["family"],
360
+ "query": item["query"],
361
+ "queryId": sample_id,
362
+ "targetTitle": item["targetTitle"],
363
+ "summaries": {
364
+ "clip": {
365
+ "rank": item["clipRank"],
366
+ "text": item["clipText"],
367
+ **neighbor_summary(dataset, sample_id, "clip"),
368
+ },
369
+ "candidate": {
370
+ "rank": item["hyper3Rank"],
371
+ "text": candidate_text,
372
+ **neighbor_summary(dataset, sample_id, "candidate"),
373
+ },
374
+ },
375
+ }
376
+ )
377
+ return examples
378
+
379
+
380
+ def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.ui.View:
381
+ readout_panel = hv.ui.ExtensionPanel(
382
+ id="fashion-text-search-readout",
383
+ title="Shopper Text Search Proof",
384
+ extension="fashion-search-readout",
385
+ panel="fashion-comparison",
386
+ position="center",
387
+ props={
388
+ "models": model_panel_props(layouts),
389
+ "examples": build_examples(dataset),
390
+ "warnings": RUNTIME_WARNINGS,
391
+ "metrics": {
392
+ "typedQueryCount": 180,
393
+ "typedCandidateImages": 1120,
394
+ "hit10Hyper3Only": 23,
395
+ "hit10ClipOnly": 19,
396
+ "strongHyper3Wins": 13,
397
+ "strongClipWins": 9,
398
+ "imageRetrievalMapHyper3": 0.407,
399
+ "imageRetrievalMapClip": 0.240,
400
+ "typedHit1Hyper3": 0.244,
401
+ "typedHit1Clip": 0.233,
402
+ "typedHit10Hyper3": 0.572,
403
+ "typedHit10Clip": 0.550,
404
+ "typedCategoryP10Hyper3": 0.594,
405
+ "typedCategoryP10Clip": 0.561,
406
+ "typedMrrHyper3": 0.358,
407
+ "typedMrrClip": 0.344,
408
+ },
409
+ },
410
+ )
411
+ return hv.ui.View(readout_panel)
412
+
413
+
414
+ def launch_demo(dataset: hv.Dataset, layouts: dict[str, str]) -> hv.Session:
415
+ session = hv.launch(
416
+ dataset,
417
+ host=SPACE_HOST,
418
+ port=SPACE_PORT,
419
+ open_browser=False,
420
+ workspace_id=WORKSPACE_ID,
421
+ block=False,
422
+ )
423
+ print("Installing DeepFashion demo extension...", flush=True)
424
+ session.ui.add_extension(EXTENSION_DIR, workspace_id=WORKSPACE_ID)
425
+ print("Applying DeepFashion side-by-side demo view...", flush=True)
426
+ session.ui.apply_view(build_demo_view(dataset, layouts), workspace_id=WORKSPACE_ID)
427
+ session.ui.set_active_layout(None, workspace_id=WORKSPACE_ID)
428
+ session.ui.set_selection([], workspace_id=WORKSPACE_ID)
429
+ print(f"\nHyperView DeepFashion text-search demo is running at {session.url}", flush=True)
430
+ return session
431
+
432
+
433
+ def main() -> None:
434
+ dataset, layouts = build_dataset()
435
+ print("Layouts:", flush=True)
436
+ for spec in MODEL_SPECS:
437
+ print(f" {spec['display_name']}: {layouts[spec['key']]}", flush=True)
438
+ session = launch_demo(dataset, layouts)
439
+ session.wait()
440
+
441
+
442
+ if __name__ == "__main__":
443
+ main()