fredcaixeta commited on
Commit
c074c6d
·
1 Parent(s): 3921875
Files changed (3) hide show
  1. Dockerfile +2 -2
  2. app.py +163 -17
  3. static/index.html +73 -14
Dockerfile CHANGED
@@ -14,8 +14,8 @@ RUN pip install --no-cache-dir -r requirements.txt
14
 
15
  COPY . .
16
 
17
- RUN --mount=type=secret,id=WORKING_PY_CONTENT \
18
- cat /run/secrets/WORKING_PY_CONTENT > working.py
19
 
20
  # Debugging: List files and show content
21
  #RUN ls -l /app
 
14
 
15
  COPY . .
16
 
17
+ # RUN --mount=type=secret,id=WORKING_PY_CONTENT \
18
+ # cat /run/secrets/WORKING_PY_CONTENT > working.py
19
 
20
  # Debugging: List files and show content
21
  #RUN ls -l /app
app.py CHANGED
@@ -1,42 +1,188 @@
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
+ import uvicorn
15
+
16
+
17
+ # --- 0. CONFIGURAÇÃO DE LOGGING ---
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
 
21
  # --- 1. SETUP DO FASTAPI ---
22
  app = FastAPI(
23
  title="Audio Classifier API",
24
+ description=""
25
  )
26
 
27
  app.add_middleware(
28
  CORSMiddleware,
29
+ allow_origins=["*"],
30
  allow_credentials=True,
31
  allow_methods=["*"],
32
  allow_headers=["*"],
33
  )
34
 
35
+ # Monta o diretório estático para servir o index.html e outros arquivos.
36
  app.mount("/static", StaticFiles(directory="static"), name="static")
37
 
38
+ # --- 2. CARREGAMENTO DO MODELO ---
39
+ try:
40
+ modelo_path = 'modelo_random_forest.joblib'
41
+ scaler_path = 'scaler.joblib'
42
+ if not os.path.exists(modelo_path) or not os.path.exists(scaler_path):
43
+ raise FileNotFoundError("Arquivos do modelo ou scaler não encontrados.")
44
+ model = joblib.load(modelo_path)
45
+ scaler = joblib.load(scaler_path)
46
+ logger.info("Modelo e scaler carregados com sucesso.")
47
+ except Exception as e:
48
+ logger.error(f"Erro ao carregar o modelo ou scaler: {e}")
49
+ # Encerra o aplicativo se os modelos não puderem ser carregados.
50
+ raise RuntimeError(f"Não foi possível carregar os artefatos do modelo: {e}") from e
51
+
52
+ # --- 3. FUNÇÕES DE PROCESSAMENTO DE ÁUDIO ---
53
+ def extract_features(waveform, sample_rate, n_mfcc=12):
54
+ """Extrai MFCCs de um waveform."""
55
+ mfcc_transform = torchaudio.transforms.MFCC(
56
+ sample_rate=sample_rate,
57
+ n_mfcc=n_mfcc,
58
+ melkwargs={'n_fft': 400, 'hop_length': 160, 'n_mels': 23, 'center': False}
59
+ )
60
+ mfcc = mfcc_transform(waveform)
61
+ return np.mean(mfcc.squeeze(0).numpy(), axis=1)
62
+
63
+ def process_audio_file(file_path: str):
64
+ """
65
+ Carrega um arquivo de áudio, extrai features e as escala.
66
+ Retorna None se o áudio for muito curto ou inválido.
67
+ """
68
+ try:
69
+ waveform, sample_rate = torchaudio.load(file_path, backend="soundfile")
70
+
71
+ # Garante que o áudio tenha pelo menos uma duração mínima
72
+ min_duration_samples = sample_rate * 1 # 1 segundo
73
+ if waveform.shape[1] < min_duration_samples:
74
+ logger.warning(f"Áudio {file_path} é muito curto para análise.")
75
+ return None
76
+
77
+ # Garante a monocanalidade somando os canais, se houver mais de um.
78
+ if waveform.shape[0] > 1:
79
+ waveform = torch.mean(waveform, dim=0, keepdim=True)
80
+
81
+ features = extract_features(waveform, sample_rate)
82
+ scaled_features = scaler.transform([features])
83
+ return scaled_features
84
+ except Exception as e:
85
+ logger.error(f"Erro ao processar o arquivo de áudio {file_path}: {e}")
86
+ raise ValueError(f"Não foi possível processar o arquivo de áudio: {e}")
87
+
88
+ def download_audio_from_url(url: str, output_path: str = "temp_audio"):
89
+ """
90
+ Baixa áudio de uma URL (YouTube, Twitter, etc.) usando yt-dlp.
91
+ Salva como .wav.
92
+ """
93
+ ydl_opts = {
94
+ 'format': 'bestaudio/best',
95
+ 'postprocessors': [{
96
+ 'key': 'FFmpegExtractAudio',
97
+ 'preferredcodec': 'wav',
98
+ 'preferredquality': '192',
99
+ }],
100
+ 'outtmpl': os.path.join(output_path, '%(id)s.%(ext)s'),
101
+ 'quiet': True,
102
+ }
103
+
104
+ if not os.path.exists(output_path):
105
+ os.makedirs(output_path)
106
+
107
+ try:
108
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
109
+ info = ydl.extract_info(url, download=True)
110
+ filename = ydl.prepare_filename(info).replace(info['ext'], 'wav')
111
+
112
+ # Verifica se o arquivo foi realmente criado
113
+ if os.path.exists(filename):
114
+ return filename
115
+ else:
116
+ # Tenta encontrar o arquivo com base no ID do vídeo
117
+ base_filename = os.path.join(output_path, f"{info['id']}.wav")
118
+ if os.path.exists(base_filename):
119
+ return base_filename
120
+ raise FileNotFoundError(f"Arquivo de áudio baixado não encontrado para a URL: {url}")
121
+ except Exception as e:
122
+ logger.error(f"Falha no download do áudio da URL {url}: {e}")
123
+ raise ConnectionError(f"Não foi possível baixar ou converter o áudio da URL fornecida. A URL é válida?")
124
+
125
 
126
+ # --- 4. ROTA DE CLASSIFICAÇÃO ---
127
  @app.post("/api/classify")
128
+ async def classify_audio(url: Optional[str] = Form(None), file: Optional[UploadFile] = File(None)):
129
+ temp_file_path = None
130
  try:
131
+ if file:
132
+ # Lógica para upload de arquivo
133
+ temp_dir = "temp_uploads"
134
+ if not os.path.exists(temp_dir):
135
+ os.makedirs(temp_dir)
136
+
137
+ temp_file_path = os.path.join(temp_dir, file.filename)
138
+ with open(temp_file_path, "wb") as buffer:
139
+ buffer.write(await file.read())
140
+
141
+ logger.info(f"Arquivo '{file.filename}' recebido.")
142
+ audio_path = temp_file_path
143
+
144
+ elif url:
145
+ # Lógica para download de URL
146
+ logger.info(f"Recebida URL para classificação: {url}")
147
+ audio_path = download_audio_from_url(url)
148
+ temp_file_path = audio_path # Marcar para exclusão posterior
149
+
150
+ else:
151
+ raise HTTPException(status_code=400, detail="Nenhum arquivo ou URL fornecido.")
152
+
153
+ # Processamento e classificação do áudio
154
+ scaled_features = process_audio_file(audio_path)
155
+ if scaled_features is None:
156
+ raise HTTPException(status_code=400, detail="O áudio é muito curto ou não pôde ser processado.")
157
+
158
+ prediction = model.predict(scaled_features)
159
+ probability = model.predict_proba(scaled_features)
160
+
161
+ # Converter numpy types para Python nativos
162
+
163
+ label_idx = int(prediction[0]) # Converte numpy.int32 para int Python
164
+ label_str = 'IA' if label_idx == 1 else 'REAL'
165
+ prob_value = float(probability[0][label_idx]) # Converte numpy.float64 para float Python
166
+
167
+ return {"label": label_str, "probability": prob_value}
168
+
169
+ except (ValueError, ConnectionError, FileNotFoundError) as e:
170
+ raise HTTPException(status_code=400, detail=str(e))
171
  except Exception as e:
172
+ logger.error(f"Erro inesperado durante a classificação: {e}")
173
+ raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {e}")
174
+ finally:
175
+ # Limpeza do arquivo temporário
176
+ if temp_file_path and os.path.exists(temp_file_path):
177
+ os.remove(temp_file_path)
178
+ logger.info(f"Arquivo temporário '{temp_file_path}' removido.")
179
 
180
+
181
+ # --- 5. ROTA DE SAÚDE ---
182
  @app.get("/", response_class=FileResponse)
183
  def home():
184
+ """Serve a página inicial da aplicação."""
185
+ return "./static/index.html"
186
+
187
+ if __name__ == "__main__":
188
+ uvicorn.run(app, host="0.0.0.0", 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
  });