From a17829014b4f6f31f6e3f16fe175f7b14419dd6a Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Wed, 7 Oct 2026 23:53:52 +0300 Subject: [PATCH] Add Docker build and deploy pipeline --- .env | 19 --- .env.example | 30 +++++ .gitea/workflows/deploy.yml | 16 +++ .gitignore | 7 ++ .idea/.gitignore | 8 -- .idea/amplicode-jpa.xml | 6 - .idea/misc.xml | 12 -- .idea/modules.xml | 8 -- .idea/vcs.xml | 6 - ai-moderation/.dockerignore | 8 ++ ai-moderation/Dockerfile | 22 ++++ ai-moderation/app/ml/image/weapon_detector.py | 109 ++++++++++++++++++ .../app/ml/image/weapon_prediction_result.py | 14 +++ ai-moderation/requirements.txt | Bin 0 -> 820 bytes 14 files changed, 206 insertions(+), 59 deletions(-) delete mode 100644 .env create mode 100644 .env.example create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore delete mode 100644 .idea/.gitignore delete mode 100644 .idea/amplicode-jpa.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 ai-moderation/.dockerignore create mode 100644 ai-moderation/Dockerfile create mode 100644 ai-moderation/app/ml/image/weapon_detector.py create mode 100644 ai-moderation/app/ml/image/weapon_prediction_result.py create mode 100644 ai-moderation/requirements.txt diff --git a/.env b/.env deleted file mode 100644 index 5771afd..0000000 --- a/.env +++ /dev/null @@ -1,19 +0,0 @@ -APP_NAME=AI Moderation Service - -APP_VERSION=1.0.0 - -HOST=0.0.0.0 - -PORT=8000 - -DEBUG=true - - # ===== AI ===== - -TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier - -MODEL_CACHE_DIR=./models - -DEVICE=auto - -HF_TOKEN: str = "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" \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..131180d --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +APP_NAME=AI Moderation Service + +APP_VERSION=1.0.0 + +HOST=0.0.0.0 + +PORT=8000 + +DEBUG=true + +# ===== AI ===== + +TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier +TEXT_TOXIC_THRESHOLD=0.90 + +IMAGE_MODEL=Falconsai/nsfw_image_detection +NSFW_THRESHOLD=0.85 + +IMAGE_CLIP_MODEL=openai/clip-vit-base-patch32 +CLIP_THRESHOLD=0.75 + +WEAPON_MODEL=google/owlv2-base-patch16-ensemble +WEAPON_THRESHOLD=0.40 +WEAPON_MIN_AREA_FRACTION=0.005 + +MODEL_CACHE_DIR=./models + +DEVICE=auto + +HF_TOKEN=<ваш-токен-hugging-face> \ No newline at end of file diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..8f3cd1f --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,16 @@ +name: deploy-ai +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: host + steps: + - uses: actions/checkout@v4 + - name: Build image + run: docker build -t nakhodka-ai:latest -t nakhodka-ai:${{ github.sha }} ai-moderation + - name: Restart ai-service + run: docker compose -f /opt/nakhodka/docker-compose.yml up -d ai-service + - name: Cleanup + run: docker image prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..247ffb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.env +.idea/ +__pycache__/ +*.pyc +venv/ +.venv/ +ai-moderation/models/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/amplicode-jpa.xml b/.idea/amplicode-jpa.xml deleted file mode 100644 index 98351bf..0000000 --- a/.idea/amplicode-jpa.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 39f665b..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index c65439d..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ai-moderation/.dockerignore b/ai-moderation/.dockerignore new file mode 100644 index 0000000..65171e6 --- /dev/null +++ b/ai-moderation/.dockerignore @@ -0,0 +1,8 @@ +__pycache__ +*.pyc +.env +venv +.venv +models +.idea +.git \ No newline at end of file diff --git a/ai-moderation/Dockerfile b/ai-moderation/Dockerfile new file mode 100644 index 0000000..68d3bc0 --- /dev/null +++ b/ai-moderation/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 +WORKDIR /app + +# torch ставим отдельно в облегчённой версии для процессора (без CUDA) +RUN pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY app ./app + +RUN useradd -r -u 1001 app \ + && mkdir -p /models \ + && chown app /models +USER app + +ENV MODEL_CACHE_DIR=/models +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/ai-moderation/app/ml/image/weapon_detector.py b/ai-moderation/app/ml/image/weapon_detector.py new file mode 100644 index 0000000..d49eded --- /dev/null +++ b/ai-moderation/app/ml/image/weapon_detector.py @@ -0,0 +1,109 @@ +import torch +from PIL import Image +from transformers import Owlv2ForObjectDetection, Owlv2Processor + +from app.config.settings import settings +from app.ml.image.weapon_prediction_result import WeaponPredictionResult + + +class WeaponDetector: + + QUERIES = [ + "a gun", + "a pistol", + "a revolver", + "a rifle", + "a shotgun", + "an assault rifle", + "a knife", + "a machete", + "a sword", + "a bomb", + "a grenade", + "a crossbow", + "a bow and arrow", + "brass knuckles", + "a nunchaku" + ] + + def __init__(self): + self.device = "cuda" if torch.cuda.is_available() else "cpu" + print(f"Loading OWLv2 weapon detector on {self.device}") + + self.model = Owlv2ForObjectDetection.from_pretrained( + settings.WEAPON_MODEL + ) + self.processor = Owlv2Processor.from_pretrained( + settings.WEAPON_MODEL + ) + + self.model.to(self.device) + self.model.eval() + + def predict( + self, + image: Image.Image + ) -> WeaponPredictionResult: + inputs = self.processor( + text=self.QUERIES, + images=image, + return_tensors="pt" + ) + + inputs = { + key: value.to(self.device) + for key, value in inputs.items() + } + + with torch.no_grad(): + outputs = self.model(**inputs) + + target_sizes = torch.tensor([image.size[::-1]]) + + results = self.processor.post_process_grounded_object_detection( + outputs, + threshold=0.0, + target_sizes=target_sizes + )[0] + + scores = results["scores"].tolist() + labels = results["labels"].tolist() + boxes = results["boxes"].tolist() + + image_area = image.width * image.height + label_scores = {} + + for score, label_index, box in zip(scores, labels, boxes): + box_area = (box[2] - box[0]) * (box[3] - box[1]) + + if box_area < image_area * settings.WEAPON_MIN_AREA_FRACTION: + continue + + if score >= settings.WEAPON_THRESHOLD: + query = self.QUERIES[label_index] + label_scores[query] = max( + label_scores.get(query, 0.0), + score + ) + + if not label_scores: + return WeaponPredictionResult( + label="normal", + score=0.0 + ) + + best_label = max( + label_scores, + key=label_scores.get + ) + + print("====================") + print("WEAPON RESULT") + print("Scores:", label_scores) + + return WeaponPredictionResult( + label=best_label, + score=label_scores[best_label], + detected_labels=sorted(label_scores.keys()), + box_count=len(label_scores) + ) \ No newline at end of file diff --git a/ai-moderation/app/ml/image/weapon_prediction_result.py b/ai-moderation/app/ml/image/weapon_prediction_result.py new file mode 100644 index 0000000..8ca967b --- /dev/null +++ b/ai-moderation/app/ml/image/weapon_prediction_result.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass, field + + +@dataclass(slots=True) +class WeaponPredictionResult: + label: str + + score: float + + detected_labels: list[str] = field( + default_factory=list + ) + + box_count: int = 0 \ No newline at end of file diff --git a/ai-moderation/requirements.txt b/ai-moderation/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f2a725673dab84640e0d471b5743684fed6ce323 GIT binary patch literal 820 zcmZWn+fIW}5S(WdKLt}-to6Zv@DG}Xf^94Z!bRoh)tOxmG;PRf%9+c~?(+A0K;ToO z$APE75eZL?9B+J9c(Rk>5f5bCb9;K80IO!W;$pQUrWVlW^72(XDDvclB4yH8OT~U8Bc|j4bE;Um<(sgMNNK%#8dY*c&Xw^S&TeG? j;@{uu3s=UTx`gV%c<;Qs*jM*$?}7J~sl94$8AtpB6!U=f literal 0 HcmV?d00001