Spaces:
Sleeping
Sleeping
Commit ·
e3bdc52
0
Parent(s):
Cleanup: Repositório otimizado (código + dashboard apenas)
Browse files- .dockerignore +12 -0
- .gitattributes +7 -0
- .gitignore +33 -0
- .vercelignore +12 -0
- ConfereAI_FastTrain_Colab.ipynb +189 -0
- Dockerfile +26 -0
- README.md +58 -0
- agent.md +21 -0
- dashboard/admin.html +139 -0
- dashboard/app.js +213 -0
- dashboard/assets/logo_base64.txt +3 -0
- dashboard/how-it-works.css +154 -0
- dashboard/index.html +0 -0
- dashboard/js/admin.js +178 -0
- dashboard/style.css +248 -0
- embed_logo.py +27 -0
- execution/__init__.py +1 -0
- execution/colab_training_script.py +94 -0
- execution/ensemble_manager.py +62 -0
- execution/fastapi_server.py +215 -0
- execution/feature_extractor.py +57 -0
- execution/inference_ast.py +75 -0
- execution/inference_wav2vec.py +122 -0
- execution/metadata_extractor.py +26 -0
- execution/train_wav2vec.py +136 -0
- main.py +12 -0
- package.json +8 -0
- requirements.txt +17 -0
- superpowers +1 -0
- vercel.json +16 -0
.dockerignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ignorar pastas densas que já estão no repo local
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
__pycache__/
|
| 5 |
+
.tmp/
|
| 6 |
+
.git/
|
| 7 |
+
.env
|
| 8 |
+
|
| 9 |
+
# Outros
|
| 10 |
+
*.wav
|
| 11 |
+
*.mp3
|
| 12 |
+
*.log
|
.gitattributes
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
local_finetuned_model/model.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
logo_base64.txt filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Venv
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
env/
|
| 5 |
+
|
| 6 |
+
# Python caching
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
|
| 11 |
+
# Temporary files
|
| 12 |
+
.tmp/
|
| 13 |
+
*.wav
|
| 14 |
+
*.mp3
|
| 15 |
+
*.png
|
| 16 |
+
*.jpg
|
| 17 |
+
*.jpeg
|
| 18 |
+
|
| 19 |
+
# Environments
|
| 20 |
+
.env
|
| 21 |
+
.flaskenv
|
| 22 |
+
|
| 23 |
+
# Models
|
| 24 |
+
local_finetuned_model/
|
| 25 |
+
*.safetensors
|
| 26 |
+
*.bin
|
| 27 |
+
*.h5
|
| 28 |
+
*.pt
|
| 29 |
+
*.onnx
|
| 30 |
+
|
| 31 |
+
# OS
|
| 32 |
+
.DS_Store
|
| 33 |
+
Thumbs.db
|
.vercelignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ignora arquivos de backend e ML para o Vercel não tentar buildar Python
|
| 2 |
+
execution/
|
| 3 |
+
directives/
|
| 4 |
+
requirements.txt
|
| 5 |
+
Dockerfile
|
| 6 |
+
Procfile
|
| 7 |
+
main.py
|
| 8 |
+
venv/
|
| 9 |
+
.tmp/
|
| 10 |
+
.env
|
| 11 |
+
# Ignora caches
|
| 12 |
+
__pycache__/
|
ConfereAI_FastTrain_Colab.ipynb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 0,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"colab": {
|
| 6 |
+
"provenance": [],
|
| 7 |
+
"gpuType": "T4"
|
| 8 |
+
},
|
| 9 |
+
"kernelspec": {
|
| 10 |
+
"name": "python3",
|
| 11 |
+
"display_name": "Python 3"
|
| 12 |
+
},
|
| 13 |
+
"language_info": {
|
| 14 |
+
"name": "python"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"cells": [
|
| 18 |
+
{
|
| 19 |
+
"cell_type": "markdown",
|
| 20 |
+
"metadata": {
|
| 21 |
+
"id": "header"
|
| 22 |
+
},
|
| 23 |
+
"source": [
|
| 24 |
+
"# 🚀 ConfereAI - Fast Training (GPU Edition)\n",
|
| 25 |
+
"Este notebook permite treinar o motor neural do ConfereAI utilizando a GPU gratuita do Google Colab. \n",
|
| 26 |
+
"\n",
|
| 27 |
+
"**Instruções:**\n",
|
| 28 |
+
"1. Vá em `Ambiente de Execução` > `Alterar tipo de ambiente` e selecione **T4 GPU**.\n",
|
| 29 |
+
"2. Preencha as configurações abaixo.\n",
|
| 30 |
+
"3. Execute as células em ordem."
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"cell_type": "code",
|
| 35 |
+
"execution_count": null,
|
| 36 |
+
"metadata": {
|
| 37 |
+
"id": "setup"
|
| 38 |
+
},
|
| 39 |
+
"outputs": [],
|
| 40 |
+
"source": [
|
| 41 |
+
"# @title 1. Instalar Dependências\n",
|
| 42 |
+
"!pip install -q transformers[torch] librosa soundfile huggingface_hub accelerate"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"cell_type": "code",
|
| 47 |
+
"execution_count": null,
|
| 48 |
+
"metadata": {
|
| 49 |
+
"id": "config"
|
| 50 |
+
},
|
| 51 |
+
"outputs": [],
|
| 52 |
+
"source": [
|
| 53 |
+
"# @title 2. Configurações do Hugging Face\n",
|
| 54 |
+
"HF_TOKEN = \"\" # @param {type:\"string\"}\n",
|
| 55 |
+
"REPO_ID = \"TEDDyx86/confereai-dev\" # @param {type:\"string\"}\n",
|
| 56 |
+
"BRANCH = \"main\" # @param {type:\"string\"}\n",
|
| 57 |
+
"\n",
|
| 58 |
+
"from huggingface_hub import HfApi, login\n",
|
| 59 |
+
"if HF_TOKEN:\n",
|
| 60 |
+
" login(token=HF_TOKEN)\n",
|
| 61 |
+
"else:\n",
|
| 62 |
+
" print(\"❌ Por favor, insira o seu HF_TOKEN!\")"
|
| 63 |
+
]
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"cell_type": "code",
|
| 67 |
+
"execution_count": null,
|
| 68 |
+
"metadata": {
|
| 69 |
+
"id": "upload"
|
| 70 |
+
},
|
| 71 |
+
"outputs": [],
|
| 72 |
+
"source": [
|
| 73 |
+
"# @title 3. Upload do Dataset (.zip)\n",
|
| 74 |
+
"from google.colab import files\n",
|
| 75 |
+
"import zipfile\n",
|
| 76 |
+
"import os\n",
|
| 77 |
+
"import shutil\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"uploaded = files.upload()\n",
|
| 80 |
+
"dataset_zip = list(uploaded.keys())[0]\n",
|
| 81 |
+
"\n",
|
| 82 |
+
"DATASET_DIR = \"dataset_training\"\n",
|
| 83 |
+
"if os.path.exists(DATASET_DIR): shutil.rmtree(DATASET_DIR)\n",
|
| 84 |
+
"os.makedirs(DATASET_DIR)\n",
|
| 85 |
+
"\n",
|
| 86 |
+
"with zipfile.ZipFile(dataset_zip, 'r') as zip_ref:\n",
|
| 87 |
+
" zip_ref.extractall(DATASET_DIR)\n",
|
| 88 |
+
"\n",
|
| 89 |
+
"print(f\"✅ Dataset extraído em: {DATASET_DIR}\")"
|
| 90 |
+
]
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"cell_type": "code",
|
| 94 |
+
"execution_count": null,
|
| 95 |
+
"metadata": {
|
| 96 |
+
"id": "training"
|
| 97 |
+
},
|
| 98 |
+
"outputs": [],
|
| 99 |
+
"source": [
|
| 100 |
+
"# @title 4. Executar Treinamento (Fine-Tuning)\n",
|
| 101 |
+
"import torch\n",
|
| 102 |
+
"from torch.utils.data import Dataset\n",
|
| 103 |
+
"from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2ForSequenceClassification, Trainer, TrainingArguments\n",
|
| 104 |
+
"import librosa\n",
|
| 105 |
+
"\n",
|
| 106 |
+
"BASE_MODEL = \"HyperMoon/wav2vec2-base-960h-finetuned-deepfake\"\n",
|
| 107 |
+
"OUTPUT_DIR = \"local_finetuned_model\"\n",
|
| 108 |
+
"\n",
|
| 109 |
+
"class DeepfakeDataset(Dataset):\n",
|
| 110 |
+
" def __init__(self, root_dir, processor):\n",
|
| 111 |
+
" self.files = []\n",
|
| 112 |
+
" self.processor = processor\n",
|
| 113 |
+
" for label, folder in enumerate(['real', 'fake']):\n",
|
| 114 |
+
" path = os.path.join(root_dir, folder)\n",
|
| 115 |
+
" if os.path.exists(path):\n",
|
| 116 |
+
" for f in os.listdir(path):\n",
|
| 117 |
+
" if f.endswith(('.wav', '.mp3', '.flac')):\n",
|
| 118 |
+
" self.files.append({\"path\": os.path.join(path, f), \"label\": label})\n",
|
| 119 |
+
"\n",
|
| 120 |
+
" def __len__(self): return len(self.files)\n",
|
| 121 |
+
" def __getitem__(self, idx):\n",
|
| 122 |
+
" item = self.files[idx]\n",
|
| 123 |
+
" speech, _ = librosa.load(item[\"path\"], sr=16000)\n",
|
| 124 |
+
" input_values = self.processor(speech, sampling_rate=16000, return_tensors=\"pt\", padding=\"max_length\", max_length=160000, truncation=True).input_values[0]\n",
|
| 125 |
+
" return {\"input_values\": input_values, \"labels\": torch.tensor(item[\"label\"], dtype=torch.long)}\n",
|
| 126 |
+
"\n",
|
| 127 |
+
"processor = Wav2Vec2FeatureExtractor.from_pretrained(BASE_MODEL)\n",
|
| 128 |
+
"model = Wav2Vec2ForSequenceClassification.from_pretrained(BASE_MODEL, num_labels=2, ignore_mismatched_sizes=True)\n",
|
| 129 |
+
"\n",
|
| 130 |
+
"# Congelar base para focar no aprendizado das novas fraudes (Lógica Robusta)\n",
|
| 131 |
+
"if hasattr(model, 'freeze_feature_extractor'):\n",
|
| 132 |
+
" model.freeze_feature_extractor()\n",
|
| 133 |
+
"elif hasattr(model, 'freeze_feature_encoder'):\n",
|
| 134 |
+
" model.freeze_feature_encoder()\n",
|
| 135 |
+
"\n",
|
| 136 |
+
"if hasattr(model, 'wav2vec2'):\n",
|
| 137 |
+
" for param in model.wav2vec2.parameters(): param.requires_grad = False\n",
|
| 138 |
+
"\n",
|
| 139 |
+
"training_args = TrainingArguments(\n",
|
| 140 |
+
" output_dir=\"./results\",\n",
|
| 141 |
+
" num_train_epochs=5,\n",
|
| 142 |
+
" per_device_train_batch_size=4,\n",
|
| 143 |
+
" gradient_accumulation_steps=2,\n",
|
| 144 |
+
" learning_rate=2e-5,\n",
|
| 145 |
+
" logging_steps=1,\n",
|
| 146 |
+
" push_to_hub=False,\n",
|
| 147 |
+
" report_to=\"none\"\n",
|
| 148 |
+
")\n",
|
| 149 |
+
"\n",
|
| 150 |
+
"trainer = Trainer(\n",
|
| 151 |
+
" model=model,\n",
|
| 152 |
+
" args=training_args,\n",
|
| 153 |
+
" train_dataset=DeepfakeDataset(DATASET_DIR, processor)\n",
|
| 154 |
+
")\n",
|
| 155 |
+
"\n",
|
| 156 |
+
"print(\"🚀 Iniciando treinamento na GPU...\")\n",
|
| 157 |
+
"trainer.train()\n",
|
| 158 |
+
"\n",
|
| 159 |
+
"model.save_pretrained(OUTPUT_DIR)\n",
|
| 160 |
+
"processor.save_pretrained(OUTPUT_DIR)\n",
|
| 161 |
+
"print(f\"✅ Treinamento concluído. Modelo salvo em {OUTPUT_DIR}\")"
|
| 162 |
+
]
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"cell_type": "code",
|
| 166 |
+
"execution_count": null,
|
| 167 |
+
"metadata": {
|
| 168 |
+
"id": "push"
|
| 169 |
+
},
|
| 170 |
+
"outputs": [],
|
| 171 |
+
"source": [
|
| 172 |
+
"# @title 5. Sincronizar com Hugging Face Space\n",
|
| 173 |
+
"api = HfApi()\n",
|
| 174 |
+
"print(f\"📦 Subindo modelo para {REPO_ID}...\")\n",
|
| 175 |
+
"\n",
|
| 176 |
+
"api.upload_folder(\n",
|
| 177 |
+
" folder_path=OUTPUT_DIR,\n",
|
| 178 |
+
" path_in_repo=OUTPUT_DIR,\n",
|
| 179 |
+
" repo_id=REPO_ID,\n",
|
| 180 |
+
" repo_type=\"space\",\n",
|
| 181 |
+
" token=HF_TOKEN,\n",
|
| 182 |
+
" commit_message=\"🤖 Auto-Update: Novo modelo treinado via Google Colab\"\n",
|
| 183 |
+
")\n",
|
| 184 |
+
"\n",
|
| 185 |
+
"print(\"✨ Sucesso! O seu Space irá reiniciar em breve com o novo modelo.\")"
|
| 186 |
+
]
|
| 187 |
+
}
|
| 188 |
+
]
|
| 189 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
# Instala dependências do sistema
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
libsndfile1 \
|
| 6 |
+
ffmpeg \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Copia arquivos de requisitos
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# Copia o resto do código
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
# Garante que a pasta .tmp existe e tem permissão
|
| 19 |
+
RUN mkdir -p .tmp && chmod 777 .tmp
|
| 20 |
+
|
| 21 |
+
# Porta padrão do Hugging Face Spaces
|
| 22 |
+
ENV PORT=7860
|
| 23 |
+
EXPOSE 7860
|
| 24 |
+
|
| 25 |
+
# Comando para iniciar o servidor
|
| 26 |
+
CMD ["python", "main.py"]
|
README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ConfereAI - Audio Fraud Detection (V2.2)
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 🛡️ CONFEREAI
|
| 12 |
+
### *Verdade na voz, integridade no som.*
|
| 13 |
+
|
| 14 |
+
O **ConfereAI** é uma plataforma de segurança cibernética de última geração projetada para identificar e neutralizar fraudes de áudio, deepfakes e vozes clonadas via Inteligência Artificial. Utilizando uma arquitetura de redes neurais profundas, o sistema analisa micro-imperfeições acústicas imperceptíveis ao ouvido humano.
|
| 15 |
+
|
| 16 |
+

|
| 17 |
+
|
| 18 |
+
## 🚀 Diferenciais Tecnológicos
|
| 19 |
+
|
| 20 |
+
- **🧠 Motor Neural Local**: Diferente de soluções que dependem de APIs instáveis, o ConfereAI utiliza um motor dedicado baseado em **Wav2Vec 2.0** (HyperMoon) rodando localmente no servidor.
|
| 21 |
+
- **📊 Evidência Espectral**: Gera espectrogramas de Mel em tempo real, permitindo uma análise forense visual das frequências de áudio.
|
| 22 |
+
- **⚡ Resposta Instantânea**: Análise completa em segundos, ideal para validação de identidade e prevenção de fraudes em tempo real.
|
| 23 |
+
- **💎 Interface Onyx**: Dashboard premium com Estética Onyx e Glassmorphism, focado em clareza e experiência do usuário (UX).
|
| 24 |
+
|
| 25 |
+
## 🛠️ Arquitetura de Software
|
| 26 |
+
|
| 27 |
+
O sistema é dividido em duas camadas principais:
|
| 28 |
+
|
| 29 |
+
1. **Backend (Python/FastAPI)**:
|
| 30 |
+
- Gerenciamento de arquivos e processamento paralelo.
|
| 31 |
+
- Extração de características com `Librosa`.
|
| 32 |
+
- Inferência neural via `PyTorch` e `Transformers`.
|
| 33 |
+
2. **Frontend (Vanilla JS/CSS)**:
|
| 34 |
+
- Interface ultra-responsiva sem dependências pesadas.
|
| 35 |
+
- Visualização dinâmica de resultados e medidores de confiança neon.
|
| 36 |
+
|
| 37 |
+
## 🔬 O Coração da IA: HyperMoon Engine
|
| 38 |
+
|
| 39 |
+
Utilizamos o modelo **HyperMoon/wav2vec2-base-960h-finetuned-deepfake**, treinado com o dataset acadêmico **ASVspoof**.
|
| 40 |
+
- **Foco**: Detecção de descontinuidades rítmicas e artefatos de compressão típicos de IAs generativas.
|
| 41 |
+
- **Veredito**: Entrega um score de probabilidade (0% a 100%) e um veredito direto: **AUTÊNTICO** ou **FRAUDE DETECTADA**.
|
| 42 |
+
|
| 43 |
+
## 📦 Como Rodar o Projeto
|
| 44 |
+
|
| 45 |
+
### Localmente (Docker)
|
| 46 |
+
```bash
|
| 47 |
+
docker build -t confereai .
|
| 48 |
+
docker run -p 7860:7860 confereai
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### Deploy no Hugging Face Spaces
|
| 52 |
+
1. Crie um novo **Space** no Hugging Face.
|
| 53 |
+
2. Selecione o SDK: **Docker**.
|
| 54 |
+
3. Faça o push deste repositório.
|
| 55 |
+
4. O sistema irá buildar e servir automaticamente na porta 7860.
|
| 56 |
+
|
| 57 |
+
---
|
| 58 |
+
**CONFEREAI** - *Protegendo a integridade da comunicação humana na era da IA.*
|
agent.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Diretrizes do Agente IA (Antigravity)
|
| 2 |
+
|
| 3 |
+
Ao trabalhar neste projeto (ConfereAI), você (a IA) DEVE obrigatoriamente referenciar e seguir as diretrizes/metodologias listadas na pasta `superpowers/skills` conforme o contexto da tarefa em mãos.
|
| 4 |
+
|
| 5 |
+
Sempre que o Humano solicitar uma alteração ou criação, verifique qual skill se aplica e aja rigorosamente de acordo com ela:
|
| 6 |
+
|
| 7 |
+
## 1. Regra Geral de Inicialização
|
| 8 |
+
- NUNCA comece a escrever código ou desenhar componentes para uma nova feature sem antes consultar e executar o processo iterativo definido em `superpowers/skills/brainstorming/SKILL.md`. O Humano deve aprovar a ideia antes do código nascer.
|
| 9 |
+
|
| 10 |
+
## 2. Para Tarefas de Machine Learning (Motor)
|
| 11 |
+
- A precisão matemática é inegociável. Para qualquer script de processamento de dados, manipulação de tensores ou modelo em si, leia e aplique OBRIGATORIAMENTE o fluxo de `superpowers/skills/test-driven-development/SKILL.md`.
|
| 12 |
+
- Se o Humano relatar anomalias no treinamento (loss não cai, acurácia baixa) ou na inferência, NÃO altere nada sem antes invocar o `superpowers/skills/systematic-debugging/SKILL.md` para isolar a causa-raiz cientificamente.
|
| 13 |
+
- Quando uma etapa crucial do modelo for finalizada, exija as provas estabelecidas em `superpowers/skills/verification-before-completion/SKILL.md`.
|
| 14 |
+
|
| 15 |
+
## 3. Para Tarefas de Frontend (Dashboard UI/UX)
|
| 16 |
+
- Antes de alterar layouts, reescrever CSS ou criar novos componentes globais, execute os passos em `superpowers/skills/writing-plans/SKILL.md`. Mostre o plano (passo a passo de 2 a 5 minutos) ao Humano e espere aprovação.
|
| 17 |
+
- Se a refatoração for de alto impacto, siga o `superpowers/skills/using-git-worktrees/SKILL.md` para criar um ambiente isolado de testes que proteja o dashboard atual.
|
| 18 |
+
- Para manter a harmonia do Design System, crie o hábito de acionar o `superpowers/skills/requesting-code-review/SKILL.md` ao final de cada etapa visual, passando a bola para o Humano aprovar a usabilidade.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
**Nota para o Agente:** Este arquivo é a sua espinha dorsal neste repositório. Confie nas metodologias do diretório *superpowers/skills* em detrimento de abordagens mais fáceis e desestruturadas.
|
dashboard/admin.html
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="pt-BR">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>ConfereAI Admin | Fine-Tuning</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@700;900&display=swap" rel="stylesheet">
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<style>
|
| 10 |
+
.hidden { display: none !important; }
|
| 11 |
+
|
| 12 |
+
/* Admin specific styles */
|
| 13 |
+
.admin-container {
|
| 14 |
+
max-width: 600px;
|
| 15 |
+
margin: 0 auto;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.form-group {
|
| 19 |
+
margin-bottom: 1.5rem;
|
| 20 |
+
text-align: left;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.form-group label {
|
| 24 |
+
display: block;
|
| 25 |
+
margin-bottom: 0.5rem;
|
| 26 |
+
color: var(--text-secondary);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.form-control {
|
| 30 |
+
width: 100%;
|
| 31 |
+
padding: 1rem;
|
| 32 |
+
background: rgba(0,0,0,0.3);
|
| 33 |
+
border: 1px solid var(--glass-border);
|
| 34 |
+
border-radius: 8px;
|
| 35 |
+
color: white;
|
| 36 |
+
font-family: 'Inter', sans-serif;
|
| 37 |
+
transition: border-color 0.3s;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.form-control:focus {
|
| 41 |
+
outline: none;
|
| 42 |
+
border-color: var(--accent);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.progress-bar-container {
|
| 46 |
+
width: 100%;
|
| 47 |
+
height: 20px;
|
| 48 |
+
background: rgba(0,0,0,0.3);
|
| 49 |
+
border-radius: 10px;
|
| 50 |
+
overflow: hidden;
|
| 51 |
+
margin-top: 1rem;
|
| 52 |
+
border: 1px solid var(--glass-border);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.progress-bar {
|
| 56 |
+
height: 100%;
|
| 57 |
+
background: linear-gradient(90deg, var(--primary), var(--cyan));
|
| 58 |
+
width: 0%;
|
| 59 |
+
transition: width 0.5s ease-in-out;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.status-text {
|
| 63 |
+
margin-top: 1rem;
|
| 64 |
+
font-size: 0.9rem;
|
| 65 |
+
color: var(--text-secondary);
|
| 66 |
+
}
|
| 67 |
+
</style>
|
| 68 |
+
</head>
|
| 69 |
+
<body>
|
| 70 |
+
<div class="aurora-mesh"></div>
|
| 71 |
+
|
| 72 |
+
<nav>
|
| 73 |
+
<div class="logo">
|
| 74 |
+
<a href="index.html" style="text-decoration: none; display: flex; align-items: center; gap: 12px;">
|
| 75 |
+
<span>Confere<span class="vibrance">AI</span> Admin</span>
|
| 76 |
+
</a>
|
| 77 |
+
</div>
|
| 78 |
+
<div class="nav-links">
|
| 79 |
+
<a href="index.html">Voltar ao App</a>
|
| 80 |
+
</div>
|
| 81 |
+
</nav>
|
| 82 |
+
|
| 83 |
+
<main>
|
| 84 |
+
<!-- Login Section -->
|
| 85 |
+
<section id="login-section" class="admin-container">
|
| 86 |
+
<div class="glass-card" style="text-align: center;">
|
| 87 |
+
<h2 style="font-family: 'Outfit'; font-size: 2rem; margin-bottom: 1rem;">Acesso Restrito</h2>
|
| 88 |
+
<p style="color: var(--text-secondary); margin-bottom: 2rem;">Insira a senha de administrador para gerenciar o aprendizado do motor neural.</p>
|
| 89 |
+
|
| 90 |
+
<form id="login-form">
|
| 91 |
+
<div class="form-group">
|
| 92 |
+
<input type="password" id="admin-password" class="form-control" placeholder="Senha do Admin" required>
|
| 93 |
+
</div>
|
| 94 |
+
<button type="submit" class="btn-primary" style="width: 100%;">Entrar</button>
|
| 95 |
+
</form>
|
| 96 |
+
<div id="login-error" class="status-text hidden" style="color: var(--danger);">Senha incorreta.</div>
|
| 97 |
+
</div>
|
| 98 |
+
</section>
|
| 99 |
+
|
| 100 |
+
<!-- Dashboard Section -->
|
| 101 |
+
<section id="dashboard-section" class="admin-container hidden">
|
| 102 |
+
<div class="glass-card" style="text-align: center;">
|
| 103 |
+
<h2 style="font-family: 'Outfit'; font-size: 2rem; margin-bottom: 1rem;">Treinar Modelo</h2>
|
| 104 |
+
<p style="color: var(--text-secondary); margin-bottom: 2rem;">Faça upload de um arquivo .zip ou .rar contendo pastas 'real' e 'fake' com áudios (.mp3, .wav, .flac).</p>
|
| 105 |
+
|
| 106 |
+
<div id="drop-zone" style="padding: 3rem; border-radius: 12px; margin-bottom: 2rem;">
|
| 107 |
+
<div class="upload-icon">
|
| 108 |
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 109 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
| 110 |
+
<polyline points="17 8 12 3 7 8"></polyline>
|
| 111 |
+
<line x1="12" y1="3" x2="12" y2="15"></line>
|
| 112 |
+
</svg>
|
| 113 |
+
</div>
|
| 114 |
+
<h3 style="margin-bottom: 10px;">Arraste o arquivo ou clique</h3>
|
| 115 |
+
<p style="color: var(--text-secondary); font-size: 0.9rem;">Limite: 50MB (max 5 arquivos recomendados por lote)</p>
|
| 116 |
+
<input type="file" id="file-input" class="hidden" accept=".zip,.rar">
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<div id="selected-file-info" class="hidden" style="margin-bottom: 1.5rem; color: var(--success); font-weight: 500;">
|
| 120 |
+
Arquivo selecionado: <span id="filename-display"></span>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<button id="btn-upload-train" class="btn-primary" style="width: 100%;" disabled>Iniciar Upload e Treinamento</button>
|
| 124 |
+
|
| 125 |
+
<!-- Training Progress -->
|
| 126 |
+
<div id="training-progress-container" class="hidden" style="margin-top: 2rem;">
|
| 127 |
+
<h4 style="color: var(--cyan);">Status do Treinamento</h4>
|
| 128 |
+
<div class="progress-bar-container">
|
| 129 |
+
<div id="training-progress-bar" class="progress-bar"></div>
|
| 130 |
+
</div>
|
| 131 |
+
<div id="training-status-text" class="status-text">Preparando ambiente...</div>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</section>
|
| 135 |
+
</main>
|
| 136 |
+
|
| 137 |
+
<script src="js/admin.js"></script>
|
| 138 |
+
</body>
|
| 139 |
+
</html>
|
dashboard/app.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const dropZone = document.getElementById('drop-zone');
|
| 2 |
+
const audioInput = document.getElementById('audio-input');
|
| 3 |
+
const selectBtn = document.getElementById('select-file-btn');
|
| 4 |
+
const resultsSection = document.getElementById('results-section');
|
| 5 |
+
const verdictText = document.getElementById('verdict-text');
|
| 6 |
+
const confidenceValue = document.getElementById('confidence-value');
|
| 7 |
+
const confidencePath = document.getElementById('confidence-path');
|
| 8 |
+
const specContainer = document.getElementById('spec-container');
|
| 9 |
+
const verdictExplanation = document.getElementById('verdict-explanation');
|
| 10 |
+
|
| 11 |
+
// Event Listeners
|
| 12 |
+
selectBtn.addEventListener('click', () => audioInput.click());
|
| 13 |
+
|
| 14 |
+
audioInput.addEventListener('change', (e) => {
|
| 15 |
+
if (e.target.files.length) handleUpload(e.target.files[0]);
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
dropZone.addEventListener('dragover', (e) => {
|
| 19 |
+
e.preventDefault();
|
| 20 |
+
dropZone.classList.add('dragover');
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
dropZone.addEventListener('dragleave', () => {
|
| 24 |
+
dropZone.classList.remove('dragover');
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
dropZone.addEventListener('drop', (e) => {
|
| 28 |
+
e.preventDefault();
|
| 29 |
+
dropZone.classList.remove('dragover');
|
| 30 |
+
if (e.dataTransfer.files.length) handleUpload(e.dataTransfer.files[0]);
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
async function handleUpload(file) {
|
| 34 |
+
// Detecta se estamos rodando localmente ou no Hugging Face
|
| 35 |
+
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
| 36 |
+
|
| 37 |
+
// Se você estiver no Vercel, mude '' para a URL do seu Space no Hugging Face
|
| 38 |
+
const API_URL = ''; // Usa o host atual (mesma porta)
|
| 39 |
+
// Reset e mostra seção de resultados
|
| 40 |
+
resultsSection.style.display = 'grid';
|
| 41 |
+
verdictText.textContent = 'PROCESSANDO...';
|
| 42 |
+
if (verdictExplanation) verdictExplanation.textContent = '';
|
| 43 |
+
confidenceValue.textContent = '0%';
|
| 44 |
+
confidencePath.setAttribute('stroke-dasharray', '0, 100');
|
| 45 |
+
specContainer.innerHTML = '<p>Analisando frequências...</p>';
|
| 46 |
+
|
| 47 |
+
const formData = new FormData();
|
| 48 |
+
formData.append('file', file);
|
| 49 |
+
|
| 50 |
+
try {
|
| 51 |
+
const response = await fetch(`${API_URL}/analyze`, {
|
| 52 |
+
method: 'POST',
|
| 53 |
+
body: formData
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
const data = await response.json();
|
| 57 |
+
displayResults(data);
|
| 58 |
+
} catch (error) {
|
| 59 |
+
console.error('Erro na análise:', error);
|
| 60 |
+
verdictText.innerText = 'ERRO NA CONEXÃO';
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function displayResults(data) {
|
| 65 |
+
console.log('Resultados recebidos:', data);
|
| 66 |
+
|
| 67 |
+
// Atualiza veredito
|
| 68 |
+
const isSpoof = data.verdict === 'SPOOF';
|
| 69 |
+
verdictText.textContent = isSpoof ? ' FRAUDE DETECTADA' : ' ÁUDIO AUTÊNTICO';
|
| 70 |
+
verdictText.style.color = isSpoof ? '#EF4444' : '#10B981';
|
| 71 |
+
|
| 72 |
+
// Atualiza explicação do veredito (Consenso dos Motores)
|
| 73 |
+
/*
|
| 74 |
+
if (verdictExplanation) {
|
| 75 |
+
verdictExplanation.textContent = data.engines_consensus || '';
|
| 76 |
+
verdictExplanation.style.color = isSpoof ? '#FCA5A5' : '#6EE7B7';
|
| 77 |
+
}
|
| 78 |
+
*/
|
| 79 |
+
verdictText.style.color = isSpoof ? '#EF4444' : '#10B981';
|
| 80 |
+
|
| 81 |
+
// Atualiza ponto de pulso
|
| 82 |
+
const pulseDot = document.querySelector('.pulse');
|
| 83 |
+
if (pulseDot) {
|
| 84 |
+
pulseDot.style.background = isSpoof ? '#EF4444' : '#10B981';
|
| 85 |
+
pulseDot.style.boxShadow = `0 0 10px ${isSpoof ? '#EF4444' : '#10B981'}`;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Agora mostramos a PROBABILIDADE DE FRAUDE no círculo, pois é o que importa para o usuário
|
| 89 |
+
const fraudProb = Math.round((data.fraud_score || 0) * 100);
|
| 90 |
+
console.log('Calculated Fraud Prob:', fraudProb);
|
| 91 |
+
|
| 92 |
+
if (confidenceValue) {
|
| 93 |
+
confidenceValue.textContent = `${fraudProb}%`;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if (confidencePath) {
|
| 97 |
+
// Cor do círculo baseada no risco
|
| 98 |
+
if (fraudProb > 80) {
|
| 99 |
+
confidencePath.style.stroke = '#EF4444'; // Vermelho (Perigo)
|
| 100 |
+
if (pulseDot) pulseDot.style.background = '#EF4444';
|
| 101 |
+
} else if (fraudProb > 40) {
|
| 102 |
+
confidencePath.style.stroke = '#F59E0B'; // Amarelo (Atenção)
|
| 103 |
+
if (pulseDot) pulseDot.style.background = '#F59E0B';
|
| 104 |
+
} else {
|
| 105 |
+
confidencePath.style.stroke = '#10B981'; // Verde (Seguro)
|
| 106 |
+
if (pulseDot) pulseDot.style.background = '#10B981';
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Animação do círculo
|
| 110 |
+
confidencePath.setAttribute('stroke-dasharray', `${fraudProb}, 100`);
|
| 111 |
+
}
|
| 112 |
+
// Atualiza Espectrograma
|
| 113 |
+
// Atualiza Espectrograma e Heatmap (XAI)
|
| 114 |
+
if (data.spectrogram_url) {
|
| 115 |
+
const specName = data.spectrogram_url.split(/[\\/]/).pop();
|
| 116 |
+
const timestamp = new Date().getTime();
|
| 117 |
+
|
| 118 |
+
let heatmapHtml = '<div class="heatmap-overlay">';
|
| 119 |
+
if (data.temporal_scores && data.temporal_scores.length > 0) {
|
| 120 |
+
data.temporal_scores.forEach(score => {
|
| 121 |
+
// Interpola cor entre verde (seguro) e vermelho (fraude)
|
| 122 |
+
// Usando HSL: 120 (verde) a 0 (vermelho)
|
| 123 |
+
const hue = 120 - (score * 120);
|
| 124 |
+
const opacity = score > 0.4 ? (score * 0.7) : (score * 0.2);
|
| 125 |
+
heatmapHtml += `<div class="heatmap-segment" style="background: hsla(${hue}, 100%, 50%, ${opacity})"></div>`;
|
| 126 |
+
});
|
| 127 |
+
}
|
| 128 |
+
heatmapHtml += '</div>';
|
| 129 |
+
|
| 130 |
+
specContainer.innerHTML = `
|
| 131 |
+
<div class="spec-wrapper">
|
| 132 |
+
<img src="/tmp/${specName}?t=${timestamp}" alt="Espectrograma de Mel">
|
| 133 |
+
${heatmapHtml}
|
| 134 |
+
</div>
|
| 135 |
+
`;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Scroll automático suave para os resultados
|
| 139 |
+
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 140 |
+
|
| 141 |
+
// Atualiza Diagnóstico
|
| 142 |
+
updateDiagnostics(data);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
function updateDiagnostics(data) {
|
| 146 |
+
const diagSection = document.getElementById('diagnostic-section');
|
| 147 |
+
const toggleBtn = document.getElementById('toggle-diagnostic');
|
| 148 |
+
const details = document.getElementById('diagnostic-details');
|
| 149 |
+
|
| 150 |
+
if (!diagSection) return;
|
| 151 |
+
|
| 152 |
+
diagSection.style.display = 'block';
|
| 153 |
+
|
| 154 |
+
const w2vScore = Math.round((data.wav2vec_score || 0) * 100);
|
| 155 |
+
const astScore = Math.round((data.ast_score || 0) * 100);
|
| 156 |
+
|
| 157 |
+
// Atualiza valores e barras com delay para animação
|
| 158 |
+
setTimeout(() => {
|
| 159 |
+
document.getElementById('w2v-val').textContent = `${w2vScore}%`;
|
| 160 |
+
document.getElementById('ast-val').textContent = `${astScore}%`;
|
| 161 |
+
document.getElementById('w2v-bar').style.width = `${w2vScore}%`;
|
| 162 |
+
document.getElementById('ast-bar').style.width = `${astScore}%`;
|
| 163 |
+
document.getElementById('rigor-logic').textContent = data.engines_consensus || 'Padrão';
|
| 164 |
+
}, 100);
|
| 165 |
+
|
| 166 |
+
// Toggle behavior
|
| 167 |
+
if (toggleBtn && !toggleBtn.dataset.hasListener) {
|
| 168 |
+
toggleBtn.addEventListener('click', () => {
|
| 169 |
+
const isHidden = details.style.display === 'none';
|
| 170 |
+
details.style.display = isHidden ? 'block' : 'none';
|
| 171 |
+
toggleBtn.textContent = isHidden ? 'Esconder' : 'Ver Detalhes';
|
| 172 |
+
|
| 173 |
+
if (isHidden) {
|
| 174 |
+
details.style.animation = 'fadeInUp 0.5s forwards';
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
toggleBtn.dataset.hasListener = "true";
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Lógica do Modal "Como Funciona" (Overlay)
|
| 182 |
+
const modal = document.getElementById('how-it-works-modal');
|
| 183 |
+
const openBtn = document.getElementById('open-how-it-works');
|
| 184 |
+
const closeBtn = document.getElementById('close-modal');
|
| 185 |
+
|
| 186 |
+
if (openBtn && modal) {
|
| 187 |
+
openBtn.addEventListener('click', (e) => {
|
| 188 |
+
e.preventDefault();
|
| 189 |
+
modal.classList.add('active');
|
| 190 |
+
document.body.style.overflow = 'hidden'; // Trava o scroll
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
if (closeBtn && modal) {
|
| 195 |
+
closeBtn.addEventListener('click', () => {
|
| 196 |
+
modal.classList.remove('active');
|
| 197 |
+
document.body.style.overflow = 'auto'; // Destrava o scroll
|
| 198 |
+
});
|
| 199 |
+
|
| 200 |
+
// Fechar ao clicar fora do conteúdo
|
| 201 |
+
modal.addEventListener('click', (e) => {
|
| 202 |
+
if (e.target === modal) {
|
| 203 |
+
closeBtn.click();
|
| 204 |
+
}
|
| 205 |
+
});
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// Fechar com a tecla ESC
|
| 209 |
+
document.addEventListener('keydown', (e) => {
|
| 210 |
+
if (e.key === 'Escape' && modal && modal.classList.contains('active')) {
|
| 211 |
+
closeBtn.click();
|
| 212 |
+
}
|
| 213 |
+
});
|
dashboard/assets/logo_base64.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c7b6ac9f86b805ee053582e8514ce0161e688ff38bde35e3e0f996dd9e012766
|
| 3 |
+
size 996806
|
dashboard/how-it-works.css
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Modal Overlay */
|
| 2 |
+
.modal-overlay {
|
| 3 |
+
position: fixed;
|
| 4 |
+
top: 0;
|
| 5 |
+
left: 0;
|
| 6 |
+
width: 100%;
|
| 7 |
+
height: 100%;
|
| 8 |
+
background: rgba(0, 0, 0, 0.85);
|
| 9 |
+
backdrop-filter: blur(20px);
|
| 10 |
+
z-index: 1000;
|
| 11 |
+
display: none; /* Escondido por padrão */
|
| 12 |
+
align-items: center;
|
| 13 |
+
justify-content: center;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
opacity: 0;
|
| 16 |
+
transition: opacity 0.4s ease;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.modal-overlay.active {
|
| 20 |
+
display: flex;
|
| 21 |
+
opacity: 1;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.modal-content {
|
| 25 |
+
max-width: 1000px;
|
| 26 |
+
width: 95%;
|
| 27 |
+
max-height: 90vh;
|
| 28 |
+
overflow-y: auto;
|
| 29 |
+
position: relative;
|
| 30 |
+
padding: 60px 40px;
|
| 31 |
+
border: 1px solid rgba(157, 80, 187, 0.3);
|
| 32 |
+
animation: modalSlide 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
@keyframes modalSlide {
|
| 36 |
+
from { transform: scale(0.8) translateY(50px); opacity: 0; }
|
| 37 |
+
to { transform: scale(1) translateY(0); opacity: 1; }
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.close-modal {
|
| 41 |
+
position: absolute;
|
| 42 |
+
top: 20px;
|
| 43 |
+
right: 25px;
|
| 44 |
+
background: none;
|
| 45 |
+
border: none;
|
| 46 |
+
color: #fff;
|
| 47 |
+
font-size: 2.5rem;
|
| 48 |
+
cursor: pointer;
|
| 49 |
+
line-height: 1;
|
| 50 |
+
transition: transform 0.3s, color 0.3s;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.close-modal:hover {
|
| 54 |
+
color: var(--accent);
|
| 55 |
+
transform: rotate(90deg);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.modal-footer {
|
| 59 |
+
margin-top: 50px;
|
| 60 |
+
text-align: center;
|
| 61 |
+
border-top: 1px solid rgba(255,255,255,0.05);
|
| 62 |
+
padding-top: 20px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.modal-footer p {
|
| 66 |
+
color: var(--text-secondary);
|
| 67 |
+
font-size: 0.8rem;
|
| 68 |
+
font-style: italic;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Re-aproveitando os cartões dentro do modal */
|
| 72 |
+
.section-title {
|
| 73 |
+
text-align: center;
|
| 74 |
+
font-family: 'Outfit', sans-serif;
|
| 75 |
+
font-size: 2.5rem;
|
| 76 |
+
margin-bottom: 50px;
|
| 77 |
+
background: linear-gradient(135deg, #fff 0%, #9d50bb 100%);
|
| 78 |
+
-webkit-background-clip: text;
|
| 79 |
+
-webkit-text-fill-color: transparent;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.steps-grid {
|
| 83 |
+
display: grid;
|
| 84 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 85 |
+
gap: 25px;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.step-card {
|
| 89 |
+
padding: 30px;
|
| 90 |
+
background: rgba(255, 255, 255, 0.03);
|
| 91 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 92 |
+
border-radius: 16px;
|
| 93 |
+
position: relative;
|
| 94 |
+
transition: transform 0.3s, border-color 0.3s;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.step-card:hover {
|
| 98 |
+
transform: translateY(-5px);
|
| 99 |
+
border-color: rgba(157, 80, 187, 0.3);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.step-number {
|
| 103 |
+
position: absolute;
|
| 104 |
+
top: 15px;
|
| 105 |
+
right: 20px;
|
| 106 |
+
font-size: 2.5rem;
|
| 107 |
+
font-weight: 900;
|
| 108 |
+
opacity: 0.1;
|
| 109 |
+
color: var(--accent);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.step-icon {
|
| 113 |
+
width: 50px;
|
| 114 |
+
height: 50px;
|
| 115 |
+
background: rgba(157, 80, 187, 0.1);
|
| 116 |
+
border-radius: 10px;
|
| 117 |
+
display: flex;
|
| 118 |
+
align-items: center;
|
| 119 |
+
justify-content: center;
|
| 120 |
+
margin-bottom: 20px;
|
| 121 |
+
color: var(--accent);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.step-card h3 {
|
| 125 |
+
font-size: 1.3rem;
|
| 126 |
+
margin-bottom: 10px;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.step-card p {
|
| 130 |
+
color: var(--text-secondary);
|
| 131 |
+
line-height: 1.5;
|
| 132 |
+
font-size: 0.9rem;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Footer Original */
|
| 136 |
+
.glass-footer {
|
| 137 |
+
padding: 40px 5%;
|
| 138 |
+
border-top: 1px solid var(--glass-border);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.footer-content {
|
| 142 |
+
max-width: 1200px;
|
| 143 |
+
margin: 0 auto;
|
| 144 |
+
display: flex;
|
| 145 |
+
justify-content: space-between;
|
| 146 |
+
align-items: center;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.footer-links a {
|
| 150 |
+
color: var(--text-secondary);
|
| 151 |
+
text-decoration: none;
|
| 152 |
+
margin-left: 20px;
|
| 153 |
+
font-size: 0.85rem;
|
| 154 |
+
}
|
dashboard/index.html
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dashboard/js/admin.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// Elements
|
| 3 |
+
const loginSection = document.getElementById('login-section');
|
| 4 |
+
const dashboardSection = document.getElementById('dashboard-section');
|
| 5 |
+
const loginForm = document.getElementById('login-form');
|
| 6 |
+
const passwordInput = document.getElementById('admin-password');
|
| 7 |
+
const loginError = document.getElementById('login-error');
|
| 8 |
+
|
| 9 |
+
const dropZone = document.getElementById('drop-zone');
|
| 10 |
+
const fileInput = document.getElementById('file-input');
|
| 11 |
+
const selectedFileInfo = document.getElementById('selected-file-info');
|
| 12 |
+
const filenameDisplay = document.getElementById('filename-display');
|
| 13 |
+
const btnUploadTrain = document.getElementById('btn-upload-train');
|
| 14 |
+
|
| 15 |
+
const progressContainer = document.getElementById('training-progress-container');
|
| 16 |
+
const progressBar = document.getElementById('training-progress-bar');
|
| 17 |
+
const statusText = document.getElementById('training-status-text');
|
| 18 |
+
|
| 19 |
+
let currentFile = null;
|
| 20 |
+
let token = null; // JWT ou Token simples para as rotas autenticadas
|
| 21 |
+
let statusInterval = null;
|
| 22 |
+
|
| 23 |
+
// Login Handling
|
| 24 |
+
loginForm.addEventListener('submit', async (e) => {
|
| 25 |
+
e.preventDefault();
|
| 26 |
+
loginError.classList.add('hidden');
|
| 27 |
+
const password = passwordInput.value;
|
| 28 |
+
|
| 29 |
+
try {
|
| 30 |
+
// Simulando chamada de login para a API
|
| 31 |
+
const response = await fetch('/admin/login', {
|
| 32 |
+
method: 'POST',
|
| 33 |
+
headers: { 'Content-Type': 'application/json' },
|
| 34 |
+
body: JSON.stringify({ password: password })
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
if (response.ok) {
|
| 38 |
+
const data = await response.json();
|
| 39 |
+
token = data.token; // Armazena token temporário
|
| 40 |
+
loginSection.classList.add('hidden');
|
| 41 |
+
dashboardSection.classList.remove('hidden');
|
| 42 |
+
} else {
|
| 43 |
+
loginError.classList.remove('hidden');
|
| 44 |
+
}
|
| 45 |
+
} catch (error) {
|
| 46 |
+
console.error('Login error:', error);
|
| 47 |
+
loginError.textContent = 'Erro ao conectar no servidor.';
|
| 48 |
+
loginError.classList.remove('hidden');
|
| 49 |
+
}
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// Drag and Drop Handling
|
| 53 |
+
dropZone.addEventListener('click', () => fileInput.click());
|
| 54 |
+
|
| 55 |
+
dropZone.addEventListener('dragover', (e) => {
|
| 56 |
+
e.preventDefault();
|
| 57 |
+
dropZone.classList.add('dragover');
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
dropZone.addEventListener('dragleave', () => {
|
| 61 |
+
dropZone.classList.remove('dragover');
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
dropZone.addEventListener('drop', (e) => {
|
| 65 |
+
e.preventDefault();
|
| 66 |
+
dropZone.classList.remove('dragover');
|
| 67 |
+
|
| 68 |
+
if (e.dataTransfer.files.length) {
|
| 69 |
+
handleFileSelect(e.dataTransfer.files[0]);
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
fileInput.addEventListener('change', (e) => {
|
| 74 |
+
if (e.target.files.length) {
|
| 75 |
+
handleFileSelect(e.target.files[0]);
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
function handleFileSelect(file) {
|
| 80 |
+
// Validações básicas (zip/rar)
|
| 81 |
+
if (!file.name.endsWith('.zip') && !file.name.endsWith('.rar')) {
|
| 82 |
+
alert('Apenas arquivos .zip ou .rar são permitidos.');
|
| 83 |
+
return;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
currentFile = file;
|
| 87 |
+
filenameDisplay.textContent = file.name;
|
| 88 |
+
selectedFileInfo.classList.remove('hidden');
|
| 89 |
+
btnUploadTrain.removeAttribute('disabled');
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Upload & Train Handling
|
| 93 |
+
btnUploadTrain.addEventListener('click', async () => {
|
| 94 |
+
if (!currentFile || !token) return;
|
| 95 |
+
|
| 96 |
+
btnUploadTrain.setAttribute('disabled', 'true');
|
| 97 |
+
progressContainer.classList.remove('hidden');
|
| 98 |
+
statusText.textContent = 'Fazendo upload e extraindo dataset...';
|
| 99 |
+
progressBar.style.width = '10%';
|
| 100 |
+
|
| 101 |
+
const formData = new FormData();
|
| 102 |
+
formData.append('file', currentFile);
|
| 103 |
+
|
| 104 |
+
try {
|
| 105 |
+
// 1. Upload
|
| 106 |
+
const uploadResponse = await fetch('/admin/upload_dataset', {
|
| 107 |
+
method: 'POST',
|
| 108 |
+
headers: {
|
| 109 |
+
'Authorization': `Bearer ${token}`
|
| 110 |
+
},
|
| 111 |
+
body: formData
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
if (!uploadResponse.ok) {
|
| 115 |
+
const errData = await uploadResponse.json();
|
| 116 |
+
throw new Error(errData.detail || 'Erro no upload');
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
statusText.textContent = 'Upload concluído. Iniciando fine-tuning...';
|
| 120 |
+
progressBar.style.width = '30%';
|
| 121 |
+
|
| 122 |
+
// 2. Start Training
|
| 123 |
+
const trainResponse = await fetch('/admin/train', {
|
| 124 |
+
method: 'POST',
|
| 125 |
+
headers: {
|
| 126 |
+
'Authorization': `Bearer ${token}`
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
if (!trainResponse.ok) {
|
| 131 |
+
const errData = await trainResponse.json();
|
| 132 |
+
throw new Error(errData.detail || 'Erro ao iniciar treino');
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// 3. Start Polling
|
| 136 |
+
startStatusPolling();
|
| 137 |
+
|
| 138 |
+
} catch (error) {
|
| 139 |
+
statusText.textContent = `Erro: ${error.message}`;
|
| 140 |
+
statusText.style.color = 'var(--danger)';
|
| 141 |
+
btnUploadTrain.removeAttribute('disabled');
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
function startStatusPolling() {
|
| 146 |
+
if (statusInterval) clearInterval(statusInterval);
|
| 147 |
+
|
| 148 |
+
statusInterval = setInterval(async () => {
|
| 149 |
+
try {
|
| 150 |
+
const response = await fetch('/admin/status', {
|
| 151 |
+
headers: { 'Authorization': `Bearer ${token}` }
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
if (response.ok) {
|
| 155 |
+
const data = await response.json();
|
| 156 |
+
|
| 157 |
+
progressBar.style.width = `${data.progress}%`;
|
| 158 |
+
statusText.textContent = data.message || `Treinamento: ${data.progress}%`;
|
| 159 |
+
|
| 160 |
+
if (data.status === 'completed') {
|
| 161 |
+
clearInterval(statusInterval);
|
| 162 |
+
statusText.textContent = 'Treinamento concluído com sucesso! Modelo atualizado.';
|
| 163 |
+
statusText.style.color = 'var(--success)';
|
| 164 |
+
btnUploadTrain.removeAttribute('disabled');
|
| 165 |
+
progressBar.style.width = '100%';
|
| 166 |
+
} else if (data.status === 'failed') {
|
| 167 |
+
clearInterval(statusInterval);
|
| 168 |
+
statusText.textContent = `Falha no treinamento: ${data.error}`;
|
| 169 |
+
statusText.style.color = 'var(--danger)';
|
| 170 |
+
btnUploadTrain.removeAttribute('disabled');
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
} catch (err) {
|
| 174 |
+
console.error("Erro ao verificar status:", err);
|
| 175 |
+
}
|
| 176 |
+
}, 2000); // Polling a cada 2 segundos
|
| 177 |
+
}
|
| 178 |
+
});
|
dashboard/style.css
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--primary: #9d50bb;
|
| 3 |
+
--accent: #6e48aa;
|
| 4 |
+
--accent-glow: rgba(139, 92, 246, 0.4);
|
| 5 |
+
--bg-dark: #0a0a0c;
|
| 6 |
+
--cyan: #06B6D4;
|
| 7 |
+
--text-primary: #F8F9FA;
|
| 8 |
+
--text-secondary: #94A3B8;
|
| 9 |
+
--glass: rgba(255, 255, 255, 0.03);
|
| 10 |
+
--glass-border: rgba(255, 255, 255, 0.1);
|
| 11 |
+
--success: #10B981;
|
| 12 |
+
--danger: #EF4444;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
* {
|
| 16 |
+
margin: 0;
|
| 17 |
+
padding: 0;
|
| 18 |
+
box-sizing: border-box;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
body {
|
| 22 |
+
background-color: var(--bg-dark);
|
| 23 |
+
color: var(--text-primary);
|
| 24 |
+
font-family: 'Inter', sans-serif;
|
| 25 |
+
line-height: 1.6;
|
| 26 |
+
overflow-x: hidden;
|
| 27 |
+
min-height: 100vh;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.aurora-mesh {
|
| 31 |
+
position: fixed;
|
| 32 |
+
top: 0;
|
| 33 |
+
left: 0;
|
| 34 |
+
width: 100%;
|
| 35 |
+
height: 100%;
|
| 36 |
+
background: radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.15) 0%, transparent 40%),
|
| 37 |
+
radial-gradient(circle at 80% 70%, rgba(6, 182, 212, 0.1) 0%, transparent 40%);
|
| 38 |
+
z-index: -1;
|
| 39 |
+
filter: blur(80px);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
nav {
|
| 43 |
+
display: flex;
|
| 44 |
+
justify-content: space-between;
|
| 45 |
+
align-items: center;
|
| 46 |
+
padding: 2rem 5%;
|
| 47 |
+
background: rgba(10, 10, 11, 0.8);
|
| 48 |
+
backdrop-filter: blur(10px);
|
| 49 |
+
position: sticky;
|
| 50 |
+
top: 0;
|
| 51 |
+
z-index: 100;
|
| 52 |
+
border-bottom: 1px solid var(--glass-border);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.logo {
|
| 56 |
+
display: flex;
|
| 57 |
+
align-items: center;
|
| 58 |
+
gap: 12px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.logo img {
|
| 62 |
+
height: 42px; /* Aumentado para melhor visibilidade com a nova logo */
|
| 63 |
+
width: auto;
|
| 64 |
+
image-rendering: -webkit-optimize-contrast;
|
| 65 |
+
object-fit: contain;
|
| 66 |
+
filter: drop-shadow(0 0 12px rgba(157, 80, 187, 0.4));
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* XAI Heatmap Styles */
|
| 70 |
+
.spec-wrapper {
|
| 71 |
+
position: relative;
|
| 72 |
+
width: 100%;
|
| 73 |
+
border-radius: 12px;
|
| 74 |
+
overflow: hidden;
|
| 75 |
+
border: 1px solid var(--glass-border);
|
| 76 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.spec-wrapper img {
|
| 80 |
+
display: block;
|
| 81 |
+
width: 100%;
|
| 82 |
+
height: auto;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.heatmap-overlay {
|
| 86 |
+
position: absolute;
|
| 87 |
+
top: 0;
|
| 88 |
+
left: 0;
|
| 89 |
+
width: 100%;
|
| 90 |
+
height: 100%;
|
| 91 |
+
display: flex;
|
| 92 |
+
pointer-events: none;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.heatmap-segment {
|
| 96 |
+
flex: 1;
|
| 97 |
+
height: 100%;
|
| 98 |
+
transition: background 0.5s ease;
|
| 99 |
+
mix-blend-mode: color-burn; /* Mistura melhor com o espectrograma */
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* Diagnostic Section Refinement */
|
| 103 |
+
.diagnostic-container {
|
| 104 |
+
margin-top: 2rem;
|
| 105 |
+
padding: 1.5rem;
|
| 106 |
+
background: rgba(255, 255, 255, 0.03);
|
| 107 |
+
border: 1px solid var(--glass-border);
|
| 108 |
+
border-radius: 12px;
|
| 109 |
+
backdrop-filter: blur(5px);
|
| 110 |
+
transition: all 0.4s ease;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.diagnostic-header {
|
| 114 |
+
display: flex;
|
| 115 |
+
justify-content: space-between;
|
| 116 |
+
align-items: center;
|
| 117 |
+
margin-bottom: 1.2rem;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.diagnostic-header span {
|
| 121 |
+
font-family: 'Outfit', sans-serif;
|
| 122 |
+
font-weight: 700;
|
| 123 |
+
font-size: 0.85rem;
|
| 124 |
+
letter-spacing: 1.5px;
|
| 125 |
+
color: var(--cyan);
|
| 126 |
+
text-transform: uppercase;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.btn-mini {
|
| 130 |
+
background: rgba(6, 182, 212, 0.1);
|
| 131 |
+
border: 1px solid rgba(6, 182, 212, 0.3);
|
| 132 |
+
color: var(--cyan);
|
| 133 |
+
padding: 6px 14px;
|
| 134 |
+
border-radius: 20px;
|
| 135 |
+
font-size: 0.75rem;
|
| 136 |
+
font-weight: 600;
|
| 137 |
+
cursor: pointer;
|
| 138 |
+
transition: all 0.3s ease;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.btn-mini:hover {
|
| 142 |
+
background: var(--cyan);
|
| 143 |
+
color: var(--bg-dark);
|
| 144 |
+
box-shadow: 0 0 10px var(--cyan);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.diagnostic-content {
|
| 148 |
+
margin-top: 1rem;
|
| 149 |
+
padding-top: 1rem;
|
| 150 |
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.engine-stat {
|
| 154 |
+
margin-bottom: 1.2rem;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.engine-stat label {
|
| 158 |
+
font-size: 0.8rem;
|
| 159 |
+
color: var(--text-secondary);
|
| 160 |
+
display: flex;
|
| 161 |
+
justify-content: space-between;
|
| 162 |
+
margin-bottom: 0.5rem;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.progress-mini {
|
| 166 |
+
height: 8px;
|
| 167 |
+
background: rgba(0,0,0,0.4);
|
| 168 |
+
border-radius: 4px;
|
| 169 |
+
overflow: hidden;
|
| 170 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.progress-mini .bar {
|
| 174 |
+
height: 100%;
|
| 175 |
+
background: linear-gradient(90deg, var(--primary), var(--cyan));
|
| 176 |
+
width: 0%;
|
| 177 |
+
transition: width 1.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.rigor-status {
|
| 181 |
+
margin-top: 1rem;
|
| 182 |
+
padding: 1rem;
|
| 183 |
+
background: rgba(0, 0, 0, 0.3);
|
| 184 |
+
border-radius: 8px;
|
| 185 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 186 |
+
text-align: center;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.rigor-status small {
|
| 190 |
+
display: block;
|
| 191 |
+
font-size: 0.75rem;
|
| 192 |
+
color: var(--text-secondary);
|
| 193 |
+
margin-bottom: 4px;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
#rigor-logic {
|
| 197 |
+
color: var(--cyan);
|
| 198 |
+
font-weight: 600;
|
| 199 |
+
font-family: 'Outfit', sans-serif;
|
| 200 |
+
letter-spacing: 0.5px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/* XAI Heatmap Overlay */
|
| 204 |
+
.spec-wrapper {
|
| 205 |
+
position: relative;
|
| 206 |
+
width: 100%;
|
| 207 |
+
border-radius: 16px;
|
| 208 |
+
overflow: hidden;
|
| 209 |
+
border: 1px solid var(--glass-border);
|
| 210 |
+
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.spec-wrapper img {
|
| 214 |
+
display: block;
|
| 215 |
+
width: 100%;
|
| 216 |
+
height: auto;
|
| 217 |
+
filter: contrast(1.1) brightness(0.9);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.heatmap-overlay {
|
| 221 |
+
position: absolute;
|
| 222 |
+
top: 0;
|
| 223 |
+
left: 0;
|
| 224 |
+
width: 100%;
|
| 225 |
+
height: 100%;
|
| 226 |
+
display: flex;
|
| 227 |
+
pointer-events: none;
|
| 228 |
+
opacity: 0.85;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.heatmap-segment {
|
| 232 |
+
flex: 1;
|
| 233 |
+
height: 100%;
|
| 234 |
+
transition: all 0.6s ease;
|
| 235 |
+
mix-blend-mode: screen;
|
| 236 |
+
border-right: 1px solid rgba(255,255,255,0.02);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
@keyframes glowPulse {
|
| 240 |
+
0% { box-shadow: 0 0 5px var(--accent); }
|
| 241 |
+
50% { box-shadow: 0 0 20px var(--accent); }
|
| 242 |
+
100% { box-shadow: 0 0 5px var(--accent); }
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.pulse {
|
| 246 |
+
animation: glowPulse 2s infinite ease-in-out;
|
| 247 |
+
}
|
| 248 |
+
|
embed_logo.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def embed_logo():
|
| 5 |
+
logo_path = 'assets/logo.png'
|
| 6 |
+
html_path = 'dashboard/index.html'
|
| 7 |
+
|
| 8 |
+
if not os.path.exists(logo_path):
|
| 9 |
+
print("Logo not found")
|
| 10 |
+
return
|
| 11 |
+
|
| 12 |
+
with open(logo_path, 'rb') as f:
|
| 13 |
+
b64_string = base64.b64encode(f.read()).decode()
|
| 14 |
+
|
| 15 |
+
with open(html_path, 'r', encoding='utf-8') as f:
|
| 16 |
+
html_content = f.read()
|
| 17 |
+
|
| 18 |
+
# Substituir no favicon e na logo do nav
|
| 19 |
+
new_html = html_content.replace('href="assets/logo.png"', f'href="data:image/png;base64,{b64_string}"')
|
| 20 |
+
new_html = new_html.replace('src="assets/logo.png"', f'src="data:image/png;base64,{b64_string}"')
|
| 21 |
+
|
| 22 |
+
with open(html_path, 'w', encoding='utf-8') as f:
|
| 23 |
+
f.write(new_html)
|
| 24 |
+
print("Logo embedded successfully in HTML")
|
| 25 |
+
|
| 26 |
+
if __name__ == "__main__":
|
| 27 |
+
embed_logo()
|
execution/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Init file to make execution a package
|
execution/colab_training_script.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @title 4. Executar Treinamento (Fine-Tuning)
|
| 2 |
+
import os
|
| 3 |
+
import torch
|
| 4 |
+
import librosa
|
| 5 |
+
from torch.utils.data import Dataset
|
| 6 |
+
from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2ForSequenceClassification, Trainer, TrainingArguments
|
| 7 |
+
|
| 8 |
+
# Configurações do Modelo
|
| 9 |
+
BASE_MODEL = "HyperMoon/wav2vec2-base-960h-finetuned-deepfake"
|
| 10 |
+
OUTPUT_DIR = "local_finetuned_model"
|
| 11 |
+
|
| 12 |
+
# Mapeamento Rígido de Labels para evitar conflitos (0=Real, 1=Fraude)
|
| 13 |
+
id2label = {0: "AUTHENTIC", 1: "FAKE"}
|
| 14 |
+
label2id = {"AUTHENTIC": 0, "FAKE": 1}
|
| 15 |
+
|
| 16 |
+
class DeepfakeDataset(Dataset):
|
| 17 |
+
def __init__(self, root_dir, processor):
|
| 18 |
+
self.files = []
|
| 19 |
+
self.processor = processor
|
| 20 |
+
|
| 21 |
+
# Carregamento explícito baseado em pastas
|
| 22 |
+
for label_name, label_id in label2id.items():
|
| 23 |
+
folder = "real" if label_name == "AUTHENTIC" else "fake"
|
| 24 |
+
path = os.path.join(root_dir, folder)
|
| 25 |
+
if os.path.exists(path):
|
| 26 |
+
print(f"Carregando audios de: {folder}...")
|
| 27 |
+
for f in os.listdir(path):
|
| 28 |
+
if f.lower().endswith(('.wav', '.mp3', '.flac')):
|
| 29 |
+
self.files.append({"path": os.path.join(path, f), "label": label_id})
|
| 30 |
+
else:
|
| 31 |
+
print(f"AVISO: Pasta {folder} não encontrada em {root_dir}")
|
| 32 |
+
|
| 33 |
+
def __len__(self): return len(self.files)
|
| 34 |
+
|
| 35 |
+
def __getitem__(self, idx):
|
| 36 |
+
item = self.files[idx]
|
| 37 |
+
try:
|
| 38 |
+
speech, _ = librosa.load(item["path"], sr=16000)
|
| 39 |
+
inputs = self.processor(speech, sampling_rate=16000, return_tensors="pt", padding="max_length", max_length=160000, truncation=True)
|
| 40 |
+
return {"input_values": inputs.input_values[0], "labels": torch.tensor(item["label"])}
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print(f"Erro ao processar {item['path']}: {e}")
|
| 43 |
+
# Retorna o primeiro item como fallback para não quebrar o loop do Trainer
|
| 44 |
+
return self.__getitem__(0)
|
| 45 |
+
|
| 46 |
+
print("Inicializando Processador e Modelo...")
|
| 47 |
+
try:
|
| 48 |
+
processor = Wav2Vec2FeatureExtractor.from_pretrained(BASE_MODEL)
|
| 49 |
+
# Adicionado id2label e label2id aqui para garantir consistência
|
| 50 |
+
model = Wav2Vec2ForSequenceClassification.from_pretrained(
|
| 51 |
+
BASE_MODEL,
|
| 52 |
+
num_labels=2,
|
| 53 |
+
id2label=id2label,
|
| 54 |
+
label2id=label2id,
|
| 55 |
+
ignore_mismatched_sizes=True
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Congelar base para focar no aprendizado das novas fraudes (Lógica Robusta)
|
| 59 |
+
if hasattr(model, 'wav2vec2'):
|
| 60 |
+
for param in model.wav2vec2.parameters():
|
| 61 |
+
param.requires_grad = False
|
| 62 |
+
print("Modelo carregado e camadas base congeladas com sucesso!")
|
| 63 |
+
|
| 64 |
+
# Dataset (Aponte para a pasta onde você subiu os áudios no Colab)
|
| 65 |
+
# Ex: /content/dataset_treino
|
| 66 |
+
dataset_path = "/content/dataset"
|
| 67 |
+
train_data = DeepfakeDataset(dataset_path, processor)
|
| 68 |
+
|
| 69 |
+
if len(train_data) == 0:
|
| 70 |
+
print("ERRO: Nenhum dado encontrado. Verifique se as pastas 'real' e 'fake' existem dentro do caminho especificado.")
|
| 71 |
+
else:
|
| 72 |
+
training_args = TrainingArguments(
|
| 73 |
+
output_dir=OUTPUT_DIR,
|
| 74 |
+
num_train_epochs=3,
|
| 75 |
+
per_device_train_batch_size=2,
|
| 76 |
+
gradient_accumulation_steps=4,
|
| 77 |
+
save_steps=50,
|
| 78 |
+
logging_steps=10,
|
| 79 |
+
learning_rate=2e-5,
|
| 80 |
+
remove_unused_columns=False
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
trainer = Trainer(model=model, args=training_args, train_dataset=train_data)
|
| 84 |
+
print("Iniciando Treinamento...")
|
| 85 |
+
trainer.train()
|
| 86 |
+
|
| 87 |
+
# Salva o resultado final
|
| 88 |
+
model.save_pretrained(OUTPUT_DIR)
|
| 89 |
+
processor.save_pretrained(OUTPUT_DIR)
|
| 90 |
+
print(f"Sucesso! Modelo salvo em: {OUTPUT_DIR}")
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"ERRO CRÍTICO: {e}")
|
| 94 |
+
print("DICA: Se o erro for de conexão, tente rodar a célula novamente. O Hugging Face pode falhar ocasionalmente no download.")
|
execution/ensemble_manager.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from execution.inference_wav2vec import run_inference as run_wav2vec
|
| 2 |
+
from execution.inference_ast import run_ast_inference as run_ast
|
| 3 |
+
|
| 4 |
+
def get_combined_verdict(file_path):
|
| 5 |
+
"""
|
| 6 |
+
Orquestra a execução dos dois motores e aplica o Protocolo de Rigor (Abordagem Conservadora).
|
| 7 |
+
"""
|
| 8 |
+
# 1. Executa Motor 1 (Wav2Vec2 - Ritmo e Nuance)
|
| 9 |
+
res_w2v = run_wav2vec(file_path)
|
| 10 |
+
score_w2v = res_w2v.get("deepfake_probability", 0.0)
|
| 11 |
+
|
| 12 |
+
# 2. Executa Motor 2 (AST - Espectrograma e Frequência)
|
| 13 |
+
res_ast = run_ast(file_path)
|
| 14 |
+
score_ast = res_ast.get("risk_score", 0.0)
|
| 15 |
+
|
| 16 |
+
# 3. Lógica do Protocolo de Rigor (Abordagem Conservadora)
|
| 17 |
+
# Se qualquer motor detectar fraude com convicção alta, o veredito é FRAUDE.
|
| 18 |
+
|
| 19 |
+
HIGH_CONFIDENCE_THRESHOLD = 0.80
|
| 20 |
+
|
| 21 |
+
is_fraud = False
|
| 22 |
+
verdict = "AUTHENTIC"
|
| 23 |
+
final_score = max(score_w2v, score_ast) # Pega o maior risco detectado
|
| 24 |
+
|
| 25 |
+
if score_w2v >= HIGH_CONFIDENCE_THRESHOLD and score_ast >= HIGH_CONFIDENCE_THRESHOLD:
|
| 26 |
+
is_fraud = True
|
| 27 |
+
verdict = "SPOOF"
|
| 28 |
+
message = "CONSENSO CRÍTICO: Ambos os motores detectaram padrões de clonagem com alta convicção."
|
| 29 |
+
elif score_w2v >= HIGH_CONFIDENCE_THRESHOLD:
|
| 30 |
+
is_fraud = True
|
| 31 |
+
verdict = "SPOOF"
|
| 32 |
+
message = "ALERTA DE VOZ: O motor Wav2Vec2 detectou irregularidades na textura fonética humana."
|
| 33 |
+
elif score_ast >= HIGH_CONFIDENCE_THRESHOLD:
|
| 34 |
+
is_fraud = True
|
| 35 |
+
verdict = "SPOOF"
|
| 36 |
+
message = "ANOMALIA ESPECTRAL: O motor AST identificou assinaturas de frequências artificiais."
|
| 37 |
+
elif final_score > 0.5:
|
| 38 |
+
is_fraud = True
|
| 39 |
+
verdict = "SPOOF"
|
| 40 |
+
message = "RISCO DETECTADO: Evidências moderadas de manipulação neural identificadas."
|
| 41 |
+
else:
|
| 42 |
+
message = "INTEGRIDADE CONFIRMADA: Nenhuma evidência significativa de manipulação detectada."
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
"verdict": verdict,
|
| 46 |
+
"fraud_probability": final_score,
|
| 47 |
+
"wav2vec_score": score_w2v,
|
| 48 |
+
"ast_score": score_ast,
|
| 49 |
+
"temporal_scores": res_w2v.get("temporal_scores", []), # Adicionado para XAI
|
| 50 |
+
"engines_consensus": message,
|
| 51 |
+
"details": {
|
| 52 |
+
"protocol": "Protocolo de Rigor (Conservador)"
|
| 53 |
+
},
|
| 54 |
+
"engines": ["Wav2Vec2-Deepfake", "AST-Spectrogram"]
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
import sys
|
| 60 |
+
if len(sys.argv) > 1:
|
| 61 |
+
import json
|
| 62 |
+
print(json.dumps(get_combined_verdict(sys.argv[1]), indent=2))
|
execution/fastapi_server.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException, Depends, Header, status
|
| 5 |
+
from fastapi.staticfiles import StaticFiles
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
import zipfile
|
| 9 |
+
import uuid
|
| 10 |
+
import uvicorn
|
| 11 |
+
|
| 12 |
+
# Carrega variáveis do arquivo .env
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
# Importamos nossos módulos de execução
|
| 16 |
+
from execution.feature_extractor import extract_features
|
| 17 |
+
from execution.ensemble_manager import get_combined_verdict
|
| 18 |
+
|
| 19 |
+
app = FastAPI(title="ConfereAI Audio Fraud Detection API")
|
| 20 |
+
|
| 21 |
+
# Configuração de CORS
|
| 22 |
+
app.add_middleware(
|
| 23 |
+
CORSMiddleware,
|
| 24 |
+
allow_origins=["*"],
|
| 25 |
+
allow_methods=["*"],
|
| 26 |
+
allow_headers=["*"],
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Estado global do treinamento (simplificado para MVP)
|
| 30 |
+
training_status = {
|
| 31 |
+
"status": "idle", # idle, processing, training, completed, failed
|
| 32 |
+
"progress": 0,
|
| 33 |
+
"message": "Aguardando",
|
| 34 |
+
"error": None
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# Verificador de token super simples
|
| 38 |
+
def verify_admin_token(authorization: str = Header(None)):
|
| 39 |
+
if not authorization or not authorization.startswith("Bearer "):
|
| 40 |
+
raise HTTPException(status_code=401, detail="Token ausente ou inválido")
|
| 41 |
+
|
| 42 |
+
token = authorization.split(" ")[1]
|
| 43 |
+
# No mundo real, usaríamos JWT decodificado
|
| 44 |
+
if token != "confereai_admin_token_2026":
|
| 45 |
+
raise HTTPException(status_code=401, detail="Token inválido")
|
| 46 |
+
return token
|
| 47 |
+
|
| 48 |
+
class AnalysisResult(BaseModel):
|
| 49 |
+
filename: str
|
| 50 |
+
fraud_score: float
|
| 51 |
+
verdict: str
|
| 52 |
+
spectrogram_url: str
|
| 53 |
+
engine: str
|
| 54 |
+
wav2vec_score: float = 0.0
|
| 55 |
+
ast_score: float = 0.0
|
| 56 |
+
engines_consensus: str = ""
|
| 57 |
+
temporal_scores: list = []
|
| 58 |
+
|
| 59 |
+
@app.post("/analyze", response_model=AnalysisResult)
|
| 60 |
+
async def analyze_audio_endpoint(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
|
| 61 |
+
# Garante diretório temporário
|
| 62 |
+
temp_dir = ".tmp"
|
| 63 |
+
if not os.path.exists(temp_dir):
|
| 64 |
+
os.makedirs(temp_dir)
|
| 65 |
+
|
| 66 |
+
# Salva arquivo temporariamente com ID único para evitar colisões
|
| 67 |
+
unique_id = str(uuid.uuid4())[:8]
|
| 68 |
+
filename = f"{unique_id}_{file.filename}"
|
| 69 |
+
file_path = os.path.join(temp_dir, filename)
|
| 70 |
+
with open(file_path, "wb") as buffer:
|
| 71 |
+
shutil.copyfileobj(file.file, buffer)
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
# 1. Extração de Imagens (Local)
|
| 75 |
+
features = extract_features(file_path, output_dir=temp_dir)
|
| 76 |
+
|
| 77 |
+
# 2. Inferência via Ensemble (Wav2Vec2 + AST)
|
| 78 |
+
analysis = get_combined_verdict(file_path)
|
| 79 |
+
|
| 80 |
+
# 3. Agenda limpeza em background (após 5 minutos para dar tempo do front ler a imagem)
|
| 81 |
+
def cleanup_temp_files(paths):
|
| 82 |
+
import time
|
| 83 |
+
time.sleep(300) # 5 minutos
|
| 84 |
+
for p in paths:
|
| 85 |
+
if os.path.exists(p):
|
| 86 |
+
try:
|
| 87 |
+
os.remove(p)
|
| 88 |
+
print(f"Cleanup: {p} removido.")
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Cleanup error: {e}")
|
| 91 |
+
|
| 92 |
+
background_tasks.add_task(cleanup_temp_files, [file_path, features.get("spectrogram_path")])
|
| 93 |
+
|
| 94 |
+
# 4. Resposta Consolidada
|
| 95 |
+
return AnalysisResult(
|
| 96 |
+
filename=file.filename,
|
| 97 |
+
fraud_score=analysis.get("fraud_probability", 0.0),
|
| 98 |
+
verdict=analysis.get("verdict", "UNKNOWN"),
|
| 99 |
+
spectrogram_url=features.get("spectrogram_path", "").replace(".tmp/", "/tmp/"),
|
| 100 |
+
engine="Dual Engine (Wav2Vec2 + AST) - Protocolo de Rigor",
|
| 101 |
+
wav2vec_score=analysis.get("wav2vec_score", 0.0),
|
| 102 |
+
ast_score=analysis.get("ast_score", 0.0),
|
| 103 |
+
engines_consensus=analysis.get("engines_consensus", ""),
|
| 104 |
+
temporal_scores=analysis.get("temporal_scores", [])
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"Erro na análise: {e}")
|
| 109 |
+
raise e
|
| 110 |
+
|
| 111 |
+
# --- ADMIN ENDPOINTS ---
|
| 112 |
+
|
| 113 |
+
class LoginRequest(BaseModel):
|
| 114 |
+
password: str
|
| 115 |
+
|
| 116 |
+
@app.post("/admin/login")
|
| 117 |
+
async def admin_login(req: LoginRequest):
|
| 118 |
+
admin_pw = os.environ.get("ADMIN_PASSWORD", "Casa102030@")
|
| 119 |
+
if req.password == admin_pw:
|
| 120 |
+
return {"token": "confereai_admin_token_2026"}
|
| 121 |
+
raise HTTPException(status_code=401, detail="Senha incorreta")
|
| 122 |
+
|
| 123 |
+
@app.post("/admin/upload_dataset")
|
| 124 |
+
async def admin_upload(file: UploadFile = File(...), token: str = Depends(verify_admin_token)):
|
| 125 |
+
global training_status
|
| 126 |
+
if not file.filename.endswith(('.zip', '.rar')):
|
| 127 |
+
raise HTTPException(status_code=400, detail="Apenas .zip ou .rar")
|
| 128 |
+
|
| 129 |
+
dataset_dir = ".tmp/dataset"
|
| 130 |
+
if os.path.exists(dataset_dir):
|
| 131 |
+
shutil.rmtree(dataset_dir)
|
| 132 |
+
os.makedirs(dataset_dir)
|
| 133 |
+
|
| 134 |
+
file_path = os.path.join(".tmp", file.filename)
|
| 135 |
+
with open(file_path, "wb") as buffer:
|
| 136 |
+
shutil.copyfileobj(file.file, buffer)
|
| 137 |
+
|
| 138 |
+
training_status["status"] = "processing"
|
| 139 |
+
training_status["progress"] = 10
|
| 140 |
+
training_status["message"] = "Arquivo recebido. Extraindo..."
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
# Extra�o
|
| 144 |
+
if file.filename.endswith('.zip'):
|
| 145 |
+
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
| 146 |
+
zip_ref.extractall(dataset_dir)
|
| 147 |
+
# RAR necessita do pacote rarfile, assumiremos ZIP para simplificar ou instruir o usuário.
|
| 148 |
+
|
| 149 |
+
training_status["progress"] = 25
|
| 150 |
+
training_status["message"] = "Dataset extraído. Aguardando início do treinamento."
|
| 151 |
+
return {"status": "success", "message": "Upload concluído."}
|
| 152 |
+
except Exception as e:
|
| 153 |
+
training_status["status"] = "failed"
|
| 154 |
+
training_status["message"] = "Erro na extração do dataset."
|
| 155 |
+
training_status["error"] = str(e)
|
| 156 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 157 |
+
|
| 158 |
+
from execution.train_wav2vec import start_finetuning
|
| 159 |
+
|
| 160 |
+
def real_training_task():
|
| 161 |
+
"""Tarefa em background que executa o fine-tuning real no dataset."""
|
| 162 |
+
global training_status
|
| 163 |
+
training_status["status"] = "training"
|
| 164 |
+
training_status["progress"] = 35
|
| 165 |
+
training_status["message"] = "Carregando modelo e dataset para treinamento..."
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
dataset_dir = ".tmp/dataset"
|
| 169 |
+
# Executa o fine-tuning
|
| 170 |
+
start_finetuning(dataset_dir)
|
| 171 |
+
|
| 172 |
+
training_status["progress"] = 100
|
| 173 |
+
training_status["status"] = "completed"
|
| 174 |
+
training_status["message"] = "Fine-Tuning concluído com sucesso! Modelo salvo localmente."
|
| 175 |
+
except Exception as e:
|
| 176 |
+
training_status["status"] = "failed"
|
| 177 |
+
training_status["message"] = f"Erro no treinamento: {str(e)}"
|
| 178 |
+
training_status["error"] = str(e)
|
| 179 |
+
print(f"Treinamento falhou: {e}")
|
| 180 |
+
|
| 181 |
+
@app.post("/admin/train")
|
| 182 |
+
async def admin_train(background_tasks: BackgroundTasks, token: str = Depends(verify_admin_token)):
|
| 183 |
+
global training_status
|
| 184 |
+
if training_status["status"] == "training":
|
| 185 |
+
raise HTTPException(status_code=400, detail="Treinamento já está em andamento.")
|
| 186 |
+
|
| 187 |
+
training_status["progress"] = 30
|
| 188 |
+
training_status["message"] = "Iniciando pipeline de treinamento..."
|
| 189 |
+
background_tasks.add_task(real_training_task)
|
| 190 |
+
return {"status": "success", "message": "Treinamento iniciado em background"}
|
| 191 |
+
|
| 192 |
+
@app.get("/admin/status")
|
| 193 |
+
async def admin_status(token: str = Depends(verify_admin_token)):
|
| 194 |
+
return training_status
|
| 195 |
+
|
| 196 |
+
# Garante diretório temporário para o mount não falhar
|
| 197 |
+
if not os.path.exists(".tmp"):
|
| 198 |
+
os.makedirs(".tmp")
|
| 199 |
+
|
| 200 |
+
# Servir arquivos do dashboard e imagens temporárias (se existirem)
|
| 201 |
+
app.mount("/tmp", StaticFiles(directory=".tmp"), name="tmp")
|
| 202 |
+
|
| 203 |
+
if os.path.exists("dashboard"):
|
| 204 |
+
app.mount("/", StaticFiles(directory="dashboard", html=True), name="dashboard")
|
| 205 |
+
else:
|
| 206 |
+
@app.get("/")
|
| 207 |
+
async def root_fallback():
|
| 208 |
+
return {"status": "ConfereAI API Running", "message": "Dashboard directory not found. Please use the Vercel frontend."}
|
| 209 |
+
|
| 210 |
+
if __name__ == "__main__":
|
| 211 |
+
import uvicorn
|
| 212 |
+
import os
|
| 213 |
+
port = int(os.environ.get("PORT", 8000))
|
| 214 |
+
host = os.environ.get("HOST", "0.0.0.0")
|
| 215 |
+
uvicorn.run(app, host=host, port=port)
|
execution/feature_extractor.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
import librosa
|
| 7 |
+
import librosa.display
|
| 8 |
+
import numpy as np
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
+
HAS_LIBS = True
|
| 11 |
+
except ImportError:
|
| 12 |
+
HAS_LIBS = False
|
| 13 |
+
|
| 14 |
+
def extract_features(audio_path, output_dir=".tmp/"):
|
| 15 |
+
"""
|
| 16 |
+
Extrai MFCC e Espectrograma de Mel do áudio.
|
| 17 |
+
"""
|
| 18 |
+
if not HAS_LIBS:
|
| 19 |
+
return {"error": "Bibliotecas librosa/numpy não instaladas."}
|
| 20 |
+
|
| 21 |
+
# Carrega áudio
|
| 22 |
+
y, sr = librosa.load(audio_path)
|
| 23 |
+
|
| 24 |
+
# Mel Spectrogram
|
| 25 |
+
S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
|
| 26 |
+
S_dB = librosa.power_to_db(S, ref=np.max)
|
| 27 |
+
|
| 28 |
+
# MFCC
|
| 29 |
+
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=40)
|
| 30 |
+
|
| 31 |
+
# Salva imagem do espectrograma para o dashboard
|
| 32 |
+
base_name = os.path.splitext(os.path.basename(audio_path))[0]
|
| 33 |
+
spec_filename = base_name + "_spec.png"
|
| 34 |
+
spec_path = os.path.join(output_dir, spec_filename)
|
| 35 |
+
|
| 36 |
+
plt.figure(figsize=(10, 4))
|
| 37 |
+
librosa.display.specshow(S_dB, sr=sr, x_axis='time', y_axis='mel')
|
| 38 |
+
plt.colorbar(format='%+2.0f dB')
|
| 39 |
+
plt.title('Mel-frequency spectrogram')
|
| 40 |
+
plt.tight_layout()
|
| 41 |
+
plt.savefig(spec_path)
|
| 42 |
+
plt.close()
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
"audio_info": {
|
| 46 |
+
"duration": librosa.get_duration(y=y, sr=sr),
|
| 47 |
+
"sample_rate": sr
|
| 48 |
+
},
|
| 49 |
+
"spectrogram_path": spec_path,
|
| 50 |
+
"mfcc_shape": mfccs.shape
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
if len(sys.argv) < 2:
|
| 55 |
+
print("Uso: python feature_extractor.py <audio_path>")
|
| 56 |
+
else:
|
| 57 |
+
print(json.dumps(extract_features(sys.argv[1]), indent=2))
|
execution/inference_ast.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import librosa
|
| 3 |
+
import numpy as np
|
| 4 |
+
from transformers import AutoFeatureExtractor, ASTForAudioClassification
|
| 5 |
+
|
| 6 |
+
# Modelo AST (Audio Spectrogram Transformer)
|
| 7 |
+
# Usamos o modelo base do MIT como referência para análise espectral
|
| 8 |
+
MODEL_NAME = "MIT/ast-finetuned-audioset-10-10-0.4593"
|
| 9 |
+
|
| 10 |
+
# Singleton para carregar o modelo apenas uma vez
|
| 11 |
+
_extractor = None
|
| 12 |
+
_model = None
|
| 13 |
+
|
| 14 |
+
def get_ast_resources():
|
| 15 |
+
global _extractor, _model
|
| 16 |
+
if _extractor is None or _model is None:
|
| 17 |
+
print(f"Carregando motor AST: {MODEL_NAME}...")
|
| 18 |
+
_extractor = AutoFeatureExtractor.from_pretrained(MODEL_NAME)
|
| 19 |
+
_model = ASTForAudioClassification.from_pretrained(MODEL_NAME)
|
| 20 |
+
_model.eval()
|
| 21 |
+
return _extractor, _model
|
| 22 |
+
|
| 23 |
+
def run_ast_inference(file_path):
|
| 24 |
+
"""
|
| 25 |
+
Executa a análise via Audio Spectrogram Transformer.
|
| 26 |
+
Identifica anomalias espectrais e inconsistências na textura sonora.
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
extractor, model = get_ast_resources()
|
| 30 |
+
|
| 31 |
+
# Carrega áudio (resample para 16kHz conforme exigido pelo AST)
|
| 32 |
+
audio, _ = librosa.load(file_path, sr=16000)
|
| 33 |
+
|
| 34 |
+
# O AST espera entradas de 10 segundos (160.000 amostras)
|
| 35 |
+
# Vamos padronizar
|
| 36 |
+
if len(audio) > 160000:
|
| 37 |
+
audio = audio[:160000]
|
| 38 |
+
else:
|
| 39 |
+
audio = np.pad(audio, (0, 160000 - len(audio)), mode='constant')
|
| 40 |
+
|
| 41 |
+
# Extração de Features (Espectrograma de Mel)
|
| 42 |
+
inputs = extractor(audio, sampling_rate=16000, return_tensors="pt")
|
| 43 |
+
|
| 44 |
+
with torch.no_grad():
|
| 45 |
+
outputs = model(**inputs)
|
| 46 |
+
logits = outputs.logits
|
| 47 |
+
|
| 48 |
+
# No AudioSet, as classes são variadas. Para detecção de fraude sem fine-tuning específico,
|
| 49 |
+
# analisamos a "entropia" ou a probabilidade de classes sintéticas/anômalas.
|
| 50 |
+
# Como fallback funcional, calculamos um score de desvio estatístico.
|
| 51 |
+
probs = torch.nn.functional.softmax(logits, dim=-1)
|
| 52 |
+
|
| 53 |
+
# Simulação de detecção de anomalia baseada na textura espectral
|
| 54 |
+
# Em um cenário real com fine-tuning, usaríamos a classe 'deepfake'
|
| 55 |
+
# Aqui, usamos a variância das probabilidades como proxy de 'instabilidade' da IA
|
| 56 |
+
anomaly_score = float(torch.var(probs) * 100) # Exemplo de métrica de dispersão
|
| 57 |
+
|
| 58 |
+
# Normalizamos para um score de 0 a 1
|
| 59 |
+
risk_score = min(max(anomaly_score * 5, 0.0), 1.0)
|
| 60 |
+
|
| 61 |
+
return {
|
| 62 |
+
"risk_score": risk_score,
|
| 63 |
+
"engine": "AST-Transformer",
|
| 64 |
+
"status": "success"
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"Erro no motor AST: {e}")
|
| 69 |
+
return {"error": str(e), "risk_score": 0.0}
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
# Teste simples
|
| 73 |
+
import sys
|
| 74 |
+
if len(sys.argv) > 1:
|
| 75 |
+
print(run_ast_inference(sys.argv[1]))
|
execution/inference_wav2vec.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import json
|
| 3 |
+
import torch
|
| 4 |
+
import librosa
|
| 5 |
+
from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Configurações de Modelo
|
| 10 |
+
WAV2VEC_MODEL_ENV = os.environ.get("WAV2VEC_MODEL")
|
| 11 |
+
LOCAL_MODEL_DIR = "./local_finetuned_model"
|
| 12 |
+
DEFAULT_HUB_MODEL = "HyperMoon/wav2vec2-base-960h-finetuned-deepfake"
|
| 13 |
+
|
| 14 |
+
def run_inference(audio_path, fallback_model_name=DEFAULT_HUB_MODEL):
|
| 15 |
+
"""
|
| 16 |
+
Realiza inferência real priorizando:
|
| 17 |
+
1. Variável de ambiente WAV2VEC_MODEL (Se definida)
|
| 18 |
+
2. Modelo fine-tuned localmente (Se existir)
|
| 19 |
+
3. Modelo padrão do Hugging Face Hub
|
| 20 |
+
"""
|
| 21 |
+
if WAV2VEC_MODEL_ENV:
|
| 22 |
+
model_path = WAV2VEC_MODEL_ENV
|
| 23 |
+
model_name = f"Env Model ({WAV2VEC_MODEL_ENV})"
|
| 24 |
+
elif os.path.exists(LOCAL_MODEL_DIR):
|
| 25 |
+
model_path = LOCAL_MODEL_DIR
|
| 26 |
+
model_name = "Local Fine-Tuned Model"
|
| 27 |
+
else:
|
| 28 |
+
model_path = fallback_model_name
|
| 29 |
+
model_name = f"Hub Model ({fallback_model_name})"
|
| 30 |
+
|
| 31 |
+
print(f"Rodando inferência REAL [{model_name}] em: {audio_path}", file=sys.stderr)
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
# 1. Carrega extrator de características e modelo
|
| 35 |
+
print("Lendo modelo...", file=sys.stderr)
|
| 36 |
+
feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
|
| 37 |
+
model = AutoModelForAudioClassification.from_pretrained(model_path)
|
| 38 |
+
|
| 39 |
+
# 2. Carrega e pré-processa o áudio
|
| 40 |
+
print(f"Lendo áudio: {audio_path}", file=sys.stderr)
|
| 41 |
+
audio, sr = librosa.load(audio_path, sr=16000)
|
| 42 |
+
print(f"Áudio carregado. Shape: {audio.shape}", file=sys.stderr)
|
| 43 |
+
|
| 44 |
+
# 3. Prepara inputs
|
| 45 |
+
inputs = feature_extractor(audio, sampling_rate=16000, return_tensors="pt", padding=True)
|
| 46 |
+
|
| 47 |
+
# 3. Inferência
|
| 48 |
+
with torch.no_grad():
|
| 49 |
+
logits = model(**inputs).logits
|
| 50 |
+
|
| 51 |
+
# 4. Processa resultados
|
| 52 |
+
scores = torch.softmax(logits, dim=-1)
|
| 53 |
+
# O modelo HyperMoon geralmente tem 2 classes: 0 (Fake/Spoof) e 1 (Real/Bonafide)
|
| 54 |
+
# ou vice-versa. Vamos checar o config id2label
|
| 55 |
+
id2label = model.config.id2label
|
| 56 |
+
|
| 57 |
+
prediction_idx = torch.argmax(scores, dim=-1).item()
|
| 58 |
+
label = id2label[prediction_idx]
|
| 59 |
+
confidence = scores[0][prediction_idx].item()
|
| 60 |
+
|
| 61 |
+
# Normaliza para o nosso formato (precisamos saber quem é fraude)
|
| 62 |
+
# Se o label contiver 'fake', 'spoof' ou 'fraud', é fraude.
|
| 63 |
+
is_fraud = any(x in label.lower() for x in ['fake', 'spoof', 'fraud'])
|
| 64 |
+
|
| 65 |
+
# Queremos o 'deepfake_probability'
|
| 66 |
+
# Se o label 0 for fake, a probabilidade de deepfake é score[0][0]
|
| 67 |
+
# Tentamos encontrar o índice do 'fake'
|
| 68 |
+
fraud_idx = 0
|
| 69 |
+
for idx, lbl in id2label.items():
|
| 70 |
+
if any(x in lbl.lower() for x in ['fake', 'spoof', 'fraud']):
|
| 71 |
+
fraud_idx = int(idx) # Importante: converter para int
|
| 72 |
+
break
|
| 73 |
+
|
| 74 |
+
fraud_prob = scores[0][fraud_idx].item()
|
| 75 |
+
|
| 76 |
+
# --- NOVO: Análise Temporal (XAI) ---
|
| 77 |
+
temporal_scores = []
|
| 78 |
+
segment_duration = 1.0 # 1 segundo
|
| 79 |
+
samples_per_segment = int(segment_duration * 16000)
|
| 80 |
+
|
| 81 |
+
for i in range(0, len(audio), samples_per_segment):
|
| 82 |
+
segment = audio[i : i + samples_per_segment]
|
| 83 |
+
if len(segment) < samples_per_segment // 2: continue # Ignora restos muito pequenos
|
| 84 |
+
|
| 85 |
+
seg_inputs = feature_extractor(segment, sampling_rate=16000, return_tensors="pt", padding=True)
|
| 86 |
+
with torch.no_grad():
|
| 87 |
+
seg_logits = model(**seg_inputs).logits
|
| 88 |
+
seg_probs = torch.softmax(seg_logits, dim=-1)
|
| 89 |
+
seg_fraud_prob = seg_probs[0][fraud_idx].item()
|
| 90 |
+
temporal_scores.append(round(seg_fraud_prob, 3))
|
| 91 |
+
# ------------------------------------
|
| 92 |
+
|
| 93 |
+
results = {
|
| 94 |
+
"model": model_name,
|
| 95 |
+
"prediction": label.upper(),
|
| 96 |
+
"confidence": confidence,
|
| 97 |
+
"deepfake_probability": fraud_prob,
|
| 98 |
+
"temporal_scores": temporal_scores, # Novo campo para XAI
|
| 99 |
+
"verdict": "SPOOF" if is_fraud else "BONAFIDE",
|
| 100 |
+
"metadata": {
|
| 101 |
+
"id2label": id2label,
|
| 102 |
+
"all_scores": scores.tolist()
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(f"Erro na inferência: {e}")
|
| 108 |
+
results = {
|
| 109 |
+
"error": str(e),
|
| 110 |
+
"verdict": "ERROR"
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return results
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
if len(sys.argv) < 2:
|
| 117 |
+
print("Uso: python inference_wav2vec.py <audio_path>")
|
| 118 |
+
else:
|
| 119 |
+
# Silenciamos warnings de transformers
|
| 120 |
+
import warnings
|
| 121 |
+
warnings.filterwarnings("ignore")
|
| 122 |
+
print(json.dumps(run_inference(sys.argv[1])))
|
execution/metadata_extractor.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
def extract_metadata(file_path):
|
| 5 |
+
"""
|
| 6 |
+
Extrai metadados básicos de um arquivo de áudio.
|
| 7 |
+
"""
|
| 8 |
+
# Mock de extração
|
| 9 |
+
metadata = {
|
| 10 |
+
"format": "WAV",
|
| 11 |
+
"sample_rate": 44100,
|
| 12 |
+
"channels": 2,
|
| 13 |
+
"duration_seconds": 12.5,
|
| 14 |
+
"encoder": "Lavf60.3.100",
|
| 15 |
+
"creation_time": "2026-04-23 19:40:00"
|
| 16 |
+
}
|
| 17 |
+
return metadata
|
| 18 |
+
|
| 19 |
+
if __name__ == "__main__":
|
| 20 |
+
if len(sys.argv) < 2:
|
| 21 |
+
print("Uso: python metadata_extractor.py <path_to_audio>")
|
| 22 |
+
sys.exit(1)
|
| 23 |
+
|
| 24 |
+
path = sys.argv[1]
|
| 25 |
+
meta = extract_metadata(path)
|
| 26 |
+
print(json.dumps(meta, indent=2))
|
execution/train_wav2vec.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import librosa
|
| 4 |
+
from torch.utils.data import Dataset
|
| 5 |
+
from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2ForSequenceClassification, Trainer, TrainingArguments
|
| 6 |
+
from typing import Dict, List
|
| 7 |
+
|
| 8 |
+
# Define o modelo base usado pelo projeto
|
| 9 |
+
BASE_MODEL_NAME = "HyperMoon/wav2vec2-base-960h-finetuned-deepfake"
|
| 10 |
+
LOCAL_MODEL_DIR = "./local_finetuned_model"
|
| 11 |
+
|
| 12 |
+
def get_processor():
|
| 13 |
+
"""Retorna o extrator de características do modelo base (processador de áudio puro, sem tokenizador de texto)"""
|
| 14 |
+
return Wav2Vec2FeatureExtractor.from_pretrained(BASE_MODEL_NAME)
|
| 15 |
+
|
| 16 |
+
class DeepfakeAudioDataset(Dataset):
|
| 17 |
+
"""
|
| 18 |
+
Dataset Customizado do Pytorch para carregar áudios de Pastas.
|
| 19 |
+
Espera-se que o diretório base tenha duas subpastas: 'real' e 'fake'.
|
| 20 |
+
"""
|
| 21 |
+
def __init__(self, root_dir: str, processor: Wav2Vec2FeatureExtractor, max_length: int = 160000):
|
| 22 |
+
self.root_dir = root_dir
|
| 23 |
+
self.processor = processor
|
| 24 |
+
self.max_length = max_length
|
| 25 |
+
self.files: List[Dict] = []
|
| 26 |
+
|
| 27 |
+
self._load_metadata()
|
| 28 |
+
|
| 29 |
+
def _load_metadata(self):
|
| 30 |
+
real_dir = os.path.join(self.root_dir, 'real')
|
| 31 |
+
fake_dir = os.path.join(self.root_dir, 'fake')
|
| 32 |
+
|
| 33 |
+
if os.path.exists(real_dir):
|
| 34 |
+
for f in os.listdir(real_dir):
|
| 35 |
+
if f.lower().endswith(('.wav', '.mp3', '.flac')):
|
| 36 |
+
self.files.append({"path": os.path.join(real_dir, f), "label": 0})
|
| 37 |
+
|
| 38 |
+
if os.path.exists(fake_dir):
|
| 39 |
+
for f in os.listdir(fake_dir):
|
| 40 |
+
if f.lower().endswith(('.wav', '.mp3', '.flac')):
|
| 41 |
+
self.files.append({"path": os.path.join(fake_dir, f), "label": 1})
|
| 42 |
+
|
| 43 |
+
def __len__(self):
|
| 44 |
+
return len(self.files)
|
| 45 |
+
|
| 46 |
+
def __getitem__(self, idx):
|
| 47 |
+
item = self.files[idx]
|
| 48 |
+
audio_path = item["path"]
|
| 49 |
+
label = item["label"]
|
| 50 |
+
|
| 51 |
+
# Load and resample audio to 16kHz
|
| 52 |
+
speech, _ = librosa.load(audio_path, sr=16000)
|
| 53 |
+
|
| 54 |
+
# Process audio to get input values
|
| 55 |
+
input_values = self.processor(
|
| 56 |
+
speech,
|
| 57 |
+
sampling_rate=16000,
|
| 58 |
+
return_tensors="pt",
|
| 59 |
+
padding="max_length",
|
| 60 |
+
max_length=self.max_length,
|
| 61 |
+
truncation=True
|
| 62 |
+
).input_values[0]
|
| 63 |
+
|
| 64 |
+
return {
|
| 65 |
+
"input_values": input_values,
|
| 66 |
+
"labels": torch.tensor(label, dtype=torch.long)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
def start_finetuning(dataset_dir: str):
|
| 70 |
+
"""
|
| 71 |
+
Inicia o treinamento congelando as camadas base para evitar OOM e focar apenas na cabeça de classificação.
|
| 72 |
+
"""
|
| 73 |
+
processor = get_processor()
|
| 74 |
+
|
| 75 |
+
# Prepara os datasets (simplificação: usando o mesmo para train e eval na V1)
|
| 76 |
+
train_dataset = DeepfakeAudioDataset(dataset_dir, processor)
|
| 77 |
+
|
| 78 |
+
if len(train_dataset) == 0:
|
| 79 |
+
raise ValueError("Nenhum áudio encontrado no dataset.")
|
| 80 |
+
|
| 81 |
+
# Mapeamento explícito para evitar confusão de labels (0=Real, 1=Fraude)
|
| 82 |
+
id2label = {0: "AUTHENTIC", 1: "FAKE"}
|
| 83 |
+
label2id = {"AUTHENTIC": 0, "FAKE": 1}
|
| 84 |
+
|
| 85 |
+
# Carrega modelo e congela base
|
| 86 |
+
model = Wav2Vec2ForSequenceClassification.from_pretrained(
|
| 87 |
+
BASE_MODEL_NAME,
|
| 88 |
+
num_labels=2,
|
| 89 |
+
id2label=id2label,
|
| 90 |
+
label2id=label2id,
|
| 91 |
+
ignore_mismatched_sizes=True
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Freeze feature extractor e a base do transformer para poupar memória e tempo (Adaptação para hardwares fracos)
|
| 95 |
+
if hasattr(model, 'freeze_feature_encoder'):
|
| 96 |
+
model.freeze_feature_encoder()
|
| 97 |
+
elif hasattr(model, 'freeze_feature_extractor'):
|
| 98 |
+
model.freeze_feature_extractor()
|
| 99 |
+
|
| 100 |
+
if hasattr(model, 'wav2vec2'):
|
| 101 |
+
for param in model.wav2vec2.parameters():
|
| 102 |
+
param.requires_grad = False
|
| 103 |
+
|
| 104 |
+
# Training args voltados para hardware modesto
|
| 105 |
+
training_args = TrainingArguments(
|
| 106 |
+
output_dir="./results",
|
| 107 |
+
num_train_epochs=5,
|
| 108 |
+
per_device_train_batch_size=2, # Batch muito pequeno para não estourar memória
|
| 109 |
+
gradient_accumulation_steps=4, # Acumula para dar efeito de batch=8
|
| 110 |
+
learning_rate=2e-5,
|
| 111 |
+
save_strategy="epoch",
|
| 112 |
+
logging_dir="./logs",
|
| 113 |
+
logging_steps=1,
|
| 114 |
+
remove_unused_columns=False,
|
| 115 |
+
report_to="none", # Evita erros de conexão com serviços externos de log
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
trainer = Trainer(
|
| 119 |
+
model=model,
|
| 120 |
+
args=training_args,
|
| 121 |
+
train_dataset=train_dataset,
|
| 122 |
+
eval_dataset=train_dataset, # Idealmente, devíamos fazer um split de 80/20
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
trainer.train()
|
| 126 |
+
|
| 127 |
+
# Salva o modelo afinado
|
| 128 |
+
model.save_pretrained(LOCAL_MODEL_DIR)
|
| 129 |
+
processor.save_pretrained(LOCAL_MODEL_DIR)
|
| 130 |
+
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
import sys
|
| 135 |
+
if len(sys.argv) > 1:
|
| 136 |
+
start_finetuning(sys.argv[1])
|
main.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from execution.fastapi_server import app
|
| 2 |
+
import uvicorn
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
# Hugging Face Spaces usa a porta 7860 por padrão
|
| 7 |
+
port = int(os.environ.get("PORT", 7860))
|
| 8 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
| 9 |
+
|
| 10 |
+
@app.get("/version-check")
|
| 11 |
+
async def version_check():
|
| 12 |
+
return {"version": "2.2", "status": "updated"}
|
package.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "confereai-frontend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"build": "echo 'Static build complete'"
|
| 7 |
+
}
|
| 8 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core Backend
|
| 2 |
+
fastapi>=0.100.0
|
| 3 |
+
uvicorn>=0.23.0
|
| 4 |
+
python-multipart>=0.0.6
|
| 5 |
+
accelerate>=1.1.0
|
| 6 |
+
|
| 7 |
+
# Machine Learning & Audio
|
| 8 |
+
torch --index-url https://download.pytorch.org/whl/cpu
|
| 9 |
+
transformers
|
| 10 |
+
librosa
|
| 11 |
+
soundfile
|
| 12 |
+
matplotlib
|
| 13 |
+
scipy
|
| 14 |
+
|
| 15 |
+
# Utilities
|
| 16 |
+
python-dotenv
|
| 17 |
+
requests
|
superpowers
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Subproject commit e7a2d16476bf042e9add4699c9d018a90f86e4a6
|
vercel.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 2,
|
| 3 |
+
"name": "confereai",
|
| 4 |
+
"builds": [
|
| 5 |
+
{
|
| 6 |
+
"src": "dashboard/**/*",
|
| 7 |
+
"use": "@vercel/static"
|
| 8 |
+
}
|
| 9 |
+
],
|
| 10 |
+
"routes": [
|
| 11 |
+
{
|
| 12 |
+
"src": "/(.*)",
|
| 13 |
+
"dest": "/dashboard/$1"
|
| 14 |
+
}
|
| 15 |
+
]
|
| 16 |
+
}
|