|
import React, { useState, useEffect } from 'react'; |
|
import { |
|
Container, Box, Button, Typography, CircularProgress, |
|
Paper, Grid, Card, CardContent, LinearProgress, |
|
FormControl, IconButton, Alert, Snackbar, useMediaQuery |
|
} from '@mui/material'; |
|
import { createTheme, ThemeProvider, styled, alpha } from '@mui/material/styles'; |
|
import MicIcon from '@mui/icons-material/Mic'; |
|
import StopIcon from '@mui/icons-material/Stop'; |
|
import UploadFileIcon from '@mui/icons-material/UploadFile'; |
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload'; |
|
import AudiotrackIcon from '@mui/icons-material/Audiotrack'; |
|
import VolumeUpIcon from '@mui/icons-material/VolumeUp'; |
|
import SecurityIcon from '@mui/icons-material/Security'; |
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; |
|
import { motion } from 'framer-motion'; |
|
|
|
|
|
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000'; |
|
|
|
|
|
const theme = createTheme({ |
|
palette: { |
|
primary: { |
|
main: '#3a86ff', |
|
light: '#83b9ff', |
|
dark: '#0056cb', |
|
}, |
|
secondary: { |
|
main: '#ff006e', |
|
light: '#ff5b9e', |
|
dark: '#c50052', |
|
}, |
|
success: { |
|
main: '#38b000', |
|
light: '#70e000', |
|
dark: '#008000', |
|
contrastText: '#ffffff', |
|
}, |
|
error: { |
|
main: '#d00000', |
|
light: '#ff5c4d', |
|
dark: '#9d0208', |
|
contrastText: '#ffffff', |
|
}, |
|
background: { |
|
default: '#f8f9fa', |
|
paper: '#ffffff', |
|
}, |
|
}, |
|
typography: { |
|
fontFamily: "'Poppins', 'Roboto', 'Helvetica', 'Arial', sans-serif", |
|
h3: { |
|
fontWeight: 700, |
|
letterSpacing: '-0.5px', |
|
}, |
|
h6: { |
|
fontWeight: 600, |
|
}, |
|
subtitle1: { |
|
fontWeight: 500, |
|
} |
|
}, |
|
shape: { |
|
borderRadius: 12, |
|
}, |
|
components: { |
|
MuiButton: { |
|
styleOverrides: { |
|
root: { |
|
textTransform: 'none', |
|
borderRadius: 8, |
|
padding: '10px 16px', |
|
boxShadow: 'none', |
|
fontWeight: 600, |
|
}, |
|
containedPrimary: { |
|
'&:hover': { |
|
boxShadow: '0 6px 20px rgba(58, 134, 255, 0.3)', |
|
}, |
|
}, |
|
}, |
|
}, |
|
MuiPaper: { |
|
styleOverrides: { |
|
root: { |
|
boxShadow: '0 8px 40px rgba(0, 0, 0, 0.08)', |
|
}, |
|
}, |
|
}, |
|
MuiCard: { |
|
styleOverrides: { |
|
root: { |
|
overflow: 'visible', |
|
}, |
|
}, |
|
}, |
|
}, |
|
}); |
|
|
|
|
|
const VisuallyHiddenInput = styled('input')({ |
|
clip: 'rect(0 0 0 0)', |
|
clipPath: 'inset(50%)', |
|
height: 1, |
|
overflow: 'hidden', |
|
position: 'absolute', |
|
bottom: 0, |
|
left: 0, |
|
whiteSpace: 'nowrap', |
|
width: 1, |
|
}); |
|
|
|
const StyledCard = styled(Card)(({ theme }) => ({ |
|
height: '100%', |
|
display: 'flex', |
|
flexDirection: 'column', |
|
transition: 'transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out', |
|
'&:hover': { |
|
transform: 'translateY(-5px)', |
|
boxShadow: '0 12px 50px rgba(0, 0, 0, 0.1)', |
|
}, |
|
})); |
|
|
|
const ResultCard = styled(Card)(({ theme, prediction }) => ({ |
|
backgroundColor: prediction === 'Real' |
|
? alpha(theme.palette.success.light, 0.3) |
|
: prediction === 'Deepfake' |
|
? alpha(theme.palette.error.light, 0.3) |
|
: theme.palette.grey[100], |
|
borderLeft: `8px solid ${ |
|
prediction === 'Real' |
|
? theme.palette.success.main |
|
: prediction === 'Deepfake' |
|
? theme.palette.error.main |
|
: theme.palette.grey[300] |
|
}`, |
|
backdropFilter: 'blur(10px)', |
|
transition: 'all 0.3s ease', |
|
})); |
|
|
|
const GradientHeader = styled(Box)(({ theme }) => ({ |
|
background: `linear-gradient(135deg, ${theme.palette.primary.dark} 0%, ${theme.palette.primary.main} 100%)`, |
|
color: '#ffffff', |
|
padding: theme.spacing(6, 2, 8), |
|
borderRadius: '0 0 24px 24px', |
|
marginBottom: -theme.spacing(6), |
|
})); |
|
|
|
const GlassCard = styled(Card)(({ theme }) => ({ |
|
backgroundColor: alpha(theme.palette.background.paper, 0.8), |
|
backdropFilter: 'blur(10px)', |
|
border: `1px solid ${alpha('#fff', 0.2)}`, |
|
})); |
|
|
|
const RecordButton = styled(Button)(({ theme, isrecording }) => ({ |
|
borderRadius: '50%', |
|
minWidth: '64px', |
|
width: '64px', |
|
height: '64px', |
|
padding: 0, |
|
boxShadow: isrecording === 'true' |
|
? `0 0 0 4px ${alpha(theme.palette.error.main, 0.3)}, 0 0 0 8px ${alpha(theme.palette.error.main, 0.15)}` |
|
: `0 0 0 4px ${alpha(theme.palette.primary.main, 0.3)}, 0 0 0 8px ${alpha(theme.palette.primary.main, 0.15)}`, |
|
animation: isrecording === 'true' ? 'pulse 1.5s infinite' : 'none', |
|
'@keyframes pulse': { |
|
'0%': { |
|
boxShadow: `0 0 0 0 ${alpha(theme.palette.error.main, 0.7)}` |
|
}, |
|
'70%': { |
|
boxShadow: `0 0 0 15px ${alpha(theme.palette.error.main, 0)}` |
|
}, |
|
'100%': { |
|
boxShadow: `0 0 0 0 ${alpha(theme.palette.error.main, 0)}` |
|
} |
|
} |
|
})); |
|
|
|
const AudioWaveAnimation = styled(Box)(({ theme, isplaying }) => ({ |
|
display: 'flex', |
|
alignItems: 'center', |
|
justifyContent: 'center', |
|
gap: '3px', |
|
height: '40px', |
|
opacity: isplaying === 'true' ? 1 : 0.3, |
|
transition: 'opacity 0.3s ease', |
|
'& .bar': { |
|
width: '3px', |
|
backgroundColor: theme.palette.primary.main, |
|
borderRadius: '3px', |
|
animation: isplaying === 'true' ? 'soundwave 1s infinite' : 'none', |
|
}, |
|
'@keyframes soundwave': { |
|
'0%': { height: '10%' }, |
|
'50%': { height: '100%' }, |
|
'100%': { height: '10%' } |
|
} |
|
})); |
|
|
|
function App() { |
|
const [file, setFile] = useState(null); |
|
const [audioUrl, setAudioUrl] = useState(null); |
|
const [isRecording, setIsRecording] = useState(false); |
|
const [isPlaying, setIsPlaying] = useState(false); |
|
const [recorder, setRecorder] = useState(null); |
|
const [isLoading, setIsLoading] = useState(false); |
|
const [result, setResult] = useState(null); |
|
const [error, setError] = useState(null); |
|
const [modelInfo, setModelInfo] = useState(null); |
|
const [openSnackbar, setOpenSnackbar] = useState(false); |
|
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); |
|
|
|
|
|
const audioBars = Array.from({ length: 10 }, (_, i) => { |
|
const randomHeight = Math.floor(Math.random() * 100) + 1; |
|
const randomDelay = Math.random(); |
|
return ( |
|
<Box |
|
key={i} |
|
className="bar" |
|
sx={{ |
|
height: `${randomHeight}%`, |
|
animationDelay: `${randomDelay}s` |
|
}} |
|
/> |
|
); |
|
}); |
|
|
|
|
|
const audioRef = React.useRef(null); |
|
|
|
const handlePlayPause = () => { |
|
if (audioRef.current) { |
|
if (isPlaying) { |
|
audioRef.current.pause(); |
|
} else { |
|
audioRef.current.play(); |
|
} |
|
setIsPlaying(!isPlaying); |
|
} |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
fetch(`${API_URL}/model-info/`) |
|
.then(response => response.json()) |
|
.then(data => setModelInfo(data)) |
|
.catch(err => console.error("Error fetching model info:", err)); |
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
const audioElement = audioRef.current; |
|
if (audioElement) { |
|
const handleEnded = () => setIsPlaying(false); |
|
audioElement.addEventListener('ended', handleEnded); |
|
return () => { |
|
audioElement.removeEventListener('ended', handleEnded); |
|
}; |
|
} |
|
}, [audioUrl]); |
|
|
|
|
|
const handleFileChange = (event) => { |
|
const selectedFile = event.target.files[0]; |
|
if (selectedFile) { |
|
setFile(selectedFile); |
|
setAudioUrl(URL.createObjectURL(selectedFile)); |
|
setIsPlaying(false); |
|
setResult(null); |
|
} |
|
}; |
|
|
|
|
|
const startRecording = async () => { |
|
try { |
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
const mediaRecorder = new MediaRecorder(stream); |
|
const audioChunks = []; |
|
|
|
mediaRecorder.addEventListener("dataavailable", event => { |
|
audioChunks.push(event.data); |
|
}); |
|
|
|
mediaRecorder.addEventListener("stop", () => { |
|
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); |
|
const audioFile = new File([audioBlob], "recorded-audio.wav", { type: 'audio/wav' }); |
|
setFile(audioFile); |
|
setAudioUrl(URL.createObjectURL(audioBlob)); |
|
setIsPlaying(false); |
|
setResult(null); |
|
}); |
|
|
|
mediaRecorder.start(); |
|
setIsRecording(true); |
|
setRecorder(mediaRecorder); |
|
} catch (err) { |
|
setError("Could not access microphone. Please check permissions."); |
|
setOpenSnackbar(true); |
|
console.error("Error accessing microphone:", err); |
|
} |
|
}; |
|
|
|
|
|
const stopRecording = () => { |
|
if (recorder && recorder.state !== "inactive") { |
|
recorder.stop(); |
|
setIsRecording(false); |
|
} |
|
}; |
|
|
|
|
|
const handleSubmit = async () => { |
|
if (!file) { |
|
setError("Please upload or record an audio file first."); |
|
setOpenSnackbar(true); |
|
return; |
|
} |
|
|
|
setIsLoading(true); |
|
setError(null); |
|
|
|
const formData = new FormData(); |
|
formData.append('file', file); |
|
|
|
try { |
|
const response = await fetch(`${API_URL}/detect/`, { |
|
method: 'POST', |
|
body: formData, |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`Server responded with status: ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
setResult(data); |
|
} catch (err) { |
|
setError(`Error analyzing audio: ${err.message}`); |
|
setOpenSnackbar(true); |
|
console.error("Error analyzing audio:", err); |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
|
|
const handleReset = () => { |
|
setFile(null); |
|
setAudioUrl(null); |
|
setResult(null); |
|
setError(null); |
|
setIsPlaying(false); |
|
}; |
|
|
|
|
|
const getChartData = () => { |
|
if (!result || !result.probabilities) return []; |
|
|
|
return Object.entries(result.probabilities).map(([name, value]) => ({ |
|
name, |
|
value: parseFloat((value * 100).toFixed(2)) |
|
})); |
|
}; |
|
|
|
|
|
const handleCloseSnackbar = (event, reason) => { |
|
if (reason === 'clickaway') { |
|
return; |
|
} |
|
setOpenSnackbar(false); |
|
}; |
|
|
|
|
|
const fadeIn = { |
|
hidden: { opacity: 0, y: 20 }, |
|
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } } |
|
}; |
|
|
|
return ( |
|
<ThemeProvider theme={theme}> |
|
<Box sx={{ |
|
backgroundColor: 'background.default', |
|
minHeight: '100vh', |
|
paddingBottom: 4 |
|
}}> |
|
<GradientHeader> |
|
<Container maxWidth="md"> |
|
<motion.div |
|
initial={{ opacity: 0, y: -20 }} |
|
animate={{ opacity: 1, y: 0 }} |
|
transition={{ duration: 0.7 }} |
|
> |
|
<Box sx={{ textAlign: 'center', position: 'relative' }}> |
|
<Typography variant="h3" component="h1" gutterBottom> |
|
Deepfake Voice Detector |
|
</Typography> |
|
|
|
<Typography variant="subtitle1" sx={{ maxWidth: '700px', mx: 'auto', opacity: 0.9 }}> |
|
Upload or record audio to instantly verify if it's authentic or AI-generated |
|
</Typography> |
|
</Box> |
|
</motion.div> |
|
</Container> |
|
</GradientHeader> |
|
|
|
<Container maxWidth="md"> |
|
<Box sx={{ mt: 2, mb: 2 }}> |
|
{modelInfo && ( |
|
<motion.div |
|
initial={{ opacity: 0 }} |
|
animate={{ opacity: 1 }} |
|
transition={{ delay: 0.3, duration: 0.5 }} |
|
> |
|
<Box sx={{ |
|
display: 'flex', |
|
alignItems: 'center', |
|
justifyContent: 'center', |
|
gap: 1 |
|
}}> |
|
<SecurityIcon fontSize="small" sx={{ color: 'text.secondary' }} /> |
|
<Typography variant="body2" color="text.secondary"> |
|
Using model: {modelInfo.model_id} | Accuracy: {(modelInfo.performance.accuracy * 100).toFixed(2)}% |
|
</Typography> |
|
</Box> |
|
</motion.div> |
|
)} |
|
</Box> |
|
|
|
<motion.div |
|
variants={fadeIn} |
|
initial="hidden" |
|
animate="visible" |
|
> |
|
<GlassCard elevation={0} sx={{ mb: 4, overflow: 'visible' }}> |
|
<CardContent sx={{ p: { xs: 2, sm: 3 } }}> |
|
<Grid container spacing={3}> |
|
<Grid item xs={12} md={6}> |
|
<StyledCard variant="outlined"> |
|
<CardContent sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
alignItems: 'center', |
|
height: '100%', |
|
p: { xs: 2, sm: 3 } |
|
}}> |
|
<Typography variant="h6" component="div" gutterBottom sx={{ mb: 3 }}> |
|
Upload Audio |
|
</Typography> |
|
|
|
<Button |
|
component="label" |
|
variant="contained" |
|
startIcon={<UploadFileIcon />} |
|
sx={{ |
|
width: '100%', |
|
py: 1.5, |
|
mb: 3, |
|
backgroundColor: theme.palette.primary.light, |
|
'&:hover': { |
|
backgroundColor: theme.palette.primary.main, |
|
} |
|
}} |
|
> |
|
Choose Audio File |
|
<VisuallyHiddenInput type="file" accept="audio/*" onChange={handleFileChange} /> |
|
</Button> |
|
|
|
<Typography variant="body2" color="text.secondary" gutterBottom> |
|
Or record audio directly |
|
</Typography> |
|
|
|
<Box sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
alignItems: 'center', |
|
mt: 2 |
|
}}> |
|
{!isRecording ? ( |
|
<RecordButton |
|
variant="contained" |
|
color="primary" |
|
onClick={startRecording} |
|
isrecording="false" |
|
> |
|
<MicIcon /> |
|
</RecordButton> |
|
) : ( |
|
<RecordButton |
|
variant="contained" |
|
color="error" |
|
onClick={stopRecording} |
|
isrecording="true" |
|
> |
|
<StopIcon /> |
|
</RecordButton> |
|
)} |
|
<Typography variant="body2" sx={{ mt: 1, color: isRecording ? 'error.main' : 'text.secondary' }}> |
|
{isRecording ? 'Recording...' : 'Tap to record'} |
|
</Typography> |
|
</Box> |
|
</CardContent> |
|
</StyledCard> |
|
</Grid> |
|
|
|
<Grid item xs={12} md={6}> |
|
<StyledCard variant="outlined"> |
|
<CardContent sx={{ |
|
display: 'flex', |
|
flexDirection: 'column', |
|
justifyContent: audioUrl ? 'space-between' : 'center', |
|
height: '100%', |
|
p: { xs: 2, sm: 3 } |
|
}}> |
|
{audioUrl ? ( |
|
<> |
|
<Box sx={{ textAlign: 'center' }}> |
|
<Typography variant="h6" component="div" gutterBottom> |
|
<AudiotrackIcon sx={{ verticalAlign: 'middle', mr: 1 }} /> |
|
Audio Preview |
|
</Typography> |
|
</Box> |
|
|
|
<Box sx={{ |
|
my: 2, |
|
display: 'flex', |
|
flexDirection: 'column', |
|
alignItems: 'center' |
|
}}> |
|
<audio |
|
ref={audioRef} |
|
src={audioUrl} |
|
style={{ display: 'none' }} |
|
onPlay={() => setIsPlaying(true)} |
|
onPause={() => setIsPlaying(false)} |
|
/> |
|
|
|
<AudioWaveAnimation isplaying={isPlaying ? 'true' : 'false'}> |
|
{audioBars} |
|
</AudioWaveAnimation> |
|
|
|
<Box sx={{ mt: 2 }}> |
|
<IconButton |
|
color="primary" |
|
onClick={handlePlayPause} |
|
size="large" |
|
sx={{ |
|
backgroundColor: alpha(theme.palette.primary.main, 0.1), |
|
'&:hover': { |
|
backgroundColor: alpha(theme.palette.primary.main, 0.2), |
|
} |
|
}} |
|
> |
|
<VolumeUpIcon /> |
|
</IconButton> |
|
</Box> |
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> |
|
{file ? file.name : "Audio loaded"} |
|
</Typography> |
|
</Box> |
|
</> |
|
) : ( |
|
<Box sx={{ |
|
p: 3, |
|
textAlign: 'center', |
|
display: 'flex', |
|
flexDirection: 'column', |
|
alignItems: 'center', |
|
justifyContent: 'center', |
|
height: '100%' |
|
}}> |
|
<CloudUploadIcon sx={{ |
|
fontSize: 60, |
|
color: alpha(theme.palette.text.secondary, 0.5), |
|
mb: 2 |
|
}} /> |
|
<Typography variant="body1" color="text.secondary"> |
|
No audio selected |
|
</Typography> |
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, opacity: 0.7 }}> |
|
Upload or record to analyze |
|
</Typography> |
|
</Box> |
|
)} |
|
</CardContent> |
|
</StyledCard> |
|
</Grid> |
|
</Grid> |
|
</CardContent> |
|
|
|
<Box sx={{ |
|
px: { xs: 2, sm: 3 }, |
|
pb: { xs: 2, sm: 3 }, |
|
textAlign: 'center' |
|
}}> |
|
<motion.div |
|
whileHover={{ scale: 1.03 }} |
|
whileTap={{ scale: 0.97 }} |
|
> |
|
<Button |
|
variant="contained" |
|
color="primary" |
|
size="large" |
|
disabled={!file || isLoading} |
|
onClick={handleSubmit} |
|
sx={{ |
|
px: 4, |
|
py: 1.2, |
|
fontSize: '1.1rem', |
|
fontWeight: 600, |
|
mx: 1, |
|
minWidth: { xs: '120px', sm: '160px' } |
|
}} |
|
> |
|
{isLoading ? <CircularProgress size={24} sx={{ mr: 1 }} /> : "Analyze Audio"} |
|
</Button> |
|
</motion.div> |
|
|
|
<Button |
|
variant="outlined" |
|
color="secondary" |
|
size="large" |
|
onClick={handleReset} |
|
sx={{ |
|
mx: 1, |
|
mt: { xs: 1, sm: 0 }, |
|
minWidth: { xs: '120px', sm: '120px' } |
|
}} |
|
disabled={isLoading || (!file && !audioUrl)} |
|
> |
|
Reset |
|
</Button> |
|
</Box> |
|
</GlassCard> |
|
</motion.div> |
|
|
|
{isLoading && ( |
|
<motion.div |
|
initial={{ opacity: 0 }} |
|
animate={{ opacity: 1 }} |
|
transition={{ duration: 0.3 }} |
|
> |
|
<Box sx={{ width: '100%', my: 4 }}> |
|
<Typography variant="body2" color="text.secondary" gutterBottom align="center"> |
|
Analyzing audio... |
|
</Typography> |
|
<LinearProgress |
|
sx={{ |
|
height: 8, |
|
borderRadius: 4, |
|
backgroundColor: alpha(theme.palette.primary.main, 0.15) |
|
}} |
|
/> |
|
</Box> |
|
</motion.div> |
|
)} |
|
|
|
{result && ( |
|
<motion.div |
|
initial={{ opacity: 0, y: 30 }} |
|
animate={{ opacity: 1, y: 0 }} |
|
transition={{ duration: 0.5 }} |
|
> |
|
<Box sx={{ my: 4 }}> |
|
<ResultCard |
|
elevation={2} |
|
prediction={result.prediction} |
|
sx={{ mb: 3 }} |
|
> |
|
<CardContent sx={{ p: { xs: 2, sm: 3 } }}> |
|
<Box sx={{ |
|
display: 'flex', |
|
flexDirection: { xs: 'column', sm: 'row' }, |
|
alignItems: { xs: 'flex-start', sm: 'center' }, |
|
justifyContent: 'space-between' |
|
}}> |
|
<Box> |
|
<Typography |
|
variant="h5" |
|
component="div" |
|
gutterBottom |
|
sx={{ |
|
fontWeight: 700, |
|
color: result.prediction === 'Real' |
|
? 'success.dark' |
|
: 'error.dark' |
|
}} |
|
> |
|
{result.prediction === 'Real' ? '✓ Authentic Voice' : '⚠ Deepfake Detected'} |
|
</Typography> |
|
<Typography variant="body1" sx={{ fontWeight: 500 }}> |
|
Confidence: {(result.confidence * 100).toFixed(2)}% |
|
</Typography> |
|
</Box> |
|
|
|
<Box |
|
sx={{ |
|
mt: { xs: 2, sm: 0 }, |
|
display: 'flex', |
|
alignItems: 'center', |
|
px: 2, |
|
py: 1, |
|
backgroundColor: alpha( |
|
result.prediction === 'Real' |
|
? theme.palette.success.main |
|
: theme.palette.error.main, |
|
0.1 |
|
), |
|
borderRadius: 2 |
|
}} |
|
> |
|
<Typography variant="body2" sx={{ fontWeight: 600, color: result.prediction === 'Real' ? 'success.dark' : 'error.dark' }}> |
|
{result.prediction === 'Real' ? 'Human Voice' : 'AI-Generated'} |
|
</Typography> |
|
</Box> |
|
</Box> |
|
</CardContent> |
|
</ResultCard> |
|
|
|
<GlassCard elevation={2} sx={{ p: { xs: 2, sm: 3 } }}> |
|
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}> |
|
Probability Distribution |
|
</Typography> |
|
<Box sx={{ height: isMobile ? 250 : 300, width: '100%', mt: 2 }}> |
|
<ResponsiveContainer width="100%" height="100%"> |
|
<BarChart |
|
data={getChartData()} |
|
margin={{ |
|
top: 30, |
|
right: 30, |
|
left: 20, |
|
bottom: 10, |
|
}} |
|
> |
|
<CartesianGrid strokeDasharray="3 3" stroke={alpha('#000', 0.1)} /> |
|
<XAxis |
|
dataKey="name" |
|
tick={{ fill: theme.palette.text.secondary }} |
|
axisLine={{ stroke: alpha('#000', 0.15) }} |
|
/> |
|
<YAxis |
|
label={{ |
|
value: 'Probability (%)', |
|
angle: -90, |
|
position: 'insideLeft', |
|
style: { fill: theme.palette.text.secondary } |
|
}} |
|
tick={{ fill: theme.palette.text.secondary }} |
|
axisLine={{ stroke: alpha('#000', 0.15) }} |
|
/> |
|
<Tooltip |
|
formatter={(value) => [`${value}%`, 'Probability']} |
|
contentStyle={{ |
|
borderRadius: 8, |
|
border: 'none', |
|
boxShadow: '0 4px 20px rgba(0,0,0,0.1)', |
|
backgroundColor: alpha('#fff', 0.95) |
|
}} |
|
/> |
|
<Bar |
|
dataKey="value" |
|
fill={(entry) => entry.name === 'Real' ? theme.palette.success.main : theme.palette.error.main} |
|
radius={[8, 8, 0, 0]} |
|
label={{ |
|
position: 'top', |
|
formatter: (value) => `${value}%`, |
|
fill: theme.palette.text.secondary, |
|
fontSize: 12, |
|
fontWeight: 600 |
|
}} |
|
/> |
|
</BarChart> |
|
</ResponsiveContainer> |
|
</Box> |
|
</GlassCard> |
|
|
|
<Box sx={{ mt: 3, textAlign: 'center' }}> |
|
<Typography variant="body2" color="text.secondary"> |
|
Note: This model claims {modelInfo ? (modelInfo.performance.accuracy * 100).toFixed(2) : ''}% accuracy, but results may vary depending on audio quality. |
|
</Typography> |
|
</Box> |
|
</Box> |
|
</motion.div> |
|
)} |
|
</Container> |
|
</Box> |
|
|
|
<Snackbar |
|
open={openSnackbar} |
|
autoHideDuration={6000} |
|
onClose={handleCloseSnackbar} |
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} |
|
> |
|
<Alert |
|
onClose={handleCloseSnackbar} |
|
severity="error" |
|
sx={{ |
|
width: '100%', |
|
borderRadius: 2, |
|
boxShadow: '0 4px 20px rgba(0,0,0,0.15)' |
|
}} |
|
> |
|
{error} |
|
</Alert> |
|
</Snackbar> |
|
</ThemeProvider> |
|
); |
|
} |
|
|
|
export default App; |