feat: new trained model + video gen
This commit is contained in:
parent
f069dfd589
commit
f6f8e3ca8f
|
|
@ -27,6 +27,7 @@ bert_trained/
|
||||||
checkpoint-*/
|
checkpoint-*/
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
|
data/*
|
||||||
data/*.csv
|
data/*.csv
|
||||||
data/*.srt
|
data/*.srt
|
||||||
data/*.txt
|
data/*.txt
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,21 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
from subtitles.parser import parse_srt_to_df # your existing parser
|
||||||
|
|
||||||
|
|
||||||
|
def load_subtitles_from_dir(folder: str | Path, ext: str = ".srt") -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Читает каждый файл из заданной папки и возвращает обьединенный pd.DataFrame
|
||||||
|
"""
|
||||||
|
folder = Path(folder)
|
||||||
|
all_frames: list[pd.DataFrame] = []
|
||||||
|
|
||||||
|
for file in folder.glob(f"*{ext}"):
|
||||||
|
df_file = parse_srt_to_df(file)
|
||||||
|
df_file["source_file"] = file.name
|
||||||
|
all_frames.append(df_file)
|
||||||
|
|
||||||
|
if not all_frames:
|
||||||
|
raise FileNotFoundError(f"No {ext} files found in {folder.resolve()}")
|
||||||
|
|
||||||
|
return pd.concat(all_frames, ignore_index=True)
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
KEYWORDS: set[str] = {
|
||||||
|
# ── Угроза / конфликт ─────────────────────────
|
||||||
|
"убью", "убить", "убил", "убийство", "смерть", "грохнуть",
|
||||||
|
"взорвётся", "взорвать", "взрыв", "детонатор", "бомба",
|
||||||
|
"ловушка", "засада", "атака", "штурм", "напасть",
|
||||||
|
"останавливать", "надо бежать", "слишком поздно", "мы проиграли",
|
||||||
|
"конец света", "апокалипсис", "катастрофа",
|
||||||
|
|
||||||
|
# ── Ранение / критическое состояние ────────────
|
||||||
|
"ранен", "ранение", "умираю", "без сознания", "остановить кровотечение",
|
||||||
|
"пульс не прослушивается", "доктор срочно", "нужна помощь",
|
||||||
|
"реанимация", "скорая", "кровь повсюду",
|
||||||
|
|
||||||
|
# ── Секреты / скрытая информация ───────────────
|
||||||
|
"секрет", "засекречено", "никому не говори", "это между нами",
|
||||||
|
"улик нет", "улик достаточно", "доказательства", "у нас есть доказательства",
|
||||||
|
"двойной агент", "крота", "подслушивают", "прослушка",
|
||||||
|
"флешка", "пароль", "шифр", "код доступа",
|
||||||
|
|
||||||
|
# ── Планы / приказ / миссия ─────────────────────
|
||||||
|
"план", "план б", "новая миссия", "операция начинается",
|
||||||
|
"отмена операции", "действуем по плану", "приказ ясен",
|
||||||
|
"координаты", "время ч", "расклад таков", "приступаем",
|
||||||
|
|
||||||
|
# ── Решения / выбор / судьба ────────────────────
|
||||||
|
"выбирай", "нет пути назад", "жертвовать", "это мой выбор",
|
||||||
|
"ты со мной", "придётся рискнуть", "спасти", "пожертвовать собой",
|
||||||
|
"мы должны решить", "остановись пока не поздно",
|
||||||
|
|
||||||
|
# ── Сюжетные твисты / раскрытия ────────────────
|
||||||
|
"на самом деле", "вся правда", "это была ложь", "он жив",
|
||||||
|
"подмена личности", "я всё время знала", "мой отец", "его брат близнец",
|
||||||
|
"это был мой план", "теперь всё ясно",
|
||||||
|
|
||||||
|
# ── Отношения / эмоции ─────────────────────────
|
||||||
|
"я тебя люблю", "выходи за меня", "у нас будет ребёнок",
|
||||||
|
"мы расстаёмся", "ты мне нравишься", "я без тебя не могу",
|
||||||
|
"он изменяет", "прости меня", "мы начнём сначала",
|
||||||
|
|
||||||
|
# ── Юмор / неловкость (часто ключевые моменты) ─
|
||||||
|
"это было случайно", "я всё испортил", "не в ту дверь",
|
||||||
|
"костюм порвался", "конфуз", "не то письмо", "он же шутил",
|
||||||
|
|
||||||
|
# ── Sci-Fi / техно-угроза ──────────────────────
|
||||||
|
"портал", "временной скачок", "искусственный интеллект", "ядро реактора",
|
||||||
|
"система отказала", "энергощит упал", "обратный отсчёт",
|
||||||
|
"спутник выведен из строя", "корабль падает",
|
||||||
|
|
||||||
|
# ── Фэнтези / магия ────────────────────────────
|
||||||
|
"заклинание", "пророчество", "артефакт", "древний орден",
|
||||||
|
"пламя дракона", "тёмный лорд", "портал закрывается",
|
||||||
|
"круг ритуала", "великий маг",
|
||||||
|
|
||||||
|
# ── Правосудие / расследование ────────────────
|
||||||
|
"вы арестованы", "расследование", "ордер на обыск",
|
||||||
|
"признательные показания", "дело раскрыто", "дело закрыто",
|
||||||
|
"улик достаточно", "главный подозреваемый",
|
||||||
|
|
||||||
|
# ── Финансы / шантаж / выкуп ───────────────────
|
||||||
|
"выкуп", "заложники", "долг", "банковский счёт",
|
||||||
|
"переведи деньги", "вымогательство", "шантаж", "биткойны",
|
||||||
|
|
||||||
|
# ── Общие триггеры (англ. формы, если субтитры смешанные) ─
|
||||||
|
"kill", "bomb", "explode", "secret", "mission", "run!",
|
||||||
|
"we're out of time", "trust me", "it's a trap"
|
||||||
|
}
|
||||||
20
main.py
20
main.py
|
|
@ -2,20 +2,36 @@ import argparse
|
||||||
|
|
||||||
from subtitles.parser import parse_srt_to_df
|
from subtitles.parser import parse_srt_to_df
|
||||||
from scoring.bert_ranker import BERTImportanceRanker
|
from scoring.bert_ranker import BERTImportanceRanker
|
||||||
|
from video_editor.highlight import VideoHighlighter
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Оценка важности фраз в субтитрах с помощью BERT.")
|
description="Оценка важности фраз в субтитрах с помощью BERT.")
|
||||||
parser.add_argument("srt_file", help="Путь к .srt файлу субтитров")
|
parser.add_argument("--srt_path", help="Путь к .srt файлу субтитров")
|
||||||
|
parser.add_argument("--video_path", help="Путь к видео файлу")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output", help="Путь к выходному CSV", default=config.CSV_OUTPUT)
|
"--output", help="Путь к выходному CSV", default=config.CSV_OUTPUT)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
df = parse_srt_to_df(args.srt_file)
|
df = parse_srt_to_df(args.srt_path)
|
||||||
ranker = BERTImportanceRanker(config.MODEL_NAME)
|
ranker = BERTImportanceRanker(config.MODEL_NAME)
|
||||||
df_scored = ranker.apply_to_dataframe(df)
|
df_scored = ranker.apply_to_dataframe(df)
|
||||||
|
important_df = (
|
||||||
|
df[df["importance_score"] > 0.9]
|
||||||
|
.loc[:, ["start", "end"]]
|
||||||
|
.reset_index(drop=True)
|
||||||
|
)
|
||||||
|
vh = VideoHighlighter(
|
||||||
|
video=args.video_path,
|
||||||
|
segments_df=important_df,
|
||||||
|
pad=1.5,
|
||||||
|
join_gap=0.3,
|
||||||
|
out_dir="clips",
|
||||||
|
concat=True,
|
||||||
|
)
|
||||||
|
vh.cut()
|
||||||
df_scored.to_csv(args.output, index=False, encoding='utf-8')
|
df_scored.to_csv(args.output, index=False, encoding='utf-8')
|
||||||
print(f"✅ Сохранено в {args.output}")
|
print(f"✅ Сохранено в {args.output}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,19 @@ certifi==2025.4.26
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
charset-normalizer==3.4.2
|
charset-normalizer==3.4.2
|
||||||
datasets==3.6.0
|
datasets==3.6.0
|
||||||
|
decorator==5.2.1
|
||||||
dill==0.3.8
|
dill==0.3.8
|
||||||
filelock==3.18.0
|
filelock==3.18.0
|
||||||
frozenlist==1.6.0
|
frozenlist==1.6.0
|
||||||
fsspec==2025.3.0
|
fsspec==2025.3.0
|
||||||
huggingface-hub==0.31.2
|
huggingface-hub==0.31.2
|
||||||
idna==3.10
|
idna==3.10
|
||||||
|
imageio==2.37.0
|
||||||
|
imageio-ffmpeg==0.6.0
|
||||||
Jinja2==3.1.6
|
Jinja2==3.1.6
|
||||||
joblib==1.5.0
|
joblib==1.5.0
|
||||||
MarkupSafe==3.0.2
|
MarkupSafe==3.0.2
|
||||||
|
moviepy==2.1.2
|
||||||
mpmath==1.3.0
|
mpmath==1.3.0
|
||||||
multidict==6.4.3
|
multidict==6.4.3
|
||||||
multiprocess==0.70.16
|
multiprocess==0.70.16
|
||||||
|
|
@ -37,11 +41,14 @@ nvidia-nvjitlink-cu12==12.6.85
|
||||||
nvidia-nvtx-cu12==12.6.77
|
nvidia-nvtx-cu12==12.6.77
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
pandas==2.2.3
|
pandas==2.2.3
|
||||||
|
pillow==10.4.0
|
||||||
|
proglog==0.1.12
|
||||||
propcache==0.3.1
|
propcache==0.3.1
|
||||||
psutil==7.0.0
|
psutil==7.0.0
|
||||||
pyarrow==20.0.0
|
pyarrow==20.0.0
|
||||||
pysrt==1.1.2
|
pysrt==1.1.2
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
|
python-dotenv==1.1.0
|
||||||
pytz==2025.2
|
pytz==2025.2
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
regex==2024.11.6
|
regex==2024.11.6
|
||||||
|
|
|
||||||
1194
result.csv
1194
result.csv
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +1,14 @@
|
||||||
import pysrt
|
import pysrt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import chardet
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def parse_srt_to_df(filepath: str, encoding: str = "cp1251") -> pd.DataFrame:
|
def parse_srt_to_df(filepath: str) -> pd.DataFrame:
|
||||||
subs = pysrt.open(filepath, encoding=encoding)
|
print(f"Загружаю {filepath}")
|
||||||
|
raw = Path(filepath).read_bytes()[:4096]
|
||||||
|
enc = chardet.detect(raw)["encoding"]
|
||||||
|
subs = pysrt.open(filepath, encoding=enc)
|
||||||
rows = []
|
rows = []
|
||||||
|
|
||||||
for sub in subs:
|
for sub in subs:
|
||||||
|
|
|
||||||
20
train.py
20
train.py
|
|
@ -4,10 +4,11 @@ from transformers.models.auto.modeling_auto import AutoModelForSequenceClassific
|
||||||
from transformers.models.bert import BertTokenizer, BertForSequenceClassification
|
from transformers.models.bert import BertTokenizer, BertForSequenceClassification
|
||||||
from transformers.trainer import Trainer
|
from transformers.trainer import Trainer
|
||||||
from transformers.training_args import TrainingArguments
|
from transformers.training_args import TrainingArguments
|
||||||
from subtitles.parser import parse_srt_to_df
|
|
||||||
import torch
|
import torch
|
||||||
from sklearn.model_selection import train_test_split
|
from sklearn.model_selection import train_test_split
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
|
from keywords import KEYWORDS
|
||||||
|
from data_loader.dir_loader import load_subtitles_from_dir
|
||||||
|
|
||||||
sentiment_model_name = "cointegrated/rubert-tiny-sentiment-balanced"
|
sentiment_model_name = "cointegrated/rubert-tiny-sentiment-balanced"
|
||||||
sentiment_tokenizer = AutoTokenizer.from_pretrained(sentiment_model_name)
|
sentiment_tokenizer = AutoTokenizer.from_pretrained(sentiment_model_name)
|
||||||
|
|
@ -16,9 +17,11 @@ sentiment_model = AutoModelForSequenceClassification.from_pretrained(
|
||||||
|
|
||||||
|
|
||||||
def is_important(text):
|
def is_important(text):
|
||||||
|
low = text.lower()
|
||||||
try:
|
try:
|
||||||
inputs = sentiment_tokenizer(
|
inputs = sentiment_tokenizer(
|
||||||
text, return_tensors="pt", truncation=True, padding=True)
|
low, return_tensors="pt", truncation=True, padding=True
|
||||||
|
)
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
logits = sentiment_model(**inputs).logits
|
logits = sentiment_model(**inputs).logits
|
||||||
probs = F.softmax(logits, dim=1)
|
probs = F.softmax(logits, dim=1)
|
||||||
|
|
@ -26,16 +29,19 @@ def is_important(text):
|
||||||
labels = ["NEGATIVE", "NEUTRAL", "POSITIVE"]
|
labels = ["NEGATIVE", "NEUTRAL", "POSITIVE"]
|
||||||
label = labels[label_id]
|
label = labels[label_id]
|
||||||
except Exception:
|
except Exception:
|
||||||
|
print("Ошибка расчета сентимента")
|
||||||
|
return 0
|
||||||
|
if len(low.split()) < 2:
|
||||||
return 0
|
return 0
|
||||||
if label in ("NEGATIVE", "POSITIVE"):
|
if label in ("NEGATIVE", "POSITIVE"):
|
||||||
return 1
|
return 1
|
||||||
if len(text.split()) > 6:
|
if any(kw in low for kw in KEYWORDS):
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
df = parse_srt_to_df("Breaking_Bad_RUS_2008_20210402033853.srt")
|
df = load_subtitles_from_dir("./data/subtitles/train")
|
||||||
df["label"] = df["text"].astype(str).apply(is_important)
|
df["label"] = df["text"].astype(str).apply(is_important)
|
||||||
|
|
||||||
train_texts, test_texts = train_test_split(
|
train_texts, test_texts = train_test_split(
|
||||||
|
|
@ -45,7 +51,11 @@ def main():
|
||||||
tokenizer = BertTokenizer.from_pretrained(model_name)
|
tokenizer = BertTokenizer.from_pretrained(model_name)
|
||||||
|
|
||||||
def tokenize_function(example):
|
def tokenize_function(example):
|
||||||
return tokenizer(example["text"], padding="max_length", truncation=True, max_length=64)
|
return tokenizer(
|
||||||
|
example["text"],
|
||||||
|
padding="max_length",
|
||||||
|
truncation=True, max_length=64
|
||||||
|
)
|
||||||
|
|
||||||
train_dataset = Dataset.from_pandas(train_texts[["text", "label"]])
|
train_dataset = Dataset.from_pandas(train_texts[["text", "label"]])
|
||||||
test_dataset = Dataset.from_pandas(test_texts[["text", "label"]])
|
test_dataset = Dataset.from_pandas(test_texts[["text", "label"]])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
# highlight_cutter.py
|
||||||
|
"""
|
||||||
|
Класс VideoHighlighter
|
||||||
|
======================
|
||||||
|
|
||||||
|
Нарезает «важные» отрезки видео по DataFrame со столбцами
|
||||||
|
`start` и `end` (секунды). Умеет:
|
||||||
|
|
||||||
|
• объединять соседние / перекрывающиеся интервалы (join_gap)
|
||||||
|
• добавлять «окно» pad слева и справа
|
||||||
|
• сохранять клипы отдельно или склеивать в единый highlights.mp4
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Union
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from moviepy.video.io.VideoFileClip import VideoFileClip
|
||||||
|
from moviepy.video.compositing.CompositeVideoClip import concatenate_videoclips
|
||||||
|
|
||||||
|
|
||||||
|
class VideoHighlighter:
|
||||||
|
"""Высвечивает (вырезает) нужные фрагменты ролика."""
|
||||||
|
|
||||||
|
# ─────────────────────────── init ────────────────────────────
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
video: Union[str, Path],
|
||||||
|
segments_df: pd.DataFrame,
|
||||||
|
pad: float = 1.0,
|
||||||
|
join_gap: float = 0.2,
|
||||||
|
out_dir: Union[str, Path] = "clips",
|
||||||
|
concat: bool = True,
|
||||||
|
) -> None:
|
||||||
|
self.video = Path(video)
|
||||||
|
self.df = segments_df.copy()
|
||||||
|
self.pad = float(pad)
|
||||||
|
self.join_gap = float(join_gap)
|
||||||
|
self.out_dir = Path(out_dir)
|
||||||
|
self.concat = concat
|
||||||
|
|
||||||
|
if not {"start", "end"} <= set(self.df.columns):
|
||||||
|
raise ValueError(
|
||||||
|
"DataFrame должен содержать колонки 'start' и 'end'")
|
||||||
|
|
||||||
|
self.out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ────────────────────────── public API ─────────────────────────
|
||||||
|
def cut(self) -> None:
|
||||||
|
intervals = self._prepare_intervals()
|
||||||
|
|
||||||
|
with VideoFileClip(str(self.video)) as video:
|
||||||
|
clips = self._make_subclips(video, intervals)
|
||||||
|
if self.concat:
|
||||||
|
self._save_concat(clips)
|
||||||
|
|
||||||
|
# ────────────────────────── helpers ───────────────────────────
|
||||||
|
def _to_seconds(self, x) -> float:
|
||||||
|
"""Любое представление времени → float секунд."""
|
||||||
|
if isinstance(x, (int, float)):
|
||||||
|
return float(x)
|
||||||
|
if isinstance(x, dt.time):
|
||||||
|
return x.hour * 3600 + x.minute * 60 + x.second + x.microsecond / 1_000_000
|
||||||
|
if isinstance(x, str):
|
||||||
|
# принимает форматы HH:MM:SS, HH:MM:SS.mmm или просто число
|
||||||
|
parts = x.replace(",", ".").split(":")
|
||||||
|
if len(parts) == 1:
|
||||||
|
return float(parts[0])
|
||||||
|
h, m, s = map(float, parts) if len(
|
||||||
|
parts) == 3 else (0, *map(float, parts))
|
||||||
|
return h * 3600 + m * 60 + s
|
||||||
|
raise TypeError(f"Неподдерживаемый тип времени: {type(x)}")
|
||||||
|
|
||||||
|
def _prepare_intervals(self) -> List[Tuple[float, float]]:
|
||||||
|
df = self.df.copy()
|
||||||
|
df["start"] = df["start"].apply(self._to_seconds)
|
||||||
|
df["end"] = df["end"].apply(self._to_seconds)
|
||||||
|
|
||||||
|
raw = list(df.sort_values("start")[
|
||||||
|
["start", "end"]].itertuples(index=False, name=None))
|
||||||
|
merged = self._merge_intervals(raw)
|
||||||
|
return self._add_padding(merged, total_dur=self._get_duration())
|
||||||
|
|
||||||
|
def _merge_intervals(self, intervals: List[Tuple[float, float]]) -> List[Tuple[float, float]]:
|
||||||
|
if not intervals:
|
||||||
|
return []
|
||||||
|
intervals.sort()
|
||||||
|
out = [list(intervals[0])]
|
||||||
|
for s, e in intervals[1:]:
|
||||||
|
last_e = out[-1][1]
|
||||||
|
if s - last_e <= self.join_gap: # перекрытие / стык
|
||||||
|
out[-1][1] = max(last_e, e)
|
||||||
|
else:
|
||||||
|
out.append([s, e])
|
||||||
|
return [tuple(x) for x in out]
|
||||||
|
|
||||||
|
def _add_padding(self, intervals: List[Tuple[float, float]], total_dur: float) -> List[Tuple[float, float]]:
|
||||||
|
return [(max(0.0, s - self.pad), min(total_dur, e + self.pad)) for s, e in intervals]
|
||||||
|
|
||||||
|
def _get_duration(self) -> float:
|
||||||
|
with VideoFileClip(str(self.video)) as v:
|
||||||
|
return v.duration
|
||||||
|
|
||||||
|
def _make_subclips(self, video: VideoFileClip, intervals: List[Tuple[float, float]]) -> List[VideoFileClip]:
|
||||||
|
clips: list[VideoFileClip] = []
|
||||||
|
for idx, (s, e) in enumerate(intervals, 1):
|
||||||
|
clip = video.subclipped(s, e)
|
||||||
|
if not self.concat:
|
||||||
|
fname = self.out_dir / f"clip_{idx:02d}.mp4"
|
||||||
|
self._save_clip(clip, fname)
|
||||||
|
clip.close()
|
||||||
|
clips.append(clip)
|
||||||
|
return clips
|
||||||
|
|
||||||
|
def _save_clip(self, clip: VideoFileClip, fname: Path) -> None:
|
||||||
|
print(f"Сохраняю {fname.name:>12}: {clip.duration:6.2f} сек")
|
||||||
|
clip.write_videofile(
|
||||||
|
fname.as_posix(),
|
||||||
|
codec="libx264",
|
||||||
|
audio_codec="aac",
|
||||||
|
temp_audiofile=str(fname.with_suffix('.m4a')),
|
||||||
|
remove_temp=True,
|
||||||
|
logger=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _save_concat(self, clips: List[VideoFileClip]) -> None:
|
||||||
|
final = concatenate_videoclips(clips, method="compose")
|
||||||
|
outfile = self.out_dir / "highlights.mp4"
|
||||||
|
print(f"Сохраняю дайджест {outfile.name}: {final.duration:6.2f} сек")
|
||||||
|
final.write_videofile(
|
||||||
|
outfile.as_posix(),
|
||||||
|
codec="libx264",
|
||||||
|
audio_codec="aac",
|
||||||
|
temp_audiofile=str(self.out_dir / "temp_audio.m4a"),
|
||||||
|
remove_temp=True,
|
||||||
|
logger=None,
|
||||||
|
)
|
||||||
|
final.close()
|
||||||
|
for c in clips:
|
||||||
|
c.close()
|
||||||
Loading…
Reference in New Issue