Как превратить домашнюю файлопомойку в умную AI-галерею на основе сборки из x99+Xeon и видеокарты за 2 тыс рублей

от автора

Привет, Хабр.

У многих дома крутится сервер или обычный NAS. На жестких дисках годами копится семейный архив: фотки из отпусков, видео с телефонов, старые кадры с мыльниц. Все это лежит гигабайтами в папках вроде “2023_06_12_дача” или просто свалено в кучу в директории DCIM.

В какой-то момент я понял, что хочу навести во всем этом порядок, но не руками. Так родился проект Gailery — локальная веб-галерея для домашнего сервера. Сегодня на моем стенде она безболезненно переваривает огромный личный архив из более чем 100 тысяч фотографий и почти 10 тысяч видеороликов. При этом оригинальные файлы лежат в полной безопасности: папка с медиа монтируется в контейнер в режиме “только чтение” (Read-Only).

Ниже подробно расскажу, как устроена система, как работает локальный ИИ на дешевом железе и почему для этого не нужно дорогое железо.

Две основы Gailery: поиск лиц и RAG-поиск по описанию

Весь проект задумывался ради двух вещей: поиска лиц и семантического поиска по смысловым описаниям через RAG. Всё остальное нарастало сверху как приятные дополнения.

Поиск лиц и автоматическая кластеризация

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

Технически это реализовано так:

  • За детекцию лиц и построение векторов отвечает библиотека InsightFace (модель buffalo_l на базе SCRFD + ArcFace, работающая через onnxruntime-gpu с CUDA-акселерацией). Скрипт находит лица на снимках и строит для каждого лица 512-мерный вектор признаков.

  • Все полученные векторы скармливаются алгоритму кластеризации DBSCAN из scikit-learn (используется косинусная метрика расстояния с порогом eps=0.4). Кластеризация работает инкрементально. Система не пересчитывает всю базу в 100 000+ фотографий заново на каждом цикле: лица, уже успешно сопоставленные с известными персонами, просто пропускаются, а алгоритм обрабатывает только новые поступления.

  • Вам остается лишь зайти в веб-интерфейс, открыть карточку персоны, где собраны сотни ее фотографий, и один раз подписать имя: “Это Анна” или “Это дедушка”. Все новые и старые фото этого человека мгновенно связываются с его профилем.

Умные описания для семантического поиска через RAG

Вторая задача — это возможность искать фотографии обычным человеческим языком. Например, написать: “лето на даче с детьми” или “мама сидит на кухне с собакой”. Для этого нужны точные текстовые описания кадров и векторная база данных для RAG.

Обычные галереи просто прогоняют фото через VLM и получают сухое “мужчина в черной куртке стоит рядом с девочкой на фоне дачи”. Это не подходит для семейного архива, ведь мы хотим искать близких людей по именам.

Поэтому в Gailery описания генерируются в два этапа локальными моделями через легковесный llama-server:

  • Первичное VLM-описание. Мультимодальная модель Qwen3.5-4B GGUF (в квантовании Q4_K_M на порту 8101) формирует базовое описание того, что происходит на снимке на русском языке.

  • Обогащение контекстом. Локальная LLM Qwen3.5-4B (на порту 8103) работает как батчевый агент с поддержкой вызова функций (Tool Calling). Она получает на вход пачку фотографий, первичное VLM-описание, координаты распознанных лиц и взаимное расположение людей на снимке (кто слева, по центру или справа).

Дальше агент сопоставляет информацию, восстанавливая картину происходящего:

  • Запросы к SQLite через инструменты. Заметив на фото лица, агент обращается к базе данных через инструменты: get_persona_info узнает имена людей по распознанным ID лиц, а search_family_facts запрашивает даты рождения членов семьи и факты о родстве (кто кому приходится мамой, папой, ребенком).

  • Анализ контекста. Агент сопоставляет положение людей на кадре, дату снимка и семейную историю. Если в базе записано, что ребенок родился в 2013 году, а фото сделано 2018-05-12, агент автоматически вычисляет возраст на момент снимка (“Алиса, 5 лет”). Он анализирует названия папок и файлов, сопоставляет участников съемки и делает логические выводы о происходящем на снимке.

  • Синтез истории. На основе этого агент пишет описание: “[День рождения Алисы, 2018-05-12]. Алексей Петров в черной куртке стоит рядом со своей дочерью Алисой Петровой (ей исполнилось 5 лет) на фоне дачного дома”.

Полученные описания векторизуются моделью Qwen3-Embedding-0.6B (через PyTorch CUDA на этапе фонового пайплайна) и укладываются в векторную базу LanceDB (1024-мерные векторы). При поиске из веб-интерфейса используется GGUF-версия этого же эмбеддера на порту 8102 для быстрой генерации вектора запроса. Когда вы пишете в поиске “Алексей Петров на даче с дочкой”, система делает векторный RAG-запрос по описаниям и выдает точные результаты.

Автоматическое переописание при изменении имени

Здесь кроется важная фича, объединяющая обе задачи. Что происходит, когда вы заходите в карточку безымянного кластера лиц и подписываете имя (или меняете имя/комментарий у созданной персоны)?

Описания всех фотографий, где присутствует этот человек, автоматически становятся устаревшими. Система перехватывает это событие в SQLite и автоматически сбрасывает флаг готовности описания у этих файлов.

На следующем круге пайплайна ИИ-агент заново берет эти кадры на переописание. Он обращается к базе данных, забирает уже обновленное, настоящее имя и новые факты, заново генерируя описания и эмбеддинги. Весь процесс происходит автоматически.

Железо и философия “никто никуда не торопится”

Я не сторонник покупки дорогого железа под домашние проекты. Мой сервер собран по классической схеме:

  • Материнская плата: китайская X99.

  • Процессор: Intel Xeon E5-2680 v4 (14 ядер, 28 потоков).

  • Гипервизор: Proxmox VE.

  • Окружение: Gailery работает в LXC-контейнере. Под него я выделил 16 ядер и 16 ГБ оперативной памяти.

  • Видеокарта: Tesla P104-100 с 8 ГБ видеопамяти (это старая майнерская карта на архитектуре Pascal, аналог GTX 1070/1080 без видеовыходов, на вторичке стоит в районе 30 долларов). Драйверы NVIDIA проброшены в LXC напрямую через /dev/dri.

При проектировании я сразу закладывал принцип: никто никуда не торопится. Семейный архив копился лет пятнадцать, поэтому нет никакого смысла пытаться проиндексировать его за одну ночь, перегревая сервер. Да, старая видеокарта работает медленно. Но фишка в том, что галерея полностью работоспособна с первого же дня: вы заходите в веб-интерфейс, можете смотреть папки, пользоваться базовыми функциями, а конвейер в это время шуршит себе на фоне. Фотографии постепенно, одна за другой, распознаются, и галерея прямо на глазах наполняется смыслом.

Как устроен фоновый пайплайн

Чтобы увязать множество процессов воедино и не допустить падения системы по нехватке памяти, я спроектировал пайплайн как строго последовательный бесконечный цикл.

Один GPU — один процесс. Никакого параллелизма в нейросетях. Если запустить несколько моделей параллельно, 8 ГБ видеопамяти Tesla P104 задохнутся мгновенно.

Пайплайн по очереди вызывает независимые специализированные утилиты-воркеры в виде процессов-сабпроцессов. Вся работа разбита на жесткие шаги:

  • Наполнение (Ingest). Дисковый этап, GPU тут отдыхает. Воркер обходит дерево каталогов. Если mtime папки не изменился с прошлого сканирования, она пропускается. Новые файлы добавляются в базу со статусом “без хеша”. Затем считаются быстрые хеши xxh128 пачками по 200 штук. Файлы группируются по контент-хешу: для каждого дубликата выбирается один каноничный путь, который отправляется в таблицу photos со статусом ingested=1, остальные помечаются как дубликаты. Если файл перенесли в другую папку, в базе просто обновится ссылка — повторно гонять его через нейросети никто не будет.

  • Чтение EXIF. Скрипт на базе Python-библиотеки ExifRead вытягивает из файлов даты съемки, GPS-координаты и модель камеры. Процесс идет очень быстро на процессоре, подготавливая фактологическую почву для будущего ИИ.

  • Детекция лиц (FACES). Включается GPU. Воркер на базе InsightFace обрабатывает пачку из 600 каноничных фото за раз. Он находит координаты лиц (bbox) и строит для каждого 512-мерный вектор признаков.

  • Описание (DESCRIBE / BATCH AGENT). Тот самый этап работы Batch Agent на модели Qwen3.5-4B (Q4_K_M) через llama-server. Воркер скармливает агенту пачку из 60 медиафайлов, собирает факты и генерирует текст.

  • Индексация (EMBED). Воркер берет готовые описания пачками по 60 штук, пропускает через эмбеддер Qwen3-Embedding-0.6B (PyTorch CUDA) и сохраняет векторные представления в базу LanceDB для семантического поиска.

  • Оптимизация. В конце цикла запускаются операции обслуживания баз данных: VACUUM для SQLite и оптимизация/сжатие таблиц в LanceDB.

Архитектурный хардкор: SQLite, MQTT и “Сторожевой пёс”

Зачем городить архитектуру с брокером сообщений и базами данных для простого просмотра фоток? При масштабах архива в 100 000+ файлов без этого начнется ад.

SQLite как единый источник правды

SQLite хранит в себе всё состояние системы и решает проблему управления жизненным циклом файлов. Каждая фотография имеет чёткие флаги обработки: ingested, has_exif, has_faces, has_description, has_embedding. Если пайплайн падает или сервер перезагружается, мы всегда точно знаем, на каком файле остановились. База хранит реляционные связи, историю именования лиц, факты о семье и настройки.

Решение проблемы блокировок через очереди MQTT

Поскольку пайплайн состоит из независимых процессов, которые крутятся параллельно с веб-сервером FastAPI, возникает классическая проблема SQLite: при одновременной записи из разных процессов база бросает исключение “database locked”.

Я решил это просто: все операции записи в базу сериализованы. Воркеры не пишут в SQLite напрямую. Вместо этого они отправляют SQL-команды на запись в MQTT-топик. Главный координатор пайплайна подписывается на этот топик, собирает сообщения в единую очередь и записывает их в SQLite в один поток. Это решение избавляет от конфликтов блокировок и дает гибкость: в будущем воркеры инференса можно будет вынести на другие машины, не переписывая логику работы с БД.

GPU-арбитраж с помощью Mosquitto

Еще одна проблема: у нас одна медленная видеокарта, на которой крутятся тяжелые нейросети пайплайна. Но пользователь в любой момент может зайти в веб-интерфейс и вбить поисковый запрос (для которого нужно сгенерировать эмбеддинг запроса на GPU) или открыть персону (где нужно нарезать превью).

Чтобы веб-интерфейс не зависал, а видеокарта не вылетала с ошибкой нехватки видеопамяти из-за одновременного инференса, я настроил GPU-арбитраж через брокер Mosquitto.

Перед началом любого тяжелого шага (детекция лиц или генерация описания) фоновый воркер запрашивает “GPU-токен” через MQTT у координатора. Если в этот момент пользователь в веб-интерфейсе совершает действие, требующее ресурсов видеокарты (например, выполняет семантический поиск, задействующий эмбеддер на порту 8102), координатор отзывает токен. Фоновый воркер мгновенно приостанавливает отправку батчей на инференс. Веб-запрос обрабатывается за доли секунды, после чего пайплайн мирно возобновляет свою фоновую работу. Это позволило обойтись без принудительной выгрузки и повторной долгой инициализации моделей в VRAM.

Сторожевой пес (Watchdog) для автономности

Мой сервер — это не просто закрытая коробка для галереи и файлопомойки, а полноценная домашняя ИИ-лаборатория. Я постоянно поднимаю там новые контейнеры, разворачиваю всякие разные штуки для локального инференса, тестирую новые модели и сборки. В таких условиях видеокарта или процессор сервера периодически перегружены “под завязку”, из-за чего фоновые воркеры галереи могут упасть по таймауту, получить GPU OOM или банально зависнуть.

Для решения этой проблемы я написал отдельную службу — Сторожевой пес (watchdog.py).

Ее задача — непрерывно мониторить состояние пайплайна и решать возникающие по ходу проблемы без моего участия:

  • Если фоновый пайплайн зависает или аварийно завершается, Watchdog это видит и перезапускает его.

  • Если завис или ушел в бесконечную задумчивость процесс инференса (llama-server), пес принудительно убивает “осиротевшие” процессы, очищает под ними видеопамять, сбрасывает зависшие блокировки БД и заново поднимает окружение.

  • Он обеспечивает полную автономность: что бы я ни творил со своей домашней лабой, как бы ни перегружал сервер тестами локальных нейронок, галерея сама залечивает свои раны и продолжает не спеша переваривать архив.

Вторичные фичи

Когда ядро с описанием и поиском заработало, проект начал обрастать дополнительными возможностями, которые сделали галерею по-настоящему удобной.

Умный кроп (Smart Crop)

При клике по лицу в профиле человека система показывает не просто вырезанный квадрат с головой, а аккуратный портрет с захватом плеч и контекста, центрируя кадр по лицу.

Транскодирование видео на лету

В архиве часто лежат ролики со старых мыльниц и телефонов в форматах .avi, .mkv, .mov или .3gp. Браузеры (особенно на iOS) их не воспроизводят напрямую — им нужны H.264 и AAC в контейнере MP4.

Чтобы не перекодировать весь архив заранее и не забивать диски дубликатами, я сделал динамический запуск ffmpeg на лету:

  • Ремультиплексирование (Remuxing). Если видео уже пожато в H.264, но лежит в контейнере mkv, сервер просто перепаковывает его в mp4 без перекодирования. Нагрузка на процессор при этом нулевая.

  • Транскодирование только звука. Если видео нормальное, а аудиодорожка записана в неподдерживаемом AC3 или DTS, мы копируем видеопоток как есть, а звук быстро пережимаем в AAC.

  • Полное транскодирование. Для старых форматов (DivX, MPEG-4) видео декодируется и сжимается в H.264 прямо во время просмотра. При этом работает стриминг: если вы мотаете видео в плеере, ffmpeg получает временную метку (seek_time) и начинает вещание сразу с нужного ключевого кадра.

Интерактивная карта

Фотки с GPS-координатами выводятся на карту (Leaflet). При отдалении они собираются в кластеры, при приближении превращаются в миниатюрные эскизы. Есть обратный геокодинг (определение адреса по координатам) и возможность привязать фото к карте вручную.

Тепловизоры FLIR

Если у вас есть телефон со встроенным тепловизором (например, Blackview или CAT), вы знаете, что они сохраняют снимки как “radiometric JPEG”. Внутри такого файла лежит обычное фото и сырые 16-битные данные сенсора Lepton.

В Gailery встроен модуль src/flir_parser.py. Он вытаскивает сырую матрицу, меняет порядок байт и парсит из EXIF параметры калибровки Planck, влажность и температуру воздуха. На основе этого система рассчитывает температуру каждого пикселя в градусах Цельсия. В галерее такой снимок становится интерактивным: можно водить мышкой по тепловой карте и видеть температуру в конкретной точке.

Текущее состояние и планы развития

Сейчас проект живет исключительно внутри домашней локальной сети. Он работает на компьютерах, планшетах и смартфонах членов семьи через домашний Wi-Fi и принципиально не вылезает в большой интернет. Это гарантирует стопроцентную приватность данных: физически невозможно слить фотоархив извне.

Но идей по развитию еще очень много. В ближайших планах:

  • Концепция альбомов. Сделать удобное объединение медиафайлов в виртуальные тематические альбомы и коллекции, независимые от исходной структуры папок на диске.

  • Безопасный шеринг с родственниками. Разработать механизм, который позволит просто и безопасно делиться снимками через интернет. Идея в том, чтобы родственник (например, бабушка) мог зайти по защищенной ссылке и увидеть только те фотографии и видео, на которых распознано его лицо, без доступа ко всему остальному архиву. И сделать это так, чтобы им не приходилось настраивать VPN.

Я развиваю Gailery постепенно, по мере сил и времени, ориентируясь прежде всего на потребности своей семьи. Но в какой-то момент подумал: почему бы не поделиться результатом со всеми? Возможно, у кого-то тоже пылится старая видеокарта и лежит заброшенный семейный архив, который давно пора оживить.

Как запустить

Развёртывание локальных нейросетей часто превращается в бесконечную настройку зависимостей. Чтобы избавить вас от этого, я написал скрипт автоматической установки, который делает всё под ключ одной командой:

curl -fsSL https://raw.githubusercontent.com/siv237/gailery/main/install.sh | sudo bash

Скрипт полностью автономен: он сам ставит системные пакеты, компилирует llama.cpp под CUDA видеокарты и скачивает все нужные модели (Qwen, эмбеддеры и модели детекции лиц) в правильные папки.

Вам не нужно возиться с конфигурационными файлами для настройки путей перед запуском. После установки просто откройте веб-интерфейс в браузере, перейдите в раздел “Каталог” и прямо оттуда добавьте нужные папки с вашего сервера. Фоновый конвейер сразу подхватит их и начнет работу.

Заключение

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

Ссылка на проект: Gailery на GitHub

ссылка на оригинал статьи https://habr.com/ru/articles/1039628/