Архитектура
Три основных контейнера плюс сервис mDNS — без отдельного брокера сообщений и без обязательных облачных сервисов. Фото загружаются или импортируются из папок NAS, а затем обрабатываются: метаданные, миниатюры, семантические векторы, лица и текст. В итоге получается фототека с поиском по естественному языку, людям и местам.
Бэкенд на Go, ИИ-сервис и PostgreSQL, плюс mDNS-сервис, объявляющий локальный домен
metadata, thumbnail, clip, face, ocr
Векторы изображение-текст Chinese-CLIP (512, 768 или 1024 в зависимости от уровня моделей); признаки лиц ArcFace — 512 измерений
Индекс косинусного расстояния pgvector; поиск и сравнение лиц выполняются в базе данных
Архитектура системы
Браузер общается только с бэкендом на Go. Он отвечает за загрузки, планирование задач, миниатюры и права доступа, отправляет превью в ИИ-сервис на инференс и записывает все результаты в PostgreSQL. ИИ-сервис не хранит состояния: он не обращается к базе данных и не сохраняет файлы, поэтому его можно вынести на более мощную машину.
Веб-приложение встроено в программу, так что для установки не нужен Node. Миграции базы данных выполняются автоматически при запуске, а обновление — это просто смена образа.
Встроенный ИИ-сервис работает во внутренней сети Docker и считает на процессоре. Укажите в веб-настройках удалённый адрес, чтобы перейти на домашний компьютер с видеокартой или Mac.
Сведения о фото, люди, настройки, очередь задач и векторы хранятся в PostgreSQL, поэтому данные приложения и состояние задач можно резервировать вместе. Сами файлы фото нужно резервировать отдельно.
Конвейер обработки фото
После импорта у фото сначала извлекаются метаданные и создаются миниатюры, затем параллельно вычисляются семантические векторы, распознаются лица и текст. Каждый шаг ставится в очередь отдельно, поэтому перезапуск или отключение ИИ-сервиса не теряет работу — обработка продолжится, когда всё вернётся.
ingest- Веб-загрузки через tus, продолжаются после обрыва
- Папки /photos сканируются каждые 15 минут
- Считается контрольная сумма SHA-1, пишется в assets
metadata- ExifTool читает размеры и время съёмки
- GPS, камера, объектив, диафрагма и выдержка
- Время без пояса — по заданному поясу
thumbnail- libvips создаёт миниатюры
- Затем превью 1440 px
- FFmpeg берёт кадры и перекодирует видео
clip- Chinese-CLIP считает векторы 1024-d
- Поиск на естественном языке, категории
- Эстетическая оценка, похожие фото
face- SCRFD находит лица, выравнивает по 5 точкам
- ArcFace считает признаки 512-d
- Постепенно группируются в людей
ocr- PP-OCR читает текст на фото
- Поиск фото по тексту
- Распознаёт документы и скриншоты
Планирование фоновых задач
Отдельного брокера сообщений нет: очередь задач — это таблица jobs в PostgreSQL, которая пишется в той же транзакции, что и данные фото, поэтому у сохранённого фото всегда есть задачи в очереди. Воркеры забирают задачи через SKIP LOCKED, аренда не даёт задачам зависнуть, а неудачи повторяются с квадратичной задержкой.
-- Забрать одну задачу (server/internal/jobs/worker.go)
UPDATE jobs SET status = 'running', attempts = attempts + 1,
locked_by = $1, locked_until = now() + $2::interval
WHERE id = (
SELECT id FROM jobs
WHERE status = 'pending' AND run_after <= now() AND type = ANY($3)
ORDER BY priority DESC, run_after
FOR UPDATE SKIP LOCKED -- параллельные воркеры не берут одну задачу
LIMIT 1
)
RETURNING id, type, asset_id, payload, attempts, max_attempts;- Задача того же типа для того же фото не ставится повторно, пока другая ждёт или выполняется.
- Пустая очередь опрашивается раз в секунду; задачи берутся по priority (сначала высший), затем по сроку (сначала самый ранний).
- Аренда каждой задачи (она же тайм-аут) — 10 минут; при перезапуске процесс возвращает в очередь свои незавершённые задачи.
- Пока кто-то загружает фото, задачи ИИ не берутся, чтобы канал и процессор сначала шли на загрузку.
- Если ИИ-сервис недоступен, весь ИИ-воркер останавливается и ждёт, а не берёт задачи по одной, чтобы они падали и тратили повторы.
- Уровень «Авто» сначала определяет оборудование ИИ-сервера и подбирает подходящие модели, а уже потом берёт задачи — это помогает не загружать на Raspberry Pi и подобных устройствах модели, которым не хватает доступной памяти.
Планирование ИИ
ИИ-сервис только принимает превью и возвращает лица, векторы и текст. Адрес сервиса, модели и параллельность определяет бэкенд; ИИ-сервис по своему оборудованию выбирает CUDA, CoreML / MPS, OpenVINO или CPU.
В режиме «Авто» уровень выбирается по оборудованию ИИ-сервера: с подходящей видеокартой — «Высокая точность»; только с процессором — «Лёгкий» при памяти меньше 6 ГБ и «Сбалансированный» при 6 ГБ и больше.
| Уровень | Подходящее оборудование | Лица | Семантический поиск |
|---|---|---|---|
| quality · Высокая точность | Mac на Apple Silicon, GPU NVIDIA | SCRFD-10G + ArcFace R100 (antelopev2) | Chinese-CLIP ViT-H/14 · 1024-d |
| balanced · Сбалансированный | NAS уровня Intel i5 | SCRFD-10G + ArcFace R100 (antelopev2) | Chinese-CLIP ViT-L/14 · 768-d |
| lite · Лёгкий | N100, Raspberry Pi 5 | SCRFD + ArcFace R50 (buffalo_l) | Chinese-CLIP ViT-B/16 · 512-d |
POST /predict (multipart: image + tasks)
Authorization: Bearer <токен>
tasks = {
"face": {"model": "antelopev2", "min_score": 0.5},
"clip": {"model": "chinese-clip-vit-h-14"},
"ocr": {}
}
→ {
"face": [{"box": [x, y, w, h], "score": 0.93,
"embedding": [512 чисел], "yaw": 0.12, "blur": 340}],
"clip": {"embedding": [1024 чисел]},
"ocr": {"text": "...", "lines": [...]},
"model_ids": {"face": "antelopev2", "clip": "chinese-clip-vit-h-14"}
}Ключевые алгоритмы
Модели, алгоритмы и пороги, на которых держатся поиск, люди, серии и выбор ИИ. Одно опирается на модели ИИ, другое — на векторные вычисления и правила.
Chinese-CLIP помещает фото и текст в одно векторное пространство. Векторы изображений считаются при импорте; при поиске фраза вроде «третий день рождения малыша» превращается в текстовый вектор, а ближайшие фото находятся по косинусному расстоянию в HNSW-индексе pgvector.
- Объединяются три источника результатов: первыми идут фото, на которых распознаны искомые слова, затем снятые в месте с таким названием, последними — самые близкие по смыслу.
- Возвращаются только явные совпадения: результат должен быть почти так же близок, как лучший, и заметно ближе обычного фото. Это помогает отсеять слабые совпадения вместо выдачи снимков, лишь отдалённо связанных с запросом.
- Если запрос — это ровно название категории (пейзажи, еда, скриншоты…), сразу возвращается эта категория.
SCRFD находит каждое лицо и 5 его опорных точек, выравнивает и обрезает до 112×112, а ArcFace вычисляет 512-мерный вектор признаков (после нормализации скалярное произведение и есть косинусное сходство). Группировка идёт постепенно, после каждого обработанного фото, не дожидаясь всей фототеки.
- «Хорошие лица» — чёткие, анфас и достаточно крупные — ищут самых похожих: если часть из них уже принадлежит человеку, лицо присоединяется по большинству голосов; если никто не привязан, но похожих не меньше 3, создаётся новый человек.
- Размытые, профильные и слишком мелкие лица никогда не создают нового человека — они лишь присоединяются к тому, на кого очень похожи. Случайные прохожие, попавшие в кадр один раз, не добавляются.
- Параллельные группировки берут одну и ту же рекомендательную блокировку базы данных, поэтому человек никогда не создаётся дважды. Для обложки выбираются чёткие (дисперсия лапласиана), фронтальные (рыскание) и крупные одиночные лица.
- У снимков одной сцены подряд сходство CLIP обычно 0,94–0,98, а при смене сцены или позы, как правило, 0,7–0,89; порог сходства 0,93 помогает находить вероятные серии.
- Рекомендуемый кадр: если есть лица — тот, где они чёткие, с открытыми глазами и смотрят в камеру; если лиц нет — файл побольше (при той же камере и настройках смазанный снимок содержит меньше деталей и сжимается сильнее).
- Несколько копий одного файла попадают в «Дубликаты», выбирать ничего не нужно. «Оставить» переносит остальное в «Недавно удалённые», откуда его можно восстановить до окончательного удаления.
Дополнительная модель не запускается — используются те же векторы CLIP. Средний вектор набора описаний «художественной фотографии» минус средний вектор набора описаний «обычных бытовых снимков» даёт направление «красиво». Проекция вектора фото на это направление и есть его оценка.
- Используется только для ранжирования, а не как абсолютная оценка; по ней карусель подборок на главном экране выбирает фото.
- Отдельный набор описаний выявляет неудачные снимки (смазанные, пересвеченные и т. п.) и оценивает их независимо от того, насколько красиво фото.
Используемые технологии
Только зрелые компоненты с открытым кодом, никаких проприетарных сервисов; у каждой части есть замена.
- GoAPI, очередь задач, планирование миниатюр
- libvipsМиниатюры и превью
- FFmpegКадры из видео, перекодирование
- ExifToolЧтение и запись метаданных
- tusВозобновляемые загрузки
- Python + FastAPIHTTP-сервис без состояния
- ONNX RuntimeИнференс лиц и текста
- PyTorchChinese-CLIP
- InsightFaceSCRFD + ArcFace
- RapidOCRРаспознавание текста PP-OCR
- PostgreSQL 17Данные приложения и очередь задач
- pgvectorХранение векторов
- HNSWИндекс косинусного расстояния
- ip2regionМестоположение по IP в истории входов (офлайн)
- NominatimОбратное геокодирование (отключается)
- React + TypeScriptВеб-приложение
- Vite + Tailwind CSSСборка и стили
- MapLibre GLСпутниковый глобус
- Docker ComposeОбразы amd64 / arm64
- mDNSЛокальный домен wanmory.local
Конфиденциальность и безопасность
По умолчанию фото и результаты ИИ остаются в вашей частной сети. Если настроить удалённый ИИ-сервис, превью будут отправляться на этот сервер для обработки.
ИИ-сервис получает только превью размером 1440 пикселей, не хранит файлов и не касается базы данных, имён и оригиналов.
Встроенный ИИ-сервис существует только во внутренней сети Docker и доступен лишь бэкенду; если для удалённого ИИ-сервиса задан токен, каждый запрос должен содержать токен Bearer.
Участники могут удалять только свои загрузки, администраторы управляют всем; скрытые фото видит только владелец; в истории входов показывается примерное местоположение по IP.
Папки NAS можно подключить только для чтения через :ro, и тогда приложение не сможет изменить или удалить файлы в них.
После загрузки моделей распознавание и поиск идут локально; функции, которым нужен интернет, такие как обратное геокодирование и спутниковые снимки, можно отключить.
Модели лиц InsightFace разрешены только для некоммерческого использования; модели не входят в образ, а лицензия каждой показывается в настройках перед загрузкой.