Add Docker build and deploy pipeline
All checks were successful
deploy-ai / deploy (push) Successful in 2m56s
All checks were successful
deploy-ai / deploy (push) Successful in 2m56s
This commit is contained in:
19
.env
19
.env
@@ -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
30
.env.example
Normal 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>
|
||||||
16
.gitea/workflows/deploy.yml
Normal file
16
.gitea/workflows/deploy.yml
Normal 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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.env
|
||||||
|
.idea/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
ai-moderation/models/
|
||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -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
|
|
||||||
6
.idea/amplicode-jpa.xml
generated
6
.idea/amplicode-jpa.xml
generated
@@ -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
12
.idea/misc.xml
generated
@@ -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
8
.idea/modules.xml
generated
@@ -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
6
.idea/vcs.xml
generated
@@ -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>
|
|
||||||
8
ai-moderation/.dockerignore
Normal file
8
ai-moderation/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
venv
|
||||||
|
.venv
|
||||||
|
models
|
||||||
|
.idea
|
||||||
|
.git
|
||||||
22
ai-moderation/Dockerfile
Normal file
22
ai-moderation/Dockerfile
Normal file
@@ -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"]
|
||||||
109
ai-moderation/app/ml/image/weapon_detector.py
Normal file
109
ai-moderation/app/ml/image/weapon_detector.py
Normal 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)
|
||||||
|
)
|
||||||
14
ai-moderation/app/ml/image/weapon_prediction_result.py
Normal file
14
ai-moderation/app/ml/image/weapon_prediction_result.py
Normal 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
|
||||||
BIN
ai-moderation/requirements.txt
Normal file
BIN
ai-moderation/requirements.txt
Normal file
Binary file not shown.
Reference in New Issue
Block a user