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="[email protected]",
|
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 |
+
)
|