Commit
·
d8240b4
1
Parent(s):
e414d7b
Deploy deepfake voice detection app
Browse files- .gitignore +26 -0
- App.js +812 -0
- Dockerfile +25 -0
- api.py +114 -0
- app.py +226 -0
- batch_processor.py +106 -0
- docker-compose.yml +30 -0
- requirements.txt +11 -0
- setup.py +22 -0
.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*.pyo
|
| 4 |
+
*.pyd
|
| 5 |
+
*.swp
|
| 6 |
+
*.log
|
| 7 |
+
|
| 8 |
+
myenv/
|
| 9 |
+
env/
|
| 10 |
+
venv/
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
.vscode/
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
.gradio/
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
.DS_Store
|
| 20 |
+
Thumbs.db
|
| 21 |
+
|
| 22 |
+
.env
|
| 23 |
+
|
| 24 |
+
dist/
|
| 25 |
+
build/
|
| 26 |
+
*.spec
|
App.js
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Container, Box, Button, Typography, CircularProgress,
|
| 4 |
+
Paper, Grid, Card, CardContent, LinearProgress,
|
| 5 |
+
FormControl, IconButton, Alert, Snackbar, useMediaQuery
|
| 6 |
+
} from '@mui/material';
|
| 7 |
+
import { createTheme, ThemeProvider, styled, alpha } from '@mui/material/styles';
|
| 8 |
+
import MicIcon from '@mui/icons-material/Mic';
|
| 9 |
+
import StopIcon from '@mui/icons-material/Stop';
|
| 10 |
+
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
| 11 |
+
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
| 12 |
+
import AudiotrackIcon from '@mui/icons-material/Audiotrack';
|
| 13 |
+
import VolumeUpIcon from '@mui/icons-material/VolumeUp';
|
| 14 |
+
import SecurityIcon from '@mui/icons-material/Security';
|
| 15 |
+
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
| 16 |
+
import { motion } from 'framer-motion';
|
| 17 |
+
|
| 18 |
+
// API endpoint
|
| 19 |
+
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
| 20 |
+
|
| 21 |
+
// Custom theme
|
| 22 |
+
const theme = createTheme({
|
| 23 |
+
palette: {
|
| 24 |
+
primary: {
|
| 25 |
+
main: '#3a86ff',
|
| 26 |
+
light: '#83b9ff',
|
| 27 |
+
dark: '#0056cb',
|
| 28 |
+
},
|
| 29 |
+
secondary: {
|
| 30 |
+
main: '#ff006e',
|
| 31 |
+
light: '#ff5b9e',
|
| 32 |
+
dark: '#c50052',
|
| 33 |
+
},
|
| 34 |
+
success: {
|
| 35 |
+
main: '#38b000',
|
| 36 |
+
light: '#70e000',
|
| 37 |
+
dark: '#008000',
|
| 38 |
+
contrastText: '#ffffff',
|
| 39 |
+
},
|
| 40 |
+
error: {
|
| 41 |
+
main: '#d00000',
|
| 42 |
+
light: '#ff5c4d',
|
| 43 |
+
dark: '#9d0208',
|
| 44 |
+
contrastText: '#ffffff',
|
| 45 |
+
},
|
| 46 |
+
background: {
|
| 47 |
+
default: '#f8f9fa',
|
| 48 |
+
paper: '#ffffff',
|
| 49 |
+
},
|
| 50 |
+
},
|
| 51 |
+
typography: {
|
| 52 |
+
fontFamily: "'Poppins', 'Roboto', 'Helvetica', 'Arial', sans-serif",
|
| 53 |
+
h3: {
|
| 54 |
+
fontWeight: 700,
|
| 55 |
+
letterSpacing: '-0.5px',
|
| 56 |
+
},
|
| 57 |
+
h6: {
|
| 58 |
+
fontWeight: 600,
|
| 59 |
+
},
|
| 60 |
+
subtitle1: {
|
| 61 |
+
fontWeight: 500,
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
shape: {
|
| 65 |
+
borderRadius: 12,
|
| 66 |
+
},
|
| 67 |
+
components: {
|
| 68 |
+
MuiButton: {
|
| 69 |
+
styleOverrides: {
|
| 70 |
+
root: {
|
| 71 |
+
textTransform: 'none',
|
| 72 |
+
borderRadius: 8,
|
| 73 |
+
padding: '10px 16px',
|
| 74 |
+
boxShadow: 'none',
|
| 75 |
+
fontWeight: 600,
|
| 76 |
+
},
|
| 77 |
+
containedPrimary: {
|
| 78 |
+
'&:hover': {
|
| 79 |
+
boxShadow: '0 6px 20px rgba(58, 134, 255, 0.3)',
|
| 80 |
+
},
|
| 81 |
+
},
|
| 82 |
+
},
|
| 83 |
+
},
|
| 84 |
+
MuiPaper: {
|
| 85 |
+
styleOverrides: {
|
| 86 |
+
root: {
|
| 87 |
+
boxShadow: '0 8px 40px rgba(0, 0, 0, 0.08)',
|
| 88 |
+
},
|
| 89 |
+
},
|
| 90 |
+
},
|
| 91 |
+
MuiCard: {
|
| 92 |
+
styleOverrides: {
|
| 93 |
+
root: {
|
| 94 |
+
overflow: 'visible',
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
+
// Styled components
|
| 102 |
+
const VisuallyHiddenInput = styled('input')({
|
| 103 |
+
clip: 'rect(0 0 0 0)',
|
| 104 |
+
clipPath: 'inset(50%)',
|
| 105 |
+
height: 1,
|
| 106 |
+
overflow: 'hidden',
|
| 107 |
+
position: 'absolute',
|
| 108 |
+
bottom: 0,
|
| 109 |
+
left: 0,
|
| 110 |
+
whiteSpace: 'nowrap',
|
| 111 |
+
width: 1,
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
const StyledCard = styled(Card)(({ theme }) => ({
|
| 115 |
+
height: '100%',
|
| 116 |
+
display: 'flex',
|
| 117 |
+
flexDirection: 'column',
|
| 118 |
+
transition: 'transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out',
|
| 119 |
+
'&:hover': {
|
| 120 |
+
transform: 'translateY(-5px)',
|
| 121 |
+
boxShadow: '0 12px 50px rgba(0, 0, 0, 0.1)',
|
| 122 |
+
},
|
| 123 |
+
}));
|
| 124 |
+
|
| 125 |
+
const ResultCard = styled(Card)(({ theme, prediction }) => ({
|
| 126 |
+
backgroundColor: prediction === 'Real'
|
| 127 |
+
? alpha(theme.palette.success.light, 0.3)
|
| 128 |
+
: prediction === 'Deepfake'
|
| 129 |
+
? alpha(theme.palette.error.light, 0.3)
|
| 130 |
+
: theme.palette.grey[100],
|
| 131 |
+
borderLeft: `8px solid ${
|
| 132 |
+
prediction === 'Real'
|
| 133 |
+
? theme.palette.success.main
|
| 134 |
+
: prediction === 'Deepfake'
|
| 135 |
+
? theme.palette.error.main
|
| 136 |
+
: theme.palette.grey[300]
|
| 137 |
+
}`,
|
| 138 |
+
backdropFilter: 'blur(10px)',
|
| 139 |
+
transition: 'all 0.3s ease',
|
| 140 |
+
}));
|
| 141 |
+
|
| 142 |
+
const GradientHeader = styled(Box)(({ theme }) => ({
|
| 143 |
+
background: `linear-gradient(135deg, ${theme.palette.primary.dark} 0%, ${theme.palette.primary.main} 100%)`,
|
| 144 |
+
color: '#ffffff',
|
| 145 |
+
padding: theme.spacing(6, 2, 8),
|
| 146 |
+
borderRadius: '0 0 24px 24px',
|
| 147 |
+
marginBottom: -theme.spacing(6),
|
| 148 |
+
}));
|
| 149 |
+
|
| 150 |
+
const GlassCard = styled(Card)(({ theme }) => ({
|
| 151 |
+
backgroundColor: alpha(theme.palette.background.paper, 0.8),
|
| 152 |
+
backdropFilter: 'blur(10px)',
|
| 153 |
+
border: `1px solid ${alpha('#fff', 0.2)}`,
|
| 154 |
+
}));
|
| 155 |
+
|
| 156 |
+
const RecordButton = styled(Button)(({ theme, isrecording }) => ({
|
| 157 |
+
borderRadius: '50%',
|
| 158 |
+
minWidth: '64px',
|
| 159 |
+
width: '64px',
|
| 160 |
+
height: '64px',
|
| 161 |
+
padding: 0,
|
| 162 |
+
boxShadow: isrecording === 'true'
|
| 163 |
+
? `0 0 0 4px ${alpha(theme.palette.error.main, 0.3)}, 0 0 0 8px ${alpha(theme.palette.error.main, 0.15)}`
|
| 164 |
+
: `0 0 0 4px ${alpha(theme.palette.primary.main, 0.3)}, 0 0 0 8px ${alpha(theme.palette.primary.main, 0.15)}`,
|
| 165 |
+
animation: isrecording === 'true' ? 'pulse 1.5s infinite' : 'none',
|
| 166 |
+
'@keyframes pulse': {
|
| 167 |
+
'0%': {
|
| 168 |
+
boxShadow: `0 0 0 0 ${alpha(theme.palette.error.main, 0.7)}`
|
| 169 |
+
},
|
| 170 |
+
'70%': {
|
| 171 |
+
boxShadow: `0 0 0 15px ${alpha(theme.palette.error.main, 0)}`
|
| 172 |
+
},
|
| 173 |
+
'100%': {
|
| 174 |
+
boxShadow: `0 0 0 0 ${alpha(theme.palette.error.main, 0)}`
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
}));
|
| 178 |
+
|
| 179 |
+
const AudioWaveAnimation = styled(Box)(({ theme, isplaying }) => ({
|
| 180 |
+
display: 'flex',
|
| 181 |
+
alignItems: 'center',
|
| 182 |
+
justifyContent: 'center',
|
| 183 |
+
gap: '3px',
|
| 184 |
+
height: '40px',
|
| 185 |
+
opacity: isplaying === 'true' ? 1 : 0.3,
|
| 186 |
+
transition: 'opacity 0.3s ease',
|
| 187 |
+
'& .bar': {
|
| 188 |
+
width: '3px',
|
| 189 |
+
backgroundColor: theme.palette.primary.main,
|
| 190 |
+
borderRadius: '3px',
|
| 191 |
+
animation: isplaying === 'true' ? 'soundwave 1s infinite' : 'none',
|
| 192 |
+
},
|
| 193 |
+
'@keyframes soundwave': {
|
| 194 |
+
'0%': { height: '10%' },
|
| 195 |
+
'50%': { height: '100%' },
|
| 196 |
+
'100%': { height: '10%' }
|
| 197 |
+
}
|
| 198 |
+
}));
|
| 199 |
+
|
| 200 |
+
function App() {
|
| 201 |
+
const [file, setFile] = useState(null);
|
| 202 |
+
const [audioUrl, setAudioUrl] = useState(null);
|
| 203 |
+
const [isRecording, setIsRecording] = useState(false);
|
| 204 |
+
const [isPlaying, setIsPlaying] = useState(false);
|
| 205 |
+
const [recorder, setRecorder] = useState(null);
|
| 206 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 207 |
+
const [result, setResult] = useState(null);
|
| 208 |
+
const [error, setError] = useState(null);
|
| 209 |
+
const [modelInfo, setModelInfo] = useState(null);
|
| 210 |
+
const [openSnackbar, setOpenSnackbar] = useState(false);
|
| 211 |
+
|
| 212 |
+
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
| 213 |
+
|
| 214 |
+
// Create audio wave bars for animation
|
| 215 |
+
const audioBars = Array.from({ length: 10 }, (_, i) => {
|
| 216 |
+
const randomHeight = Math.floor(Math.random() * 100) + 1;
|
| 217 |
+
const randomDelay = Math.random();
|
| 218 |
+
return (
|
| 219 |
+
<Box
|
| 220 |
+
key={i}
|
| 221 |
+
className="bar"
|
| 222 |
+
sx={{
|
| 223 |
+
height: `${randomHeight}%`,
|
| 224 |
+
animationDelay: `${randomDelay}s`
|
| 225 |
+
}}
|
| 226 |
+
/>
|
| 227 |
+
);
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
// Audio player logic
|
| 231 |
+
const audioRef = React.useRef(null);
|
| 232 |
+
|
| 233 |
+
const handlePlayPause = () => {
|
| 234 |
+
if (audioRef.current) {
|
| 235 |
+
if (isPlaying) {
|
| 236 |
+
audioRef.current.pause();
|
| 237 |
+
} else {
|
| 238 |
+
audioRef.current.play();
|
| 239 |
+
}
|
| 240 |
+
setIsPlaying(!isPlaying);
|
| 241 |
+
}
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
// Fetch model info on component mount
|
| 245 |
+
useEffect(() => {
|
| 246 |
+
fetch(`${API_URL}/model-info/`)
|
| 247 |
+
.then(response => response.json())
|
| 248 |
+
.then(data => setModelInfo(data))
|
| 249 |
+
.catch(err => console.error("Error fetching model info:", err));
|
| 250 |
+
}, []);
|
| 251 |
+
|
| 252 |
+
// Handle audio events
|
| 253 |
+
useEffect(() => {
|
| 254 |
+
const audioElement = audioRef.current;
|
| 255 |
+
if (audioElement) {
|
| 256 |
+
const handleEnded = () => setIsPlaying(false);
|
| 257 |
+
audioElement.addEventListener('ended', handleEnded);
|
| 258 |
+
return () => {
|
| 259 |
+
audioElement.removeEventListener('ended', handleEnded);
|
| 260 |
+
};
|
| 261 |
+
}
|
| 262 |
+
}, [audioUrl]);
|
| 263 |
+
|
| 264 |
+
// Handle file selection
|
| 265 |
+
const handleFileChange = (event) => {
|
| 266 |
+
const selectedFile = event.target.files[0];
|
| 267 |
+
if (selectedFile) {
|
| 268 |
+
setFile(selectedFile);
|
| 269 |
+
setAudioUrl(URL.createObjectURL(selectedFile));
|
| 270 |
+
setIsPlaying(false);
|
| 271 |
+
setResult(null); // Clear previous results
|
| 272 |
+
}
|
| 273 |
+
};
|
| 274 |
+
|
| 275 |
+
// Start audio recording
|
| 276 |
+
const startRecording = async () => {
|
| 277 |
+
try {
|
| 278 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 279 |
+
const mediaRecorder = new MediaRecorder(stream);
|
| 280 |
+
const audioChunks = [];
|
| 281 |
+
|
| 282 |
+
mediaRecorder.addEventListener("dataavailable", event => {
|
| 283 |
+
audioChunks.push(event.data);
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
mediaRecorder.addEventListener("stop", () => {
|
| 287 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
| 288 |
+
const audioFile = new File([audioBlob], "recorded-audio.wav", { type: 'audio/wav' });
|
| 289 |
+
setFile(audioFile);
|
| 290 |
+
setAudioUrl(URL.createObjectURL(audioBlob));
|
| 291 |
+
setIsPlaying(false);
|
| 292 |
+
setResult(null); // Clear previous results
|
| 293 |
+
});
|
| 294 |
+
|
| 295 |
+
mediaRecorder.start();
|
| 296 |
+
setIsRecording(true);
|
| 297 |
+
setRecorder(mediaRecorder);
|
| 298 |
+
} catch (err) {
|
| 299 |
+
setError("Could not access microphone. Please check permissions.");
|
| 300 |
+
setOpenSnackbar(true);
|
| 301 |
+
console.error("Error accessing microphone:", err);
|
| 302 |
+
}
|
| 303 |
+
};
|
| 304 |
+
|
| 305 |
+
// Stop audio recording
|
| 306 |
+
const stopRecording = () => {
|
| 307 |
+
if (recorder && recorder.state !== "inactive") {
|
| 308 |
+
recorder.stop();
|
| 309 |
+
setIsRecording(false);
|
| 310 |
+
}
|
| 311 |
+
};
|
| 312 |
+
|
| 313 |
+
// Submit audio for analysis
|
| 314 |
+
const handleSubmit = async () => {
|
| 315 |
+
if (!file) {
|
| 316 |
+
setError("Please upload or record an audio file first.");
|
| 317 |
+
setOpenSnackbar(true);
|
| 318 |
+
return;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
setIsLoading(true);
|
| 322 |
+
setError(null);
|
| 323 |
+
|
| 324 |
+
const formData = new FormData();
|
| 325 |
+
formData.append('file', file);
|
| 326 |
+
|
| 327 |
+
try {
|
| 328 |
+
const response = await fetch(`${API_URL}/detect/`, {
|
| 329 |
+
method: 'POST',
|
| 330 |
+
body: formData,
|
| 331 |
+
});
|
| 332 |
+
|
| 333 |
+
if (!response.ok) {
|
| 334 |
+
throw new Error(`Server responded with status: ${response.status}`);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
const data = await response.json();
|
| 338 |
+
setResult(data);
|
| 339 |
+
} catch (err) {
|
| 340 |
+
setError(`Error analyzing audio: ${err.message}`);
|
| 341 |
+
setOpenSnackbar(true);
|
| 342 |
+
console.error("Error analyzing audio:", err);
|
| 343 |
+
} finally {
|
| 344 |
+
setIsLoading(false);
|
| 345 |
+
}
|
| 346 |
+
};
|
| 347 |
+
|
| 348 |
+
// Reset everything
|
| 349 |
+
const handleReset = () => {
|
| 350 |
+
setFile(null);
|
| 351 |
+
setAudioUrl(null);
|
| 352 |
+
setResult(null);
|
| 353 |
+
setError(null);
|
| 354 |
+
setIsPlaying(false);
|
| 355 |
+
};
|
| 356 |
+
|
| 357 |
+
// Format chart data
|
| 358 |
+
const getChartData = () => {
|
| 359 |
+
if (!result || !result.probabilities) return [];
|
| 360 |
+
|
| 361 |
+
return Object.entries(result.probabilities).map(([name, value]) => ({
|
| 362 |
+
name,
|
| 363 |
+
value: parseFloat((value * 100).toFixed(2))
|
| 364 |
+
}));
|
| 365 |
+
};
|
| 366 |
+
|
| 367 |
+
// Handle snackbar close
|
| 368 |
+
const handleCloseSnackbar = (event, reason) => {
|
| 369 |
+
if (reason === 'clickaway') {
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
setOpenSnackbar(false);
|
| 373 |
+
};
|
| 374 |
+
|
| 375 |
+
// Animation variants
|
| 376 |
+
const fadeIn = {
|
| 377 |
+
hidden: { opacity: 0, y: 20 },
|
| 378 |
+
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } }
|
| 379 |
+
};
|
| 380 |
+
|
| 381 |
+
return (
|
| 382 |
+
<ThemeProvider theme={theme}>
|
| 383 |
+
<Box sx={{
|
| 384 |
+
backgroundColor: 'background.default',
|
| 385 |
+
minHeight: '100vh',
|
| 386 |
+
paddingBottom: 4
|
| 387 |
+
}}>
|
| 388 |
+
<GradientHeader>
|
| 389 |
+
<Container maxWidth="md">
|
| 390 |
+
<motion.div
|
| 391 |
+
initial={{ opacity: 0, y: -20 }}
|
| 392 |
+
animate={{ opacity: 1, y: 0 }}
|
| 393 |
+
transition={{ duration: 0.7 }}
|
| 394 |
+
>
|
| 395 |
+
<Box sx={{ textAlign: 'center', position: 'relative' }}>
|
| 396 |
+
<Typography variant="h3" component="h1" gutterBottom>
|
| 397 |
+
Deepfake Voice Detector
|
| 398 |
+
</Typography>
|
| 399 |
+
|
| 400 |
+
<Typography variant="subtitle1" sx={{ maxWidth: '700px', mx: 'auto', opacity: 0.9 }}>
|
| 401 |
+
Upload or record audio to instantly verify if it's authentic or AI-generated
|
| 402 |
+
</Typography>
|
| 403 |
+
</Box>
|
| 404 |
+
</motion.div>
|
| 405 |
+
</Container>
|
| 406 |
+
</GradientHeader>
|
| 407 |
+
|
| 408 |
+
<Container maxWidth="md">
|
| 409 |
+
<Box sx={{ mt: 2, mb: 2 }}>
|
| 410 |
+
{modelInfo && (
|
| 411 |
+
<motion.div
|
| 412 |
+
initial={{ opacity: 0 }}
|
| 413 |
+
animate={{ opacity: 1 }}
|
| 414 |
+
transition={{ delay: 0.3, duration: 0.5 }}
|
| 415 |
+
>
|
| 416 |
+
<Box sx={{
|
| 417 |
+
display: 'flex',
|
| 418 |
+
alignItems: 'center',
|
| 419 |
+
justifyContent: 'center',
|
| 420 |
+
gap: 1
|
| 421 |
+
}}>
|
| 422 |
+
<SecurityIcon fontSize="small" sx={{ color: 'text.secondary' }} />
|
| 423 |
+
<Typography variant="body2" color="text.secondary">
|
| 424 |
+
Using model: {modelInfo.model_id} | Accuracy: {(modelInfo.performance.accuracy * 100).toFixed(2)}%
|
| 425 |
+
</Typography>
|
| 426 |
+
</Box>
|
| 427 |
+
</motion.div>
|
| 428 |
+
)}
|
| 429 |
+
</Box>
|
| 430 |
+
|
| 431 |
+
<motion.div
|
| 432 |
+
variants={fadeIn}
|
| 433 |
+
initial="hidden"
|
| 434 |
+
animate="visible"
|
| 435 |
+
>
|
| 436 |
+
<GlassCard elevation={0} sx={{ mb: 4, overflow: 'visible' }}>
|
| 437 |
+
<CardContent sx={{ p: { xs: 2, sm: 3 } }}>
|
| 438 |
+
<Grid container spacing={3}>
|
| 439 |
+
<Grid item xs={12} md={6}>
|
| 440 |
+
<StyledCard variant="outlined">
|
| 441 |
+
<CardContent sx={{
|
| 442 |
+
display: 'flex',
|
| 443 |
+
flexDirection: 'column',
|
| 444 |
+
alignItems: 'center',
|
| 445 |
+
height: '100%',
|
| 446 |
+
p: { xs: 2, sm: 3 }
|
| 447 |
+
}}>
|
| 448 |
+
<Typography variant="h6" component="div" gutterBottom sx={{ mb: 3 }}>
|
| 449 |
+
Upload Audio
|
| 450 |
+
</Typography>
|
| 451 |
+
|
| 452 |
+
<Button
|
| 453 |
+
component="label"
|
| 454 |
+
variant="contained"
|
| 455 |
+
startIcon={<UploadFileIcon />}
|
| 456 |
+
sx={{
|
| 457 |
+
width: '100%',
|
| 458 |
+
py: 1.5,
|
| 459 |
+
mb: 3,
|
| 460 |
+
backgroundColor: theme.palette.primary.light,
|
| 461 |
+
'&:hover': {
|
| 462 |
+
backgroundColor: theme.palette.primary.main,
|
| 463 |
+
}
|
| 464 |
+
}}
|
| 465 |
+
>
|
| 466 |
+
Choose Audio File
|
| 467 |
+
<VisuallyHiddenInput type="file" accept="audio/*" onChange={handleFileChange} />
|
| 468 |
+
</Button>
|
| 469 |
+
|
| 470 |
+
<Typography variant="body2" color="text.secondary" gutterBottom>
|
| 471 |
+
Or record audio directly
|
| 472 |
+
</Typography>
|
| 473 |
+
|
| 474 |
+
<Box sx={{
|
| 475 |
+
display: 'flex',
|
| 476 |
+
flexDirection: 'column',
|
| 477 |
+
alignItems: 'center',
|
| 478 |
+
mt: 2
|
| 479 |
+
}}>
|
| 480 |
+
{!isRecording ? (
|
| 481 |
+
<RecordButton
|
| 482 |
+
variant="contained"
|
| 483 |
+
color="primary"
|
| 484 |
+
onClick={startRecording}
|
| 485 |
+
isrecording="false"
|
| 486 |
+
>
|
| 487 |
+
<MicIcon />
|
| 488 |
+
</RecordButton>
|
| 489 |
+
) : (
|
| 490 |
+
<RecordButton
|
| 491 |
+
variant="contained"
|
| 492 |
+
color="error"
|
| 493 |
+
onClick={stopRecording}
|
| 494 |
+
isrecording="true"
|
| 495 |
+
>
|
| 496 |
+
<StopIcon />
|
| 497 |
+
</RecordButton>
|
| 498 |
+
)}
|
| 499 |
+
<Typography variant="body2" sx={{ mt: 1, color: isRecording ? 'error.main' : 'text.secondary' }}>
|
| 500 |
+
{isRecording ? 'Recording...' : 'Tap to record'}
|
| 501 |
+
</Typography>
|
| 502 |
+
</Box>
|
| 503 |
+
</CardContent>
|
| 504 |
+
</StyledCard>
|
| 505 |
+
</Grid>
|
| 506 |
+
|
| 507 |
+
<Grid item xs={12} md={6}>
|
| 508 |
+
<StyledCard variant="outlined">
|
| 509 |
+
<CardContent sx={{
|
| 510 |
+
display: 'flex',
|
| 511 |
+
flexDirection: 'column',
|
| 512 |
+
justifyContent: audioUrl ? 'space-between' : 'center',
|
| 513 |
+
height: '100%',
|
| 514 |
+
p: { xs: 2, sm: 3 }
|
| 515 |
+
}}>
|
| 516 |
+
{audioUrl ? (
|
| 517 |
+
<>
|
| 518 |
+
<Box sx={{ textAlign: 'center' }}>
|
| 519 |
+
<Typography variant="h6" component="div" gutterBottom>
|
| 520 |
+
<AudiotrackIcon sx={{ verticalAlign: 'middle', mr: 1 }} />
|
| 521 |
+
Audio Preview
|
| 522 |
+
</Typography>
|
| 523 |
+
</Box>
|
| 524 |
+
|
| 525 |
+
<Box sx={{
|
| 526 |
+
my: 2,
|
| 527 |
+
display: 'flex',
|
| 528 |
+
flexDirection: 'column',
|
| 529 |
+
alignItems: 'center'
|
| 530 |
+
}}>
|
| 531 |
+
<audio
|
| 532 |
+
ref={audioRef}
|
| 533 |
+
src={audioUrl}
|
| 534 |
+
style={{ display: 'none' }}
|
| 535 |
+
onPlay={() => setIsPlaying(true)}
|
| 536 |
+
onPause={() => setIsPlaying(false)}
|
| 537 |
+
/>
|
| 538 |
+
|
| 539 |
+
<AudioWaveAnimation isplaying={isPlaying ? 'true' : 'false'}>
|
| 540 |
+
{audioBars}
|
| 541 |
+
</AudioWaveAnimation>
|
| 542 |
+
|
| 543 |
+
<Box sx={{ mt: 2 }}>
|
| 544 |
+
<IconButton
|
| 545 |
+
color="primary"
|
| 546 |
+
onClick={handlePlayPause}
|
| 547 |
+
size="large"
|
| 548 |
+
sx={{
|
| 549 |
+
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
| 550 |
+
'&:hover': {
|
| 551 |
+
backgroundColor: alpha(theme.palette.primary.main, 0.2),
|
| 552 |
+
}
|
| 553 |
+
}}
|
| 554 |
+
>
|
| 555 |
+
<VolumeUpIcon />
|
| 556 |
+
</IconButton>
|
| 557 |
+
</Box>
|
| 558 |
+
|
| 559 |
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
| 560 |
+
{file ? file.name : "Audio loaded"}
|
| 561 |
+
</Typography>
|
| 562 |
+
</Box>
|
| 563 |
+
</>
|
| 564 |
+
) : (
|
| 565 |
+
<Box sx={{
|
| 566 |
+
p: 3,
|
| 567 |
+
textAlign: 'center',
|
| 568 |
+
display: 'flex',
|
| 569 |
+
flexDirection: 'column',
|
| 570 |
+
alignItems: 'center',
|
| 571 |
+
justifyContent: 'center',
|
| 572 |
+
height: '100%'
|
| 573 |
+
}}>
|
| 574 |
+
<CloudUploadIcon sx={{
|
| 575 |
+
fontSize: 60,
|
| 576 |
+
color: alpha(theme.palette.text.secondary, 0.5),
|
| 577 |
+
mb: 2
|
| 578 |
+
}} />
|
| 579 |
+
<Typography variant="body1" color="text.secondary">
|
| 580 |
+
No audio selected
|
| 581 |
+
</Typography>
|
| 582 |
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, opacity: 0.7 }}>
|
| 583 |
+
Upload or record to analyze
|
| 584 |
+
</Typography>
|
| 585 |
+
</Box>
|
| 586 |
+
)}
|
| 587 |
+
</CardContent>
|
| 588 |
+
</StyledCard>
|
| 589 |
+
</Grid>
|
| 590 |
+
</Grid>
|
| 591 |
+
</CardContent>
|
| 592 |
+
|
| 593 |
+
<Box sx={{
|
| 594 |
+
px: { xs: 2, sm: 3 },
|
| 595 |
+
pb: { xs: 2, sm: 3 },
|
| 596 |
+
textAlign: 'center'
|
| 597 |
+
}}>
|
| 598 |
+
<motion.div
|
| 599 |
+
whileHover={{ scale: 1.03 }}
|
| 600 |
+
whileTap={{ scale: 0.97 }}
|
| 601 |
+
>
|
| 602 |
+
<Button
|
| 603 |
+
variant="contained"
|
| 604 |
+
color="primary"
|
| 605 |
+
size="large"
|
| 606 |
+
disabled={!file || isLoading}
|
| 607 |
+
onClick={handleSubmit}
|
| 608 |
+
sx={{
|
| 609 |
+
px: 4,
|
| 610 |
+
py: 1.2,
|
| 611 |
+
fontSize: '1.1rem',
|
| 612 |
+
fontWeight: 600,
|
| 613 |
+
mx: 1,
|
| 614 |
+
minWidth: { xs: '120px', sm: '160px' }
|
| 615 |
+
}}
|
| 616 |
+
>
|
| 617 |
+
{isLoading ? <CircularProgress size={24} sx={{ mr: 1 }} /> : "Analyze Audio"}
|
| 618 |
+
</Button>
|
| 619 |
+
</motion.div>
|
| 620 |
+
|
| 621 |
+
<Button
|
| 622 |
+
variant="outlined"
|
| 623 |
+
color="secondary"
|
| 624 |
+
size="large"
|
| 625 |
+
onClick={handleReset}
|
| 626 |
+
sx={{
|
| 627 |
+
mx: 1,
|
| 628 |
+
mt: { xs: 1, sm: 0 },
|
| 629 |
+
minWidth: { xs: '120px', sm: '120px' }
|
| 630 |
+
}}
|
| 631 |
+
disabled={isLoading || (!file && !audioUrl)}
|
| 632 |
+
>
|
| 633 |
+
Reset
|
| 634 |
+
</Button>
|
| 635 |
+
</Box>
|
| 636 |
+
</GlassCard>
|
| 637 |
+
</motion.div>
|
| 638 |
+
|
| 639 |
+
{isLoading && (
|
| 640 |
+
<motion.div
|
| 641 |
+
initial={{ opacity: 0 }}
|
| 642 |
+
animate={{ opacity: 1 }}
|
| 643 |
+
transition={{ duration: 0.3 }}
|
| 644 |
+
>
|
| 645 |
+
<Box sx={{ width: '100%', my: 4 }}>
|
| 646 |
+
<Typography variant="body2" color="text.secondary" gutterBottom align="center">
|
| 647 |
+
Analyzing audio...
|
| 648 |
+
</Typography>
|
| 649 |
+
<LinearProgress
|
| 650 |
+
sx={{
|
| 651 |
+
height: 8,
|
| 652 |
+
borderRadius: 4,
|
| 653 |
+
backgroundColor: alpha(theme.palette.primary.main, 0.15)
|
| 654 |
+
}}
|
| 655 |
+
/>
|
| 656 |
+
</Box>
|
| 657 |
+
</motion.div>
|
| 658 |
+
)}
|
| 659 |
+
|
| 660 |
+
{result && (
|
| 661 |
+
<motion.div
|
| 662 |
+
initial={{ opacity: 0, y: 30 }}
|
| 663 |
+
animate={{ opacity: 1, y: 0 }}
|
| 664 |
+
transition={{ duration: 0.5 }}
|
| 665 |
+
>
|
| 666 |
+
<Box sx={{ my: 4 }}>
|
| 667 |
+
<ResultCard
|
| 668 |
+
elevation={2}
|
| 669 |
+
prediction={result.prediction}
|
| 670 |
+
sx={{ mb: 3 }}
|
| 671 |
+
>
|
| 672 |
+
<CardContent sx={{ p: { xs: 2, sm: 3 } }}>
|
| 673 |
+
<Box sx={{
|
| 674 |
+
display: 'flex',
|
| 675 |
+
flexDirection: { xs: 'column', sm: 'row' },
|
| 676 |
+
alignItems: { xs: 'flex-start', sm: 'center' },
|
| 677 |
+
justifyContent: 'space-between'
|
| 678 |
+
}}>
|
| 679 |
+
<Box>
|
| 680 |
+
<Typography
|
| 681 |
+
variant="h5"
|
| 682 |
+
component="div"
|
| 683 |
+
gutterBottom
|
| 684 |
+
sx={{
|
| 685 |
+
fontWeight: 700,
|
| 686 |
+
color: result.prediction === 'Real'
|
| 687 |
+
? 'success.dark'
|
| 688 |
+
: 'error.dark'
|
| 689 |
+
}}
|
| 690 |
+
>
|
| 691 |
+
{result.prediction === 'Real' ? '✓ Authentic Voice' : '⚠ Deepfake Detected'}
|
| 692 |
+
</Typography>
|
| 693 |
+
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
| 694 |
+
Confidence: {(result.confidence * 100).toFixed(2)}%
|
| 695 |
+
</Typography>
|
| 696 |
+
</Box>
|
| 697 |
+
|
| 698 |
+
<Box
|
| 699 |
+
sx={{
|
| 700 |
+
mt: { xs: 2, sm: 0 },
|
| 701 |
+
display: 'flex',
|
| 702 |
+
alignItems: 'center',
|
| 703 |
+
px: 2,
|
| 704 |
+
py: 1,
|
| 705 |
+
backgroundColor: alpha(
|
| 706 |
+
result.prediction === 'Real'
|
| 707 |
+
? theme.palette.success.main
|
| 708 |
+
: theme.palette.error.main,
|
| 709 |
+
0.1
|
| 710 |
+
),
|
| 711 |
+
borderRadius: 2
|
| 712 |
+
}}
|
| 713 |
+
>
|
| 714 |
+
<Typography variant="body2" sx={{ fontWeight: 600, color: result.prediction === 'Real' ? 'success.dark' : 'error.dark' }}>
|
| 715 |
+
{result.prediction === 'Real' ? 'Human Voice' : 'AI-Generated'}
|
| 716 |
+
</Typography>
|
| 717 |
+
</Box>
|
| 718 |
+
</Box>
|
| 719 |
+
</CardContent>
|
| 720 |
+
</ResultCard>
|
| 721 |
+
|
| 722 |
+
<GlassCard elevation={2} sx={{ p: { xs: 2, sm: 3 } }}>
|
| 723 |
+
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
|
| 724 |
+
Probability Distribution
|
| 725 |
+
</Typography>
|
| 726 |
+
<Box sx={{ height: isMobile ? 250 : 300, width: '100%', mt: 2 }}>
|
| 727 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 728 |
+
<BarChart
|
| 729 |
+
data={getChartData()}
|
| 730 |
+
margin={{
|
| 731 |
+
top: 30,
|
| 732 |
+
right: 30,
|
| 733 |
+
left: 20,
|
| 734 |
+
bottom: 10,
|
| 735 |
+
}}
|
| 736 |
+
>
|
| 737 |
+
<CartesianGrid strokeDasharray="3 3" stroke={alpha('#000', 0.1)} />
|
| 738 |
+
<XAxis
|
| 739 |
+
dataKey="name"
|
| 740 |
+
tick={{ fill: theme.palette.text.secondary }}
|
| 741 |
+
axisLine={{ stroke: alpha('#000', 0.15) }}
|
| 742 |
+
/>
|
| 743 |
+
<YAxis
|
| 744 |
+
label={{
|
| 745 |
+
value: 'Probability (%)',
|
| 746 |
+
angle: -90,
|
| 747 |
+
position: 'insideLeft',
|
| 748 |
+
style: { fill: theme.palette.text.secondary }
|
| 749 |
+
}}
|
| 750 |
+
tick={{ fill: theme.palette.text.secondary }}
|
| 751 |
+
axisLine={{ stroke: alpha('#000', 0.15) }}
|
| 752 |
+
/>
|
| 753 |
+
<Tooltip
|
| 754 |
+
formatter={(value) => [`${value}%`, 'Probability']}
|
| 755 |
+
contentStyle={{
|
| 756 |
+
borderRadius: 8,
|
| 757 |
+
border: 'none',
|
| 758 |
+
boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
|
| 759 |
+
backgroundColor: alpha('#fff', 0.95)
|
| 760 |
+
}}
|
| 761 |
+
/>
|
| 762 |
+
<Bar
|
| 763 |
+
dataKey="value"
|
| 764 |
+
fill={(entry) => entry.name === 'Real' ? theme.palette.success.main : theme.palette.error.main}
|
| 765 |
+
radius={[8, 8, 0, 0]}
|
| 766 |
+
label={{
|
| 767 |
+
position: 'top',
|
| 768 |
+
formatter: (value) => `${value}%`,
|
| 769 |
+
fill: theme.palette.text.secondary,
|
| 770 |
+
fontSize: 12,
|
| 771 |
+
fontWeight: 600
|
| 772 |
+
}}
|
| 773 |
+
/>
|
| 774 |
+
</BarChart>
|
| 775 |
+
</ResponsiveContainer>
|
| 776 |
+
</Box>
|
| 777 |
+
</GlassCard>
|
| 778 |
+
|
| 779 |
+
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
| 780 |
+
<Typography variant="body2" color="text.secondary">
|
| 781 |
+
Note: This model claims {modelInfo ? (modelInfo.performance.accuracy * 100).toFixed(2) : ''}% accuracy, but results may vary depending on audio quality.
|
| 782 |
+
</Typography>
|
| 783 |
+
</Box>
|
| 784 |
+
</Box>
|
| 785 |
+
</motion.div>
|
| 786 |
+
)}
|
| 787 |
+
</Container>
|
| 788 |
+
</Box>
|
| 789 |
+
|
| 790 |
+
<Snackbar
|
| 791 |
+
open={openSnackbar}
|
| 792 |
+
autoHideDuration={6000}
|
| 793 |
+
onClose={handleCloseSnackbar}
|
| 794 |
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
| 795 |
+
>
|
| 796 |
+
<Alert
|
| 797 |
+
onClose={handleCloseSnackbar}
|
| 798 |
+
severity="error"
|
| 799 |
+
sx={{
|
| 800 |
+
width: '100%',
|
| 801 |
+
borderRadius: 2,
|
| 802 |
+
boxShadow: '0 4px 20px rgba(0,0,0,0.15)'
|
| 803 |
+
}}
|
| 804 |
+
>
|
| 805 |
+
{error}
|
| 806 |
+
</Alert>
|
| 807 |
+
</Snackbar>
|
| 808 |
+
</ThemeProvider>
|
| 809 |
+
);
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
export default App;
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
ffmpeg \
|
| 8 |
+
libsndfile1 \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
RUN pip install --no-cache-dir fastapi uvicorn python-multipart
|
| 15 |
+
|
| 16 |
+
ENV TRANSFORMERS_CACHE=/app/model_cache
|
| 17 |
+
ENV HF_HOME=/app/model_cache
|
| 18 |
+
|
| 19 |
+
COPY app.py api.py ./
|
| 20 |
+
|
| 21 |
+
RUN mkdir -p /app/model_cache
|
| 22 |
+
|
| 23 |
+
CMD ["python", "api.py"]
|
| 24 |
+
|
| 25 |
+
EXPOSE 8000
|
api.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
import uvicorn
|
| 4 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.responses import JSONResponse
|
| 7 |
+
import shutil
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
from typing import Optional, Dict, Any, List
|
| 10 |
+
|
| 11 |
+
# Import our detection module
|
| 12 |
+
from app import DeepfakeDetector, convert_audio
|
| 13 |
+
|
| 14 |
+
# Initialize the FastAPI app
|
| 15 |
+
app = FastAPI(
|
| 16 |
+
title="Deepfake Voice Detection API",
|
| 17 |
+
description="API for detecting deepfake audio using the MelodyMachine/Deepfake-audio-detection-V2 model",
|
| 18 |
+
version="0.1.0",
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Add CORS middleware
|
| 22 |
+
app.add_middleware(
|
| 23 |
+
CORSMiddleware,
|
| 24 |
+
allow_origins=["*"], # Allows all origins
|
| 25 |
+
allow_credentials=True,
|
| 26 |
+
allow_methods=["*"], # Allows all methods
|
| 27 |
+
allow_headers=["*"], # Allows all headers
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# Initialize the detector at startup
|
| 31 |
+
detector = None
|
| 32 |
+
|
| 33 |
+
@app.on_event("startup")
|
| 34 |
+
async def startup_event():
|
| 35 |
+
global detector
|
| 36 |
+
detector = DeepfakeDetector()
|
| 37 |
+
print("Deepfake Detector model loaded and ready to use")
|
| 38 |
+
|
| 39 |
+
class PredictionResponse(BaseModel):
|
| 40 |
+
prediction: str
|
| 41 |
+
confidence: float
|
| 42 |
+
probabilities: Dict[str, float]
|
| 43 |
+
|
| 44 |
+
@app.post("/detect/", response_model=PredictionResponse)
|
| 45 |
+
async def detect_audio(file: UploadFile = File(...)):
|
| 46 |
+
"""
|
| 47 |
+
Detect if an audio file contains a deepfake voice
|
| 48 |
+
"""
|
| 49 |
+
if not file:
|
| 50 |
+
raise HTTPException(status_code=400, detail="No file provided")
|
| 51 |
+
|
| 52 |
+
# Validate file type
|
| 53 |
+
if not file.filename.lower().endswith(('.wav', '.mp3', '.ogg', '.flac')):
|
| 54 |
+
raise HTTPException(
|
| 55 |
+
status_code=400,
|
| 56 |
+
detail="Invalid file format. Only WAV, MP3, OGG, and FLAC files are supported."
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
# Create a temporary file
|
| 61 |
+
temp_dir = tempfile.gettempdir()
|
| 62 |
+
temp_path = os.path.join(temp_dir, file.filename)
|
| 63 |
+
|
| 64 |
+
# Save uploaded file to the temp location
|
| 65 |
+
with open(temp_path, "wb") as buffer:
|
| 66 |
+
shutil.copyfileobj(file.file, buffer)
|
| 67 |
+
|
| 68 |
+
# Convert audio to required format
|
| 69 |
+
processed_audio = convert_audio(temp_path)
|
| 70 |
+
|
| 71 |
+
# Detect if it's a deepfake
|
| 72 |
+
result = detector.detect(processed_audio)
|
| 73 |
+
|
| 74 |
+
# Clean up the temporary files
|
| 75 |
+
try:
|
| 76 |
+
os.remove(temp_path)
|
| 77 |
+
os.remove(processed_audio) if processed_audio != temp_path else None
|
| 78 |
+
except:
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
return result
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
raise HTTPException(status_code=500, detail=f"Error processing audio: {str(e)}")
|
| 85 |
+
|
| 86 |
+
@app.get("/health/")
|
| 87 |
+
async def health_check():
|
| 88 |
+
"""
|
| 89 |
+
Check if the API is running and the model is loaded
|
| 90 |
+
"""
|
| 91 |
+
if detector is None:
|
| 92 |
+
return JSONResponse(
|
| 93 |
+
status_code=503,
|
| 94 |
+
content={"status": "error", "message": "Model not loaded"}
|
| 95 |
+
)
|
| 96 |
+
return {"status": "ok", "model_loaded": True}
|
| 97 |
+
|
| 98 |
+
@app.get("/model-info/")
|
| 99 |
+
async def model_info():
|
| 100 |
+
"""
|
| 101 |
+
Get information about the model being used
|
| 102 |
+
"""
|
| 103 |
+
return {
|
| 104 |
+
"model_id": "MelodyMachine/Deepfake-audio-detection-V2",
|
| 105 |
+
"base_model": "facebook/wav2vec2-base",
|
| 106 |
+
"performance": {
|
| 107 |
+
"loss": 0.0141,
|
| 108 |
+
"accuracy": 0.9973
|
| 109 |
+
},
|
| 110 |
+
"description": "Fine-tuned model for binary classification distinguishing between real and deepfake audio"
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)
|
app.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import gradio as gr
|
| 4 |
+
import numpy as np
|
| 5 |
+
import librosa
|
| 6 |
+
import soundfile as sf
|
| 7 |
+
from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2ForSequenceClassification
|
| 8 |
+
from pydub import AudioSegment
|
| 9 |
+
import tempfile
|
| 10 |
+
import matplotlib
|
| 11 |
+
matplotlib.use('Agg')
|
| 12 |
+
|
| 13 |
+
# Constants
|
| 14 |
+
MODEL_ID = "MelodyMachine/Deepfake-audio-detection-V2"
|
| 15 |
+
SAMPLE_RATE = 16000
|
| 16 |
+
MAX_DURATION = 30 # maximum audio duration in seconds
|
| 17 |
+
|
| 18 |
+
class DeepfakeDetector:
|
| 19 |
+
def __init__(self, model_id=MODEL_ID):
|
| 20 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 21 |
+
print(f"Using device: {self.device}")
|
| 22 |
+
|
| 23 |
+
print(f"Loading model from {model_id}...")
|
| 24 |
+
self.feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_id)
|
| 25 |
+
self.model = Wav2Vec2ForSequenceClassification.from_pretrained(model_id).to(self.device)
|
| 26 |
+
print("Model loaded successfully!")
|
| 27 |
+
|
| 28 |
+
# Labels for classification
|
| 29 |
+
self.id2label = {0: "Real", 1: "Deepfake"}
|
| 30 |
+
|
| 31 |
+
def preprocess_audio(self, audio_path):
|
| 32 |
+
"""Process audio file to match model requirements."""
|
| 33 |
+
try:
|
| 34 |
+
# Load audio file
|
| 35 |
+
y, sr = librosa.load(audio_path, sr=SAMPLE_RATE, mono=True)
|
| 36 |
+
|
| 37 |
+
# Trim silence from the beginning and end
|
| 38 |
+
y, _ = librosa.effects.trim(y, top_db=20)
|
| 39 |
+
|
| 40 |
+
# If audio is longer than MAX_DURATION seconds, take the first MAX_DURATION seconds
|
| 41 |
+
if len(y) > MAX_DURATION * SAMPLE_RATE:
|
| 42 |
+
y = y[:MAX_DURATION * SAMPLE_RATE]
|
| 43 |
+
|
| 44 |
+
return y
|
| 45 |
+
except Exception as e:
|
| 46 |
+
raise ValueError(f"Error preprocessing audio: {str(e)}")
|
| 47 |
+
|
| 48 |
+
def detect(self, audio_path):
|
| 49 |
+
"""Detect if audio is real or deepfake."""
|
| 50 |
+
try:
|
| 51 |
+
# Preprocess audio
|
| 52 |
+
audio_array = self.preprocess_audio(audio_path)
|
| 53 |
+
|
| 54 |
+
# Extract features
|
| 55 |
+
inputs = self.feature_extractor(
|
| 56 |
+
audio_array,
|
| 57 |
+
sampling_rate=SAMPLE_RATE,
|
| 58 |
+
return_tensors="pt",
|
| 59 |
+
padding=True
|
| 60 |
+
).to(self.device)
|
| 61 |
+
|
| 62 |
+
# Get prediction
|
| 63 |
+
with torch.no_grad():
|
| 64 |
+
outputs = self.model(**inputs)
|
| 65 |
+
logits = outputs.logits
|
| 66 |
+
predictions = torch.softmax(logits, dim=1)
|
| 67 |
+
|
| 68 |
+
# Get results
|
| 69 |
+
predicted_class = torch.argmax(predictions, dim=1).item()
|
| 70 |
+
confidence = predictions[0][predicted_class].item()
|
| 71 |
+
|
| 72 |
+
result = {
|
| 73 |
+
"prediction": self.id2label[predicted_class],
|
| 74 |
+
"confidence": float(confidence),
|
| 75 |
+
"probabilities": {
|
| 76 |
+
"Real": float(predictions[0][0].item()),
|
| 77 |
+
"Deepfake": float(predictions[0][1].item())
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return result
|
| 82 |
+
except Exception as e:
|
| 83 |
+
raise ValueError(f"Error during detection: {str(e)}")
|
| 84 |
+
|
| 85 |
+
def convert_audio(input_file):
|
| 86 |
+
"""Convert the audio file to the required format."""
|
| 87 |
+
# Create temp file with .wav extension
|
| 88 |
+
temp_dir = tempfile.gettempdir()
|
| 89 |
+
temp_path = os.path.join(temp_dir, "temp_audio_file.wav")
|
| 90 |
+
|
| 91 |
+
# Handle various input formats
|
| 92 |
+
if input_file.endswith('.mp3'):
|
| 93 |
+
audio = AudioSegment.from_mp3(input_file)
|
| 94 |
+
audio = audio.set_channels(1) # Convert to mono
|
| 95 |
+
audio = audio.set_frame_rate(SAMPLE_RATE) # Set sample rate
|
| 96 |
+
audio.export(temp_path, format="wav")
|
| 97 |
+
elif input_file.endswith('.wav'):
|
| 98 |
+
audio = AudioSegment.from_wav(input_file)
|
| 99 |
+
audio = audio.set_channels(1) # Convert to mono
|
| 100 |
+
audio = audio.set_frame_rate(SAMPLE_RATE) # Set sample rate
|
| 101 |
+
audio.export(temp_path, format="wav")
|
| 102 |
+
elif input_file.endswith('.ogg'):
|
| 103 |
+
audio = AudioSegment.from_ogg(input_file)
|
| 104 |
+
audio = audio.set_channels(1) # Convert to mono
|
| 105 |
+
audio = audio.set_frame_rate(SAMPLE_RATE) # Set sample rate
|
| 106 |
+
audio.export(temp_path, format="wav")
|
| 107 |
+
elif input_file.endswith('.flac'):
|
| 108 |
+
audio = AudioSegment.from_file(input_file, format="flac")
|
| 109 |
+
audio = audio.set_channels(1) # Convert to mono
|
| 110 |
+
audio = audio.set_frame_rate(SAMPLE_RATE) # Set sample rate
|
| 111 |
+
audio.export(temp_path, format="wav")
|
| 112 |
+
else:
|
| 113 |
+
# Try to convert using pydub's generic from_file
|
| 114 |
+
try:
|
| 115 |
+
audio = AudioSegment.from_file(input_file)
|
| 116 |
+
audio = audio.set_channels(1) # Convert to mono
|
| 117 |
+
audio = audio.set_frame_rate(SAMPLE_RATE) # Set sample rate
|
| 118 |
+
audio.export(temp_path, format="wav")
|
| 119 |
+
except:
|
| 120 |
+
raise ValueError(f"Unsupported audio format for file: {input_file}")
|
| 121 |
+
|
| 122 |
+
return temp_path
|
| 123 |
+
|
| 124 |
+
def detect_deepfake(audio_file, detector):
|
| 125 |
+
"""Process audio and detect if it's a deepfake."""
|
| 126 |
+
if audio_file is None:
|
| 127 |
+
return {
|
| 128 |
+
"error": "Please upload an audio file."
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
# Convert audio to required format
|
| 133 |
+
processed_audio = convert_audio(audio_file)
|
| 134 |
+
|
| 135 |
+
# Detect deepfake
|
| 136 |
+
result = detector.detect(processed_audio)
|
| 137 |
+
|
| 138 |
+
# Create a visually appealing output
|
| 139 |
+
prediction = result["prediction"]
|
| 140 |
+
confidence = result["confidence"] * 100
|
| 141 |
+
|
| 142 |
+
# Prepare visualization data
|
| 143 |
+
labels = list(result["probabilities"].keys())
|
| 144 |
+
values = list(result["probabilities"].values())
|
| 145 |
+
|
| 146 |
+
output = {
|
| 147 |
+
"prediction": prediction,
|
| 148 |
+
"confidence": f"{confidence:.2f}%",
|
| 149 |
+
"chart_labels": labels,
|
| 150 |
+
"chart_values": values
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
# Create result text with confidence
|
| 154 |
+
result_text = f"Prediction: {prediction} (Confidence: {confidence:.2f}%)"
|
| 155 |
+
|
| 156 |
+
return result_text, output
|
| 157 |
+
except Exception as e:
|
| 158 |
+
return f"Error: {str(e)}", None
|
| 159 |
+
|
| 160 |
+
def create_interface():
|
| 161 |
+
"""Create Gradio interface for the application."""
|
| 162 |
+
# Initialize the deepfake detector
|
| 163 |
+
detector = DeepfakeDetector()
|
| 164 |
+
|
| 165 |
+
with gr.Blocks(title="Deepfake Voice Detector") as interface:
|
| 166 |
+
gr.Markdown("""
|
| 167 |
+
# Deepfake Voice Detector
|
| 168 |
+
|
| 169 |
+
Upload an audio file to check if it's a real human voice or an AI-generated deepfake.
|
| 170 |
+
|
| 171 |
+
**Model:** MelodyMachine/Deepfake-audio-detection-V2 (Accuracy: 99.73%)
|
| 172 |
+
""")
|
| 173 |
+
|
| 174 |
+
with gr.Row():
|
| 175 |
+
with gr.Column(scale=1):
|
| 176 |
+
audio_input = gr.Audio(
|
| 177 |
+
type="filepath",
|
| 178 |
+
label="Upload Audio File",
|
| 179 |
+
sources=["upload", "microphone"]
|
| 180 |
+
)
|
| 181 |
+
submit_btn = gr.Button("Analyze Audio", variant="primary")
|
| 182 |
+
|
| 183 |
+
with gr.Column(scale=1):
|
| 184 |
+
result_text = gr.Textbox(label="Result")
|
| 185 |
+
|
| 186 |
+
# Visualization component
|
| 187 |
+
with gr.Accordion("Detailed Analysis", open=False):
|
| 188 |
+
gr.Markdown("### Confidence Scores")
|
| 189 |
+
confidence_plot = gr.Plot(label="Confidence Scores")
|
| 190 |
+
|
| 191 |
+
# Process function for the submit button
|
| 192 |
+
def process_and_visualize(audio_file):
|
| 193 |
+
result_text, output = detect_deepfake(audio_file, detector)
|
| 194 |
+
|
| 195 |
+
if output:
|
| 196 |
+
# Create bar chart visualization
|
| 197 |
+
import matplotlib.pyplot as plt
|
| 198 |
+
|
| 199 |
+
fig, ax = plt.subplots(figsize=(6, 4))
|
| 200 |
+
bars = ax.bar(output["chart_labels"], output["chart_values"], color=['green', 'red'])
|
| 201 |
+
|
| 202 |
+
# Add percentage labels on top of each bar
|
| 203 |
+
for bar in bars:
|
| 204 |
+
height = bar.get_height()
|
| 205 |
+
ax.text(bar.get_x() + bar.get_width()/2., height + 0.02,
|
| 206 |
+
f'{height*100:.1f}%', ha='center', va='bottom')
|
| 207 |
+
|
| 208 |
+
ax.set_ylim(0, 1.1)
|
| 209 |
+
ax.set_title('Confidence Scores')
|
| 210 |
+
ax.set_ylabel('Probability')
|
| 211 |
+
|
| 212 |
+
return result_text, fig
|
| 213 |
+
else:
|
| 214 |
+
return result_text, None
|
| 215 |
+
|
| 216 |
+
submit_btn.click(
|
| 217 |
+
process_and_visualize,
|
| 218 |
+
inputs=[audio_input],
|
| 219 |
+
outputs=[result_text, confidence_plot]
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
return interface
|
| 223 |
+
|
| 224 |
+
if __name__ == "__main__":
|
| 225 |
+
interface = create_interface()
|
| 226 |
+
interface.launch()
|
batch_processor.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from tqdm import tqdm
|
| 6 |
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
| 7 |
+
from app import DeepfakeDetector, convert_audio
|
| 8 |
+
|
| 9 |
+
def process_single_file(file_path, detector):
|
| 10 |
+
"""Process a single audio file and return the detection result."""
|
| 11 |
+
try:
|
| 12 |
+
# Convert audio to the required format
|
| 13 |
+
processed_audio = convert_audio(file_path)
|
| 14 |
+
|
| 15 |
+
# Detect if it's a deepfake
|
| 16 |
+
result = detector.detect(processed_audio)
|
| 17 |
+
|
| 18 |
+
# Add the file path to the result
|
| 19 |
+
result["file_path"] = file_path
|
| 20 |
+
result["file_name"] = os.path.basename(file_path)
|
| 21 |
+
|
| 22 |
+
# Clean up temporary files if needed
|
| 23 |
+
if processed_audio != file_path:
|
| 24 |
+
try:
|
| 25 |
+
os.remove(processed_audio)
|
| 26 |
+
except:
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
return result
|
| 30 |
+
except Exception as e:
|
| 31 |
+
return {
|
| 32 |
+
"file_path": file_path,
|
| 33 |
+
"file_name": os.path.basename(file_path),
|
| 34 |
+
"error": str(e)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
def process_directory(directory_path, output_format='json', max_workers=None, recursive=False):
|
| 38 |
+
"""Process all audio files in a directory."""
|
| 39 |
+
# Initialize the detector
|
| 40 |
+
detector = DeepfakeDetector()
|
| 41 |
+
|
| 42 |
+
# Find all audio files
|
| 43 |
+
audio_extensions = ('.wav', '.mp3', '.ogg', '.flac')
|
| 44 |
+
audio_files = []
|
| 45 |
+
|
| 46 |
+
if recursive:
|
| 47 |
+
for root, _, files in os.walk(directory_path):
|
| 48 |
+
for file in files:
|
| 49 |
+
if file.lower().endswith(audio_extensions):
|
| 50 |
+
audio_files.append(os.path.join(root, file))
|
| 51 |
+
else:
|
| 52 |
+
audio_files = [os.path.join(directory_path, f) for f in os.listdir(directory_path)
|
| 53 |
+
if f.lower().endswith(audio_extensions)]
|
| 54 |
+
|
| 55 |
+
if not audio_files:
|
| 56 |
+
print(f"No audio files found in {directory_path}")
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
print(f"Found {len(audio_files)} audio files to process")
|
| 60 |
+
|
| 61 |
+
# Process files with a progress bar
|
| 62 |
+
results = []
|
| 63 |
+
|
| 64 |
+
# Use parallel processing for faster analysis
|
| 65 |
+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
| 66 |
+
futures = {executor.submit(process_single_file, file, detector): file for file in audio_files}
|
| 67 |
+
|
| 68 |
+
for future in tqdm(as_completed(futures), total=len(audio_files), desc="Processing audio files"):
|
| 69 |
+
result = future.result()
|
| 70 |
+
results.append(result)
|
| 71 |
+
|
| 72 |
+
# Save results based on output format
|
| 73 |
+
if output_format == 'json':
|
| 74 |
+
output_file = os.path.join(directory_path, "deepfake_detection_results.json")
|
| 75 |
+
with open(output_file, 'w') as f:
|
| 76 |
+
json.dump(results, f, indent=2)
|
| 77 |
+
print(f"Results saved to {output_file}")
|
| 78 |
+
|
| 79 |
+
elif output_format == 'csv':
|
| 80 |
+
output_file = os.path.join(directory_path, "deepfake_detection_results.csv")
|
| 81 |
+
df = pd.DataFrame(results)
|
| 82 |
+
df.to_csv(output_file, index=False)
|
| 83 |
+
print(f"Results saved to {output_file}")
|
| 84 |
+
|
| 85 |
+
# Print summary
|
| 86 |
+
total = len(results)
|
| 87 |
+
real_count = sum(1 for r in results if 'prediction' in r and r['prediction'] == 'Real')
|
| 88 |
+
fake_count = sum(1 for r in results if 'prediction' in r and r['prediction'] == 'Deepfake')
|
| 89 |
+
error_count = sum(1 for r in results if 'error' in r)
|
| 90 |
+
|
| 91 |
+
print("\nSummary:")
|
| 92 |
+
print(f"Total files processed: {total}")
|
| 93 |
+
print(f"Detected as real: {real_count} ({real_count/total*100:.1f}%)")
|
| 94 |
+
print(f"Detected as deepfake: {fake_count} ({fake_count/total*100:.1f}%)")
|
| 95 |
+
print(f"Errors during processing: {error_count} ({error_count/total*100:.1f}%)")
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
parser = argparse.ArgumentParser(description='Batch process audio files for deepfake detection')
|
| 99 |
+
parser.add_argument('directory', help='Directory containing audio files to process')
|
| 100 |
+
parser.add_argument('--format', choices=['json', 'csv'], default='json', help='Output format (default: json)')
|
| 101 |
+
parser.add_argument('--workers', type=int, default=None, help='Number of worker processes (default: CPU count)')
|
| 102 |
+
parser.add_argument('--recursive', action='store_true', help='Search for audio files recursively in subdirectories')
|
| 103 |
+
|
| 104 |
+
args = parser.parse_args()
|
| 105 |
+
|
| 106 |
+
process_directory(args.directory, args.format, args.workers, args.recursive)
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
api:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
ports:
|
| 9 |
+
- "8000:8000"
|
| 10 |
+
command: python api.py
|
| 11 |
+
environment:
|
| 12 |
+
- MODEL_ID=MelodyMachine/Deepfake-audio-detection-V2
|
| 13 |
+
volumes:
|
| 14 |
+
- ./model_cache:/app/model_cache
|
| 15 |
+
deploy:
|
| 16 |
+
resources:
|
| 17 |
+
reservations:
|
| 18 |
+
devices:
|
| 19 |
+
- driver: nvidia
|
| 20 |
+
count: 1
|
| 21 |
+
capabilities: [gpu]
|
| 22 |
+
|
| 23 |
+
web:
|
| 24 |
+
build:
|
| 25 |
+
context: ./frontend
|
| 26 |
+
dockerfile: Dockerfile
|
| 27 |
+
ports:
|
| 28 |
+
- "80:80"
|
| 29 |
+
depends_on:
|
| 30 |
+
- api
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch>=1.10.0
|
| 2 |
+
transformers>=4.16.0
|
| 3 |
+
librosa>=0.8.0
|
| 4 |
+
soundfile>=0.10.3
|
| 5 |
+
gradio>=3.0.0
|
| 6 |
+
matplotlib>=3.4.0
|
| 7 |
+
numpy>=1.20.0
|
| 8 |
+
pydub>=0.25.1
|
| 9 |
+
fastapi>=0.68.0
|
| 10 |
+
uvicorn>=0.15.0
|
| 11 |
+
python-multipart
|
setup.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import setup, find_packages
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name="deepfake_voice_detector",
|
| 5 |
+
version="0.1.0",
|
| 6 |
+
packages=find_packages(),
|
| 7 |
+
install_requires=[
|
| 8 |
+
"torch>=1.10.0",
|
| 9 |
+
"transformers>=4.16.0",
|
| 10 |
+
"librosa>=0.8.0",
|
| 11 |
+
"soundfile>=0.10.3",
|
| 12 |
+
"gradio>=3.0.0",
|
| 13 |
+
"matplotlib>=3.4.0",
|
| 14 |
+
"numpy>=1.20.0",
|
| 15 |
+
"pydub>=0.25.1",
|
| 16 |
+
],
|
| 17 |
+
author="DeepfakeDetector",
|
| 18 |
+
author_email="info@deepfakedetector.app",
|
| 19 |
+
description="An application for detecting deepfake audio using the MelodyMachine/Deepfake-audio-detection-V2 model",
|
| 20 |
+
keywords="deepfake, audio, detection, ai",
|
| 21 |
+
python_requires=">=3.7",
|
| 22 |
+
)
|