ARCHITECTURE

Архитектура

Три основных контейнера плюс сервис mDNS — без отдельного брокера сообщений и без обязательных облачных сервисов. Фото загружаются или импортируются из папок NAS, а затем обрабатываются: метаданные, миниатюры, семантические векторы, лица и текст. В итоге получается фототека с поиском по естественному языку, людям и местам.

3 + 1контейнера

Бэкенд на Go, ИИ-сервис и PostgreSQL, плюс mDNS-сервис, объявляющий локальный домен

5типов задач

metadata, thumbnail, clip, face, ocr

512–1024измерений

Векторы изображение-текст Chinese-CLIP (512, 768 или 1024 в зависимости от уровня моделей); признаки лиц ArcFace — 512 измерений

HNSWвекторный индекс

Индекс косинусного расстояния pgvector; поиск и сравнение лиц выполняются в базе данных

SYSTEM

Архитектура системы

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

КЛИЕНТЫБраузер на компьютереВеб-приложение встроено в программу GoБраузер на телефонеiPhone · Androidwanmory.localЛокальный домен, не нужно помнить IPHTTP :8080DOCKER COMPOSEБэкенд Go · serverAPI, загрузки, очередь задач, миниатюры, права, вебREST APIЗагрузки tusВоркеры задачУчётки · входыМиниатюры libvipsМетаданные ExifToolВидео FFmpegГеокодированиеАльбомы · умныеСерии · похожиеВыбор ИИ · этот деньСкан раз в 15 минmdnsОбъявляет wanmory.localИИ-сервис · mlFastAPI + ONNX Runtime / PyTorch, без состоянияДетекция SCRFDПризнаки ArcFaceChinese-CLIPТекст PP-OCRPostgreSQL 17 + pgvectorДанные, очередь задач и векторы в одной базе, Redis не нуженassetsfaces · peopleочередь jobssettingsиндекс HNSWSQL/predict · превью 1440 pxХРАНЕНИЕ · РАСШИРЕНИЯ/dataЗагруженные оригиналы, миниатюры, превью 1440 px/photos/* (можно только чтение :ro)Папки с фото на NAS, сколько угодно./modelsФайлы моделей, загружаются из веб-настроекУдалённый ИИ-сервис (по желанию)Mac (CoreML / MPS) · NVIDIA CUDAДоступ по токену; получает только превью 1440 px, не оригиналычтение/записьскан · только чтениезагрузкаHTTP + токен
Бэкенд — одна программа на Go

Веб-приложение встроено в программу, так что для установки не нужен Node. Миграции базы данных выполняются автоматически при запуске, а обновление — это просто смена образа.

ИИ-сервис можно вынести

Встроенный ИИ-сервис работает во внутренней сети Docker и считает на процессоре. Укажите в веб-настройках удалённый адрес, чтобы перейти на домашний компьютер с видеокартой или Mac.

Одна база для всех данных

Сведения о фото, люди, настройки, очередь задач и векторы хранятся в PostgreSQL, поэтому данные приложения и состояние задач можно резервировать вместе. Сами файлы фото нужно резервировать отдельно.

PIPELINE

Конвейер обработки фото

После импорта у фото сначала извлекаются метаданные и создаются миниатюры, затем параллельно вычисляются семантические векторы, распознаются лица и текст. Каждый шаг ставится в очередь отдельно, поэтому перезапуск или отключение ИИ-сервиса не теряет работу — обработка продолжится, когда всё вернётся.

01ingest
Импорт
  • Веб-загрузки через tus, продолжаются после обрыва
  • Папки /photos сканируются каждые 15 минут
  • Считается контрольная сумма SHA-1, пишется в assets
Бэкенд
02metadata
Метаданные
  • ExifTool читает размеры и время съёмки
  • GPS, камера, объектив, диафрагма и выдержка
  • Время без пояса — по заданному поясу
Бэкенд
03thumbnail
Миниатюры
  • libvips создаёт миниатюры
  • Затем превью 1440 px
  • FFmpeg берёт кадры и перекодирует видео
Бэкенд
04clip
Векторы
  • Chinese-CLIP считает векторы 1024-d
  • Поиск на естественном языке, категории
  • Эстетическая оценка, похожие фото
ИИ · параллельно
05face
Лица
  • SCRFD находит лица, выравнивает по 5 точкам
  • ArcFace считает признаки 512-d
  • Постепенно группируются в людей
ИИ · параллельно
06ocr
Текст
  • PP-OCR читает текст на фото
  • Поиск фото по тексту
  • Распознаёт документы и скриншоты
ИИ · параллельно
SCHEDULING

Планирование фоновых задач

Отдельного брокера сообщений нет: очередь задач — это таблица jobs в PostgreSQL, которая пишется в той же транзакции, что и данные фото, поэтому у сохранённого фото всегда есть задачи в очереди. Воркеры забирают задачи через SKIP LOCKED, аренда не даёт задачам зависнуть, а неудачи повторяются с квадратичной задержкой.

pendingв очередиrunningвзята воркеромdoneвыполненаfailedпопытки исчерпанызабрать через SKIP LOCKEDуспехповторы исчерпаныошибка: повтор через n² × 30 сИИ недоступен: повтор через 15 с, без засчёта ошибкиАренда 10 мин: без результата в срок задача каждые 30 с возвращается в pending
jobs/worker.go · claim
-- Забрать одну задачу (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 и подобных устройствах модели, которым не хватает доступной памяти.
AI SCHEDULING

Планирование ИИ

ИИ-сервис только принимает превью и возвращает лица, векторы и текст. Адрес сервиса, модели и параллельность определяет бэкенд; ИИ-сервис по своему оборудованию выбирает CUDA, CoreML / MPS, OpenVINO или CPU.

Очередь задач ИИclip · face · ocrПараллельность по настройкам, ограничение регулируетсяБэкенд Go · ИИ-клиентВыбор сервиса и уровня, сверка model_ids· Проверяет /health раз в 15 с· Нет связи → задачи ждут· Уровень авто → по оборудованиюзабратьВстроенный ИИ-сервисВнутренняя сеть Docker · CPUУдалённый ИИ-сервисДомашний Mac или ПК с GPUУдалённый рабочий узелСам забирает задачи (в планах)одно из двухCPUONNX RuntimeCoreML / MPS · CUDAВыбирает доступное ускорениеOpenVINOВстроенная графика Intel
Три уровня моделей

В режиме «Авто» уровень выбирается по оборудованию ИИ-сервера: с подходящей видеокартой — «Высокая точность»; только с процессором — «Лёгкий» при памяти меньше 6 ГБ и «Сбалансированный» при 6 ГБ и больше.

УровеньПодходящее оборудованиеЛицаСемантический поиск
quality · Высокая точностьMac на Apple Silicon, GPU NVIDIASCRFD-10G + ArcFace R100 (antelopev2)Chinese-CLIP ViT-H/14 · 1024-d
balanced · СбалансированныйNAS уровня Intel i5SCRFD-10G + ArcFace R100 (antelopev2)Chinese-CLIP ViT-L/14 · 768-d
lite · ЛёгкийN100, Raspberry Pi 5SCRFD + ArcFace R50 (buffalo_l)Chinese-CLIP ViT-B/16 · 512-d
API ИИ-сервиса · /predict
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"}
}
ALGORITHMS

Ключевые алгоритмы

Модели, алгоритмы и пороги, на которых держатся поиск, люди, серии и выбор ИИ. Одно опирается на модели ИИ, другое — на векторные вычисления и правила.

Поиск на естественном языке

Chinese-CLIP помещает фото и текст в одно векторное пространство. Векторы изображений считаются при импорте; при поиске фраза вроде «третий день рождения малыша» превращается в текстовый вектор, а ближайшие фото находятся по косинусному расстоянию в HNSW-индексе pgvector.

sim(q, p) = q · p / (‖q‖ ‖p‖)
  • Объединяются три источника результатов: первыми идут фото, на которых распознаны искомые слова, затем снятые в месте с таким названием, последними — самые близкие по смыслу.
  • Возвращаются только явные совпадения: результат должен быть почти так же близок, как лучший, и заметно ближе обычного фото. Это помогает отсеять слабые совпадения вместо выдачи снимков, лишь отдалённо связанных с запросом.
  • Если запрос — это ровно название категории (пейзажи, еда, скриншоты…), сразу возвращается эта категория.
Группировка лиц

SCRFD находит каждое лицо и 5 его опорных точек, выравнивает и обрезает до 112×112, а ArcFace вычисляет 512-мерный вектор признаков (после нормализации скалярное произведение и есть косинусное сходство). Группировка идёт постепенно, после каждого обработанного фото, не дожидаясь всей фототеки.

d(a, b) = 1 − a · b хорошее лицо d < 0.55 · размытое/профиль d < 0.45
  • «Хорошие лица» — чёткие, анфас и достаточно крупные — ищут самых похожих: если часть из них уже принадлежит человеку, лицо присоединяется по большинству голосов; если никто не привязан, но похожих не меньше 3, создаётся новый человек.
  • Размытые, профильные и слишком мелкие лица никогда не создают нового человека — они лишь присоединяются к тому, на кого очень похожи. Случайные прохожие, попавшие в кадр один раз, не добавляются.
  • Параллельные группировки берут одну и ту же рекомендательную блокировку базы данных, поэтому человек никогда не создаётся дважды. Для обложки выбираются чёткие (дисперсия лапласиана), фронтальные (рыскание) и крупные одиночные лица.
Серии и похожие фото
одна камера Δt ≤ 2s или Δt ≤ 60s и sim ≥ 0.93
  • У снимков одной сцены подряд сходство CLIP обычно 0,94–0,98, а при смене сцены или позы, как правило, 0,7–0,89; порог сходства 0,93 помогает находить вероятные серии.
  • Рекомендуемый кадр: если есть лица — тот, где они чёткие, с открытыми глазами и смотрят в камеру; если лиц нет — файл побольше (при той же камере и настройках смазанный снимок содержит меньше деталей и сжимается сильнее).
  • Несколько копий одного файла попадают в «Дубликаты», выбирать ничего не нужно. «Оставить» переносит остальное в «Недавно удалённые», откуда его можно восстановить до окончательного удаления.
Эстетическая оценка для выбора ИИ

Дополнительная модель не запускается — используются те же векторы CLIP. Средний вектор набора описаний «художественной фотографии» минус средний вектор набора описаний «обычных бытовых снимков» даёт направление «красиво». Проекция вектора фото на это направление и есть его оценка.

score(p) = p · (μ_работа − μ_бытовое)
  • Используется только для ранжирования, а не как абсолютная оценка; по ней карусель подборок на главном экране выбирает фото.
  • Отдельный набор описаний выявляет неудачные снимки (смазанные, пересвеченные и т. п.) и оценивает их независимо от того, насколько красиво фото.
TECH STACK

Используемые технологии

Только зрелые компоненты с открытым кодом, никаких проприетарных сервисов; у каждой части есть замена.

БЭКЕНД
  • Go
    API, очередь задач, планирование миниатюр
  • libvips
    Миниатюры и превью
  • FFmpeg
    Кадры из видео, перекодирование
  • ExifTool
    Чтение и запись метаданных
  • tus
    Возобновляемые загрузки
ИИ-СЕРВИС
  • Python + FastAPI
    HTTP-сервис без состояния
  • ONNX Runtime
    Инференс лиц и текста
  • PyTorch
    Chinese-CLIP
  • InsightFace
    SCRFD + 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
PRIVACY

Конфиденциальность и безопасность

По умолчанию фото и результаты ИИ остаются в вашей частной сети. Если настроить удалённый ИИ-сервис, превью будут отправляться на этот сервер для обработки.

Минимум данных

ИИ-сервис получает только превью размером 1440 пикселей, не хранит файлов и не касается базы данных, имён и оригиналов.

Аутентификация

Встроенный ИИ-сервис существует только во внутренней сети Docker и доступен лишь бэкенду; если для удалённого ИИ-сервиса задан токен, каждый запрос должен содержать токен Bearer.

Права доступа

Участники могут удалять только свои загрузки, администраторы управляют всем; скрытые фото видит только владелец; в истории входов показывается примерное местоположение по IP.

Оригиналы только для чтения

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

Работает без интернета

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

Лицензии моделей

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