Привет, Хабр.
У многих дома крутится сервер или обычный 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/