Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a989031270 | ||
|
|
a17829014b |
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
|
||||
23
ai-moderation/Dockerfile
Normal file
23
ai-moderation/Dockerfile
Normal 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"]
|
||||
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