244 lines
5.8 KiB
Python
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,
|
|
} |