Spaces:
Sleeping
Sleeping
fix: Reverter motor para a versão estável pré-motor pessoal (v2.7)
Browse files- execution/ensemble_manager.py +26 -44
- execution/fastapi_server.py +13 -38
- execution/inference_wav2vec.py +8 -4
execution/ensemble_manager.py
CHANGED
|
@@ -11,65 +11,47 @@ def get_combined_verdict(file_path):
|
|
| 11 |
|
| 12 |
# 2. Executa Motor 2 (AST - Espectrograma e Frequência)
|
| 13 |
res_ast = run_ast(file_path)
|
| 14 |
-
|
| 15 |
-
# 1. Extração de scores brutos
|
| 16 |
-
# Se houver erro em algum motor, o score padrão é 0.0, mas o veredito deve refletir o erro
|
| 17 |
-
has_error = False
|
| 18 |
-
error_msg = ""
|
| 19 |
-
|
| 20 |
-
if "error" in res_w2v:
|
| 21 |
-
has_error = True
|
| 22 |
-
error_msg += f"Wav2Vec2 Error: {res_w2v['error']}. "
|
| 23 |
-
|
| 24 |
-
if "error" in res_ast:
|
| 25 |
-
# Erro no AST não é fatal, usamos apenas W2V se possível
|
| 26 |
-
print(f"AST Warning: {res_ast['error']}")
|
| 27 |
-
|
| 28 |
-
score_w2v = res_w2v.get("deepfake_probability", 0.0)
|
| 29 |
score_ast = res_ast.get("risk_score", 0.0)
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
-
#
|
| 38 |
final_score = (score_w2v * WEIGHT_W2V) + (score_ast * WEIGHT_AST)
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
|
| 42 |
-
|
| 43 |
|
|
|
|
| 44 |
verdict = "AUTHENTIC"
|
| 45 |
|
| 46 |
-
#
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
if has_error and score_w2v == 0.0:
|
| 52 |
-
verdict = "ERROR"
|
| 53 |
-
elif score_w2v > 0.45:
|
| 54 |
-
verdict = "SPOOF" # Detecção direta pelo motor principal
|
| 55 |
elif final_score >= SPOOF_THRESHOLD:
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
| 61 |
return {
|
| 62 |
"verdict": verdict,
|
| 63 |
"fraud_probability": final_score,
|
| 64 |
"wav2vec_score": score_w2v,
|
| 65 |
"ast_score": score_ast,
|
| 66 |
-
"temporal_scores": res_w2v.get("temporal_scores", []),
|
| 67 |
-
"engines_consensus":
|
| 68 |
-
"error": error_msg if has_error else None,
|
| 69 |
"details": {
|
| 70 |
-
"protocol": "Protocolo de Rigor
|
| 71 |
-
"weights": {"wav2vec": WEIGHT_W2V, "ast": WEIGHT_AST}
|
| 72 |
-
"threshold": SPOOF_THRESHOLD
|
| 73 |
},
|
| 74 |
"engines": ["HyperMoon-Wav2Vec2", "AST-Spectrogram"]
|
| 75 |
}
|
|
|
|
| 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 V3 (Soberania HyperMoon)
|
| 17 |
+
# O motor Wav2Vec2 (HyperMoon) é o comandante principal (70% do peso)
|
| 18 |
+
|
| 19 |
+
# Pesos definidos para dar comando ao HyperMoon
|
| 20 |
+
WEIGHT_W2V = 0.7
|
| 21 |
+
WEIGHT_AST = 0.3
|
| 22 |
|
| 23 |
+
# Cálculo ponderado do score
|
| 24 |
final_score = (score_w2v * WEIGHT_W2V) + (score_ast * WEIGHT_AST)
|
| 25 |
|
| 26 |
+
# Thresholds de decisão
|
| 27 |
+
SPOOF_THRESHOLD = 0.50
|
| 28 |
+
HIGH_CONFIDENCE = 0.85
|
| 29 |
|
| 30 |
+
is_fraud = False
|
| 31 |
verdict = "AUTHENTIC"
|
| 32 |
|
| 33 |
+
# Lógica de decisão baseada na confiança do comandante (HyperMoon)
|
| 34 |
+
if score_w2v >= HIGH_CONFIDENCE:
|
| 35 |
+
is_fraud = True
|
| 36 |
+
verdict = "SPOOF"
|
| 37 |
+
message = "COMANDO HYPERMOON: Detecção crítica de padrões neurais sintéticos com alta convicção."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
elif final_score >= SPOOF_THRESHOLD:
|
| 39 |
+
is_fraud = True
|
| 40 |
+
verdict = "SPOOF"
|
| 41 |
+
message = f"ALERTA COMBINADO: Risco de {(final_score*100):.1f}% identificado, com base na análise espectral e fonética."
|
| 42 |
+
else:
|
| 43 |
+
message = "INTEGRIDADE CONFIRMADA: Padrões de voz condizentes com gravação humana autêntica."
|
| 44 |
+
|
| 45 |
return {
|
| 46 |
"verdict": verdict,
|
| 47 |
"fraud_probability": final_score,
|
| 48 |
"wav2vec_score": score_w2v,
|
| 49 |
"ast_score": score_ast,
|
| 50 |
+
"temporal_scores": res_w2v.get("temporal_scores", []),
|
| 51 |
+
"engines_consensus": message,
|
|
|
|
| 52 |
"details": {
|
| 53 |
+
"protocol": "Protocolo de Rigor V3 (Soberania HyperMoon)",
|
| 54 |
+
"weights": {"wav2vec": WEIGHT_W2V, "ast": WEIGHT_AST}
|
|
|
|
| 55 |
},
|
| 56 |
"engines": ["HyperMoon-Wav2Vec2", "AST-Spectrogram"]
|
| 57 |
}
|
execution/fastapi_server.py
CHANGED
|
@@ -12,10 +12,6 @@ import zipfile
|
|
| 12 |
import rarfile
|
| 13 |
import uuid
|
| 14 |
import uvicorn
|
| 15 |
-
import secrets
|
| 16 |
-
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 17 |
-
from slowapi.util import get_remote_address
|
| 18 |
-
from slowapi.errors import RateLimitExceeded
|
| 19 |
|
| 20 |
# Carrega variáveis do arquivo .env
|
| 21 |
load_dotenv()
|
|
@@ -25,14 +21,7 @@ from execution.feature_extractor import extract_features
|
|
| 25 |
from execution.ensemble_manager import get_combined_verdict
|
| 26 |
|
| 27 |
# Configurações de Segurança e Limites
|
| 28 |
-
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN")
|
| 29 |
-
if not ADMIN_TOKEN:
|
| 30 |
-
# Gera um token aleatório e seguro caso não esteja no .env (Hardening)
|
| 31 |
-
ADMIN_TOKEN = secrets.token_urlsafe(32)
|
| 32 |
-
print(f"\n[SECURITY WARNING] ADMIN_TOKEN não configurado no ambiente.")
|
| 33 |
-
print(f"[SECURITY WARNING] Token gerado dinamicamente: {ADMIN_TOKEN}\n")
|
| 34 |
-
|
| 35 |
-
limiter = Limiter(key_func=get_remote_address)
|
| 36 |
UPLOAD_MAX_SIZE = 10 * 1024 * 1024 # 10MB para análises comuns
|
| 37 |
ALLOWED_ORIGINS = os.environ.get("ALLOWED_ORIGINS", "*").split(",")
|
| 38 |
|
|
@@ -46,24 +35,15 @@ app.add_middleware(
|
|
| 46 |
allow_methods=["*"],
|
| 47 |
allow_headers=["*"],
|
| 48 |
)
|
| 49 |
-
app.state.limiter = limiter
|
| 50 |
-
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 51 |
|
| 52 |
-
# --- MIDDLEWARE
|
| 53 |
from fastapi import Request
|
| 54 |
from fastapi.responses import JSONResponse
|
| 55 |
|
| 56 |
-
# Garante que o diretório .tmp existe
|
| 57 |
-
if not os.path.exists(".tmp"):
|
| 58 |
-
os.makedirs(".tmp")
|
| 59 |
-
|
| 60 |
-
# Monta o diretório .tmp para servir os espectrogramas gerados
|
| 61 |
-
app.mount("/tmp", StaticFiles(directory=".tmp"), name="tmp")
|
| 62 |
-
|
| 63 |
@app.middleware("http")
|
| 64 |
async def limit_upload_size(request: Request, call_next):
|
| 65 |
# O limite de 10MB não se aplica às rotas de admin (datasets são maiores)
|
| 66 |
-
if request.method == "POST" and not request.url.path.startswith("/admin")
|
| 67 |
if "content-length" in request.headers:
|
| 68 |
if int(request.headers["content-length"]) > UPLOAD_MAX_SIZE:
|
| 69 |
return JSONResponse(
|
|
@@ -122,8 +102,7 @@ class AnalysisResult(BaseModel):
|
|
| 122 |
temporal_scores: list = []
|
| 123 |
|
| 124 |
@app.post("/analyze", response_model=AnalysisResult)
|
| 125 |
-
|
| 126 |
-
async def analyze_audio_endpoint(request: Request, background_tasks: BackgroundTasks, file: UploadFile = File(...)):
|
| 127 |
# Validação rigorosa de extensão
|
| 128 |
ALLOWED_EXTENSIONS = {'.wav', '.mp3', '.flac', '.ogg', '.m4a', '.aac'}
|
| 129 |
ext = os.path.splitext(file.filename)[1].lower()
|
|
@@ -152,19 +131,19 @@ async def analyze_audio_endpoint(request: Request, background_tasks: BackgroundT
|
|
| 152 |
# 2. Inferência via Ensemble (Wav2Vec2 + AST)
|
| 153 |
analysis = get_combined_verdict(file_path)
|
| 154 |
|
| 155 |
-
# 3. Agenda limpeza em background (após
|
| 156 |
def cleanup_temp_files(paths):
|
| 157 |
import time
|
| 158 |
-
time.sleep(
|
| 159 |
for p in paths:
|
| 160 |
-
if
|
| 161 |
try:
|
| 162 |
os.remove(p)
|
| 163 |
-
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
|
| 166 |
-
spec_local_path = features.get("spectrogram_path")
|
| 167 |
-
background_tasks.add_task(cleanup_temp_files, [file_path, spec_local_path])
|
| 168 |
|
| 169 |
# 4. Resposta Consolidada
|
| 170 |
return AnalysisResult(
|
|
@@ -172,7 +151,7 @@ async def analyze_audio_endpoint(request: Request, background_tasks: BackgroundT
|
|
| 172 |
fraud_score=analysis.get("fraud_probability", 0.0),
|
| 173 |
verdict=analysis.get("verdict", "UNKNOWN"),
|
| 174 |
spectrogram_url=features.get("spectrogram_path", "").replace(".tmp/", "/tmp/"),
|
| 175 |
-
engine="
|
| 176 |
wav2vec_score=analysis.get("wav2vec_score", 0.0),
|
| 177 |
ast_score=analysis.get("ast_score", 0.0),
|
| 178 |
engines_consensus=analysis.get("engines_consensus", ""),
|
|
@@ -181,11 +160,7 @@ async def analyze_audio_endpoint(request: Request, background_tasks: BackgroundT
|
|
| 181 |
|
| 182 |
except Exception as e:
|
| 183 |
print(f"Erro na análise: {e}")
|
| 184 |
-
|
| 185 |
-
raise HTTPException(
|
| 186 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 187 |
-
detail="Não foi possível processar este arquivo de áudio. Verifique se o arquivo não está corrompido ou protegido por DRM."
|
| 188 |
-
)
|
| 189 |
|
| 190 |
# --- ADMIN ENDPOINTS ---
|
| 191 |
|
|
|
|
| 12 |
import rarfile
|
| 13 |
import uuid
|
| 14 |
import uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Carrega variáveis do arquivo .env
|
| 17 |
load_dotenv()
|
|
|
|
| 21 |
from execution.ensemble_manager import get_combined_verdict
|
| 22 |
|
| 23 |
# Configurações de Segurança e Limites
|
| 24 |
+
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "confereai_admin_token_2026")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
UPLOAD_MAX_SIZE = 10 * 1024 * 1024 # 10MB para análises comuns
|
| 26 |
ALLOWED_ORIGINS = os.environ.get("ALLOWED_ORIGINS", "*").split(",")
|
| 27 |
|
|
|
|
| 35 |
allow_methods=["*"],
|
| 36 |
allow_headers=["*"],
|
| 37 |
)
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
# --- MIDDLEWARE DE TAMANHO DE UPLOAD ---
|
| 40 |
from fastapi import Request
|
| 41 |
from fastapi.responses import JSONResponse
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
@app.middleware("http")
|
| 44 |
async def limit_upload_size(request: Request, call_next):
|
| 45 |
# O limite de 10MB não se aplica às rotas de admin (datasets são maiores)
|
| 46 |
+
if request.method == "POST" and not request.url.path.startswith("/admin"):
|
| 47 |
if "content-length" in request.headers:
|
| 48 |
if int(request.headers["content-length"]) > UPLOAD_MAX_SIZE:
|
| 49 |
return JSONResponse(
|
|
|
|
| 102 |
temporal_scores: list = []
|
| 103 |
|
| 104 |
@app.post("/analyze", response_model=AnalysisResult)
|
| 105 |
+
async def analyze_audio_endpoint(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
|
|
|
|
| 106 |
# Validação rigorosa de extensão
|
| 107 |
ALLOWED_EXTENSIONS = {'.wav', '.mp3', '.flac', '.ogg', '.m4a', '.aac'}
|
| 108 |
ext = os.path.splitext(file.filename)[1].lower()
|
|
|
|
| 131 |
# 2. Inferência via Ensemble (Wav2Vec2 + AST)
|
| 132 |
analysis = get_combined_verdict(file_path)
|
| 133 |
|
| 134 |
+
# 3. Agenda limpeza em background (após 5 minutos para dar tempo do front ler a imagem)
|
| 135 |
def cleanup_temp_files(paths):
|
| 136 |
import time
|
| 137 |
+
time.sleep(300) # 5 minutos
|
| 138 |
for p in paths:
|
| 139 |
+
if os.path.exists(p):
|
| 140 |
try:
|
| 141 |
os.remove(p)
|
| 142 |
+
print(f"Cleanup: {p} removido.")
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print(f"Cleanup error: {e}")
|
| 145 |
|
| 146 |
+
background_tasks.add_task(cleanup_temp_files, [file_path, features.get("spectrogram_path")])
|
|
|
|
|
|
|
| 147 |
|
| 148 |
# 4. Resposta Consolidada
|
| 149 |
return AnalysisResult(
|
|
|
|
| 151 |
fraud_score=analysis.get("fraud_probability", 0.0),
|
| 152 |
verdict=analysis.get("verdict", "UNKNOWN"),
|
| 153 |
spectrogram_url=features.get("spectrogram_path", "").replace(".tmp/", "/tmp/"),
|
| 154 |
+
engine="Dual Engine (Wav2Vec2 + AST) - Protocolo de Rigor",
|
| 155 |
wav2vec_score=analysis.get("wav2vec_score", 0.0),
|
| 156 |
ast_score=analysis.get("ast_score", 0.0),
|
| 157 |
engines_consensus=analysis.get("engines_consensus", ""),
|
|
|
|
| 160 |
|
| 161 |
except Exception as e:
|
| 162 |
print(f"Erro na análise: {e}")
|
| 163 |
+
raise e
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
# --- ADMIN ENDPOINTS ---
|
| 166 |
|
execution/inference_wav2vec.py
CHANGED
|
@@ -8,7 +8,7 @@ from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
|
|
| 8 |
import os
|
| 9 |
|
| 10 |
LOCAL_MODEL_DIR = "./local_finetuned_model"
|
| 11 |
-
# Prioridade: 1. Pasta Local | 2. Variável de Ambiente | 3. Modelo Base
|
| 12 |
CUSTOM_MODEL_REPO = os.environ.get("CUSTOM_MODEL_REPO", None)
|
| 13 |
BASE_MODEL = "HyperMoon/wav2vec2-base-960h-finetuned-deepfake"
|
| 14 |
|
|
@@ -26,13 +26,17 @@ def get_wav2vec_resources(model_path):
|
|
| 26 |
_feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
|
| 27 |
model = AutoModelForAudioClassification.from_pretrained(model_path)
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
_model = model
|
| 30 |
_model.eval()
|
| 31 |
_last_model_path = model_path
|
| 32 |
|
| 33 |
-
# Log para depuração de labels
|
| 34 |
-
print(f"Mapeamento de Labels: {model.config.id2label}", file=sys.stderr)
|
| 35 |
-
|
| 36 |
return _feature_extractor, _model
|
| 37 |
|
| 38 |
def run_inference(audio_path, fallback_model_name=None):
|
|
|
|
| 8 |
import os
|
| 9 |
|
| 10 |
LOCAL_MODEL_DIR = "./local_finetuned_model"
|
| 11 |
+
# Prioridade: 1. Pasta Local (Upload direto) | 2. Repo Customizado (Variável de Ambiente) | 3. Modelo Base
|
| 12 |
CUSTOM_MODEL_REPO = os.environ.get("CUSTOM_MODEL_REPO", None)
|
| 13 |
BASE_MODEL = "HyperMoon/wav2vec2-base-960h-finetuned-deepfake"
|
| 14 |
|
|
|
|
| 26 |
_feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
|
| 27 |
model = AutoModelForAudioClassification.from_pretrained(model_path)
|
| 28 |
|
| 29 |
+
# --- OTIMIZAÇÃO: Quantização Dinâmica para CPU ---
|
| 30 |
+
if not torch.cuda.is_available():
|
| 31 |
+
print("Aplicando Quantização Dinâmica (CPU Optimization)...", file=sys.stderr)
|
| 32 |
+
model = torch.quantization.quantize_dynamic(
|
| 33 |
+
model, {torch.nn.Linear}, dtype=torch.qint8
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
_model = model
|
| 37 |
_model.eval()
|
| 38 |
_last_model_path = model_path
|
| 39 |
|
|
|
|
|
|
|
|
|
|
| 40 |
return _feature_extractor, _model
|
| 41 |
|
| 42 |
def run_inference(audio_path, fallback_model_name=None):
|