Spaces:
Running
Running
Mark Duppenthaler
commited on
Commit
·
eb27538
1
Parent(s):
503a577
Add audio examples, wip
Browse files- backend/examples.py +20 -0
- frontend/src/components/AudioGallery.tsx +74 -7
- frontend/src/components/AudioPlayer.tsx +27 -29
- frontend/src/components/ExampleDetailsSection.tsx +14 -0
- frontend/src/components/ExampleMetadata.tsx +20 -0
- frontend/src/components/ExampleVariantSelector.tsx +46 -0
- frontend/src/components/ImageGallery.tsx +7 -72
- frontend/src/components/VideoGallery.tsx +67 -6
- frontend/src/components/galleryUtils.ts +14 -12
backend/examples.py
CHANGED
@@ -178,6 +178,26 @@ def build_infos(abs_path: Path, datatype: str, dataset_name: str, db_key: str):
|
|
178 |
)
|
179 |
files = files[2:] + files[:2]
|
180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
files = [
|
182 |
{
|
183 |
"image_url": f,
|
|
|
178 |
)
|
179 |
files = files[2:] + files[:2]
|
180 |
|
181 |
+
# For audio and video, we need to check the image_url and make sure it is an image, else convert it:
|
182 |
+
if datatype == "audio":
|
183 |
+
files = [
|
184 |
+
(
|
185 |
+
(f, n)
|
186 |
+
if f.endswith(".png")
|
187 |
+
else (f.replace(".wav", ".png"), n)
|
188 |
+
)
|
189 |
+
for f, n in files
|
190 |
+
]
|
191 |
+
elif datatype == "video":
|
192 |
+
files = [
|
193 |
+
(
|
194 |
+
(f, n)
|
195 |
+
if f.endswith(".mkv")
|
196 |
+
else (f.replace(".png", ".mkv"), n)
|
197 |
+
)
|
198 |
+
for f, n in files
|
199 |
+
]
|
200 |
+
|
201 |
files = [
|
202 |
{
|
203 |
"image_url": f,
|
frontend/src/components/AudioGallery.tsx
CHANGED
@@ -1,6 +1,10 @@
|
|
1 |
import React from 'react'
|
2 |
import AudioPlayer from './AudioPlayer'
|
3 |
import type { ExamplesData } from './Examples'
|
|
|
|
|
|
|
|
|
4 |
|
5 |
interface GalleryProps {
|
6 |
selectedModel: string
|
@@ -14,15 +18,78 @@ interface GalleryProps {
|
|
14 |
|
15 |
const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
|
16 |
const exampleItems = examples[selectedModel][selectedAttack]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
return (
|
18 |
-
<div className="
|
19 |
-
|
20 |
-
<div
|
21 |
-
<
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
</div>
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
</div>
|
27 |
)
|
28 |
}
|
|
|
1 |
import React from 'react'
|
2 |
import AudioPlayer from './AudioPlayer'
|
3 |
import type { ExamplesData } from './Examples'
|
4 |
+
import { groupByNameAndVariant } from './galleryUtils'
|
5 |
+
import ExampleMetadata from './ExampleMetadata'
|
6 |
+
import ExampleDetailsSection from './ExampleDetailsSection'
|
7 |
+
import ExampleVariantSelector from './ExampleVariantSelector'
|
8 |
|
9 |
interface GalleryProps {
|
10 |
selectedModel: string
|
|
|
18 |
|
19 |
const AudioGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
|
20 |
const exampleItems = examples[selectedModel][selectedAttack]
|
21 |
+
const grouped = groupByNameAndVariant(exampleItems)
|
22 |
+
console.log('Audio examples:', exampleItems)
|
23 |
+
console.log('Grouped audio examples:', grouped)
|
24 |
+
const audioNames = Object.keys(grouped)
|
25 |
+
const [selectedAudio, setSelectedAudio] = React.useState(audioNames[0] || '')
|
26 |
+
const variants = grouped[selectedAudio] || {}
|
27 |
+
const variantKeys = Object.keys(variants)
|
28 |
+
const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
|
29 |
+
|
30 |
+
React.useEffect(() => {
|
31 |
+
setSelectedVariant(variantKeys[0] || '')
|
32 |
+
}, [selectedAudio])
|
33 |
+
|
34 |
+
if (!audioNames.length) {
|
35 |
+
return (
|
36 |
+
<div className="w-full mt-12 flex items-center justify-center">
|
37 |
+
<div className="text-gray-500">
|
38 |
+
No audio examples available. Please select another model and attack.
|
39 |
+
</div>
|
40 |
+
</div>
|
41 |
+
)
|
42 |
+
}
|
43 |
+
|
44 |
+
console.log(variants[selectedVariant])
|
45 |
+
|
46 |
return (
|
47 |
+
<div className="w-full overflow-auto" style={{ minHeight: '100vh' }}>
|
48 |
+
<div className="example-display">
|
49 |
+
<div className="mb-4">
|
50 |
+
<fieldset className="fieldset">
|
51 |
+
<legend className="fieldset-legend">Audio Example</legend>
|
52 |
+
<select
|
53 |
+
className="select select-bordered"
|
54 |
+
value={selectedAudio || ''}
|
55 |
+
onChange={(e) => {
|
56 |
+
setSelectedAudio(e.target.value || '')
|
57 |
+
}}
|
58 |
+
>
|
59 |
+
{audioNames.map((name) => (
|
60 |
+
<option key={name} value={name}>
|
61 |
+
{name}
|
62 |
+
</option>
|
63 |
+
))}
|
64 |
+
</select>
|
65 |
+
</fieldset>
|
66 |
</div>
|
67 |
+
{selectedAudio && selectedVariant && variants[selectedVariant] && (
|
68 |
+
<>
|
69 |
+
<ExampleVariantSelector
|
70 |
+
variantKeys={variantKeys}
|
71 |
+
selectedVariant={selectedVariant}
|
72 |
+
setSelectedVariant={setSelectedVariant}
|
73 |
+
/>
|
74 |
+
<ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
|
75 |
+
<ExampleDetailsSection>
|
76 |
+
<div className="flex flex-col items-center gap-4">
|
77 |
+
{variants[selectedVariant].image_url && (
|
78 |
+
<img
|
79 |
+
src={variants[selectedVariant].image_url}
|
80 |
+
alt={selectedAudio}
|
81 |
+
className="example-image"
|
82 |
+
style={{ display: 'block' }}
|
83 |
+
/>
|
84 |
+
)}
|
85 |
+
{variants[selectedVariant].audio_url && (
|
86 |
+
<AudioPlayer src={variants[selectedVariant].audio_url} />
|
87 |
+
)}
|
88 |
+
</div>
|
89 |
+
</ExampleDetailsSection>
|
90 |
+
</>
|
91 |
+
)}
|
92 |
+
</div>
|
93 |
</div>
|
94 |
)
|
95 |
}
|
frontend/src/components/AudioPlayer.tsx
CHANGED
@@ -1,46 +1,46 @@
|
|
1 |
-
import { useEffect,
|
2 |
import WaveSurfer from 'wavesurfer.js'
|
3 |
// @ts-ignore: No types for timeline.esm.js
|
4 |
-
// import Timeline from 'wavesurfer.js/dist/plugins/timeline.esm.js'
|
5 |
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
|
6 |
-
import API from '../API'
|
7 |
|
8 |
const AudioPlayer = ({ src }: { src: string }) => {
|
9 |
const containerRef = useRef<HTMLDivElement>(null)
|
10 |
const wavesurferRef = useRef<WaveSurfer | null>(null)
|
11 |
const [isPlaying, setIsPlaying] = useState(false)
|
12 |
-
// const plugins = useMemo(() => [TimelinePlugin.create()], [])
|
13 |
|
14 |
-
const bottomTimeline = TimelinePlugin.create({
|
15 |
-
height: 16,
|
16 |
-
timeInterval: 0.1,
|
17 |
-
primaryLabelInterval: 1,
|
18 |
-
style: {
|
19 |
-
fontSize: '10px',
|
20 |
-
// color: '#6A3274',
|
21 |
-
},
|
22 |
-
})
|
23 |
-
|
24 |
-
// Initialize WaveSurfer when component mounts
|
25 |
useEffect(() => {
|
26 |
if (!containerRef.current) return
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
// Get proxied URL to bypass CORS
|
29 |
const proxiedUrl = API.getProxiedUrl(src)
|
30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
// Create an instance of WaveSurfer
|
32 |
wavesurferRef.current = WaveSurfer.create({
|
33 |
container: containerRef.current,
|
34 |
waveColor: 'rgb(200, 0, 200)',
|
35 |
progressColor: 'rgb(100, 0, 100)',
|
36 |
-
url: proxiedUrl,
|
37 |
minPxPerSec: 100,
|
38 |
-
barWidth: 10,
|
39 |
-
barRadius: 10,
|
40 |
-
barGap: 2,
|
41 |
mediaControls: true,
|
42 |
-
|
43 |
-
// plugins: [bottomTimeline],
|
44 |
})
|
45 |
|
46 |
// Play on click
|
@@ -48,29 +48,27 @@ const AudioPlayer = ({ src }: { src: string }) => {
|
|
48 |
wavesurferRef.current?.play()
|
49 |
setIsPlaying(true)
|
50 |
})
|
51 |
-
|
52 |
-
// Rewind to the beginning on finished playing
|
53 |
wavesurferRef.current.on('finish', () => {
|
54 |
wavesurferRef.current?.setTime(0)
|
55 |
setIsPlaying(false)
|
56 |
})
|
57 |
-
|
58 |
-
// Update playing state
|
59 |
wavesurferRef.current.on('play', () => setIsPlaying(true))
|
60 |
wavesurferRef.current.on('pause', () => setIsPlaying(false))
|
61 |
|
62 |
// Cleanup on unmount
|
63 |
return () => {
|
64 |
wavesurferRef.current?.destroy()
|
|
|
65 |
}
|
66 |
}, [src])
|
67 |
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
71 |
|
72 |
return (
|
73 |
-
<div className="">
|
74 |
<div ref={containerRef} />
|
75 |
{/* <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> */}
|
76 |
</div>
|
|
|
1 |
+
import { useEffect, useRef, useState } from 'react'
|
2 |
import WaveSurfer from 'wavesurfer.js'
|
3 |
// @ts-ignore: No types for timeline.esm.js
|
|
|
4 |
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js'
|
5 |
+
import API from '../API'
|
6 |
|
7 |
const AudioPlayer = ({ src }: { src: string }) => {
|
8 |
const containerRef = useRef<HTMLDivElement>(null)
|
9 |
const wavesurferRef = useRef<WaveSurfer | null>(null)
|
10 |
const [isPlaying, setIsPlaying] = useState(false)
|
|
|
11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
useEffect(() => {
|
13 |
if (!containerRef.current) return
|
14 |
|
15 |
+
// Destroy previous instance if exists
|
16 |
+
if (wavesurferRef.current) {
|
17 |
+
wavesurferRef.current.destroy()
|
18 |
+
wavesurferRef.current = null
|
19 |
+
}
|
20 |
+
|
21 |
// Get proxied URL to bypass CORS
|
22 |
const proxiedUrl = API.getProxiedUrl(src)
|
23 |
|
24 |
+
// Create plugin instance inside effect
|
25 |
+
const bottomTimeline = TimelinePlugin.create({
|
26 |
+
height: 16,
|
27 |
+
timeInterval: 0.1,
|
28 |
+
primaryLabelInterval: 1,
|
29 |
+
style: { fontSize: '10px' },
|
30 |
+
})
|
31 |
+
|
32 |
// Create an instance of WaveSurfer
|
33 |
wavesurferRef.current = WaveSurfer.create({
|
34 |
container: containerRef.current,
|
35 |
waveColor: 'rgb(200, 0, 200)',
|
36 |
progressColor: 'rgb(100, 0, 100)',
|
37 |
+
url: proxiedUrl,
|
38 |
minPxPerSec: 100,
|
39 |
+
// barWidth: 10,
|
40 |
+
// barRadius: 10,
|
41 |
+
// barGap: 2,
|
42 |
mediaControls: true,
|
43 |
+
plugins: [bottomTimeline],
|
|
|
44 |
})
|
45 |
|
46 |
// Play on click
|
|
|
48 |
wavesurferRef.current?.play()
|
49 |
setIsPlaying(true)
|
50 |
})
|
|
|
|
|
51 |
wavesurferRef.current.on('finish', () => {
|
52 |
wavesurferRef.current?.setTime(0)
|
53 |
setIsPlaying(false)
|
54 |
})
|
|
|
|
|
55 |
wavesurferRef.current.on('play', () => setIsPlaying(true))
|
56 |
wavesurferRef.current.on('pause', () => setIsPlaying(false))
|
57 |
|
58 |
// Cleanup on unmount
|
59 |
return () => {
|
60 |
wavesurferRef.current?.destroy()
|
61 |
+
wavesurferRef.current = null
|
62 |
}
|
63 |
}, [src])
|
64 |
|
65 |
+
// Optionally, add a play/pause button
|
66 |
+
// const handlePlayPause = () => {
|
67 |
+
// wavesurferRef.current?.playPause()
|
68 |
+
// }
|
69 |
|
70 |
return (
|
71 |
+
<div className="w-full">
|
72 |
<div ref={containerRef} />
|
73 |
{/* <button onClick={handlePlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> */}
|
74 |
</div>
|
frontend/src/components/ExampleDetailsSection.tsx
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react'
|
2 |
+
|
3 |
+
interface ExampleDetailsSectionProps {
|
4 |
+
children: React.ReactNode
|
5 |
+
}
|
6 |
+
|
7 |
+
const ExampleDetailsSection: React.FC<ExampleDetailsSectionProps> = ({ children }) => (
|
8 |
+
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mt-6">
|
9 |
+
<legend className="fieldset-legend font-semibold">Example</legend>
|
10 |
+
{children}
|
11 |
+
</fieldset>
|
12 |
+
)
|
13 |
+
|
14 |
+
export default ExampleDetailsSection
|
frontend/src/components/ExampleMetadata.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react'
|
2 |
+
|
3 |
+
interface ExampleMetadataProps {
|
4 |
+
metadata: Record<string, string | number | boolean>
|
5 |
+
}
|
6 |
+
|
7 |
+
const ExampleMetadata: React.FC<ExampleMetadataProps> = ({ metadata }) => (
|
8 |
+
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mt-6">
|
9 |
+
<legend className="fieldset-legend font-semibold">Example Info</legend>
|
10 |
+
<div className="flex flex-wrap gap-x-6 gap-y-2 text-xs">
|
11 |
+
{Object.entries(metadata).map(([k, v]) => (
|
12 |
+
<div key={k} className="flex items-center">
|
13 |
+
<span className="font-mono">{k}</span>: {String(v)}
|
14 |
+
</div>
|
15 |
+
))}
|
16 |
+
</div>
|
17 |
+
</fieldset>
|
18 |
+
)
|
19 |
+
|
20 |
+
export default ExampleMetadata
|
frontend/src/components/ExampleVariantSelector.tsx
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
|
3 |
+
interface ExampleVariantSelectorProps {
|
4 |
+
variantKeys: string[];
|
5 |
+
selectedVariant: string;
|
6 |
+
setSelectedVariant: (v: string) => void;
|
7 |
+
}
|
8 |
+
|
9 |
+
const ExampleVariantSelector: React.FC<ExampleVariantSelectorProps> = ({ variantKeys, selectedVariant, setSelectedVariant }) => {
|
10 |
+
// Keyboard shortcut for variant switching (keys 1-N)
|
11 |
+
React.useEffect(() => {
|
12 |
+
const handler = (e: KeyboardEvent) => {
|
13 |
+
if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'INPUT') return;
|
14 |
+
const idx = parseInt(e.key, 10);
|
15 |
+
if (!isNaN(idx) && idx > 0 && idx <= variantKeys.length) {
|
16 |
+
setSelectedVariant(variantKeys[idx - 1]);
|
17 |
+
}
|
18 |
+
};
|
19 |
+
window.addEventListener('keydown', handler);
|
20 |
+
return () => window.removeEventListener('keydown', handler);
|
21 |
+
}, [variantKeys, setSelectedVariant]);
|
22 |
+
|
23 |
+
return (
|
24 |
+
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
|
25 |
+
<legend className="fieldset-legend font-semibold">Variants</legend>
|
26 |
+
<div className="mb-2 flex gap-4 flex-wrap">
|
27 |
+
{variantKeys.map((variant, idx) => (
|
28 |
+
<label key={variant} className="flex items-center gap-1 cursor-pointer">
|
29 |
+
<input
|
30 |
+
type="radio"
|
31 |
+
name={`variant-selector`}
|
32 |
+
value={variant}
|
33 |
+
checked={selectedVariant === variant}
|
34 |
+
onChange={() => setSelectedVariant(variant)}
|
35 |
+
/>
|
36 |
+
<span className="text-xs font-semibold">
|
37 |
+
{variant} <span className="opacity-60">[{idx + 1}]</span>
|
38 |
+
</span>
|
39 |
+
</label>
|
40 |
+
))}
|
41 |
+
</div>
|
42 |
+
</fieldset>
|
43 |
+
);
|
44 |
+
};
|
45 |
+
|
46 |
+
export default ExampleVariantSelector;
|
frontend/src/components/ImageGallery.tsx
CHANGED
@@ -1,6 +1,9 @@
|
|
1 |
import React from 'react'
|
2 |
import type { ExamplesData } from './Examples'
|
3 |
import { groupByNameAndVariant } from './galleryUtils'
|
|
|
|
|
|
|
4 |
|
5 |
interface GalleryProps {
|
6 |
selectedModel: string
|
@@ -12,61 +15,6 @@ interface GalleryProps {
|
|
12 |
}
|
13 |
}
|
14 |
|
15 |
-
const Metadata: React.FC<{ metadata: Record<string, string | number | boolean> }> = ({
|
16 |
-
metadata,
|
17 |
-
}) => (
|
18 |
-
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
|
19 |
-
<legend className="fieldset-legend font-semibold">Metadata</legend>
|
20 |
-
<ul className="text-xs flex flex-row gap-4">
|
21 |
-
{(() => {
|
22 |
-
const entries = Object.entries(metadata || {}).filter(([k]) => k !== 'image_url')
|
23 |
-
const detectedIdx = entries.findIndex(([k]) => k === 'detected')
|
24 |
-
if (detectedIdx > -1) {
|
25 |
-
const [detected] = entries.splice(detectedIdx, 1)
|
26 |
-
entries.unshift(detected)
|
27 |
-
}
|
28 |
-
return entries.map(([k, v]) => (
|
29 |
-
<li key={k}>
|
30 |
-
<span className="font-mono">{k}</span>: {String(v)}
|
31 |
-
</li>
|
32 |
-
))
|
33 |
-
})()}
|
34 |
-
</ul>
|
35 |
-
</fieldset>
|
36 |
-
)
|
37 |
-
|
38 |
-
const VariantSelector: React.FC<{
|
39 |
-
variantKeys: string[]
|
40 |
-
selectedVariant: string
|
41 |
-
setSelectedVariant: (v: string) => void
|
42 |
-
}> = ({ variantKeys, selectedVariant, setSelectedVariant }) => (
|
43 |
-
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mb-4">
|
44 |
-
<legend className="fieldset-legend font-semibold">Variants</legend>
|
45 |
-
<div className="mb-2 flex gap-4 flex-wrap">
|
46 |
-
{variantKeys.map((variant, idx) => (
|
47 |
-
<label key={variant} className="flex items-center gap-1 cursor-pointer">
|
48 |
-
<input
|
49 |
-
type="radio"
|
50 |
-
name={`variant-selector`}
|
51 |
-
value={variant}
|
52 |
-
checked={selectedVariant === variant}
|
53 |
-
onChange={() => setSelectedVariant(variant)}
|
54 |
-
/>
|
55 |
-
<span className="text-xs font-semibold">
|
56 |
-
{variant} <span className="opacity-60">[{idx + 1}]</span>
|
57 |
-
</span>
|
58 |
-
</label>
|
59 |
-
))}
|
60 |
-
</div>
|
61 |
-
</fieldset>
|
62 |
-
)
|
63 |
-
|
64 |
-
const ExampleDetailsSection: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
65 |
-
<fieldset className="fieldset w-full p-4 rounded border border-gray-700 bg-base-200 mt-6">
|
66 |
-
{children}
|
67 |
-
</fieldset>
|
68 |
-
)
|
69 |
-
|
70 |
const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
|
71 |
const exampleItems = examples[selectedModel][selectedAttack]
|
72 |
|
@@ -90,20 +38,6 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
|
|
90 |
const ZOOM_BOX_SIZE = 300
|
91 |
const [zoomLevel, setZoomLevel] = React.useState(2) // 2x default zoom
|
92 |
|
93 |
-
// Keyboard shortcut for variant switching
|
94 |
-
React.useEffect(() => {
|
95 |
-
const handler = (e: KeyboardEvent) => {
|
96 |
-
if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'INPUT')
|
97 |
-
return
|
98 |
-
const idx = parseInt(e.key, 10)
|
99 |
-
if (!isNaN(idx) && idx > 0 && idx <= variantKeys.length) {
|
100 |
-
setSelectedVariant(variantKeys[idx - 1])
|
101 |
-
}
|
102 |
-
}
|
103 |
-
window.addEventListener('keydown', handler)
|
104 |
-
return () => window.removeEventListener('keydown', handler)
|
105 |
-
}, [variantKeys])
|
106 |
-
|
107 |
// Calculate the minimum width and height across all variants for the selected image
|
108 |
const variantImages = variantKeys.map((v) => variants[v]?.image_url).filter(Boolean)
|
109 |
const [naturalDims, setNaturalDims] = React.useState<{ width: number; height: number } | null>(
|
@@ -192,14 +126,15 @@ const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, e
|
|
192 |
</div>
|
193 |
{selectedImage && selectedVariant && variants[selectedVariant] && (
|
194 |
<>
|
195 |
-
<
|
196 |
variantKeys={variantKeys}
|
197 |
selectedVariant={selectedVariant}
|
198 |
setSelectedVariant={setSelectedVariant}
|
199 |
/>
|
200 |
-
<
|
201 |
-
|
202 |
<ExampleDetailsSection>
|
|
|
203 |
<div
|
204 |
style={{
|
205 |
width: '100%',
|
|
|
1 |
import React from 'react'
|
2 |
import type { ExamplesData } from './Examples'
|
3 |
import { groupByNameAndVariant } from './galleryUtils'
|
4 |
+
import ExampleMetadata from './ExampleMetadata'
|
5 |
+
import ExampleDetailsSection from './ExampleDetailsSection'
|
6 |
+
import ExampleVariantSelector from './ExampleVariantSelector'
|
7 |
|
8 |
interface GalleryProps {
|
9 |
selectedModel: string
|
|
|
15 |
}
|
16 |
}
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
const ImageGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
|
19 |
const exampleItems = examples[selectedModel][selectedAttack]
|
20 |
|
|
|
38 |
const ZOOM_BOX_SIZE = 300
|
39 |
const [zoomLevel, setZoomLevel] = React.useState(2) // 2x default zoom
|
40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
// Calculate the minimum width and height across all variants for the selected image
|
42 |
const variantImages = variantKeys.map((v) => variants[v]?.image_url).filter(Boolean)
|
43 |
const [naturalDims, setNaturalDims] = React.useState<{ width: number; height: number } | null>(
|
|
|
126 |
</div>
|
127 |
{selectedImage && selectedVariant && variants[selectedVariant] && (
|
128 |
<>
|
129 |
+
<ExampleVariantSelector
|
130 |
variantKeys={variantKeys}
|
131 |
selectedVariant={selectedVariant}
|
132 |
setSelectedVariant={setSelectedVariant}
|
133 |
/>
|
134 |
+
<ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
|
135 |
+
|
136 |
<ExampleDetailsSection>
|
137 |
+
{zoomSlider}
|
138 |
<div
|
139 |
style={{
|
140 |
width: '100%',
|
frontend/src/components/VideoGallery.tsx
CHANGED
@@ -1,5 +1,9 @@
|
|
1 |
import React from 'react'
|
2 |
import type { ExamplesData } from './Examples'
|
|
|
|
|
|
|
|
|
3 |
|
4 |
interface GalleryProps {
|
5 |
selectedModel: string
|
@@ -13,14 +17,71 @@ interface GalleryProps {
|
|
13 |
|
14 |
const VideoGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
|
15 |
const exampleItems = examples[selectedModel][selectedAttack]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
return (
|
17 |
-
<div className="
|
18 |
-
|
19 |
-
<div
|
20 |
-
<
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
</div>
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
</div>
|
25 |
)
|
26 |
}
|
|
|
1 |
import React from 'react'
|
2 |
import type { ExamplesData } from './Examples'
|
3 |
+
import { groupByNameAndVariant } from './galleryUtils'
|
4 |
+
import ExampleMetadata from './ExampleMetadata'
|
5 |
+
import ExampleDetailsSection from './ExampleDetailsSection'
|
6 |
+
import ExampleVariantSelector from './ExampleVariantSelector'
|
7 |
|
8 |
interface GalleryProps {
|
9 |
selectedModel: string
|
|
|
17 |
|
18 |
const VideoGallery: React.FC<GalleryProps> = ({ selectedModel, selectedAttack, examples }) => {
|
19 |
const exampleItems = examples[selectedModel][selectedAttack]
|
20 |
+
const grouped = groupByNameAndVariant(exampleItems)
|
21 |
+
const videoNames = Object.keys(grouped)
|
22 |
+
const [selectedVideo, setSelectedVideo] = React.useState(videoNames[0] || '')
|
23 |
+
const variants = grouped[selectedVideo] || {}
|
24 |
+
const variantKeys = Object.keys(variants)
|
25 |
+
const [selectedVariant, setSelectedVariant] = React.useState(variantKeys[0] || '')
|
26 |
+
|
27 |
+
React.useEffect(() => {
|
28 |
+
setSelectedVariant(variantKeys[0] || '')
|
29 |
+
}, [selectedVideo])
|
30 |
+
|
31 |
+
if (!videoNames.length) {
|
32 |
+
return (
|
33 |
+
<div className="w-full mt-12 flex items-center justify-center">
|
34 |
+
<div className="text-gray-500">
|
35 |
+
No video examples available. Please select another model and attack.
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
)
|
39 |
+
}
|
40 |
+
|
41 |
return (
|
42 |
+
<div className="w-full overflow-auto" style={{ minHeight: '100vh' }}>
|
43 |
+
<div className="example-display">
|
44 |
+
<div className="mb-4">
|
45 |
+
<fieldset className="fieldset">
|
46 |
+
<legend className="fieldset-legend">Video Example</legend>
|
47 |
+
<select
|
48 |
+
className="select select-bordered"
|
49 |
+
value={selectedVideo || ''}
|
50 |
+
onChange={(e) => {
|
51 |
+
setSelectedVideo(e.target.value || '')
|
52 |
+
}}
|
53 |
+
>
|
54 |
+
{videoNames.map((name) => (
|
55 |
+
<option key={name} value={name}>
|
56 |
+
{name}
|
57 |
+
</option>
|
58 |
+
))}
|
59 |
+
</select>
|
60 |
+
</fieldset>
|
61 |
</div>
|
62 |
+
{selectedVideo && selectedVariant && variants[selectedVariant] && (
|
63 |
+
<>
|
64 |
+
<ExampleVariantSelector
|
65 |
+
variantKeys={variantKeys}
|
66 |
+
selectedVariant={selectedVariant}
|
67 |
+
setSelectedVariant={setSelectedVariant}
|
68 |
+
/>
|
69 |
+
<ExampleMetadata metadata={variants[selectedVariant].metadata || {}} />
|
70 |
+
<ExampleDetailsSection>
|
71 |
+
<div className="flex flex-col items-center gap-4">
|
72 |
+
{variants[selectedVariant].video_url && (
|
73 |
+
<video
|
74 |
+
controls
|
75 |
+
src={variants[selectedVariant].video_url}
|
76 |
+
className="example-video"
|
77 |
+
style={{ maxWidth: 400, maxHeight: 300 }}
|
78 |
+
/>
|
79 |
+
)}
|
80 |
+
</div>
|
81 |
+
</ExampleDetailsSection>
|
82 |
+
</>
|
83 |
+
)}
|
84 |
+
</div>
|
85 |
</div>
|
86 |
)
|
87 |
}
|
frontend/src/components/galleryUtils.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1 |
// Common parsing and grouping utilities for galleries
|
2 |
|
|
|
|
|
3 |
export const VARIANT_PREFIX_MAP: { [variant: string]: string } = {
|
4 |
attacked_wmd: 'attacked_wmd_',
|
5 |
attacked: 'attacked_',
|
@@ -13,24 +15,24 @@ export function getVariant(name: string): string {
|
|
13 |
)
|
14 |
}
|
15 |
|
16 |
-
export function
|
17 |
-
if (variant
|
18 |
return name.replace(VARIANT_PREFIX_MAP[variant], '')
|
19 |
}
|
20 |
return name
|
21 |
}
|
22 |
|
23 |
-
export function groupByNameAndVariant
|
24 |
-
|
25 |
-
): {
|
26 |
-
[name: string]: { [variant: string]: T }
|
27 |
} {
|
28 |
-
const grouped: { [name: string]: { [variant: string]:
|
29 |
-
|
30 |
-
const variant = getVariant(
|
31 |
-
const imageName =
|
32 |
-
if (!grouped[imageName])
|
33 |
-
|
|
|
|
|
34 |
})
|
35 |
return grouped
|
36 |
}
|
|
|
1 |
// Common parsing and grouping utilities for galleries
|
2 |
|
3 |
+
import { ExamplesData } from './Examples'
|
4 |
+
|
5 |
export const VARIANT_PREFIX_MAP: { [variant: string]: string } = {
|
6 |
attacked_wmd: 'attacked_wmd_',
|
7 |
attacked: 'attacked_',
|
|
|
15 |
)
|
16 |
}
|
17 |
|
18 |
+
export function getExampleName(name: string, variant: string): string {
|
19 |
+
if (variant in VARIANT_PREFIX_MAP) {
|
20 |
return name.replace(VARIANT_PREFIX_MAP[variant], '')
|
21 |
}
|
22 |
return name
|
23 |
}
|
24 |
|
25 |
+
export function groupByNameAndVariant(examples: ExamplesData[]): {
|
26 |
+
[name: string]: { [variant: string]: ExamplesData }
|
|
|
|
|
27 |
} {
|
28 |
+
const grouped: { [name: string]: { [variant: string]: ExamplesData } } = {}
|
29 |
+
examples.forEach((example) => {
|
30 |
+
const variant = getVariant(example.name)
|
31 |
+
const imageName = getExampleName(example.name, variant)
|
32 |
+
if (!grouped[imageName]) {
|
33 |
+
grouped[imageName] = {}
|
34 |
+
}
|
35 |
+
grouped[imageName][variant] = example
|
36 |
})
|
37 |
return grouped
|
38 |
}
|