Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 |
}
|