nsfwapp/backend/ai_server.py
2026-05-05 14:05:56 +02:00

244 lines
5.8 KiB
Python

# backend\ai_server.py
import os
from typing import List, Optional
from fastapi import FastAPI
from pydantic import BaseModel
from ultralytics import YOLO
BODY_LABELS = {
"anus",
"ass",
"breasts",
"penis",
"tongue",
"pussy",
}
OBJECT_LABELS = {
"blindfold",
"buttplug",
"collar",
"dildo",
"handcuffs",
"shower",
"strapon",
"towel",
"vibrator",
}
CLOTHING_LABELS = {
"bikini",
"bra",
"dress",
"heels",
"hotpants",
"lingerie",
"panties",
"skirt",
"stockings",
"croptop",
}
POSITION_LABELS = {
"missionary",
"doggy",
"cowgirl",
"reverse_cowgirl",
"cunnilingus",
"prone_bone",
"standing",
"standing_doggy",
"spooning",
"sitting",
"facesitting",
"handjob",
"blowjob",
"toy_play",
"fingering",
"69",
"other",
}
class PredictBatchRequest(BaseModel):
paths: List[str]
detectorOnly: bool = False
imageSize: int = 640
model: Optional[str] = None
app = FastAPI()
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
DEFAULT_MODEL_PATH = BASE_DIR / "generated" / "training" / "detector" / "model" / "best.pt"
def resolve_model_path() -> str:
env_path = os.environ.get("YOLO_MODEL", "").strip()
if env_path:
p = Path(env_path)
if p.exists():
return str(p)
raise RuntimeError(f"YOLO_MODEL not found: {p}")
default_path = DEFAULT_MODEL_PATH
if default_path.exists():
return str(default_path)
raise RuntimeError(f"YOLO model not found: {default_path}")
_MODEL_PATH = resolve_model_path()
_DEVICE = os.environ.get("YOLO_DEVICE", "")
_CONF = float(os.environ.get("YOLO_CONF", "0.25"))
_BATCH = int(os.environ.get("YOLO_BATCH", "16"))
_IMGSZ = int(os.environ.get("YOLO_IMGSZ", "640"))
_HALF = os.environ.get("YOLO_HALF", "0").lower() in {"1", "true", "yes", "on"}
model = YOLO(_MODEL_PATH)
def scored(label: str, score: float) -> dict:
return {
"label": label,
"score": float(score),
}
def best_score(items: list[dict], label: str, score: float) -> None:
for item in items:
if item["label"] == label:
if score > item["score"]:
item["score"] = float(score)
return
items.append(scored(label, score))
def prediction_from_result(result) -> dict:
names = result.names or {}
boxes_out = []
body_parts = []
objects = []
clothing = []
sex_position = "unknown"
sex_position_score = 0.0
if result.boxes is not None:
xywhn = result.boxes.xywhn.cpu().tolist()
cls_values = result.boxes.cls.cpu().tolist()
conf_values = result.boxes.conf.cpu().tolist()
for box_xywhn, cls_id, conf in zip(xywhn, cls_values, conf_values):
label = str(names.get(int(cls_id), int(cls_id))).strip().lower()
score = float(conf)
if not label:
continue
cx, cy, w, h = [float(v) for v in box_xywhn]
x = max(0.0, min(1.0, cx - w / 2.0))
y = max(0.0, min(1.0, cy - h / 2.0))
w = max(0.0, min(1.0 - x, w))
h = max(0.0, min(1.0 - y, h))
boxes_out.append({
"label": label,
"score": score,
"x": x,
"y": y,
"w": w,
"h": h,
})
if label in BODY_LABELS:
best_score(body_parts, label, score)
if label in OBJECT_LABELS:
best_score(objects, label, score)
if label in CLOTHING_LABELS:
best_score(clothing, label, score)
if label in POSITION_LABELS and score > sex_position_score:
sex_position = label
sex_position_score = score
people_count = sum(
1 for box in boxes_out
if box["label"] in {"person", "person_male", "person_female", "person_unknown"}
)
male_count = sum(1 for box in boxes_out if box["label"] in {"person_male", "male_person"})
female_count = sum(1 for box in boxes_out if box["label"] in {"person_female", "female_person"})
unknown_count = max(0, people_count - male_count - female_count)
return {
"modelAvailable": True,
"source": f"yolo-server:{Path(_MODEL_PATH).name}",
"peopleCount": people_count,
"maleCount": male_count,
"femaleCount": female_count,
"unknownCount": unknown_count,
"sexPosition": sex_position,
"sexPositionScore": sex_position_score,
"bodyPartsPresent": body_parts,
"objectsPresent": objects,
"clothingPresent": clothing,
"boxes": boxes_out,
}
@app.post("/predict-batch")
def predict_batch(req: PredictBatchRequest):
paths = [str(path).strip() for path in req.paths if str(path).strip()]
if not paths:
return {
"ok": False,
"predictions": [],
"error": "no paths supplied",
}
imgsz = int(req.imageSize or _IMGSZ or 640)
try:
results = model.predict(
source=paths,
imgsz=imgsz,
conf=_CONF,
batch=_BATCH,
device=_DEVICE or None,
half=_HALF,
verbose=False,
)
predictions = [prediction_from_result(result) for result in results]
return {
"ok": True,
"predictions": predictions,
}
except Exception as exc:
return {
"ok": False,
"predictions": [],
"error": str(exc),
}
@app.get("/health")
def health():
names = getattr(model, "names", {}) or {}
return {
"ok": True,
"model": _MODEL_PATH,
"classCount": len(names),
"classes": list(names.values())[:80] if isinstance(names, dict) else names,
}