
Поводом для написания этой заметки стало обсуждение на недавнем отраслевом мероприятии задач мультимодерации контента: как быстро и надёжно находить «взрослые» сцены в длинных видео и автоматически подсвечивать фрагменты для ручной проверки. Похожие кейсы регулярно встречаются и в открытых соревнованиях по ИИ (например, в подборке задач Wink AI Challenge на Codenrock).
Зачем это всё?
Киноиндустрия очень нуждается в программных системах для поиска по текстам, видео и звуковой дорожке. Зачем это нужно? Ну, например, для того, чтобы определить возрастную категорию сценария, фильма или его фрагмента. Представьте себе режиссера, который отсматривает снятые сцены или занимается монтажом. Каждый день ему приходится проводить часы у экрана, определяя, что вырезать, а что - оставить.
Просмотр 100‑минутного видео с паузами и перепроверкой может занимать 3–4 часа и более. Естественно, после нескольких дней такой работы люди устают и могут что-то упустить, разойтись в оценке разных эпизодов, забыть что-то важное.
Хорошо иметь интеллектуального помощника, который анализирует сценарии, видео и аудио (на вышеупомянутом хакатоне — только сценарии), автоматически находит сцены с насилием, ненормативной лексикой, интимным контентом, с любыми сомнительными материалами. Он в разы улучшит скорость и эффективность работы модератора-человека. Это ключевая причина, по которой индустрия переходит от чисто человеческой модерации к гибридным моделям с ИИ. Такие системы не будут принимать финальные решения — они лишь усилят возможности человека, отслеживая контент и классифицируя его. Это так называемая «гибридная модерация», где ИИ работает как первый, скоростной фильтр, а человек сосредотачивается на сложных, неоднозначных случаях.
Как будет работать наша система, или три органа чувств ИИ
Очевидно, что для модерации потребуется анализ контента. Система должна оценивать его почти так же, как это делает человек. На входе будут текст (сценарий или субтитры), видео (кадры) и аудио (речь и звуки). В упрощённом варианте поиск сцены, подлежащей ограничению, может выглядеть так:
В сценарии или субтитрах система находит слова «удар», «пистолет», «обнажённый», «выпить».
На кадрах из видео ищем нож, бутылку, драку, интим.
В аудио находим крики, выстрелы, ругательства. Все наши каналы работают независимо, но в итоге сигналы объединяются.
Если в тексте идёт речь об оружии, а в кадре виден нож или бутылка, — это гораздо более надёжное свидетельство, чем каждый сигнал в отдельности.
Контекст важнее слов
В тексте можно было бы искать слова, относящиеся к запретным темам, согласно заранее составленному списку, но такая технология нормально работать не будет, потому что одно и то же слово может иметь разные значения в зависимости от контекста. Сравним фразы и их возможное значение:
«В баре началась драка. Кто-то разбил бутылку об стол.» — насилие (риск ограничения — высокий)
«Они не стали драться, а просто поговорили. Без насилия.» — ограничения не нужны (риск снижен)
«В шутку он сказал, что убьёт его взглядом. Все засмеялись.» — ничего криминального (риска нет)
Поэтому необходимо использовать не прямой поиск слов, а скользящее окно поиска — классический подход в обработке естественного языка, который позволяет учитывать локальный контекст по 3–5 предложений. Важно искать не только потенциально опасные слова, которые легко распознать с помощью регулярных выражений, но и контекст — слова, которые в сочетании с другими образуют отрицание, например, частицу «не» или предлог «без», фразы, которые могут свидетельствовать о нереальности намерений («шутка», «фантазия»).
Необходимо учитывать и вес найденных выражений. Например, элементы насилия в нашем поиске должны быть приоритетнее и важнее упоминаний о сигаретах. Размер окна в 3–5 предложений, в нашем случае, будет оптимальным для захвата смыслового фрагмента. Можно оставлять 3–4 предложения в окне и «шагать» через 1–2.
# pip install razdel numpy
# Импортируем библиотеки
import re
import numpy as np
from razdel import sentenize
from collections import defaultdict
# Словари для категорий риска в виде регулярных выражений
LEX = {
"profanity": [r"\b(слово_1|слово_2)\b"], # матерные слова (заглушка)
"violence": [r"\b(вред\w*|удар\w*|дра\w*|оруж\w*|угроз\w*)\b"],
"sex": [r"\b(поцелу\w*|интим\w*|обнаж\w*)\b"],
"substances":[r"\b(алкогол\w*|вино\w*|наркот\w*|сигарет\w*)\b"]
}
# Компилируем регулярные выражения
PAT = {k: [re.compile(p, re.I) for p in v] for k, v in LEX.items()}
# Регулярные выражения для смягчающих риски обстоятельств
NEG = re.compile(r"\b(не|без|нет)\b", re.I)
MIT = re.compile(r"\b(шутк\w+|ирони\w+)\b", re.I)
# Расширенные паттерны (для advanced_scoring)
NEG_PATTERN = re.compile(r"\b(не|без|нет|ничего|отсутствует)\b", re.I)
IRONY_PATTERN = re.compile(r"\b(шутк\w+|ирон\w+|сарказм\w+|прикол\w+)\b", re.I)
HYPOTHETICAL_PATTERN = re.compile(r"\b(если\s+бы|представь|вообрази|гипотетически)\b", re.I)
def windows(scene_text, win=4, stride=1):
# Разбиваем текст на предложения
sents = [s.text.strip() for s in sentenize(scene_text)]
out = []
# Создаем скользящие окна
for i in range(0, max(1, len(sents) - win + 1), stride):
seg = " ".join(sents[i:i+win])
sc = defaultdict(float)
for cat, pats in PAT.items():
for rgx in pats:
sc[cat] += len(list(rgx.finditer(seg)))
# Учитываем отрицания и шутки
if NEG.search(seg) or MIT.search(seg):
sc = {k: v * 0.6 for k, v in sc.items()}
total = sum(sc.values())
out.append({"i0": i, "i1": i+win, "txt": seg, "score": total, "cats": dict(sc)})
return out
def smooth(ws, a=0.6):
# Сглаживаем оценки: текущее окно усредняем с соседними
res = []
for j, w in enumerate(ws):
l = ws[j-1]["score"] if j > 0 else 0
r = ws[j+1]["score"] if j+1 < len(ws) else 0
w2 = dict(w)
w2["s"] = a * w["score"] + (1 - a) * (l + r) / 2
res.append(w2)
return res
def hysteresis(ws, hi=1.2, lo=0.6):
# Объединяем окна в сегменты с гистерезисом
segs = []
cur = None
for w in ws:
s = w.get("s", w["score"])
if cur is None and s >= hi:
cur = {"i0": w["i0"], "i1": w["i1"], "peak": s, "ev": w["txt"]}
elif cur is not None:
if s >= lo:
cur["i1"] = w["i1"]
cur["peak"] = max(cur["peak"], s)
if s == cur["peak"]:
cur["ev"] = w["txt"]
else:
segs.append(cur)
cur = None
if cur:
segs.append(cur)
return segs
def advanced_scoring(segment):
base_score = defaultdict(float)
context_penalty = 1.0
# Базовый поиск по словарям
for category, patterns in PAT.items():
for pattern in patterns:
matches = pattern.findall(segment)
base_score[category] += len(matches) * 0.3 # вес каждого совпадения
# Контекстуальные модификаторы
negation_context = NEG_PATTERN.search(segment)
irony_context = IRONY_PATTERN.search(segment)
hypothetical_context = HYPOTHETICAL_PATTERN.search(segment)
if negation_context:
context_penalty *= 0.3 # сильно снижаем при отрицаниях
if irony_context:
context_penalty *= 0.5 # умеренно снижаем при иронии
if hypothetical_context:
context_penalty *= 0.7 # немного снижаем при отсутствии реальных намерений
# Применяем штрафы
for category in base_score:
base_score[category] *= context_penalty
return base_score
Пример работы анализатора, который читает фразу «Я не хочу причинять вред»
Найдено слово «вред» (+0.3 балла)
Найдено отрицание «не» (вес умножается на 0.3)
Итог: 0.3 * 0.3 = 0.09 (низкий вес)
Глаза и уши системы: видео- и аудиоанализ
Текстовый анализатор уже может «поймать» многие эпизоды, которые нам нужны, но он не видит и не слышит. Для него фраза «обнажённая натура» будет опасной, а на видео в этот момент может быть и просто закат солнца и неясные контуры или намёки, которые не содержат ничего плохого. Поэтому нам надо анализировать видео и искать на нём соответствующие тексту объекты.
Используем YOLO (You Only Look Once) — нейросеть для обнаружения объектов в реальном времени. Чтобы точно понять, что происходит, не обязательно смотреть всё подряд: система может «отсматривать» 2–3 кадра в секунду вместо 25–30.
YOLO делит изображение на сетку ячеек и определяет, что за объект находится в кадре, учитывая вероятность совпадения. Ей не составит труда найти в кадре нож, бутылку или бокал вина. Но, к примеру, стандартная модель COCO (Common Objects in Context), самый популярный датасет для компьютерного зрения, не найдёт пистолет потому, что для оружия понадобится специализированная или дообученная модель. Если подключить NSFW-детектор (Not Safe For Work, например GantMan), можно отфильтровать и обнажённую натуру.
# pip install opencv-python ultralytics
import cv2
from ultralytics import YOLO
def extract_frames(video_path, target_fps=2):
"""Извлекаем кадры с заданной частотой, равномерно по времени"""
cap = cv2.VideoCapture(video_path)
actual_fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
dt_frame = 1.0 / actual_fps
dt_target = 1.0 / target_fps
frames = []
t = 0.0
next_t = 0.0
while True:
ret, frame = cap.read()
if not ret:
break
if t + 1e-9 >= next_t:
frames.append((t, frame))
next_t += dt_target
t += dt_frame
cap.release()
return frames
# Загружаем детектор объектов (можно дообучить его на своих данных)
yolo = YOLO("yolov8n.pt")
video_frames = extract_frames("film.mp4", target_fps=2)
visual_risks = []
for timestamp, frame in video_frames:
results = yolo(frame)[0]
frame_risk = 0.0
for box in results.boxes:
class_id = int(box.cls[0])
confidence = float(box.conf[0])
label = yolo.names[class_id]
# Наши "рисковые" объекты (COCO): нож, бутылка, бокал вина, человек
if label in {"knife", "bottle", "wine glass"}:
frame_risk += confidence * 0.7
elif label in {"person"}:
# Можно добавить NSFW-детектор для откровенных сцен
frame_risk += confidence * 0.1
visual_risks.append((timestamp, min(1.0, frame_risk)))
Не только смотрим, но и слушаем: аудиоанализ
Аудиоанализ начинается с распознавания речи (ASR — Automatic Speech Recognition) и звуков. Модель Whisper от OpenAI способна понимать речь на разных языках и расставлять временные метки (когда какое слово было сказано). Анализ звуковых событий включает распознавание неречевых звуков: выстрелов, криков, разбитого стекла и т. д. Чтобы их «услышала» наша система, используем модель, обученную на наборе данных AudioSet (например, VGGish), она распознаёт сотни типов звуков.
# pip install openai-whisper
import whisper
# Распознаем речь с таймкодами
asr_model = whisper.load_model("small")
transcription = asr_model.transcribe("film.mp4", language="ru")
# Получаем сегменты с временными метками
speech_segments = [
(segment["start"], segment["end"], segment["text"])
for segment in transcription["segments"]
]
# Анализируем звуковые события (VGGish + классификатор) — заглушка
def analyze_audio_events(audio_path):
"""Отслеживаем крики, выстрелы, треск разбитого стекла — пример-стаб"""
return [
(5.2, 5.8, "gunshot", 0.9),
(12.1, 12.4, "scream", 0.7),
(45.6, 45.9, "breaking_glass", 0.8)
]
audio_events = analyze_audio_events("film.mp4")
С помощью нашего модуля можно разобрать на события, например сцену из фильма «Криминальное чтиво»:
Текст: «Хочешь, я ему устрою мозговой штурм?»
Аудио: звук взвода курка + напряжённая музыка
Собираем пазл данных
Синхронизация временных линий
Теперь у нас три независимых сигнала: текст, видео и аудио. Но настоящая магия начинается, когда мы их объединяем.
Каждая из трёх систем-экспертов анализирует видео по собственным критериям, недоступным остальным, — и регистрирует события (events в коде выше). Видеоанализатор на 13-й секунде видит нож, аудиоанализатор на 21,1 секунде определяет крик, текстовый эксперт с 12,0 по 12,5 секунды детектирует угрозы. Создадим общую временную шкалу, на которой будем отмечать все события, скажем, раз в полсекунды.
# pip install numpy
import numpy as np
def create_timeline_tracks(events, total_duration, time_step=0.5):
"""Преобразуем события в равномерную временную шкалу"""
time_points = np.arange(0, total_duration + time_step, time_step)
track = np.zeros_like(time_points, dtype=float)
for event in events:
if len(event) == 2: # Точечное событие (время, оценка)
event_time, score = event
index = int(round(event_time / time_step))
if 0 <= index < len(track):
track[index] = max(track[index], score)
elif len(event) >= 3: # Длительное событие (начало, конец, оценка, ...)
start, end, score = event[:3]
start_idx = int(round(start / time_step))
end_idx = int(round(end / time_step))
for idx in range(start_idx, min(end_idx + 1, len(track))):
track[idx] = max(track[idx], score)
return track, time_points
# Определяем общую длительность
total_duration = max(
speech_segments[-1][1] if speech_segments else 0,
visual_risks[-1][0] if visual_risks else 0,
audio_events[-1][1] if audio_events else 0
)
# Создаем треки для каждого набора критериев
text_track, times = create_timeline_tracks(
[(start, end, 0.8) for start, end, text in speech_segments],
total_duration
)
visual_track, _ = create_timeline_tracks(visual_risks, total_duration)
audio_track, _ = create_timeline_tracks(
[(start, end, score) for start, end, event, score in audio_events],
total_duration
)
Взвешивание и объединение сигналов
Теперь, когда все данные синхронизированы, нужно решить, какой системе-эксперту доверять больше. Текст часто прямо указывает на намерения — дадим ему наибольший вес: 50%. Видео даёт реальные доказательства — 35%. Аудио добавляет важный контекст — 15%.
# Взвешиваем и объединяем
text_weight, visual_weight, audio_weight = 0.5, 0.35, 0.15
combined_risk = (
text_weight * text_track +
visual_weight * visual_track +
audio_weight * audio_track
)
# Сглаживаем (фильтр скользящего среднего)
smooth_kernel = np.array([0.15, 0.7, 0.15])
smoothed_risk = np.convolve(combined_risk, smooth_kernel, mode='same')
В реальном контенте опасные сцены редко длятся доли секунды — они развиваются во времени. Чтобы не дробить одну сцену на десятки мелких предупреждений, сделаем систему “нерешительной”. Она начнет контроль опасного сегмента при высоком риске (0.6) и продолжит его, пока риск не упадёт значительно ниже (0.35). Это даст целостные, осмысленные сегменты вместо рваных фрагментов.
def merge_risk_segments(risk_signal, times, text_track, visual_track, audio_track,
high_threshold=0.6, low_threshold=0.35):
"""Объединяем смежные рискованные сегменты (с передачей треков явно)"""
segments = []
current_segment = None
for i, risk_value in enumerate(risk_signal):
segment_time = times[i]
if current_segment is None and risk_value >= high_threshold:
current_segment = {
"start": segment_time,
"end": segment_time,
"peak_risk": risk_value,
"modalities": {
"text": text_track[i],
"visual": visual_track[i],
"audio": audio_track[i]
}
}
elif current_segment is not None:
if risk_value >= low_threshold:
current_segment["end"] = segment_time
if risk_value > current_segment["peak_risk"]:
current_segment["peak_risk"] = risk_value
current_segment["modalities"] = {
"text": text_track[i],
"visual": visual_track[i],
"audio": audio_track[i]
}
else:
segments.append(current_segment)
current_segment = None
if current_segment is not None:
segments.append(current_segment)
return segments
risk_segments = merge_risk_segments(smoothed_risk, times, text_track, visual_track, audio_track)
Калибровка и оценка качества
Слишком чувствительная система завалит редактора ложными срабатываниями, а слишком грубая — пропустит реальные проблемы. Борьба с ложными срабатываниями — одна из главных задач при настройке ИИ-модерации.
Мы используем три типа диагностических кривых: диаграмму надёжности (показывает, можно ли доверять цифрам), PR-кривую (баланс между точностью и полнотой) и ROC-кривую (соотношение реальных угроз и ложных тревог).
# pip install scikit-learn matplotlib
from sklearn.calibration import calibration_curve
from sklearn.metrics import precision_recall_curve, roc_curve
import matplotlib.pyplot as plt
def evaluate_calibration(true_labels, predicted_probs):
"""Оцениваем и визуализируем калибровку модели"""
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# Диаграмма надёжности
prob_true, prob_pred = calibration_curve(true_labels, predicted_probs, n_bins=10)
axes[0].plot(prob_pred, prob_true, "s-", label="Наша модель")
axes[0].plot([0, 1], [0, 1], "--", label="Идеально калиброванная")
axes[0].set_xlabel("Предсказанная вероятность")
axes[0].set_ylabel("Истинная вероятность")
axes[0].legend()
# PR-кривая
precision, recall, _ = precision_recall_curve(true_labels, predicted_probs)
axes[1].plot(recall, precision, label="PR-кривая")
axes[1].set_xlabel("Recall")
axes[1].set_ylabel("Precision")
# ROC-кривая
fpr, tpr, _ = roc_curve(true_labels, predicted_probs)
axes[2].plot(fpr, tpr, label="ROC-кривая")
axes[2].plot([0, 1], [0, 1], "--", label="Случайный классификатор")
axes[2].set_xlabel("False Positive Rate")
axes[2].set_ylabel("True Positive Rate")
plt.tight_layout()
return fig
Пороги и веса лучше хранить в конфигурационных файлах и обновлять по расписанию на специальном наборе.
Интерфейс для человека
Конечно, нейросетями человека не заменишь… Пока. Поэтому интерфейс программы показывает, почему те или иные эпизоды отмечены, что сказано в диалогах, какие объекты обнаружены на видео и какие звуки и речь сопровождают видеоряд. А редактор уже решает, как поступить. Человеческий надзор и работа со «сложными случаями» остаются критически важными компонентами системы.
# pip install streamlit pandas
import cv2
import streamlit as st
import pandas as pd
# Вспомогательные функции (упрощенные заглушки)
def extract_frame_at_time(video_path, t_sec):
cap = cv2.VideoCapture(video_path)
cap.set(cv2.CAP_PROP_POS_MSEC, t_sec * 1000)
ret, frame = cap.read()
cap.release()
if not ret or frame is None:
return None
# BGR -> RGB для корректного отображения
return frame[:, :, ::-1]
def get_subtitle_context(speech_segments, start, end):
parts = [txt for s, e, txt in speech_segments if not (e < start or s > end)]
return " ".join(parts) if parts else "(нет субтитров в этом интервале)"
def extract_risky_phrases(text):
found = set()
for cat, pats in PAT.items():
for rgx in pats:
for m in rgx.finditer(text):
found.add(m.group(0))
return list(found) or ["—"]
def get_detected_objects(segment):
# Заглушка: в реальном приложении здесь анализ детекций по кадрам сегмента
return ["knife", "bottle"]
def get_audio_events_for_segment(audio_events, start, end):
evs = [evt for (s, e, label, score) in audio_events if not (e < start or s > end)]
return [label for (s, e, label, score) in audio_events if not (e < start or s > end)] or ["—"]
# Простые обработки кликов (в реальном проекте надо связать их с базой)
def mark_segment_as_approved(segment): st.success("Сегмент подтвержден")
def mark_segment_as_false_positive(segment): st.warning("Сегмент отклонен (ложное срабатывание)")
def mark_segment_as_high_priority(segment): st.info("Сегмент помечен как высокий приоритет")
def add_to_calibration_set(segment): st.write("Сегмент добавлен в набор для калибровки")
def create_review_interface(risk_segments, video_path, speech_segments, audio_events):
"""Создаем интерфейс для ревью рискованных сцен"""
st.title("Редактор возрастного рейтинга")
st.markdown("### Обнаруженные рискованные сцены")
for i, segment in enumerate(risk_segments):
with st.expander(
f"Сцена {i+1} | Риск: {segment['peak_risk']:.2f} | "
f"{segment['start']:.1f}–{segment['end']:.1f} сек"
):
col1, col2 = st.columns([1, 2])
with col1:
frame_time = (segment["start"] + segment["end"]) / 2
frame = extract_frame_at_time(video_path, frame_time)
if frame is not None:
st.image(frame, caption=f"Кадр в {frame_time:.1f} сек")
else:
st.write("Не удалось извлечь кадр")
mod_data = segment["modalities"]
mod_df = pd.DataFrame({
"Наборы информации": ["Текст", "Видео", "Аудио"],
"Вклад": [float(mod_data.get("text", 0.0)),
float(mod_data.get("visual", 0.0)),
float(mod_data.get("audio", 0.0))]
})
st.bar_chart(mod_df.set_index("Наборы информации"))
with col2:
context_text = get_subtitle_context(speech_segments, segment["start"], segment["end"])
st.markdown("**Контекст из субтитров:**")
st.write(context_text)
st.markdown("**Доказательства риска:**")
if segment["modalities"]["text"] > 0.3:
risky_phrases = extract_risky_phrases(context_text)
st.write(f"Текст: {', '.join(risky_phrases)}")
if segment["modalities"]["visual"] > 0.3:
detected_objects = get_detected_objects(segment)
st.write(f"Видео: {', '.join(detected_objects)}")
if segment["modalities"]["audio"] > 0.3:
audio_labels = get_audio_events_for_segment(audio_events, segment["start"], segment["end"])
st.write(f"Аудио: {', '.join(audio_labels)}")
col_btns = st.columns(4)
with col_btns[0]:
if st.button("? Подтвердить", key=f"confirm_{i}"):
mark_segment_as_approved(segment)
with col_btns[1]:
if st.button("? Отклонить", key=f"reject_{i}"):
mark_segment_as_false_positive(segment)
with col_btns[2]:
if st.button("? Высокий приоритет", key=f"priority_{i}"):
mark_segment_as_high_priority(segment)
with col_btns[3]:
if st.button("?? Нужна калибровка", key=f"calibrate_{i}"):
add_to_calibration_set(segment)
# Пример запуска интерфейса (раскомментируйте при использовании Streamlit)
# create_review_interface(risk_segments, "film.mp4", speech_segments, audio_events)
Возможные вопросы
В: А если актёр просто держит бутылку, но не пьёт?
О: Система покажет риск, но редактор примет взвешенное решение. Со временем модель можно дообучить на размеченных данных, чтобы она различала такие нюансы.В: Как быть с сарказмом и иронией?
О: Текущая система снижает вес при словах «шутка», «ирония». Это примитивный подход; анализ шуток и сарказма — отдельная сложная задача для NLP, которую ещё нужно решить.В: Как система отличает боевые искусства от реальной драки?
О: Пока что плохо отличает — сцены, в обоих случаях включают бой и удары. Но можно добавить анализ жанра фильма и контекста сцены, и дообучить модель на примерах.В: Как быть с анимацией и CGI, где нет реальных людей?
О: Компьютерная графика и анимация обрабатываются так же — системы компьютерного зрения распознают объекты и сцены независимо от их происхождения. Однако понадобятся дообученные модели, так как мультперсонажи на людей не совсем похожи.В: Не слишком ли сложно поддерживать такую систему?
О: Словари и пороги можно вынести в конфиги, а калибровка по свежим данным занимает часы. Это быстрее и дешевле, чем месяцы просмотра видео, и даёт возможность масштабирования системы.-
В: Что делать с контентом на разных языках?
: Модель Whisper поддерживает мультиязычность, а YOLO - работает с уже имеющими названия визуальными обьектами, для поиска которых мы настроили её заранее. Но для каждого используемого языка нам потребуются словари и тонкая настройка контекстных правил. Можно начать с русского/английского и постепенно добавлять другие.В: Что это за словари, в каком они формате?
О: Словари обычно, представлены в виде структурированных данных, часто в формате JSON или XML, которые позволяют задавать не только термины, но и дополнительные параметры. Они группируются по тематикам, что позволяет системе оценивать степень риска использования тех или иных слов и выражений.
Словари рискованных слов и выражений требуют постоянной тонкой настройки. Это один из самых трудоёмких компонентов будущей системы. Над их составлением работают целые команды, которые регулярно дополняют списки новыми словами, сленгом, эвфемизмами и специфическими выражениями.
На первом уровне создаются базовые списки для каждой категории риска: например, словарь по насилию включает не только прямые обозначения («убить», «драка»), но и метафоры («разобраться», «наказать»), сленг («замочить», «завалить»). На втором уровне терминам присваивается вес — угроза «пристрелю» получает более высокий балл, чем «врежу». Требуется много чётких контекстных правил и исключений. Например, слово «стрельба» в контексте «стрельба из лука» должно игнорироваться системой, а в сочетании с «в упор» — указывать на высокий риск.
Третий уровень работы — адаптация к культурным особенностям и изменениям, которые происходят с течением времени. То, что считалось допустимым год назад, сегодня может потребовать строгой модерации. Молодёжный сленг, интернет-мемы постоянно обновляются, их надо мониторить, обновлять словари.
Каждый новый термин тестируется на тысячах примеров перед добавлением в рабочий словарь, а вес устаревших выражений снижается. Эта работа никогда не заканчивается, так как язык постоянно эволюционирует, появляются новые выражения и меняются культурные нормы, требуя от команды модераторов практически ежедневного вмешательства и корректировок.
Заключение
Cобранная «на коленке» система демонстрирует, как современные технологии компьютерного зрения, обработки естественного языка и аудиоанализа могут работать вместе для решения сложной практической задачи — автоматической модерации видеоконтента.
Какие возможности обеспечивает система:
Мультимодальность — объединение сигналов из разных источников
Гибридная модерация — ИИ фильтрует, человек принимает решения
Масштабируемость — обработка часов видео за минуты
Адаптивность — возможность дообучения на новых данных
Код из статьи можно использовать как основу для построения своего проекта, адаптировав его под конкретные требования и задачи.
Комментарии (2)

sushka0
28.10.2025 16:44В первом пункте хорошо бы LLMку использовать. А то такая система ломается об предложение:
Я не маньяк, но убью тебя!
"Не" есть, но предложение всё равно может быть опасным
autoanswerzzz
в конце нулевых с ночным часом рентв шла борьба региональными каналами делающими аналогичное - только рентв переозвучивал фильмцы психеделик трансом а регионали брали прям прон вырезая оттуда все моменты что нельзя показывать (создавая так эро версию) а хронометраш видео выравнивали замедлениями оставшихся кусков (типа сильно мазаное слоумо в самых сценах) оставляя звук не тронутым (хотя скорость картинки под авктивность звука всё же подгоняли) ... люди не могли выбрать какое говнецо смотреть тк был синхроный показ - а ещё кто занимается такой фигнёй