Compare commits

2 Commits
master ... main

Author SHA1 Message Date
SlimusMinus
a989031270 Fix HuggingFace cache permissions
All checks were successful
deploy-ai / deploy (push) Successful in 12s
2026-10-08 00:00:26 +03:00
SlimusMinus
a17829014b Add Docker build and deploy pipeline
All checks were successful
deploy-ai / deploy (push) Successful in 2m56s
2026-10-07 23:53:52 +03:00
14 changed files with 207 additions and 59 deletions

19
.env
View File

@@ -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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"

30
.env.example Normal file
View File

@@ -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>

View File

@@ -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

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.env
.idea/
__pycache__/
*.pyc
venv/
.venv/
ai-moderation/models/

8
.idea/.gitignore generated vendored
View File

@@ -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

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AmplicodeJpaIdeaProjectConfig">
<option name="renamerInitialized" value="true" />
</component>
</project>

12
.idea/misc.xml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (moderation-post)" />
</component>
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="ProjectType">
<option name="id" value="jpab" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/moderation-post.iml" filepath="$PROJECT_DIR$/moderation-post.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,8 @@
__pycache__
*.pyc
.env
venv
.venv
models
.idea
.git

23
ai-moderation/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
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 -m -u 1001 app \
&& mkdir -p /models \
&& chown app /models
USER app
ENV MODEL_CACHE_DIR=/models
ENV HF_HOME=/models/hf
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -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)
)

View File

@@ -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

Binary file not shown.