First try
Browse files- Dockerfile +2 -0
- main.py +70 -31
- requirements.txt +1 -6
Dockerfile
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
#Imagem base
|
| 2 |
FROM python:3.10
|
| 3 |
|
|
|
|
| 4 |
ENV HUGGINGFACE_HUB_CACHE="/tmp/huggingface"
|
|
|
|
| 5 |
|
| 6 |
#Define um diretório de trabalho limpo
|
| 7 |
WORKDIR /app
|
|
|
|
| 1 |
#Imagem base
|
| 2 |
FROM python:3.10
|
| 3 |
|
| 4 |
+
#Variáveis de Ambiente para Caches
|
| 5 |
ENV HUGGINGFACE_HUB_CACHE="/tmp/huggingface"
|
| 6 |
+
ENV NUMBA_CACHE_DIR="/tmp/numba_cache"
|
| 7 |
|
| 8 |
#Define um diretório de trabalho limpo
|
| 9 |
WORKDIR /app
|
main.py
CHANGED
|
@@ -1,24 +1,18 @@
|
|
| 1 |
import os
|
| 2 |
import shutil
|
|
|
|
|
|
|
| 3 |
from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, status
|
| 4 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 5 |
-
from feature_extractor_single import process_single_image
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
app = FastAPI(title="
|
| 9 |
-
|
| 10 |
-
#Autenticação ---
|
| 11 |
security = HTTPBearer()
|
| 12 |
-
|
| 13 |
-
#1. Lê o Token Secreto da Variável de Ambiente
|
| 14 |
-
#No teste local, vamos injetar isso com 'docker run -e ...'
|
| 15 |
-
#No Hugging Face, vamos usar os "Secrets"
|
| 16 |
API_SECRET_TOKEN = os.environ.get("API_SECRET_TOKEN")
|
| 17 |
|
| 18 |
if API_SECRET_TOKEN is None:
|
| 19 |
print("AVISO: Variável de ambiente API_SECRET_TOKEN não definida.")
|
| 20 |
-
#Podemos lançar um erro aqui se não quisermos que a app inicie sem um token
|
| 21 |
-
#raise ValueError("API_SECRET_TOKEN não pode ser nula")
|
| 22 |
|
| 23 |
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
| 24 |
"""Verifica se o token enviado pelo cliente é o correto."""
|
|
@@ -27,7 +21,6 @@ async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(secur
|
|
| 27 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 28 |
detail="Token de segurança não configurado no servidor",
|
| 29 |
)
|
| 30 |
-
|
| 31 |
if credentials.scheme != "Bearer" or credentials.credentials != API_SECRET_TOKEN:
|
| 32 |
raise HTTPException(
|
| 33 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
@@ -35,35 +28,81 @@ async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(secur
|
|
| 35 |
headers={"WWW-Authenticate": "Bearer"},
|
| 36 |
)
|
| 37 |
return credentials.credentials
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
@app.post("/extract_features/")
|
| 42 |
-
#2. Adiciona 'Depends(verify_token)' para proteger endpoint
|
| 43 |
async def extract_features(file: UploadFile = File(...), token: str = Depends(verify_token)):
|
| 44 |
"""
|
| 45 |
-
Endpoint
|
| 46 |
-
e retorna vetor de características extraído.
|
| 47 |
"""
|
| 48 |
-
#A única pasta em um contêiner Docker que tem permissão de escrita garantida é a pasta /tmp
|
| 49 |
-
#Precisamos direcionar todos os arquivos temporários para lá
|
| 50 |
temp_path = f"/tmp/temp_{file.filename}"
|
| 51 |
-
|
| 52 |
with open(temp_path, "wb") as buffer:
|
| 53 |
shutil.copyfileobj(file.file, buffer)
|
| 54 |
|
| 55 |
-
features_array = process_single_image(temp_path
|
| 56 |
-
|
| 57 |
-
#Remove imagem temporária
|
| 58 |
os.remove(temp_path)
|
| 59 |
|
| 60 |
-
#Converte o array Numpy (ex: (1536,)) em uma lista Python padrão
|
| 61 |
features_list = features_array.tolist()
|
| 62 |
-
|
| 63 |
-
#Retorna o vetor de features REAL no JSON
|
| 64 |
-
return {"features": features_list}
|
| 65 |
-
|
| 66 |
-
@app.get("/health", status_code=status.HTTP_200_OK)
|
| 67 |
-
async def health_check():
|
| 68 |
-
"""Endpoint simples para verificar se a API está no ar."""
|
| 69 |
-
return {"status": "ok"}
|
|
|
|
| 1 |
import os
|
| 2 |
import shutil
|
| 3 |
+
import joblib # <-- Importa o joblib
|
| 4 |
+
import numpy as np # <-- Importa o numpy
|
| 5 |
from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, status
|
| 6 |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 7 |
+
from feature_extractor_single import process_single_image # Esta é a extração do ConvNext
|
| 8 |
|
| 9 |
+
#1. CONFIGURAÇÃO DA APLICAÇÃO E AUTENTICAÇÃO
|
| 10 |
+
app = FastAPI(title="SojaClassifierAPI")
|
|
|
|
|
|
|
| 11 |
security = HTTPBearer()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
API_SECRET_TOKEN = os.environ.get("API_SECRET_TOKEN")
|
| 13 |
|
| 14 |
if API_SECRET_TOKEN is None:
|
| 15 |
print("AVISO: Variável de ambiente API_SECRET_TOKEN não definida.")
|
|
|
|
|
|
|
| 16 |
|
| 17 |
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
| 18 |
"""Verifica se o token enviado pelo cliente é o correto."""
|
|
|
|
| 21 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 22 |
detail="Token de segurança não configurado no servidor",
|
| 23 |
)
|
|
|
|
| 24 |
if credentials.scheme != "Bearer" or credentials.credentials != API_SECRET_TOKEN:
|
| 25 |
raise HTTPException(
|
| 26 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
| 28 |
headers={"WWW-Authenticate": "Bearer"},
|
| 29 |
)
|
| 30 |
return credentials.credentials
|
| 31 |
+
|
| 32 |
+
#2. CARREGAMENTO DOS MODELOS DE CLASSIFICAÇÃO (ML)
|
| 33 |
+
#Carrega os 4 arquivos .pkl UMA VEZ quando a API inicia.
|
| 34 |
+
print("Carregando modelos de classificação (.pkl)...")
|
| 35 |
+
try:
|
| 36 |
+
SCALER = joblib.load('scaler.pkl')
|
| 37 |
+
UMAP = joblib.load('umap_reducer.pkl')
|
| 38 |
+
SVM = joblib.load('svm_model.pkl')
|
| 39 |
+
ENCODER = joblib.load('encoder.pkl')
|
| 40 |
+
print("Modelos de classificação carregados com sucesso.")
|
| 41 |
+
except FileNotFoundError:
|
| 42 |
+
print("ERRO: Arquivos .pkl do modelo não encontrados. Certifique-se de que 'scaler.pkl', 'umap_reducer.pkl', 'svm_model.pkl', e 'encoder.pkl' estão no repositório.")
|
| 43 |
+
#Em um cenário real, poderíamos impedir a API de iniciar aqui
|
| 44 |
+
|
| 45 |
+
#3. ENDPOINTS DA API
|
| 46 |
+
|
| 47 |
+
@app.post("/classify/")
|
| 48 |
+
async def classify_image(file: UploadFile = File(...), token: str = Depends(verify_token)):
|
| 49 |
+
"""
|
| 50 |
+
Endpoint principal: Recebe uma imagem, extrai features e classifica.
|
| 51 |
+
"""
|
| 52 |
+
temp_path = f"/tmp/temp_{file.filename}"
|
| 53 |
+
|
| 54 |
+
#Salva a imagem temporariamente
|
| 55 |
+
try:
|
| 56 |
+
with open(temp_path, "wb") as buffer:
|
| 57 |
+
shutil.copyfileobj(file.file, buffer)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
raise HTTPException(status_code=500, detail=f"Erro ao salvar arquivo: {e}")
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
#Extrai Features (ConvNext - 1536 dimensões)
|
| 63 |
+
#(process_single_image vem do seu feature_extractor_single.py)
|
| 64 |
+
features_array = process_single_image(temp_path)
|
| 65 |
+
|
| 66 |
+
#Prepara o vetor para o Scikit-learn
|
| 67 |
+
#O sklearn espera um array 2D (1, 1536) e não (1536,)
|
| 68 |
+
nova_feature = features_array.reshape(1, -1)
|
| 69 |
+
|
| 70 |
+
#Executa o Pipeline de Classificação (a lógica do seu notebook)
|
| 71 |
+
#Normaliza a nova feature com o mesmo scaler usado no treino
|
| 72 |
+
nova_feature_scaled = SCALER.transform(nova_feature)
|
| 73 |
+
|
| 74 |
+
#Reduz usando o mesmo UMAP já treinado
|
| 75 |
+
nova_feature_umap = UMAP.transform(nova_feature_scaled)
|
| 76 |
+
|
| 77 |
+
#Faz a predição
|
| 78 |
+
pred_numerica = SVM.predict(nova_feature_umap)
|
| 79 |
+
|
| 80 |
+
#Converte a predição numérica (ex: 2) de volta para o nome da classe (ex: "Ferrugem")
|
| 81 |
+
classe_predita = ENCODER.inverse_transform(pred_numerica)[0]
|
| 82 |
+
|
| 83 |
+
return {"diagnostico": classe_predita}
|
| 84 |
+
|
| 85 |
+
except Exception as e:
|
| 86 |
+
#Pega qualquer erro que acontecer durante a extração ou classificação
|
| 87 |
+
raise HTTPException(status_code=500, detail=f"Erro no processamento: {e}")
|
| 88 |
+
|
| 89 |
+
finally:
|
| 90 |
+
#Remove a imagem temporária, aconteça o que acontecer
|
| 91 |
+
if os.path.exists(temp_path):
|
| 92 |
+
os.remove(temp_path)
|
| 93 |
|
| 94 |
|
| 95 |
@app.post("/extract_features/")
|
|
|
|
| 96 |
async def extract_features(file: UploadFile = File(...), token: str = Depends(verify_token)):
|
| 97 |
"""
|
| 98 |
+
Endpoint de debug: Apenas extrai as features sem classificar.
|
|
|
|
| 99 |
"""
|
|
|
|
|
|
|
| 100 |
temp_path = f"/tmp/temp_{file.filename}"
|
|
|
|
| 101 |
with open(temp_path, "wb") as buffer:
|
| 102 |
shutil.copyfileobj(file.file, buffer)
|
| 103 |
|
| 104 |
+
features_array = process_single_image(temp_path)
|
|
|
|
|
|
|
| 105 |
os.remove(temp_path)
|
| 106 |
|
|
|
|
| 107 |
features_list = features_array.tolist()
|
| 108 |
+
return {"features": features_list}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -9,7 +9,6 @@ torchvision==0.17.2+cpu
|
|
| 9 |
-f https://download.pytorch.org/whl/torch_stable.html
|
| 10 |
|
| 11 |
transformers==4.39.3
|
| 12 |
-
pretrainedmodels
|
| 13 |
|
| 14 |
#Processamento de imagem
|
| 15 |
pillow==10.3.0
|
|
@@ -21,8 +20,4 @@ umap-learn==0.5.6
|
|
| 21 |
joblib==1.4.2
|
| 22 |
|
| 23 |
#Utilitários gerais
|
| 24 |
-
requests==2.32.3
|
| 25 |
-
timm
|
| 26 |
-
tdqm
|
| 27 |
-
seaborn==0.13.2
|
| 28 |
-
matplotlib==3.8.4
|
|
|
|
| 9 |
-f https://download.pytorch.org/whl/torch_stable.html
|
| 10 |
|
| 11 |
transformers==4.39.3
|
|
|
|
| 12 |
|
| 13 |
#Processamento de imagem
|
| 14 |
pillow==10.3.0
|
|
|
|
| 20 |
joblib==1.4.2
|
| 21 |
|
| 22 |
#Utilitários gerais
|
| 23 |
+
requests==2.32.3
|
|
|
|
|
|
|
|
|
|
|
|