Нейросортировщик видео

Локальная нейросетевая программа для оценки и фильтрации видеороликов по
«эмоциональной» составляющей.
Пользователь оценивает ролики от 1 до 5,
программа извлекает высококачественные мультимодальные эмбеддинги
(видео + аудио), обучает компактный регрессор на GPU, и затем фильтрует
новые ролики по обученной модели — копируя «понравившиеся» в отдельный
каталог.

Полностью портативная: Python, виртуальное окружение, все зависимости и
загружаемые модели хранятся внутри папки проекта. Никаких системных
установок, кроме наличия Python (или WinPython).


Содержание

  1. Возможности
  2. Быстрый старт
  3. Архитектура
  4. Модели и эмбеддинги
  5. Модули программы (GUI)
  6. Настройки (config.ini)
  7. Портативность и кеши
  8. Производительность
  9. Решённые проблемы («грабли»)
  10. Устранение неполадок
  11. Структура проекта

1. Возможности

  • GUI с тёмной темой (PySide6 + python-mpv), три вкладки.
  • Оценка роликов встроенным мини-плеером (пауза по клику, прогресс,
    громкость, шкала 1–5, навигация назад/вперёд).
  • Мультимодальные эмбеддинги на базе SOTA открытых моделей:
    Qwen3-VL-Embedding-8B (видео) + CLAP (настроение музыки) + MERT
    (музыкальные признаки).
  • Обучение локального MLP-регрессора на GPU с ранним стопом и
    стратифицированным валидационным сплитом.
  • Фильтрация новых роликов с тремя режимами: мягкий (сигмоида +
    температура), строгий (порог), рассортировка по rating_1..rating_5.
  • Управление процессом: пауза/отмена фильтрации, батч-извлечение с
    настраиваемым размером.
  • Полное GPU-ускорение (RTX 5090 / sm_120 / CUDA 12.8).
  • Кеширование эмбеддингов в .npy — повторное обучение мгновенно.
  • Портативность: всё внутри папки проекта, никаких изменений системы.

2. Быстрый старт

Требования

  • Windows 10/11 x64 (тестировалось на 10.0.19045).
  • Python 3.10–3.13 в системе (через py launcher или в PATH), либо
    распакованный WinPython в python/.
  • NVIDIA GPU с поддержкой CUDA 12.8 (тестировалось на RTX 5090, sm_120).
  • 7-Zip в C:\Program Files\7-Zip\7z.exe (для распаковки libmpv).
  • ≈ 20 ГБ свободного места (для моделей и виртуального окружения).

Установка

  1. Скачайте проект в любую папку, например C:\neuro_sort_video2.
  2. Запустите setup.bat. Скрипт:
    • найдёт Python (системный py/python или портативный WinPython);
    • создаст виртуальное окружение .venv/;
    • установит PyTorch с CUDA 12.8 для Blackwell (RTX 5090, sm_120);
    • установит остальные зависимости из requirements.txt;
    • скачает и распакует libmpv-2.dll (для видео-плеера);
    • при наличии ttt\ffmpeg-master-latest-win64-gpl-shared.zip — установит
      ffmpeg.exe + DLL из него, иначе попробует скачать с gyan.dev;
    • проверит, что CUDA доступна и определена правильно.
  3. Дождитесь сообщения SETUP COMPLETE и строки
    Device: NVIDIA GeForce RTX 5090 / Capability: (12, 0).

Запуск

start.bat

Откроется окно «Нейросортировщик видео». При первом обучении будет
скачана модель Qwen3-VL-Embedding-8B (~16 ГБ) в model/ — нужен стабильный
интернет.


3. Архитектура

              ┌──────────────────────────────────────────────┐
                           Вкладка 1: Оценка                
                mpv-плеер  шкала 15  SQLite (data.db)    
              └────────────────────┬─────────────────────────┘
                                     ratings[] = [(path, 1..5)]
                                   
┌──────────────────────────────────────────────────────────────┐
            ЭМБЕДДИНГИ (EmbeddingExtractor)                   
                                                              
   ┌─────────────────┐   ┌──────────────┐  ┌──────────────┐   
     VideoEncoder       AudioEncoder     AudioEncoder   
     Qwen3-VL-Emb        CLAP (48k)      MERT (24k)     
      4096-d             512-d          1024-d       
   └────────┬────────┘   └──────┬───────┘  └──────┬───────┘   
            └─────────┬─────────┴─────────────────┘           
                                                             
      concat [vid | clap | mert?] = 4608 или 5632-d          
                                                             
      L2-нормализация всего вектора  кеш .npy               
└──────────────────────────────┬───────────────────────────────┘
                               
┌──────────────────────────────────────────────────────────────┐
            ОБУЧЕНИЕ (trainer.py)                             
   stratified split  AdamW + MSE  early stop по val_MAE    
    END_Model/regressor.pt + meta.json                       
└──────────────────────────────┬───────────────────────────────┘
                               
┌──────────────────────────────────────────────────────────────┐
            Вкладка 3: Фильтрация                             
   extract_batch  Predictor.predict_rating  clamp(1,5)     
    порог/температура/сортировка  копирование в out_dir     
└──────────────────────────────────────────────────────────────┘

Ключевая идея: тяжёлые предобученные модели (Qwen3-VL, CLAP, MERT)
работают как замороженные feature-extractor'ы — извлекают
высококачественные эмбеддинги, которые кешируются. Обучается только
компактный MLP-регрессор (~1–20 МБ), что делает обучение быстрым и
устойчивым даже на малых выборках (от 10–20 роликов).


4. Модели и эмбеддинги

Двухбашенная архитектура

Открытых моделей, одинаково хорошо понимающих и видео, и музыку, не
существует: единые video-LLM (InternVL3, VideoLLaMA3) воспринимают аудио
только как речевые/событийные токены. Поэтому применяется двухбашенный
подход с независимыми оптимальными энкодерами для каждой модальности.

Башня Модель (HF ID) Размерность Назначение
Видео Qwen/Qwen3-VL-Embedding-8B 4096 SOTA видеоэмбеддинги (рисовка, монтаж, персонажи, динамика)
Аудио-настроение laion/larger_clap_music 512 Настроение/вайб музыки (эпик, грусть, энергия, чилл)
Аудио-музыка (опц.) m-a-p/MERT-v1-330M 1024 Музыкальные признаки (темп, тональность, жанр, бит)

Итоговая размерность: 4096 + 512 + 1024 = 5632 (с MERT) или
4096 + 512 = 4608 (без MERT).

Особенности извлечения

  • Видео: N равномерно распределённых кадров извлекаются через PyAV с
    container.seek()
    (10–50× быстрее линейного декодирования), подаются
    модели по одному, эмбеддинги mean-pool'ятся и L2-нормализуются.
  • Аудио: ffmpeg извлекает wav 48 кГц моно. CLAP работает на 48 кГц
    напрямую, MERT даунсемплируется до 24 кГц. Берётся центральный фрагмент
    длиной audio_sample_sec.
  • Финальная нормализация: весь склеенный вектор [vid | clap | mert]
    L2-нормализуется целиком — это критично для стабильности downstream-MLP.

Регрессор

Компактный MLP на PyTorch:

input (4608/5632-d) → [Linear → GELU → Dropout] × n_layers → Linear → 1
  • Без активации на выходе (raw-рейтинг). Раньше использовался
    3 + 2·tanh(x) — но tanh насыщается, градиенты умирают, и модель
    застревала на константном прогнозе «все = 5».
  • head.weight = 0, head.bias = 3.0 — инициализация в центре диапазона.
  • clamp(1, 5) применяется только на inference.
  • Loss = MSE, оптимизатор AdamW, early stop по val_MAE.

5. Модули программы (GUI)

Вкладка 1 — «Оценка роликов»

  • Выбор папки с роликами, рекурсивное сканирование (.mp4 .mkv .webm .avi .mov .m4v .mpg .mpeg .ts .wmv .flv).
  • mpv-плеер (python-mpv + libmpv-2.dll): прогресс, громкость,
    play/pause по клику.
  • Шкала 1–5 (звёзды), навигация «◀ Назад» / авто-переход вперёд.
  • Сохранение оценок в SQLite (data.db).
  • Кнопка «Обучить нейромодель» — фоновый поток (QThread):
    извлечение эмбеддингов батчами + обучение регрессора →
    END_Model/regressor.pt.

Вкладка 2 — «Настройка обучения»

Все параметры с подробными русскими тултипами и рекомендациями:

Группа Параметр Диапазон По умолч.
Эмбеддинги n_video_frames 4–32 16
frame_resolution 224 / 336 / 448 448
audio_sample_sec 5–60 сек 20
extract_batch_size 1–16 4
audio_use_mert вкл/выкл вкл
Нейросеть hidden_dim 128–4096 1024
n_layers 1–4 2
dropout 0.0–0.6 0.2
Оптимизация learning_rate 1e-5–1e-1 1e-3
epochs 10–2000 100
batch_size (обучения) 4–256 32
weight_decay 0–0.5 1e-4
val_split 0.0–0.5 0.15

Живой расчёт размера модели (в Б/КБ/МБ) и числа параметров.

Вкладка 3 — «Фильтрация»

  • Выбор обученной модели, входного и выходного каталогов.
  • Температура (0.00–1.00): ползунок + поле, синхронизированы.
  • Порог рейтинга (1.0–5.0).
  • Чекбоксы:
    • «Строгий режим» — простой отбор rating ≥ threshold.
    • «Рассортировать по рейтингу» — все ролики копируются в подпапки
      rating_1..rating_5 (порог/температура игнорируются).
  • Кнопки «▶ Старт» / «⏸ Пауза» / «✕ Отмена».
  • Прогресс-бар и лог с прогнозами по каждому ролику.

Логика «понравилось» (мягкий режим):
prob = 1 / (1 + exp(-(rating - threshold) / temperature)),
копируется при prob ≥ 0.5. Меньшая температура → жёстче отбор.


6. Настройки (config.ini)

Все настройки сохраняются в config.ini (создаётся при первом запуске).
Разделы:

1
2
3
4
5
6
[paths]            # каталоги и путь к модели
[rating]           # последний просмотренный индекс
[embedding]        # параметры извлечения + ID моделей HF
[training]         # гиперпараметры MLP-регрессора
[filter]           # температура, порог, строгий режим, сортировка
[ui]               # тема и геометрия окна

Полный список ключей с умолчаниями — в src/config.py в словаре DEFAULTS.

Важно: при наличии config.ini значения берутся из него, а не из
DEFAULTS. Если после обновления кода параметры ведут себя странно —
удалите config.ini, он пересоздастся с актуальными умолчаниями.


7. Портативность и кеши

Все данные хранятся внутри папки проекта — никаких системных изменений:

Путь Содержимое
python/ Портативный WinPython (если используется)
.venv/ Виртуальное окружение со всеми pip-зависимостями
lib/ libmpv-2.dll, ffmpeg.exe, ffprobe.exe, av*.dll
model/ Кеш HuggingFace (HF_HOME, TORCH_HOME) — загруженные модели (~16 ГБ)
cache/ XDG_CACHE_HOME + cache/embeddings/*.npy — кеш эмбеддингов
END_Model/ regressor.pt + meta.json — результат обучения
data.db SQLite с оценками и метаданными эмбеддингов
config.ini Настройки

Переменные окружения (устанавливаются в start.bat перед запуском Python):

1
2
3
4
5
6
HF_HOME         = <ROOT>\model
HF_HUB_CACHE    = <ROOT>\model\hub
TRANSFORMERS_CACHE = <ROOT>\model\hub
TORCH_HOME      = <ROOT>\model
XDG_CACHE_HOME  = <ROOT>\cache
PATH           += <ROOT>\lib

Благодаря этому проект можно перенести на другой компьютер простым
копированием папки (при совпадении версии GPU-драйвера).


8. Производительность

Измерения на RTX 5090 (32 ГБ VRAM, sm_120), видеоклипы ~60 сек:

Операция Время
Загрузка моделей (Qwen3-VL + CLAP + MERT) ~17 сек
_sample_frames 60-сек ролика, 8 кадров (с seek) 0.09 сек
Полный encode одного видео (видео + аудио) ~0.76 сек
Батч из 4 видео (с extract_batch_size=4) ~1.9 сек
Обучение MLP на 60 роликах (100 эпох) несколько секунд
Прогноз одного ролика <10 мс

Ускорения, внедрённые в код:

  1. seek вместо линейного декодирования — основное (раньше сэмплинг
    длинного ролика занимал 10–40 сек, теперь <0.1 сек).
  2. Батч-извлечение (extract_batch) — один вызов model.encode() на
    все кадры всех видео батча → ~2× ускорение, GPU не простаивает.
  3. Кеширование эмбеддингов — повторное обучение идёт мгновенно.
  4. bf16 на инференсе — вдвое меньше VRAM, без потери качества.

9. Решённые проблемы («грабли»)

Ниже — реальные проблемы, с которыми столкнулись в ходе разработки, и их
решения. Полезно для будущей отладки и расширения.

9.1. Видео-энкодер: нельзя использовать прямой forward()

  • Симптом: Эмбеддинги получались мусорными (все нули или шум).
  • Причина: Qwen3-VL-Embedding — это LLM-архитектура. Качественный
    эмбеддинг получается только через SentenceTransformer.encode(), который
    внутри применяет корректный pooling и projection-head. Ручной
    model(**inputs).last_hidden_state даёт скрытое состояние LLM, а не
    embedding.
  • Решение: Использовать SentenceTransformer API, кадры подавать как
    PIL-изображения.

9.2. На Windows нельзя подавать путь к видео напрямую в модель

  • Симптом: torchvision.io has no attribute 'read_video' или
    libtorchcodec_core4.dll not found.
  • Причина: В torchvision ≥0.26 read_video удалён, а замена (torchcodec)
    имеет проблемы с DLL на Windows.
  • Решение: Кадры сэмплируются сами через PyAV (av.open + seek),
    подаются модели по одному, эмбеддинги mean-pool'ятся.

9.3. Сэмплинг seek'ом вместо линейного декодирования

  • Симптом: Между батчами было ~40 секунд простоя (GPU 0%, CPU 13%).
  • Причина: _sample_frames декодировал видео линейно от начала, чтобы
    дойти до целевых индексов — тысячи кадров чистого CPU-декодирования в
    одном потоке.
  • Решение: container.seek(timestamp) для каждого целевого кадра.
    Ускорение ~70×. Есть fallback на линейное декодирование при сбое seek.

9.4. CLAP: ClapProcessor падает с HTTP 401

  • Симптом: ClapProcessor.from_pretrainedRepositoryNotFoundError 401.
  • Причина: В transformers ≥5.x ClapProcessor пытается листать
    additional_chat_templates на сервере, и на некоторых репозиториях это
    падает.
  • Решение: Использовать ClapFeatureExtractor напрямую (минует Processor).
    Параметр — raw_speech= (не audios=).

9.5. CLAP на 48 кГц, MERT на 24 кГц

  • Симптом: Предупреждение «trained using sampling rate of 48000...» и
    мусорные эмбеддинги.
  • Причина: CLAP обучен на 48 кГц, MERT — на 24 кГц. Подача одного wav
    в обе модели искажает результат.
  • Решение: ffmpeg извлекает wav сразу на 48 кГц; CLAP работает напрямую,
    MERT даунсемплируется до 24 кГц (_resample).

9.6. MERT требует nnAudio

  • Симптом: Кастомный код MERT (trust_remote_code=True) падал на Windows.
  • Причина: MERT использует nnAudio для CQT-спектрограмм.
  • Решение: nnAudio>=0.3 добавлен в requirements.txt.

9.7. Регрессор застревал на константном прогнозе («все = 5»)

  • Симптом: После обучения MAE ≈ 1.17, все прогнозы ≈ 5, фильтрация
    копировала низкооценённые ролики.
  • Причина: Активация 3 + 2·tanh(raw) насыщается у краёв диапазона,
    градиенты через tanh умирают, модель не может из них выбраться.
  • Решение: Убрать tanh. Финальный слой — Linear без активации (raw).
    head.bias = 3.0 для старта в центре диапазона. clamp(1,5) только на
    inference. После правки: MAE 0.000 на синтетике, Pearson r = 1.000.

9.8. Нужна финальная L2-нормализация всего вектора

  • Симптом: Обучение шло нестабильно, изредка нулевые векторы ломали
    датасет.
  • Причина: Подвекторы из разных башен имеют разные шкалы; конкатенация
    без общей нормировки путает downstream-MLP.
  • Решение: L2-нормализация всего склеенного вектора перед кешированием.
    Если итоговая норма < 1e-6 — кеш НЕ сохраняется, файл помечается None.

9.9. Версия кеша EMB_VER

  • Симптом: После изменения логики extractor'а старый кеш отдавал
    некорректные эмбеддинги.
  • Решение: Введён EMB_VER (сейчас = 2). Включается в хэш-строку —
    при бампе версии старый кеш автоматически становится невалидным.
    v1 — без общей L2-нормализации; v2 — с ней.

9.10. Случайный сплит ломал обучение на малых данных

  • Симптом: На 15–20 роликах валидация бессмысленна или один из классов
    целиком уходил в val.
  • Решение: (а) Stratified split по рейтингам — каждый класс
    представлен пропорционально; (б) при n < 30 принудительно val_split = 0
    (обучение на всём датасете).

9.11. Чёрные окна ffmpeg перехватывали фокус

  • Симптом: При пакетной обработке постоянно мигали консольные окна,
    мешая работать в других программах.
  • Решение: subprocess.run(..., creationflags=CREATE_NO_WINDOW) на
    Windows (флаг 0x08000000).

9.12. setup.bat: SourceForge отдаёт HTML вместо архива

  • Симптом: Автоскачивание libmpv-2.dll ломалось — приходил HTML-редирект.
  • Решение: Скачать через GitHub API
    (api.github.com/repos/zhongfly/mpv-winbuild/releases/latest), распаковать
    локальным 7-Zip.

9.13. setup.bat: ffmpeg shared-сборка требует DLL рядом

  • Симптом: После копирования одного ffmpeg.exe он падал с «avcodec-63.dll
    не найден».
  • Решение: Копировать не только ffmpeg.exe, но и весь набор DLL из
    bin/ (avcodec, avformat, avutil, swscale, swresample и т.д.).

9.14. PyTorch не видел RTX 5090

  • Симптом: torch.cuda.is_available() = False, или
    «no kernel image available for executing op on sm_120».
  • Причина: Обычный pip install torch тянет старый CUDA 12.1 без поддержки
    Blackwell (sm_120).
  • Решение: Устанавливать torch из индекса CUDA 12.8:
    pip install torch --index-url https://download.pytorch.org/whl/cu128.
    Требуется torch ≥ 2.7.

9.15. bat-файлы в UTF-8 ломали cmd

  • Симптом: При запуске setup.bat — кракозябры и
    'слово' is not recognized as a command.
  • Причина: Windows cmd читает .bat в OEM-кодировке (CP866); кириллица и
    умные кавычки в UTF-8 ломают парсер. chcp 65001 помогает только для
    вывода, не для разбора самого bat-файла.
  • Решение: Оба .bat-файла написаны на чистом ASCII (без кириллицы,
    без «», без ), без BOM. Кириллица — только в Python-коде.

9.16. Скобки в echo внутри if (...) закрывали блок

  • Симптом: 'from' is not recognized as a command.
  • Причина: Строка echo Download (64-bit) from ... внутри if (...)
    блока: скобки (64-bit) закрывали блок раньше времени.
  • Решение: Сложную логику вынести в подпрограммы (:label + call),
    убрать скобки из echo-строк.

10. Устранение неполадок

«CUDA available: False»

  • Обновите драйвер NVIDIA до последней версии (требуется поддержка CUDA 12.8).
  • Проверьте установку: python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_capability())". Должно быть True (12, 0) для RTX 5090.
  • Если ставили torch не из cu128 — переустановите:
    pip install torch --index-url https://download.pytorch.org/whl/cu128 --force-reinstall.

«Плеер не инициализируется / нет модуля mpv»

  • Проверьте, что lib/libmpv-2.dll существует (см. setup.bat, шаг 4).
  • Проверьте, что python-mpv установлен: pip show python-mpv.

«ffmpeg не найден»

  • Проверьте lib/ffmpeg.exe и DLL рядом (avcodec-63.dll, avformat-63.dll,
    avutil-61.dll и т.д. — нужны все, т.к. используется shared-сборка).
  • При отсутствии — перезапустите setup.bat (он докачает).

Обучение даёт плохой результат (MAE > 1, Pearson < 0.3)

  • Мало данных: нужно минимум 10–20 оценённых роликов, лучше 50+.
  • Однородные оценки: если все ролики оценены на «5», модели нечему
    учиться — нужны контрастные оценки (1–2 и 4–5).
  • Удалите старый кеш: del /Q cache\embeddings\* (после правок формата).
  • Удалите старую модель: del END_Model\regressor.pt.

«401 Unauthorized» при загрузке CLAP/MERT

  • Это известная проблема ClapProcessor в transformers ≥5.x. Программа
    использует ClapFeatureExtractor напрямую. Если ошибка повторяется —
    проверьте, что в config.ini стоит
    clap_model_id = laion/larger_clap_music (а не laion/clap_htsat_music).

Фильтрация копирует низкооценённые ролики

  • Модель обучена плохо (см. выше про MAE).
  • Проверьте порог: он должен быть выше среднего прогноза для «не
    понравившихся».
  • Попробуйте строгий режим или уменьшите температуру.

Медленное извлечение эмбеддингов (простой GPU)

  • Проверьте, что _sample_frames использует seek (новая версия кода).
  • Увеличьте extract_batch_size во вкладке 2 (4–8 для RTX 5090).
  • Убедитесь, что ролики не на медленном сетевом диске.

11. Структура проекта

neuro_sort_video2/
├── setup.bat                    # установка зависимостей
├── start.bat                    # запуск (env vars + python main.py)
├── requirements.txt             # pip-зависимости (кроме torch)
├── config.ini                   # настройки (создаётся автоматически)
├── data.db                      # SQLite с оценками
├── README.md                    # этот файл

├── src/
   ├── main.py                  # точка входа: QApplication + MainWindow
   ├── config.py                # пути, DEFAULTS, чтение/запись config.ini
   ├── db.py                    # SQLite: ratings, embeddings_meta
   ├── theme.py                 # тёмная тема Fusion + QSS
   ├── utils.py                 # GPU, ffmpeg (CREATE_NO_WINDOW), сканер
   
   ├── embeddings/
      ├── video_encoder.py     # Qwen3-VL-Embedding (sentence-transformers)
                              #   VIDEO_EMBED_DIM = 4096
                              #   seek-based _sample_frames, encode_batch
      ├── audio_encoder.py     # CLAP (512-d) + MERT (1024-d)
                              #   48 kHz / 24 kHz ресемплинг
      ├── extractor.py         # оркестратор: vid+clap+mert → L2-норм
                              #   extract / extract_batch + кеш
      └── cache.py             # хэш-кеширование .npy, EMB_VER=2
   
   ├── model/
      ├── regressor.py         # VideoRegressor (MLP без tanh)
      ├── trainer.py           # stratified split, MSE, early stop
      └── predictor.py         # инференс + clamp(1,5)
   
   └── gui/
       ├── main_window.py       # QMainWindow + 3 вкладки
       ├── tab_rating.py        # вкладка 1: mpv-плеер + оценка + обучение
       ├── tab_settings.py      # вкладка 2: гиперпараметры
       └── tab_filter.py        # вкладка 3: фильтрация (пауза/отмена/сорт)

├── lib/                         # libmpv-2.dll, ffmpeg.exe + av*.dll
├── model/                       # кеш HF (Qwen3-VL ~16 ГБ + CLAP + MERT)
├── cache/                       # XDG_CACHE + embeddings/*.npy
├── END_Model/                   # regressor.pt + meta.json
└── .venv/                       # виртуальное окружение

Технические заметки

Используемый стек

  • GUI: PySide6 6.11 + python-mpv 1.0.8
  • ML: PyTorch 2.11+cu128, transformers 5.14, sentence-transformers 5.6
  • Видео: PyAV 17.1 (декодирование), ffmpeg (извлечение аудио)
  • Аудио: soundfile 0.14, nnAudio 0.3
  • Хранение: SQLite (встроенный), .npy (numpy)
  • Портативность: venv, переменные окружения для кешей HF/Torch

Версии моделей (зафиксированы в config.ini)

Модель HF ID Размер
Видео Qwen/Qwen3-VL-Embedding-8B ~16 ГБ
CLAP laion/larger_clap_music ~600 МБ
MERT m-a-p/MERT-v1-330M ~1.5 ГБ

Лицензии

  • Код проекта — собственный.
  • Зависимости: PySide6 (LGPL), python-mpv (GPLv2+), PyTorch (BSD),
    transformers (Apache 2.0), модели — согласно их карточкам на HuggingFace.
Edit

Pub: 27 Jul 2026 19:47 UTC

Views: 24