fredcaixeta commited on
Commit
bc70eaa
·
1 Parent(s): 512e050
Files changed (2) hide show
  1. app.py +157 -17
  2. static/index.html +73 -14
app.py CHANGED
@@ -1,42 +1,182 @@
1
- from fastapi import FastAPI, HTTPException
 
 
 
 
 
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from fastapi.responses import FileResponse
4
  from fastapi.staticfiles import StaticFiles
5
- from pydantic import BaseModel
6
- from working import main
 
 
 
 
 
7
 
8
  # --- 1. SETUP DO FASTAPI ---
9
  app = FastAPI(
10
  title="Audio Classifier API",
11
- description="Uma API para classificar áudios como 'REAL' ou 'IA'."
12
  )
13
 
14
  app.add_middleware(
15
  CORSMiddleware,
16
- allow_origins=["*"],
17
  allow_credentials=True,
18
  allow_methods=["*"],
19
  allow_headers=["*"],
20
  )
21
 
 
22
  app.mount("/static", StaticFiles(directory="static"), name="static")
23
 
24
- # --- 2. MODELO DE DADOS ---
25
- class URLPayload(BaseModel):
26
- url: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # --- 3. ROTA DE CLASSIFICAÇÃO ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  @app.post("/api/classify")
30
- def classify_audio(payload: URLPayload):
 
31
  try:
32
- result = main(payload.url)
33
- if "error" in result:
34
- raise HTTPException(status_code=500, detail=result["error"])
35
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  except Exception as e:
37
- raise HTTPException(status_code=500, detail=f"Erro interno: {e}")
 
 
 
 
 
 
38
 
39
- # --- 4. ROTA DE SAÚDE ---
 
40
  @app.get("/", response_class=FileResponse)
41
  def home():
42
- return "./static/index.html"
 
 
 
 
1
+ import os
2
+ import joblib
3
+ import numpy as np
4
+ import torch
5
+ import torchaudio
6
+ import yt_dlp
7
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import FileResponse
10
  from fastapi.staticfiles import StaticFiles
11
+ from pydantic import BaseModel, HttpUrl
12
+ from typing import Optional, Union
13
+ import logging
14
+
15
+ # --- 0. CONFIGURAÇÃO DE LOGGING ---
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
 
19
  # --- 1. SETUP DO FASTAPI ---
20
  app = FastAPI(
21
  title="Audio Classifier API",
22
+ description="Uma API para classificar áudios como 'REAL' ou 'IA', aceitando uploads de arquivo ou URLs."
23
  )
24
 
25
  app.add_middleware(
26
  CORSMiddleware,
27
+ allow_origins=["*"],
28
  allow_credentials=True,
29
  allow_methods=["*"],
30
  allow_headers=["*"],
31
  )
32
 
33
+ # Monta o diretório estático para servir o index.html e outros arquivos.
34
  app.mount("/static", StaticFiles(directory="static"), name="static")
35
 
36
+ # --- 2. CARREGAMENTO DO MODELO ---
37
+ try:
38
+ modelo_path = 'modelo_random_forest.joblib'
39
+ scaler_path = 'scaler.joblib'
40
+ if not os.path.exists(modelo_path) or not os.path.exists(scaler_path):
41
+ raise FileNotFoundError("Arquivos do modelo ou scaler não encontrados.")
42
+ model = joblib.load(modelo_path)
43
+ scaler = joblib.load(scaler_path)
44
+ logger.info("Modelo e scaler carregados com sucesso.")
45
+ except Exception as e:
46
+ logger.error(f"Erro ao carregar o modelo ou scaler: {e}")
47
+ # Encerra o aplicativo se os modelos não puderem ser carregados.
48
+ raise RuntimeError(f"Não foi possível carregar os artefatos do modelo: {e}") from e
49
+
50
+ # --- 3. FUNÇÕES DE PROCESSAMENTO DE ÁUDIO ---
51
+ def extract_features(waveform, sample_rate, n_mfcc=13):
52
+ """Extrai MFCCs de um waveform."""
53
+ mfcc_transform = torchaudio.transforms.MFCC(
54
+ sample_rate=sample_rate,
55
+ n_mfcc=n_mfcc,
56
+ melkwargs={'n_fft': 400, 'hop_length': 160, 'n_mels': 23, 'center': False}
57
+ )
58
+ mfcc = mfcc_transform(waveform)
59
+ return np.mean(mfcc.squeeze(0).numpy(), axis=1)
60
+
61
+ def process_audio_file(file_path: str):
62
+ """
63
+ Carrega um arquivo de áudio, extrai features e as escala.
64
+ Retorna None se o áudio for muito curto ou inválido.
65
+ """
66
+ try:
67
+ waveform, sample_rate = torchaudio.load(file_path)
68
+
69
+ # Garante que o áudio tenha pelo menos uma duração mínima
70
+ min_duration_samples = sample_rate * 1 # 1 segundo
71
+ if waveform.shape[1] < min_duration_samples:
72
+ logger.warning(f"Áudio {file_path} é muito curto para análise.")
73
+ return None
74
+
75
+ # Garante a monocanalidade somando os canais, se houver mais de um.
76
+ if waveform.shape[0] > 1:
77
+ waveform = torch.mean(waveform, dim=0, keepdim=True)
78
 
79
+ features = extract_features(waveform, sample_rate)
80
+ scaled_features = scaler.transform([features])
81
+ return scaled_features
82
+ except Exception as e:
83
+ logger.error(f"Erro ao processar o arquivo de áudio {file_path}: {e}")
84
+ raise ValueError(f"Não foi possível processar o arquivo de áudio: {e}")
85
+
86
+ def download_audio_from_url(url: str, output_path: str = "temp_audio"):
87
+ """
88
+ Baixa áudio de uma URL (YouTube, Twitter, etc.) usando yt-dlp.
89
+ Salva como .wav.
90
+ """
91
+ ydl_opts = {
92
+ 'format': 'bestaudio/best',
93
+ 'postprocessors': [{
94
+ 'key': 'FFmpegExtractAudio',
95
+ 'preferredcodec': 'wav',
96
+ 'preferredquality': '192',
97
+ }],
98
+ 'outtmpl': os.path.join(output_path, '%(id)s.%(ext)s'),
99
+ 'quiet': True,
100
+ }
101
+
102
+ if not os.path.exists(output_path):
103
+ os.makedirs(output_path)
104
+
105
+ try:
106
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
107
+ info = ydl.extract_info(url, download=True)
108
+ filename = ydl.prepare_filename(info).replace(info['ext'], 'wav')
109
+
110
+ # Verifica se o arquivo foi realmente criado
111
+ if os.path.exists(filename):
112
+ return filename
113
+ else:
114
+ # Tenta encontrar o arquivo com base no ID do vídeo
115
+ base_filename = os.path.join(output_path, f"{info['id']}.wav")
116
+ if os.path.exists(base_filename):
117
+ return base_filename
118
+ raise FileNotFoundError(f"Arquivo de áudio baixado não encontrado para a URL: {url}")
119
+ except Exception as e:
120
+ logger.error(f"Falha no download do áudio da URL {url}: {e}")
121
+ raise ConnectionError(f"Não foi possível baixar ou converter o áudio da URL fornecida. A URL é válida?")
122
+
123
+
124
+ # --- 4. ROTA DE CLASSIFICAÇÃO ---
125
  @app.post("/api/classify")
126
+ async def classify_audio(url: Optional[str] = Form(None), file: Optional[UploadFile] = File(None)):
127
+ temp_file_path = None
128
  try:
129
+ if file:
130
+ # Lógica para upload de arquivo
131
+ temp_dir = "temp_uploads"
132
+ if not os.path.exists(temp_dir):
133
+ os.makedirs(temp_dir)
134
+
135
+ temp_file_path = os.path.join(temp_dir, file.filename)
136
+ with open(temp_file_path, "wb") as buffer:
137
+ buffer.write(await file.read())
138
+
139
+ logger.info(f"Arquivo '{file.filename}' recebido.")
140
+ audio_path = temp_file_path
141
+
142
+ elif url:
143
+ # Lógica para download de URL
144
+ logger.info(f"Recebida URL para classificação: {url}")
145
+ audio_path = download_audio_from_url(url)
146
+ temp_file_path = audio_path # Marcar para exclusão posterior
147
+
148
+ else:
149
+ raise HTTPException(status_code=400, detail="Nenhum arquivo ou URL fornecido.")
150
+
151
+ # Processamento e classificação do áudio
152
+ scaled_features = process_audio_file(audio_path)
153
+ if scaled_features is None:
154
+ raise HTTPException(status_code=400, detail="O áudio é muito curto ou não pôde ser processado.")
155
+
156
+ prediction = model.predict(scaled_features)
157
+ probability = model.predict_proba(scaled_features)
158
+
159
+ label = prediction[0]
160
+ prob = probability[0][1] if label == 'IA' else probability[0][0]
161
+
162
+ return {"label": label, "probability": float(prob)}
163
+
164
+ except (ValueError, ConnectionError, FileNotFoundError) as e:
165
+ raise HTTPException(status_code=400, detail=str(e))
166
  except Exception as e:
167
+ logger.error(f"Erro inesperado durante a classificação: {e}")
168
+ raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {e}")
169
+ finally:
170
+ # Limpeza do arquivo temporário
171
+ if temp_file_path and os.path.exists(temp_file_path):
172
+ os.remove(temp_file_path)
173
+ logger.info(f"Arquivo temporário '{temp_file_path}' removido.")
174
 
175
+
176
+ # --- 5. ROTA DE SAÚDE ---
177
  @app.get("/", response_class=FileResponse)
178
  def home():
179
+ """Serve a página inicial da aplicação."""
180
+ return "./static/index.html"
181
+
182
+ # Para executar localmente: uvicorn app:app --reload --port 8000
static/index.html CHANGED
@@ -28,14 +28,14 @@
28
  }
29
  p {
30
  color: #666;
31
- margin-bottom: 2rem;
32
  }
33
  form {
34
  display: flex;
35
  flex-direction: column;
36
- gap: 1rem;
37
  }
38
- input {
39
  padding: 0.75rem;
40
  border: 1px solid #ccc;
41
  border-radius: 4px;
@@ -61,40 +61,99 @@
61
  border-radius: 4px;
62
  background-color: #eaf5ff;
63
  min-height: 50px;
 
 
 
 
 
 
 
64
  }
65
  </style>
66
  </head>
67
  <body>
68
  <div class="container">
69
  <h1>AI or Real Audio?</h1>
70
- <p>Enter the URL of a video containing speech to detect deepfakes or narrative insertions. The program operates by analyzing the video's audio, which will be classified as "Real" or "AI".</p>
 
 
 
 
 
 
 
 
 
 
71
  <form id="classifyForm">
72
- <input type="text" id="videoUrl" placeholder="Example: https://video.twimg.com/..." required>
 
 
 
 
 
73
  <button type="submit">Classify</button>
74
  </form>
 
75
  <div id="result" class="result-box">
76
- Waiting for URL...
77
  </div>
78
  </div>
79
 
80
  <script>
81
  const form = document.getElementById('classifyForm');
82
- const videoUrlInput = document.getElementById('videoUrl');
 
83
  const resultBox = document.getElementById('result');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  form.addEventListener('submit', async (e) => {
86
  e.preventDefault();
87
- const url = videoUrlInput.value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  resultBox.textContent = "Classifying...";
89
  resultBox.style.color = '#007bff';
90
 
91
  try {
92
  const response = await fetch('/api/classify', {
93
  method: 'POST',
94
- headers: {
95
- 'Content-Type': 'application/json',
96
- },
97
- body: JSON.stringify({ url: url }),
98
  });
99
 
100
  const data = await response.json();
@@ -103,11 +162,11 @@
103
  resultBox.textContent = `Result: ${data.label} (Probability: ${(data.probability * 100).toFixed(2)}%)`;
104
  resultBox.style.color = 'green';
105
  } else {
106
- resultBox.textContent = `Erro: ${data.detail || 'Not possible to classify audio.'}`;
107
  resultBox.style.color = 'red';
108
  }
109
  } catch (error) {
110
- resultBox.textContent = `Erro de conexão: ${error.message}`;
111
  resultBox.style.color = 'red';
112
  }
113
  });
 
28
  }
29
  p {
30
  color: #666;
31
+ margin-bottom: 1.5rem;
32
  }
33
  form {
34
  display: flex;
35
  flex-direction: column;
36
+ gap: 1.5rem; /* Aumentado o espaço */
37
  }
38
+ input[type="text"], input[type="file"] {
39
  padding: 0.75rem;
40
  border: 1px solid #ccc;
41
  border-radius: 4px;
 
61
  border-radius: 4px;
62
  background-color: #eaf5ff;
63
  min-height: 50px;
64
+ word-wrap: break-word;
65
+ }
66
+ .input-options {
67
+ display: flex;
68
+ justify-content: center;
69
+ gap: 1rem;
70
+ margin-bottom: 1rem;
71
  }
72
  </style>
73
  </head>
74
  <body>
75
  <div class="container">
76
  <h1>AI or Real Audio?</h1>
77
+ <p>Select an input method: upload an audio/video file (MP3, MP4, WAV) or provide a URL from a platform like YouTube or Twitter.</p>
78
+
79
+ <div class="input-options">
80
+ <label>
81
+ <input type="radio" name="inputType" value="url" checked> URL
82
+ </label>
83
+ <label>
84
+ <input type="radio" name="inputType" value="file"> File Upload
85
+ </label>
86
+ </div>
87
+
88
  <form id="classifyForm">
89
+ <div id="urlInputContainer">
90
+ <input type="text" id="url" placeholder="Example: https://www.youtube.com/watch?v=...">
91
+ </div>
92
+ <div id="fileInputContainer" style="display: none;">
93
+ <input type="file" id="file" accept=".mp3,.mp4,.wav,.m4a">
94
+ </div>
95
  <button type="submit">Classify</button>
96
  </form>
97
+
98
  <div id="result" class="result-box">
99
+ Waiting for input...
100
  </div>
101
  </div>
102
 
103
  <script>
104
  const form = document.getElementById('classifyForm');
105
+ const urlInput = document.getElementById('url');
106
+ const fileInput = document.getElementById('file');
107
  const resultBox = document.getElementById('result');
108
+
109
+ const urlInputContainer = document.getElementById('urlInputContainer');
110
+ const fileInputContainer = document.getElementById('fileInputContainer');
111
+
112
+ const inputTypeRadios = document.querySelectorAll('input[name="inputType"]');
113
+
114
+ inputTypeRadios.forEach(radio => {
115
+ radio.addEventListener('change', (e) => {
116
+ if (e.target.value === 'url') {
117
+ urlInputContainer.style.display = 'block';
118
+ fileInputContainer.style.display = 'none';
119
+ } else {
120
+ urlInputContainer.style.display = 'none';
121
+ fileInputContainer.style.display = 'block';
122
+ }
123
+ });
124
+ });
125
 
126
  form.addEventListener('submit', async (e) => {
127
  e.preventDefault();
128
+
129
+ const selectedInputType = document.querySelector('input[name="inputType"]:checked').value;
130
+ const formData = new FormData();
131
+
132
+ if (selectedInputType === 'url') {
133
+ const url = urlInput.value;
134
+ if (!url) {
135
+ resultBox.textContent = 'Please, provide an URL.';
136
+ resultBox.style.color = 'red';
137
+ return;
138
+ }
139
+ formData.append('url', url);
140
+ } else {
141
+ const file = fileInput.files[0];
142
+ if (!file) {
143
+ resultBox.textContent = 'Please, select a file.';
144
+ resultBox.style.color = 'red';
145
+ return;
146
+ }
147
+ formData.append('file', file);
148
+ }
149
+
150
  resultBox.textContent = "Classifying...";
151
  resultBox.style.color = '#007bff';
152
 
153
  try {
154
  const response = await fetch('/api/classify', {
155
  method: 'POST',
156
+ body: formData, // FormData é enviado sem o header 'Content-Type'
 
 
 
157
  });
158
 
159
  const data = await response.json();
 
162
  resultBox.textContent = `Result: ${data.label} (Probability: ${(data.probability * 100).toFixed(2)}%)`;
163
  resultBox.style.color = 'green';
164
  } else {
165
+ resultBox.textContent = `Error: ${data.detail || 'Not possible to classify audio.'}`;
166
  resultBox.style.color = 'red';
167
  }
168
  } catch (error) {
169
+ resultBox.textContent = `Conection error: ${error.message}`;
170
  resultBox.style.color = 'red';
171
  }
172
  });