mnm-matin commited on
Commit
707079b
·
verified ·
1 Parent(s): 501ccb5

Sync visual safety demo assets and startup build

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +9 -0
  2. .gitattributes +12 -0
  3. .hyperview/extensions/safety-readout/extension.toml +8 -0
  4. .hyperview/extensions/safety-readout/panel.js +501 -0
  5. Dockerfile +62 -0
  6. README.md +66 -4
  7. demo.py +399 -0
  8. demo_assets/images/01b9021aa05e6e46.jpg +3 -0
  9. demo_assets/images/0565bcccb1266f62.jpg +0 -0
  10. demo_assets/images/05f4f97b3833086e.jpg +0 -0
  11. demo_assets/images/06a449c6ef03c172.jpg +0 -0
  12. demo_assets/images/07de9510e802a400.jpg +0 -0
  13. demo_assets/images/084a9d12a2a486dc.jpg +0 -0
  14. demo_assets/images/09b96636ea4f4ce9.jpg +0 -0
  15. demo_assets/images/0ba3f8b8a3a2e93c.jpg +0 -0
  16. demo_assets/images/0c4b629e801e8181.jpg +0 -0
  17. demo_assets/images/11cd71943afb477e.jpg +0 -0
  18. demo_assets/images/120d233407f1d14c.jpg +0 -0
  19. demo_assets/images/1228bfe2f3dc2d29.jpg +0 -0
  20. demo_assets/images/12b06cbeaa8b634a.jpg +0 -0
  21. demo_assets/images/14d45a56a0fe95bf.jpg +0 -0
  22. demo_assets/images/1519b2618048f525.jpg +0 -0
  23. demo_assets/images/15cc8c6a07fd3f0f.jpg +0 -0
  24. demo_assets/images/178f262c9f18c8ba.jpg +0 -0
  25. demo_assets/images/1a9d5f7cc9a88a1d.jpg +0 -0
  26. demo_assets/images/1de9f211102093d0.jpg +0 -0
  27. demo_assets/images/20d46de74f618aef.jpg +0 -0
  28. demo_assets/images/21d68297ff292660.jpg +0 -0
  29. demo_assets/images/2c87374e8d2f3a34.jpg +0 -0
  30. demo_assets/images/318c91bd306e52e1.jpg +0 -0
  31. demo_assets/images/318e12be63daa7bb.jpg +0 -0
  32. demo_assets/images/32255e4104d8f9b7.jpg +0 -0
  33. demo_assets/images/33969a398effdb29.jpg +0 -0
  34. demo_assets/images/33ac3bd2015c44b2.jpg +0 -0
  35. demo_assets/images/34ec1592f40cdfaa.jpg +3 -0
  36. demo_assets/images/3722904d5b45c676.jpg +0 -0
  37. demo_assets/images/3ba5380490084697.jpg +0 -0
  38. demo_assets/images/3d0d7ec99241aec7.jpg +0 -0
  39. demo_assets/images/419e1902dcac2d77.jpg +0 -0
  40. demo_assets/images/41bf39cbdf4b0a05.jpg +0 -0
  41. demo_assets/images/43040be867118184.jpg +0 -0
  42. demo_assets/images/474d23651d5ca02a.jpg +0 -0
  43. demo_assets/images/477ab6b26dd5a667.jpg +0 -0
  44. demo_assets/images/483ade136f9e676d.jpg +0 -0
  45. demo_assets/images/48e7aacdb6423790.jpg +0 -0
  46. demo_assets/images/499cfd3e29e31662.jpg +3 -0
  47. demo_assets/images/4af30ade5ff3deb1.jpg +0 -0
  48. demo_assets/images/4b00a3af27c5b2c6.jpg +3 -0
  49. demo_assets/images/4cc6516b9be3d77d.jpg +0 -0
  50. demo_assets/images/4d5a6b0ed65de97e.jpg +0 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ .env
5
+ .env.*
6
+ demo_data/datasets/
7
+ demo_data/media/
8
+ demo_data/openimages_cache/
9
+ demo_data/providers.json
.gitattributes CHANGED
@@ -33,3 +33,15 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ demo_assets/images/01b9021aa05e6e46.jpg filter=lfs diff=lfs merge=lfs -text
37
+ demo_assets/images/34ec1592f40cdfaa.jpg filter=lfs diff=lfs merge=lfs -text
38
+ demo_assets/images/499cfd3e29e31662.jpg filter=lfs diff=lfs merge=lfs -text
39
+ demo_assets/images/4b00a3af27c5b2c6.jpg filter=lfs diff=lfs merge=lfs -text
40
+ demo_assets/images/92a7c03c939944a3.jpg filter=lfs diff=lfs merge=lfs -text
41
+ demo_assets/images/ab93a8b5592ce5bd.jpg filter=lfs diff=lfs merge=lfs -text
42
+ demo_assets/images/c97b0813480f1f26.jpg filter=lfs diff=lfs merge=lfs -text
43
+ demo_assets/images/d7bf42f6b20e96c2.jpg filter=lfs diff=lfs merge=lfs -text
44
+ demo_assets/images/dbe3012525cc3fec.jpg filter=lfs diff=lfs merge=lfs -text
45
+ demo_assets/images/ea39fa9dc50811b0.jpg filter=lfs diff=lfs merge=lfs -text
46
+ demo_assets/images/eeed1e944c331791.jpg filter=lfs diff=lfs merge=lfs -text
47
+ vendor/hyperview-0.6.2-py3-none-any.whl filter=lfs diff=lfs merge=lfs -text
.hyperview/extensions/safety-readout/extension.toml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ name = "safety-readout"
2
+ description = "Visual safety CLIP vs hyper3-clip moderation readout"
3
+
4
+ [[panels]]
5
+ id = "safety-comparison"
6
+ title = "Safety Review Readout"
7
+ position = "right"
8
+ file = "panel.js"
.hyperview/extensions/safety-readout/panel.js ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const sdk = globalThis.HyperViewPanelSDK;
2
+ if (!sdk) throw new Error("HyperViewPanelSDK is not available on window.");
3
+
4
+ const { React, components, hooks } = sdk;
5
+ const { Panel, PanelToolbar, PanelToolbarButton } = components;
6
+ const {
7
+ usePanelSelection,
8
+ usePanelSamples,
9
+ usePanelCommands,
10
+ usePanelProps,
11
+ } = hooks;
12
+
13
+ const colors = {
14
+ panelBg: "#0f172a",
15
+ cardBg: "#162032",
16
+ buttonBg: "#233047",
17
+ border: "#334155",
18
+ text: "#e5e7eb",
19
+ strongText: "#f8fafc",
20
+ mutedText: "#94a3b8",
21
+ bodyText: "#cbd5e1",
22
+ accent: "#38bdf8",
23
+ review: "#fbbf24",
24
+ safe: "#34d399",
25
+ error: "#fca5a5",
26
+ };
27
+
28
+ function normalizeModels(value) {
29
+ if (!Array.isArray(value)) return [];
30
+ return value
31
+ .map((model, index) => ({
32
+ key: String(model.key || `model-${index}`),
33
+ displayName: String(model.displayName || model.display_name || model.key || `Model ${index + 1}`),
34
+ buttonLabel: String(
35
+ model.buttonLabel || model.button_label || `${model.displayName || model.key || "Model"} query`,
36
+ ),
37
+ layoutKey: model.layoutKey || model.layout_key || null,
38
+ }))
39
+ .filter((model) => model.layoutKey);
40
+ }
41
+
42
+ function titleLabel(label) {
43
+ return String(label || "unlabeled")
44
+ .replaceAll("_", " ")
45
+ .toLowerCase()
46
+ .replace(/\b\w/g, (char) => char.toUpperCase());
47
+ }
48
+
49
+ function formatMetric(value) {
50
+ if (typeof value !== "number" || Number.isNaN(value)) return "-";
51
+ return value.toFixed(3);
52
+ }
53
+
54
+ function Button({ children, onClick, title, disabled }) {
55
+ return React.createElement(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ onClick,
60
+ title,
61
+ disabled,
62
+ style: {
63
+ border: `1px solid ${colors.border}`,
64
+ background: colors.buttonBg,
65
+ color: colors.text,
66
+ borderRadius: 4,
67
+ padding: "8px 9px",
68
+ fontSize: 11,
69
+ lineHeight: 1.2,
70
+ cursor: disabled ? "default" : "pointer",
71
+ textAlign: "left",
72
+ opacity: disabled ? 0.65 : 1,
73
+ },
74
+ },
75
+ children,
76
+ );
77
+ }
78
+
79
+ function SectionTitle({ children }) {
80
+ return React.createElement(
81
+ "h3",
82
+ {
83
+ style: {
84
+ margin: 0,
85
+ color: colors.strongText,
86
+ fontSize: 13,
87
+ fontWeight: 700,
88
+ },
89
+ },
90
+ children,
91
+ );
92
+ }
93
+
94
+ function Hero({ bucketCounts }) {
95
+ return React.createElement(
96
+ "div",
97
+ {
98
+ style: {
99
+ border: `1px solid ${colors.border}`,
100
+ borderRadius: 6,
101
+ background: colors.cardBg,
102
+ padding: 12,
103
+ display: "flex",
104
+ flexDirection: "column",
105
+ gap: 8,
106
+ },
107
+ },
108
+ React.createElement(
109
+ "div",
110
+ { style: { color: colors.accent, fontSize: 11, fontWeight: 700, textTransform: "uppercase" } },
111
+ "Marketplace upload triage",
112
+ ),
113
+ React.createElement(
114
+ "div",
115
+ { style: { color: colors.strongText, fontSize: 15, fontWeight: 800, lineHeight: 1.25 } },
116
+ "Classify each new listing as safe or needs review before it goes live.",
117
+ ),
118
+ React.createElement(
119
+ "div",
120
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
121
+ "This public demo uses harmless Open Images examples to stand in for a marketplace upload stream: normal listings should pass, regulated or risky objects should enter the review queue.",
122
+ ),
123
+ React.createElement(
124
+ "div",
125
+ { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 } },
126
+ React.createElement(StatChip, {
127
+ label: "Safe",
128
+ value: String(bucketCounts?.safe ?? 60),
129
+ color: colors.safe,
130
+ }),
131
+ React.createElement(StatChip, {
132
+ label: "Needs review",
133
+ value: String(bucketCounts?.needs_review ?? 60),
134
+ color: colors.review,
135
+ }),
136
+ ),
137
+ );
138
+ }
139
+
140
+ function StatChip({ label, value, color }) {
141
+ return React.createElement(
142
+ "div",
143
+ {
144
+ style: {
145
+ border: `1px solid ${colors.border}`,
146
+ borderRadius: 4,
147
+ padding: "7px 8px",
148
+ background: "#101827",
149
+ },
150
+ },
151
+ React.createElement("div", { style: { color, fontSize: 16, fontWeight: 800 } }, value),
152
+ React.createElement("div", { style: { color: colors.mutedText, fontSize: 11 } }, label),
153
+ );
154
+ }
155
+
156
+ function ScenarioCard({ item, models, loadingKey, onSelectQuery }) {
157
+ const isReview = item.bucket === "needs_review";
158
+ const chipColor = isReview ? colors.review : colors.safe;
159
+ const gridColumns = models.length > 1 ? "repeat(2, minmax(0, 1fr))" : "1fr";
160
+
161
+ return React.createElement(
162
+ "div",
163
+ {
164
+ style: {
165
+ border: `1px solid ${colors.border}`,
166
+ borderRadius: 6,
167
+ background: colors.cardBg,
168
+ padding: 10,
169
+ display: "flex",
170
+ flexDirection: "column",
171
+ gap: 8,
172
+ },
173
+ },
174
+ React.createElement(
175
+ "div",
176
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
177
+ React.createElement(
178
+ "div",
179
+ { style: { display: "flex", alignItems: "center", gap: 8 } },
180
+ React.createElement(
181
+ "div",
182
+ {
183
+ style: {
184
+ width: 22,
185
+ height: 22,
186
+ borderRadius: 4,
187
+ border: `1px solid ${colors.border}`,
188
+ display: "flex",
189
+ alignItems: "center",
190
+ justifyContent: "center",
191
+ color: colors.accent,
192
+ fontSize: 12,
193
+ fontWeight: 800,
194
+ },
195
+ },
196
+ item.step || "?",
197
+ ),
198
+ React.createElement(
199
+ "div",
200
+ { style: { color: colors.strongText, fontSize: 13, fontWeight: 800 } },
201
+ item.title,
202
+ ),
203
+ ),
204
+ React.createElement(
205
+ "div",
206
+ {
207
+ style: {
208
+ border: `1px solid ${chipColor}`,
209
+ color: chipColor,
210
+ borderRadius: 4,
211
+ padding: "2px 5px",
212
+ fontSize: 10,
213
+ whiteSpace: "nowrap",
214
+ },
215
+ },
216
+ item.family,
217
+ ),
218
+ ),
219
+ React.createElement(
220
+ "div",
221
+ {
222
+ style: {
223
+ display: "grid",
224
+ gridTemplateColumns: "74px 1fr",
225
+ gap: 8,
226
+ color: colors.bodyText,
227
+ fontSize: 11,
228
+ lineHeight: 1.35,
229
+ },
230
+ },
231
+ React.createElement("div", { style: { color: colors.accent, fontWeight: 700 } }, "Decision"),
232
+ React.createElement("div", null, item.decision),
233
+ React.createElement("div", { style: { color: colors.accent, fontWeight: 700 } }, "Value"),
234
+ React.createElement("div", null, item.businessValue),
235
+ React.createElement("div", { style: { color: colors.accent, fontWeight: 700 } }, "Inspect"),
236
+ React.createElement("div", null, item.inspect),
237
+ ),
238
+ React.createElement(
239
+ "div",
240
+ { style: { color: colors.mutedText, fontSize: 10, lineHeight: 1.3 } },
241
+ `Dataset labels: ${(item.labels || []).map(titleLabel).join(", ")}`,
242
+ ),
243
+ React.createElement(
244
+ "div",
245
+ { style: { display: "grid", gridTemplateColumns: gridColumns, gap: 6 } },
246
+ models.map((model) => {
247
+ const choiceKey = `${item.queryId}:${model.key}`;
248
+ return React.createElement(
249
+ Button,
250
+ {
251
+ key: model.key,
252
+ onClick: () => onSelectQuery(item, model),
253
+ disabled: loadingKey === choiceKey,
254
+ title: `Show ${model.displayName} neighbors for this upload`,
255
+ },
256
+ loadingKey === choiceKey ? "Loading..." : model.buttonLabel,
257
+ );
258
+ }),
259
+ ),
260
+ );
261
+ }
262
+
263
+ function MetricTable({ metrics }) {
264
+ const rows = [
265
+ ["Accuracy", "accuracy"],
266
+ ["AUROC", "auroc"],
267
+ ["AP, review class", "apReview"],
268
+ ["F1, review class", "f1Review"],
269
+ ["Review recall", "reviewRecall"],
270
+ ["Review precision", "reviewPrecision"],
271
+ ["Safe recall", "safeRecall"],
272
+ ];
273
+
274
+ return React.createElement(
275
+ "table",
276
+ {
277
+ style: {
278
+ width: "100%",
279
+ borderCollapse: "collapse",
280
+ color: colors.bodyText,
281
+ fontSize: 11,
282
+ },
283
+ },
284
+ React.createElement(
285
+ "thead",
286
+ null,
287
+ React.createElement(
288
+ "tr",
289
+ null,
290
+ ["Metric", "CLIP", "hyper3-clip"].map((header) =>
291
+ React.createElement(
292
+ "th",
293
+ {
294
+ key: header,
295
+ style: {
296
+ padding: "6px 8px",
297
+ textAlign: header === "Metric" ? "left" : "right",
298
+ color: colors.strongText,
299
+ borderBottom: `1px solid ${colors.border}`,
300
+ },
301
+ },
302
+ header,
303
+ ),
304
+ ),
305
+ ),
306
+ ),
307
+ React.createElement(
308
+ "tbody",
309
+ null,
310
+ rows.map(([label, key]) =>
311
+ React.createElement(
312
+ "tr",
313
+ { key },
314
+ React.createElement("td", { style: { padding: "5px 8px" } }, label),
315
+ React.createElement(
316
+ "td",
317
+ { style: { padding: "5px 8px", textAlign: "right" } },
318
+ formatMetric(metrics?.clip?.[key]),
319
+ ),
320
+ React.createElement(
321
+ "td",
322
+ { style: { padding: "5px 8px", textAlign: "right" } },
323
+ formatMetric(metrics?.candidate?.[key]),
324
+ ),
325
+ ),
326
+ ),
327
+ ),
328
+ );
329
+ }
330
+
331
+ function Details({ title, children }) {
332
+ return React.createElement(
333
+ "details",
334
+ {
335
+ style: {
336
+ border: `1px solid ${colors.border}`,
337
+ borderRadius: 6,
338
+ background: colors.cardBg,
339
+ padding: 10,
340
+ },
341
+ },
342
+ React.createElement(
343
+ "summary",
344
+ {
345
+ style: {
346
+ color: colors.strongText,
347
+ fontSize: 12,
348
+ fontWeight: 700,
349
+ cursor: "pointer",
350
+ },
351
+ },
352
+ title,
353
+ ),
354
+ React.createElement(
355
+ "div",
356
+ { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 8 } },
357
+ children,
358
+ ),
359
+ );
360
+ }
361
+
362
+ export default function SafetyComparisonPanel() {
363
+ const selection = usePanelSelection();
364
+ const samplesState = usePanelSamples();
365
+ const commands = usePanelCommands();
366
+ const panelProps = usePanelProps();
367
+ const didRequestInitialReset = React.useRef(false);
368
+ const [panelError, setPanelError] = React.useState(null);
369
+ const [loadingKey, setLoadingKey] = React.useState(null);
370
+
371
+ const models = React.useMemo(() => normalizeModels(panelProps.models), [panelProps.models]);
372
+ const examples = Array.isArray(panelProps.examples) ? panelProps.examples : [];
373
+ const metrics = panelProps.metrics || {};
374
+ const bucketCounts = panelProps.bucketCounts || {};
375
+
376
+ React.useEffect(() => {
377
+ if (!models.length) return undefined;
378
+ const resetKey = "openimages-safety-demo:initial-reset:v5";
379
+ const storage = typeof window !== "undefined" ? window.sessionStorage : null;
380
+ if (didRequestInitialReset.current || storage?.getItem(resetKey) === "1") {
381
+ return undefined;
382
+ }
383
+ didRequestInitialReset.current = true;
384
+
385
+ let cancelled = false;
386
+ const resetInitialState = async () => {
387
+ try {
388
+ if (commands.setLabelFilter) commands.setLabelFilter(null);
389
+ await commands.clearQueryContext({ persist: true });
390
+ if (!cancelled) storage?.setItem(resetKey, "1");
391
+ } catch (error) {
392
+ didRequestInitialReset.current = false;
393
+ if (!cancelled) console.warn("Could not reset Open Images safety demo state:", error);
394
+ }
395
+ };
396
+
397
+ const timers = [0, 900, 2200].map((delay) => window.setTimeout(resetInitialState, delay));
398
+ return () => {
399
+ cancelled = true;
400
+ timers.forEach((timer) => window.clearTimeout(timer));
401
+ };
402
+ }, [commands, models.length]);
403
+
404
+ const clearSelection = async () => {
405
+ if (commands.setLabelFilter) commands.setLabelFilter(null);
406
+ setPanelError(null);
407
+ await commands.clearQueryContext({ persist: true });
408
+ };
409
+
410
+ const selectModelQuery = async (item, model) => {
411
+ const key = `${item.queryId}:${model.key}`;
412
+ setPanelError(null);
413
+
414
+ if (!model.layoutKey) {
415
+ setPanelError(`${model.displayName} layout is not ready yet. Try again in a moment.`);
416
+ return;
417
+ }
418
+
419
+ setLoadingKey(key);
420
+ try {
421
+ await commands.setActiveLayout(model.layoutKey);
422
+ await commands.showSimilar({
423
+ sampleId: item.queryId,
424
+ layoutKey: model.layoutKey,
425
+ k: 10,
426
+ source: `openimages-safety-demo:${model.key}`,
427
+ focus: "samples",
428
+ });
429
+ } catch (error) {
430
+ const message = error instanceof Error ? error.message : String(error);
431
+ setPanelError(`Could not select query: ${message}`);
432
+ } finally {
433
+ setLoadingKey(null);
434
+ }
435
+ };
436
+
437
+ return React.createElement(
438
+ Panel,
439
+ { className: "h-full" },
440
+ React.createElement(PanelToolbar, {
441
+ items: [
442
+ { id: "dataset", label: "Data", value: "Open Images" },
443
+ { id: "samples", label: "Items", value: String(samplesState.totalSamples ?? "-") },
444
+ { id: "selected", label: "Selected", value: String(selection.selectedIds?.length ?? 0) },
445
+ ],
446
+ actions: React.createElement(PanelToolbarButton, { onClick: clearSelection }, "Reset"),
447
+ }),
448
+ React.createElement(
449
+ "div",
450
+ {
451
+ style: {
452
+ height: "100%",
453
+ overflow: "auto",
454
+ padding: 12,
455
+ display: "flex",
456
+ flexDirection: "column",
457
+ gap: 12,
458
+ background: colors.panelBg,
459
+ },
460
+ },
461
+ React.createElement(Hero, { bucketCounts }),
462
+ React.createElement(
463
+ "section",
464
+ { style: { display: "flex", flexDirection: "column", gap: 8 } },
465
+ React.createElement(SectionTitle, null, "Three upload decisions"),
466
+ examples.length && models.length
467
+ ? examples.map((item) =>
468
+ React.createElement(ScenarioCard, {
469
+ key: item.id,
470
+ item,
471
+ models,
472
+ loadingKey,
473
+ onSelectQuery: selectModelQuery,
474
+ }),
475
+ )
476
+ : React.createElement(
477
+ "div",
478
+ { style: { color: colors.bodyText, fontSize: 12, lineHeight: 1.45 } },
479
+ "Demo scenarios are not configured.",
480
+ ),
481
+ ),
482
+ React.createElement(
483
+ Details,
484
+ { title: "Benchmark details" },
485
+ React.createElement(
486
+ "div",
487
+ { style: { color: colors.bodyText, fontSize: 11, lineHeight: 1.4 } },
488
+ `${metrics.dataset || "Open Images proxy"}: ${metrics.classBalance || ""}. ${metrics.readout || ""}`,
489
+ ),
490
+ React.createElement(MetricTable, { metrics }),
491
+ ),
492
+ panelError
493
+ ? React.createElement(
494
+ "div",
495
+ { style: { color: colors.error, fontSize: 11, lineHeight: 1.35 } },
496
+ panelError,
497
+ )
498
+ : null,
499
+ ),
500
+ );
501
+ }
Dockerfile ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 HYPER_MODELS_VERSION=0.3.0
24
+
25
+ # Install CPU-only PyTorch first so the Space does not pull the default CUDA bundle.
26
+ RUN pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
27
+ COPY --chown=user vendor/hyperview-*.whl /tmp/
28
+ RUN echo "Installing vendored HyperView wheel" \
29
+ && pip install /tmp/hyperview-*.whl \
30
+ && python - <<'PY'
31
+ import inspect
32
+ import hyperview as hv
33
+
34
+ print("hyperview", hv.__version__, inspect.signature(hv.launch))
35
+ assert hasattr(hv.ui, "Samples"), "HyperView build must support explicit Samples panels"
36
+ PY
37
+ RUN pip install \
38
+ "hyper-models[ml]==${HYPER_MODELS_VERSION}" \
39
+ "datasets>=4.5.0" \
40
+ "Pillow>=12.0.0" \
41
+ "timm>=1.0.0" \
42
+ "transformers>=5.0.0,<6.0.0" \
43
+ "safetensors>=0.4.0" \
44
+ "pyyaml>=6.0.0" \
45
+ "sentencepiece>=0.2.0" \
46
+ "protobuf>=4.25.0"
47
+
48
+ COPY --chown=user . .
49
+
50
+ ENV HYPERVIEW_HOST=0.0.0.0 \
51
+ HYPERVIEW_PORT=7860 \
52
+ HYPERVIEW_WORKSPACE_ID=visual-safety-content-clip-hyper3clip \
53
+ HYPERVIEW_DATASETS_DIR=/home/user/.cache/hyperview/datasets \
54
+ HYPERVIEW_MEDIA_DIR=/home/user/.cache/hyperview/media \
55
+ SAFETY_ASSET_REPO_TYPE=space
56
+
57
+ EXPOSE 7860
58
+
59
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=600s --retries=3 \
60
+ CMD curl -f http://localhost:7860/__hyperview__/health || exit 1
61
+
62
+ CMD ["python", "demo.py"]
README.md CHANGED
@@ -1,10 +1,72 @@
1
  ---
2
  title: HyperView Visual Safety
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: yellow
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 Visual Safety
3
+ emoji: 🛡️
4
+ colorFrom: yellow
5
+ colorTo: gray
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # HyperView - Open Images Visual Safety Demo
12
+
13
+ This demo shows a marketplace-style visual moderation workflow without showing
14
+ explicit NSFW material.
15
+
16
+ It loads a small curated Open Images V7 validation subset from the Space assets
17
+ and builds the HyperView dataset at startup with two buckets:
18
+
19
+ - **Safe**: ordinary marketplace/public-image objects such as clothing, shoes,
20
+ furniture, toys, books, fruit, laptops, and mobile phones.
21
+ - **Needs review**: public-displayable but policy-relevant objects such as
22
+ alcohol, cigarettes, knives, and firearms-related labels.
23
+
24
+ HyperView computes embeddings and opens two pinned maps at startup:
25
+
26
+ - CLIP ViT-B/32 in a Euclidean 2D layout
27
+ - hyper3-clip `hyper3-clip-v0.5` from `hyper-models` in a Poincare 2D layout
28
+
29
+ The right-hand panel includes the measured 120-image zero-shot smoke test:
30
+ CLIP is stronger overall on this first proxy, while hyper3-clip catches slightly
31
+ more review-class items at the cost of more false positives.
32
+
33
+ Run locally from the HyperView repo:
34
+
35
+ ```bash
36
+ uv run python hyperview-spaces/spaces/visual-safety-content-clip-hyper3clip/demo.py
37
+ ```
38
+
39
+ Useful overrides:
40
+
41
+ ```bash
42
+ HYPERVIEW_PORT=6265 SAFETY_SAMPLES_PER_BUCKET=40 \
43
+ uv run python hyperview-spaces/spaces/visual-safety-content-clip-hyper3clip/demo.py
44
+ ```
45
+
46
+ ## Dataset And Rights
47
+
48
+ The demo uses a checked-in public Open Images V7 validation subset under
49
+ `demo_assets/`:
50
+
51
+ - Dataset: https://storage.googleapis.com/openimages/web/index.html
52
+ - Images are loaded through Hugging Face Hub asset resolution when the Space
53
+ runs on Hugging Face.
54
+ - Individual source licenses are preserved in sample metadata.
55
+
56
+ This is a public demo proxy for marketplace moderation triage, not a claim that
57
+ either model is a production safety classifier.
58
+
59
+ ## Deploy Source
60
+
61
+ This folder is intended to deploy to `mnm-matin/HyperView-Visual-Safety` from
62
+ the `hyperview-spaces` deployment repository. The org Space
63
+ `hyper3labs/HyperView-Visual-Safety` is kept commit-synced but paused.
64
+
65
+ hyper3-clip weights are loaded through the `hyper-models` catalog entry for the
66
+ gated `hyper3labs/hyper3-clip-v0.5` model repository at runtime. The Space needs
67
+ an `HF_TOKEN` secret with access to that model.
68
+
69
+ If that secret is missing, the Space starts in a clearly labeled fallback mode:
70
+ the hyper3-clip panel reuses the CLIP layout and the readout shows a runtime
71
+ warning. Set `HYPERVIEW_ALLOW_CANDIDATE_FALLBACK=0` to make missing hyper3-clip
72
+ access fail loudly instead.
demo.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Open Images visual safety moderation demo for CLIP vs hyper3-clip."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ from collections import Counter, defaultdict
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from huggingface_hub import hf_hub_download
13
+ import hyperview as hv
14
+ from hyperview.core.sample import Sample
15
+
16
+
17
+ SPACE_DIR = Path(__file__).resolve().parent
18
+ SPACE_HOST = os.environ.get("HYPERVIEW_HOST", "127.0.0.1")
19
+ SPACE_PORT = int(os.environ.get("HYPERVIEW_PORT", "6262"))
20
+ WORKSPACE_ID = os.environ.get("HYPERVIEW_WORKSPACE_ID", "visual-safety-content-clip-hyper3clip")
21
+ DATASET_NAME = os.environ.get("HYPERVIEW_DATASET_NAME", "openimages_visual_safety_marketplace_triage_assets_v1")
22
+ EXTENSION_DIR = SPACE_DIR / ".hyperview" / "extensions" / "safety-readout"
23
+ SAMPLES_PER_BUCKET = int(os.environ.get("SAFETY_SAMPLES_PER_BUCKET", "60"))
24
+ ASSET_MANIFEST_PATH = os.environ.get("SAFETY_ASSET_MANIFEST", "demo_assets/manifest.json")
25
+ ASSET_REPO_ID = os.environ.get("SAFETY_ASSET_REPO_ID") or os.environ.get("SPACE_ID")
26
+ ASSET_REPO_TYPE = os.environ.get("SAFETY_ASSET_REPO_TYPE", "space")
27
+ ASSET_REVISION = os.environ.get("SAFETY_ASSET_REVISION") or None
28
+ ASSET_LOCAL_ROOT = Path(os.environ.get("SAFETY_ASSET_LOCAL_ROOT", str(SPACE_DIR)))
29
+
30
+ DEMO_SCENARIOS = (
31
+ {
32
+ "labels": ("Knife", "Kitchen knife"),
33
+ "title": "Kitchen knife upload",
34
+ "step": "1",
35
+ "decision": "Route to policy review",
36
+ "business_value": "Catches listings that may be allowed in some contexts but need marketplace policy checks before broad distribution.",
37
+ "inspect": "A useful model should pull nearby knives/tools into the same neighborhood instead of mixing them with ordinary kitchenware.",
38
+ },
39
+ {
40
+ "labels": ("Beer", "Wine", "Alcoholic beverage"),
41
+ "title": "Alcohol listing",
42
+ "step": "2",
43
+ "decision": "Restrict or age-gate",
44
+ "business_value": "Separates regulated goods from normal shopping content so the queue can apply the correct country and age policy.",
45
+ "inspect": "Look for alcohol examples grouped together, not scattered across safe food or glassware listings.",
46
+ },
47
+ {
48
+ "labels": ("Shoe", "Clothing", "Dress", "Toy", "Chair"),
49
+ "title": "Normal seller upload",
50
+ "step": "3",
51
+ "decision": "Approve without review",
52
+ "business_value": "Avoids false positives that slow down legitimate sellers and create unnecessary moderator load.",
53
+ "inspect": "A useful model keeps ordinary listings away from the needs-review neighborhood.",
54
+ },
55
+ )
56
+
57
+ SMOKE_METRICS = {
58
+ "dataset": "Open Images V7 validation safety proxy",
59
+ "nImages": 120,
60
+ "classBalance": "60 safe / 60 needs review",
61
+ "clip": {
62
+ "accuracy": 0.758,
63
+ "auroc": 0.865,
64
+ "apReview": 0.870,
65
+ "f1Review": 0.768,
66
+ "reviewRecall": 0.800,
67
+ "reviewPrecision": 0.738,
68
+ "safeRecall": 0.717,
69
+ },
70
+ "candidate": {
71
+ "accuracy": 0.725,
72
+ "auroc": 0.839,
73
+ "apReview": 0.812,
74
+ "f1Review": 0.752,
75
+ "reviewRecall": 0.833,
76
+ "reviewPrecision": 0.685,
77
+ "safeRecall": 0.617,
78
+ },
79
+ "readout": (
80
+ "CLIP is stronger overall on the first zero-shot smoke test. hyper3-clip catches slightly more "
81
+ "review items, which is useful for moderation triage but creates more false positives."
82
+ ),
83
+ }
84
+
85
+ MODEL_SPECS = [
86
+ {
87
+ "key": "clip",
88
+ "display_name": os.environ.get("SAFETY_BASELINE_DISPLAY_NAME", "CLIP-B/32"),
89
+ "button_label": os.environ.get("SAFETY_BASELINE_BUTTON_LABEL", "Inspect with CLIP"),
90
+ "provider": os.environ.get("SAFETY_BASELINE_PROVIDER", "embed-anything"),
91
+ "model": os.environ.get("SAFETY_BASELINE_MODEL", "openai/clip-vit-base-patch32"),
92
+ "layout": os.environ.get("SAFETY_BASELINE_LAYOUT", "euclidean:2d"),
93
+ "geometry": os.environ.get("SAFETY_BASELINE_GEOMETRY", "euclidean"),
94
+ "layout_dimension": int(os.environ.get("SAFETY_BASELINE_LAYOUT_DIMENSION", "2")),
95
+ "metric": os.environ.get("SAFETY_BASELINE_METRIC", "cosine"),
96
+ "panel_title": os.environ.get("SAFETY_BASELINE_PANEL_TITLE", "CLIP-B/32 - Safety Review Map"),
97
+ },
98
+ {
99
+ "key": "candidate",
100
+ "display_name": os.environ.get("SAFETY_CANDIDATE_DISPLAY_NAME", "hyper3-clip"),
101
+ "button_label": os.environ.get("SAFETY_CANDIDATE_BUTTON_LABEL", "Inspect with hyper3-clip"),
102
+ "provider": os.environ.get("SAFETY_CANDIDATE_PROVIDER", "hyper-models"),
103
+ "model": os.environ.get("SAFETY_CANDIDATE_MODEL", "hyper3-clip-v0.5"),
104
+ "layout": os.environ.get("SAFETY_CANDIDATE_LAYOUT", "poincare:2d"),
105
+ "geometry": os.environ.get("SAFETY_CANDIDATE_GEOMETRY", "poincare"),
106
+ "layout_dimension": int(os.environ.get("SAFETY_CANDIDATE_LAYOUT_DIMENSION", "2")),
107
+ "metric": os.environ.get("SAFETY_CANDIDATE_METRIC", "cosine"),
108
+ "panel_title": os.environ.get("SAFETY_CANDIDATE_PANEL_TITLE", "hyper3-clip - Safety Review Map"),
109
+ },
110
+ ]
111
+
112
+
113
+ def resolve_asset(relative_path: str) -> Path:
114
+ if ASSET_REPO_ID:
115
+ return Path(
116
+ hf_hub_download(
117
+ repo_id=ASSET_REPO_ID,
118
+ repo_type=ASSET_REPO_TYPE,
119
+ revision=ASSET_REVISION,
120
+ filename=relative_path,
121
+ )
122
+ )
123
+
124
+ local_path = ASSET_LOCAL_ROOT / relative_path
125
+ if not local_path.exists():
126
+ raise FileNotFoundError(
127
+ f"Asset '{relative_path}' was not found locally and SAFETY_ASSET_REPO_ID/SPACE_ID is unset."
128
+ )
129
+ return local_path
130
+
131
+
132
+ def safe_sample_id(record: dict[str, Any]) -> str:
133
+ return f"openimages_{record['index']:03d}_{record['bucket']}_{record['image_id']}"
134
+
135
+
136
+ def choose_records() -> list[dict[str, Any]]:
137
+ manifest_path = resolve_asset(ASSET_MANIFEST_PATH)
138
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
139
+ records: list[dict[str, Any]] = []
140
+ for item in manifest.get("records", []):
141
+ bucket = item["bucket"]
142
+ if sum(1 for record in records if record["bucket"] == bucket) >= SAMPLES_PER_BUCKET:
143
+ continue
144
+ image_path = resolve_asset(item["asset_path"])
145
+ records.append(
146
+ {
147
+ "index": len(records),
148
+ "image_id": item["image_id"],
149
+ "bucket": bucket,
150
+ "label": item["label"],
151
+ "labels": item["labels"],
152
+ "path": image_path,
153
+ "source_url": item["source_url"],
154
+ "license": item.get("license", ""),
155
+ "title": item.get("title", ""),
156
+ }
157
+ )
158
+ if all(
159
+ sum(1 for record in records if record["bucket"] == bucket_name) >= SAMPLES_PER_BUCKET
160
+ for bucket_name in ("needs_review", "safe")
161
+ ):
162
+ break
163
+
164
+ if not records:
165
+ raise RuntimeError("No Open Images records could be prepared.")
166
+
167
+ records = order_records_for_demo(records)
168
+ for index, record in enumerate(records):
169
+ record["index"] = index
170
+ counts = Counter(record["bucket"] for record in records)
171
+ print(f"Prepared Open Images safety records: {dict(counts)}", flush=True)
172
+ source = f"Hugging Face {ASSET_REPO_TYPE} '{ASSET_REPO_ID}'" if ASSET_REPO_ID else "local demo_assets"
173
+ print(f"Loaded visual-safety assets from {source}.", flush=True)
174
+ return records
175
+
176
+
177
+ def order_records_for_demo(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
178
+ """Put landing-page-friendly examples first in the Samples panel."""
179
+ used: set[str] = set()
180
+ ordered: list[dict[str, Any]] = []
181
+
182
+ def append_unique(items: list[dict[str, Any]]) -> None:
183
+ for item in items:
184
+ if item["image_id"] in used:
185
+ continue
186
+ used.add(item["image_id"])
187
+ ordered.append(item)
188
+
189
+ by_label: dict[str, list[dict[str, Any]]] = defaultdict(list)
190
+ for record in records:
191
+ by_label[record["label"]].append(record)
192
+
193
+ for scenario in DEMO_SCENARIOS:
194
+ record = next(
195
+ (
196
+ candidate
197
+ for label in scenario["labels"]
198
+ for candidate in by_label.get(label, [])
199
+ if candidate["image_id"] not in used
200
+ ),
201
+ None,
202
+ )
203
+ if record is None:
204
+ continue
205
+ append_unique([record])
206
+
207
+ append_unique(sorted(
208
+ (record for record in records if record["bucket"] == "safe" and record["image_id"] not in used),
209
+ key=lambda record: (record["label"], record["image_id"]),
210
+ ))
211
+ append_unique([record for record in records if record["bucket"] == "needs_review" and record["image_id"] not in used])
212
+ append_unique([record for record in records if record["image_id"] not in used])
213
+
214
+ return ordered
215
+
216
+
217
+ def add_safety_samples(dataset: hv.Dataset) -> list[dict[str, Any]]:
218
+ existing_ids = {sample.id for sample in dataset.samples}
219
+ records = choose_records()
220
+ samples: list[Sample] = []
221
+ existing_selected_ids = set()
222
+
223
+ for record in records:
224
+ sample_id = safe_sample_id(record)
225
+ existing_selected_ids.add(sample_id)
226
+ metadata = {
227
+ "policy_bucket": record["bucket"],
228
+ "open_images_labels": record["labels"],
229
+ "primary_label": record["label"],
230
+ "source_dataset": "Open Images V7 validation",
231
+ "source_url": record["source_url"],
232
+ "license": record.get("license", ""),
233
+ "title": record.get("title", ""),
234
+ "business_use_case": "marketplace listing moderation triage",
235
+ "display_order": record["index"],
236
+ }
237
+
238
+ samples.append(
239
+ Sample(
240
+ id=sample_id,
241
+ filepath=str(record["path"]),
242
+ label=record["bucket"],
243
+ metadata=metadata,
244
+ )
245
+ )
246
+
247
+ added = len(existing_selected_ids - existing_ids)
248
+ updated = len(existing_selected_ids & existing_ids)
249
+ dataset.add_samples(samples, skip_existing=False)
250
+ print(f"Prepared Open Images safety samples ({added} added, {updated} updated).", flush=True)
251
+ return records
252
+
253
+
254
+ def ensure_layouts(dataset: hv.Dataset) -> dict[str, str]:
255
+ layouts: dict[str, str] = {}
256
+ for spec in MODEL_SPECS:
257
+ print(f"Ensuring {spec['display_name']} embeddings...", flush=True)
258
+ space_key = dataset.compute_embeddings(
259
+ model=spec["model"],
260
+ provider=spec["provider"],
261
+ batch_size=32,
262
+ show_progress=True,
263
+ )
264
+ print(f"Ensuring {spec['display_name']} layout...", flush=True)
265
+ layouts[spec["key"]] = dataset.compute_visualization(
266
+ space_key=space_key,
267
+ layout=spec["layout"],
268
+ n_neighbors=20,
269
+ min_dist=0.08,
270
+ metric=spec["metric"],
271
+ )
272
+ return layouts
273
+
274
+
275
+ def build_dataset() -> tuple[hv.Dataset, dict[str, str], list[dict[str, Any]]]:
276
+ dataset = hv.Dataset(DATASET_NAME)
277
+ records = add_safety_samples(dataset)
278
+ layouts = ensure_layouts(dataset)
279
+ return dataset, layouts, records
280
+
281
+
282
+ def model_panel_props(layouts: dict[str, str]) -> list[dict[str, Any]]:
283
+ show_baseline = os.environ.get("SAFETY_SHOW_BASELINE_BUTTONS", "").lower() in {"1", "true", "yes"}
284
+ props = []
285
+ for spec in MODEL_SPECS:
286
+ if spec["key"] == "clip" and not show_baseline:
287
+ continue
288
+ props.append(
289
+ {
290
+ "key": spec["key"],
291
+ "displayName": spec["display_name"],
292
+ "buttonLabel": spec["button_label"],
293
+ "layoutKey": layouts[spec["key"]],
294
+ }
295
+ )
296
+ return props
297
+
298
+
299
+ def build_examples(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
300
+ by_label: dict[str, dict[str, Any]] = {}
301
+ for record in records:
302
+ by_label.setdefault(record["label"], record)
303
+
304
+ examples = []
305
+ for scenario in DEMO_SCENARIOS:
306
+ record = next((by_label.get(label) for label in scenario["labels"] if label in by_label), None)
307
+ if record is None:
308
+ continue
309
+ bucket = record["bucket"]
310
+ examples.append(
311
+ {
312
+ "id": record["image_id"],
313
+ "step": scenario["step"],
314
+ "title": scenario["title"],
315
+ "family": "Needs review" if bucket == "needs_review" else "Safe listing",
316
+ "decision": scenario["decision"],
317
+ "businessValue": scenario["business_value"],
318
+ "inspect": scenario["inspect"],
319
+ "queryId": safe_sample_id(record),
320
+ "queryLabel": record["label"],
321
+ "bucket": bucket,
322
+ "labels": record["labels"],
323
+ "sourceUrl": record["source_url"],
324
+ }
325
+ )
326
+ return examples
327
+
328
+
329
+ def build_demo_view(dataset: hv.Dataset, layouts: dict[str, str], records: list[dict[str, Any]]) -> hv.ui.View:
330
+ primary_spec = next(spec for spec in MODEL_SPECS if spec["key"] == "candidate")
331
+ samples_panel = hv.ui.Samples(
332
+ id="safety-samples",
333
+ title="Samples",
334
+ position="center",
335
+ layout=hv.ui.PanelLayout(min_height=420),
336
+ )
337
+ primary_scatter = hv.ui.Scatter(
338
+ id="hyper3-clip-safety-map-bottom",
339
+ title=primary_spec["panel_title"],
340
+ position="bottom",
341
+ reference_panel_id="safety-samples",
342
+ direction="below",
343
+ layout_key=layouts[primary_spec["key"]],
344
+ geometry=primary_spec["geometry"],
345
+ layout_dimension=primary_spec["layout_dimension"],
346
+ layout=hv.ui.PanelLayout(height=280, min_height=220),
347
+ )
348
+ readout_panel = hv.ui.ExtensionPanel(
349
+ id="visual-safety-readout",
350
+ extension="safety-readout",
351
+ panel="safety-comparison",
352
+ title="Safety triage",
353
+ reference_panel_id="safety-samples",
354
+ direction="right",
355
+ props={
356
+ "models": model_panel_props(layouts),
357
+ "examples": build_examples(records),
358
+ "metrics": SMOKE_METRICS,
359
+ "bucketCounts": dict(Counter(record["bucket"] for record in records)),
360
+ },
361
+ layout=hv.ui.PanelLayout(width=520, min_width=420),
362
+ )
363
+ return hv.ui.View(
364
+ samples_panel,
365
+ readout_panel,
366
+ primary_scatter,
367
+ )
368
+
369
+
370
+ def launch_demo(dataset: hv.Dataset, layouts: dict[str, str], records: list[dict[str, Any]]) -> hv.Session:
371
+ session = hv.launch(
372
+ dataset,
373
+ host=SPACE_HOST,
374
+ port=SPACE_PORT,
375
+ open_browser=False,
376
+ workspace_id=WORKSPACE_ID,
377
+ block=False,
378
+ )
379
+ print("Installing Open Images visual-safety demo extension...", flush=True)
380
+ session.ui.add_extension(EXTENSION_DIR, workspace_id=WORKSPACE_ID)
381
+ print("Applying Open Images visual-safety triage demo view...", flush=True)
382
+ session.ui.apply_view(build_demo_view(dataset, layouts, records), workspace_id=WORKSPACE_ID)
383
+ session.ui.set_active_layout(layouts["candidate"], workspace_id=WORKSPACE_ID)
384
+ session.ui.set_selection([], workspace_id=WORKSPACE_ID)
385
+ print(f"\nHyperView Open Images visual-safety demo is running at {session.url}", flush=True)
386
+ return session
387
+
388
+
389
+ def main() -> None:
390
+ dataset, layouts, records = build_dataset()
391
+ print("Layouts:", flush=True)
392
+ for spec in MODEL_SPECS:
393
+ print(f" {spec['display_name']}: {layouts[spec['key']]}", flush=True)
394
+ session = launch_demo(dataset, layouts, records)
395
+ session.wait()
396
+
397
+
398
+ if __name__ == "__main__":
399
+ main()
demo_assets/images/01b9021aa05e6e46.jpg ADDED

Git LFS Details

  • SHA256: fba941662774fd4413fd889c131c8ceab2bb2316e36256777ac26aef8f38751b
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB
demo_assets/images/0565bcccb1266f62.jpg ADDED
demo_assets/images/05f4f97b3833086e.jpg ADDED
demo_assets/images/06a449c6ef03c172.jpg ADDED
demo_assets/images/07de9510e802a400.jpg ADDED
demo_assets/images/084a9d12a2a486dc.jpg ADDED
demo_assets/images/09b96636ea4f4ce9.jpg ADDED
demo_assets/images/0ba3f8b8a3a2e93c.jpg ADDED
demo_assets/images/0c4b629e801e8181.jpg ADDED
demo_assets/images/11cd71943afb477e.jpg ADDED
demo_assets/images/120d233407f1d14c.jpg ADDED
demo_assets/images/1228bfe2f3dc2d29.jpg ADDED
demo_assets/images/12b06cbeaa8b634a.jpg ADDED
demo_assets/images/14d45a56a0fe95bf.jpg ADDED
demo_assets/images/1519b2618048f525.jpg ADDED
demo_assets/images/15cc8c6a07fd3f0f.jpg ADDED
demo_assets/images/178f262c9f18c8ba.jpg ADDED
demo_assets/images/1a9d5f7cc9a88a1d.jpg ADDED
demo_assets/images/1de9f211102093d0.jpg ADDED
demo_assets/images/20d46de74f618aef.jpg ADDED
demo_assets/images/21d68297ff292660.jpg ADDED
demo_assets/images/2c87374e8d2f3a34.jpg ADDED
demo_assets/images/318c91bd306e52e1.jpg ADDED
demo_assets/images/318e12be63daa7bb.jpg ADDED
demo_assets/images/32255e4104d8f9b7.jpg ADDED
demo_assets/images/33969a398effdb29.jpg ADDED
demo_assets/images/33ac3bd2015c44b2.jpg ADDED
demo_assets/images/34ec1592f40cdfaa.jpg ADDED

Git LFS Details

  • SHA256: 053b0f767fcc3ef6b03a44ea75111c47f983b6bb4db61b81f6ed19a82b425a97
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
demo_assets/images/3722904d5b45c676.jpg ADDED
demo_assets/images/3ba5380490084697.jpg ADDED
demo_assets/images/3d0d7ec99241aec7.jpg ADDED
demo_assets/images/419e1902dcac2d77.jpg ADDED
demo_assets/images/41bf39cbdf4b0a05.jpg ADDED
demo_assets/images/43040be867118184.jpg ADDED
demo_assets/images/474d23651d5ca02a.jpg ADDED
demo_assets/images/477ab6b26dd5a667.jpg ADDED
demo_assets/images/483ade136f9e676d.jpg ADDED
demo_assets/images/48e7aacdb6423790.jpg ADDED
demo_assets/images/499cfd3e29e31662.jpg ADDED

Git LFS Details

  • SHA256: a37dcda34674bf21e94599166bfe9892cf9d34553217b351dbfa76f518cf338d
  • Pointer size: 131 Bytes
  • Size of remote file: 133 kB
demo_assets/images/4af30ade5ff3deb1.jpg ADDED
demo_assets/images/4b00a3af27c5b2c6.jpg ADDED

Git LFS Details

  • SHA256: ca61ae3e37973ca3971fc9f07587340f7bfd2c18ddc6563adfd9fa6f709fab9a
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
demo_assets/images/4cc6516b9be3d77d.jpg ADDED
demo_assets/images/4d5a6b0ed65de97e.jpg ADDED