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