Прогон пайплайна · 15 мая 2026

Тест новой архитектуры — 5 новых статей, 191 фотка

Первый запуск на VPS после внедрения автоматического визуального фильтра, развилки upscale/regen, text-first геолокации и JSON-per-photo. Сайт-донор: findbestbeaches.com.

Главные цифры

5
новых статей
191
фоток скачано
(новая когорта)
191
прошло через скрин
(все 100%)
159
обогащено
метаданными
1131
FINAL рендера
(SEO-готовых)
4
упало (regen
403 + enrich)
Что тестировали: новый автомат-фильтр (CLEAN/UPSCALE/ENHANCE/REJECT) после pre-mod, автоматическую развилку screen-photo → regen/upscale, text-first определение страны и города по заголовку и первому абзацу, промежуточный photo.json как single source of truth.
Что не тестировали: реальный апскейл (модель ещё не выбрана — используется stub-passthrough), ветку enhance (фотки паркуются с PENDING_ENHANCE), векторный поиск (Phase 4).

1 · Геолокация — text-first работает

Раньше: 1 vision-call на каждую фотку с большим контекстом — дорого и неточно для "общих" фоток. Теперь: 1 cheap text-call на статью даёт страну и город, vision используется только для POI конкретной фотки (или вообще не используется если subheaderPath уже содержит достопримечательность).

Slug статьиCountryCityФоток
best-beaches-in-koh-phanganThailandKo Pha Ngan45
best-beaches-in-lorcaSpainLorca35
best-beaches-in-north-rhine-westphaliaGermany(не определён, регион)31
best-beaches-in-ourenseSpainOurense35
best-beaches-in-pattayaThailandPattaya42

2 · Визуальный фильтр — распределение вердиктов

Все 191 фотка новой когорты прошли через фильтр. Распределение:

CLEAN
122 (64%)
UPSCALE
39 (20%)
REJECT
23 (12%)
ENHANCE
7 (4%)

Почему именно так — по критериям

ВердиктКритерийЧто значитФоток
UPSCALElow_qualityнизкое разрешение — апскейлим перед regen39
ENHANCEdull_photoтусклое фото — пока паркуется, ждёт модель7
REJECTfaces_visibleлица людей — приватность13
REJECToverexposureсильные пересветы — спасти нечем8
REJECTconstructionстройка / реконструкция4
REJECTlow_qualityнизкое разрешение + другие проблемы5
REJECTwatermark_textводяные знаки / текст1
REJECThorizon_tiltсильный наклон горизонта1

3 · Распределение по статьям

СтатьяФотокCleanUpscaleEnhanceRejectОбогащено
koh-phangan453810639
lorca352680134
north-rhine-westphalia316154620
ourense352182429
pattaya423061536

Статья north-rhine-westphalia — больше всего UPSCALE (15/31) и ENHANCE (4/31). Это говорит о том, что у сайта-донора по этому региону залиты более слабые фотки. Хороший индикатор для будущих курации источников.

4 · Поток "оригинал → FINAL" на примерах

10 примеров (по 2 на статью): слева — что лежало на сайте-доноре, справа — что получилось после визуального фильтра, регенерации (или апскейла-stub) и обогащения метаданными. Все справа — рендер 800-jpg из набора SEO-рендеров (полный набор: 480/800/1200 в 3 форматах + og-image).

Оригинал
original
FINAL
final
Aerial view of Chaloklum Beach with clear blue waters and lush green palms in Koh Phangan
Chaloklum Beach on the island of Koh Phangan.
CLEAN · Koh Phangan, Thailand · REGEN_RAW → 7 renditions
Оригинал
original
FINAL
final
Malibu Beach with leaning palm trees, white sand, and longtail boats in Koh Phangan, Thailand
Malibu Beach with white sands and blue waters.
UPSCALE · Koh Phangan, Thailand · UPSCALE_RAW → 7 renditions
Оригинал
original
FINAL
final
Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia
Puntas de Calnegre Beach and coastal village, Lorca.
CLEAN · Lorca, Spain · REGEN_RAW → 7 renditions
Оригинал
original
FINAL
final
Man swimming in turquoise waters near limestone cliffs at Cala Blanca Beach Lorca Spain
Tranquil green waters at Cala Blanca Beach Lorca.
UPSCALE · Lorca, Spain · UPSCALE_RAW → 13 renditions (включая hero 1200)
Оригинал
original
FINAL
final
People relaxing on the sandy shore of Möhnesee Beach with sailboats docked in the water
Möhnesee Beach: a perfect place for leisure.
CLEAN · Germany · REGEN_RAW → 7 renditions
Оригинал
original
FINAL
final
Aerial view of vacation homes with red roofs beside Biggesee lake and boat docks in Germany
Biggesee lake, vacation homes, and boat docks.
UPSCALE · Germany · UPSCALE_RAW → 13 renditions
Оригинал
original
FINAL
final
Lush green trees and grassy banks along Miño River, a tranquil fluvial beach in Ourense, Spain
Tranquil Miño River, a fluvial beach in Ourense.
CLEAN · Ourense, Spain · REGEN_RAW → 7 renditions
Оригинал
original
FINAL
final
Lush green forest lining the tranquil Miño River under a blue sky in Ourense, Spain
Tranquil Miño River flowing through Ourense, Spain.
UPSCALE · Ourense, Spain · UPSCALE_RAW → 7 renditions
Оригинал
original
FINAL
final
Elevated view of Pattaya Beach and cityscape with modern skyscrapers along the coastline in Thailand
Pattaya Beach and city coastline from above.
CLEAN · Pattaya, Thailand · REGEN_RAW → 7 renditions
Оригинал
original
FINAL
final
Pattaya City Beach shoreline with swimmers, sandy shore, blue sea and urban buildings.
Pattaya Beach, with its vibrant urban backdrop.
UPSCALE · Pattaya, Thailand · UPSCALE_RAW → 7 renditions

5 · Ошибки и что они значат

В этом прогоне мы поймали 2 реальных бага и 2 типа моделей-сбоев, которые при ручном тестировании раньше не вылезали. Это и есть основная польза массового прогона.

Баг #1 · APPROVED-guard в regen-photo и enrich-photo (фикснуто в прогоне)

После того как §7 спринта отключил автозапуск регенерации из /label (ручная разметка больше не обязательна — её заменил автомат-фильтр), забыли убрать старый guard внутри самих джоб regen-photo и enrich-photo. Они продолжали проверять PhotoLabel.decision === 'APPROVED' и молча скипали всё. Симптом был: screen=191, regen=0, FINAL=0.

Поймано в логах:

[regen-photo] skip ap=cmp6vqhyu00bfidl8gsw2o4x6 — not APPROVED
[enrich-photo] skip ap=cmp6vqdhp002zidl80dj3yxfz — not APPROVED

Фикс: 2 коммита (ab5beb6, 0787bc1) — убрали guard, новый контракт "если задача в очереди, значит вышестоящий гейт прошёл (screen-photo verdict=CLEAN)".

Сбой #2 · fal.ai возвращает 403 Forbidden

4 фотки за прогон. Похоже на контент-модерацию провайдера (fal.ai блокирует определённые сюжеты). Стэктрейс:

[worker:regen-photo] failed job=232: Forbidden
[worker:regen-photo] failed job=234: Forbidden
[worker:regen-photo] failed job=332: Forbidden
[worker:regen-photo] failed job=333: Forbidden

Что делать: пока ничего — это конкретные фотки. Если такое будет массово, нужно ловить ошибку и помечать фотку как REGEN_FAILED в БД, чтобы не пытаться повторно.

Сбой #3 · enrich-валидатор не уложился в лимиты

LLM 3 раза подряд не сгенерил alt/og нужной длины (для конкретных фоток):

[enrich-photo] failed job=217: altText words 17, expected 8-16
[enrich-photo] failed job=218: ogAltText length 52, expected 60-95
[enrich-photo] failed job=227: altText words 17, expected 8-16
[enrich-photo] failed job=232: altText length 78, expected 80-125
[enrich-photo] failed job=233: altText words 19, expected 8-16
[enrich-photo] failed job=244: ogAltText words 13, expected 6-12
[enrich-photo] failed job=249: ogAltText length 57, expected 60-95

Что делать: это известная граница, есть rescue для caption на 3-й попытке. Похожий rescue можно добавить для alt/og — обрезать до лимита если других проблем нет. Помечу как улучшение, не блокер.

6 · Метаданные — пример того что записалось в БД

Для каждой обогащённой фотки в photo_metadata лежит:

altText:      "Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia"
ogAltText:    "Coastal village with beach and mountains at Puntas de Calnegre, Lorca, Spain"
caption:      "Puntas de Calnegre Beach and coastal village, Lorca."
keywords:     ["coastal village", "puntas de calnegre beach", "costa calida", "murcia",
               "lorca", "beach", "mountains", "seaside"]
filenameStem: "coastal-village-calnegre-beach-spain"

Эти же данные параллельно пишутся в photo.json (один файл на фотку, рядом с её байтами) — single source of truth для будущей переотрисовки HTML / IPTC без повторного запроса к LLM.

Развернуть · полный набор полей на примере одной фотки

Фотка cmp6vqbys000aidl81fzbij85 — оригинал Lorca_Spain1.jpg, прошла как CLEAN, регенерирована через flux-2-pro/edit, обогащена за 1 LLM-вызов (без ретраев). Что у нас по ней есть на трёх уровнях:

A · Строка в таблице photo_metadata

{
  "id": "cmp6wex9y001x9h00q78kcbz8",
  "articlePhotoId": "cmp6vqbys000aidl81fzbij85",
  "filenameStem": "coastal-village-calnegre-beach-spain",
  "title": "Puntas de Calnegre Coastal Village",
  "altText": "Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia",
  "ogAltText": "Coastal village with beach and mountains at Puntas de Calnegre, Lorca, Spain",
  "caption": "Puntas de Calnegre Beach and coastal village, Lorca.",
  "keywords": [
    "coastal village", "puntas de calnegre beach", "costa calida",
    "murcia", "lorca", "beach", "mountains", "seaside"
  ],
  "creator": "Elena Melnic",
  "creditText": "FindBestBeaches.com",
  "copyrightNotice": "© 2026 Alto Media Limited",
  "llmModel": "google/gemini-2.5-flash",
  "llmRawJson": { /* сырой ответ LLM для аудита, с sourceVariantKind и photoJsonStoragePath */ },
  "createdAt": "2026-05-15T12:33:29.591"
}

B · Файл photo.json (рядом с байтами, single source of truth)

{
  "schemaVersion": "1.0",
  "id": "2b919b7bdc482ce89bbbad527ab671b4cde9447c244c3182f37829fce1a32ce4",
  "sourceUrl": "https://findbestbeaches.com/wp-content/uploads/2026/05/Lorca_Spain1.jpg",
  "filename": "coastal-village-calnegre-beach-spain.jpg",
  "dimensions": { "width": 1024, "height": 576 },
  "fileSize": 362941,
  "title": "Puntas de Calnegre Coastal Village",
  "altText": "Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia",
  "ogAltText": "Coastal village with beach and mountains at Puntas de Calnegre, Lorca, Spain",
  "caption": "Puntas de Calnegre Beach and coastal village, Lorca.",
  "keywords": [
    "coastal village", "puntas de calnegre beach", "costa calida",
    "murcia", "lorca", "beach", "mountains", "seaside"
  ],
  "creator": { "type": "Person", "name": "Elena Melnic" },
  "creditText": "FindBestBeaches.com",
  "copyrightNotice": "© 2026 Alto Media Limited",
  "generation": {
    "model": "google/gemini-2.5-flash",
    "latencyMs": 2785,
    "generatedAt": "2026-05-15T12:33:27.232Z",
    "validationPassed": true,
    "validationRetries": 0
  }
}

C · IPTC / XMP / EXIF, вписанные в байты каждого FINAL рендера

Прочитано через exiftool прямо с coastal-village-calnegre-beach-spain-zbij85-800-jpg.jpg:

— IPTC (для CMS / стоков / Google):
Headline:                     "Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia"
ObjectName:                   "Puntas de Calnegre Coastal Village"
Caption-Abstract:             "Puntas de Calnegre Beach and coastal village, Lorca."
Keywords:                     ["coastal village", "puntas de calnegre beach", ...8 шт.]
By-line:                      "Elena Melnic"
Credit:                       "FindBestBeaches.com"
CopyrightNotice:              "© 2026 Alto Media Limited"
City:                         "Lorca"
Country-PrimaryLocationName:  "Spain"

— XMP (Dublin Core, для Adobe-инструментов):
Title:                        "Puntas de Calnegre Coastal Village"
Description:                  "Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia"
Subject:                      ["coastal village", ...8 шт.]
Creator:                      ["Elena Melnic"]
Rights:                       "© 2026 Alto Media Limited"

— EXIF (для Windows и старых CMS):
ImageDescription:             "Coastal village and beach with mountains in the background at Puntas de Calnegre Beach, Lorca, Murcia"
XPTitle:                      "Puntas de Calnegre Coastal Village"
XPComment:                    "Puntas de Calnegre Beach and coastal village, Lorca."
XPKeywords:                   "coastal village;puntas de calnegre beach;costa calida;murcia;lorca;beach;mountains;seaside"
Copyright:                    "© 2026 Alto Media Limited"
Artist:                       "Elena Melnic"
Orientation:                  1 (Horizontal/normal)
ColorSpace:                   sRGB

— Преднамеренно вырезано (по SEO-стандартам):
GPSLatitude, GPSLongitude     — нет (приватность локации)
DateTimeOriginal              — нет (не путать робота поиска)
Make, Model                   — нет (камера-данные не нужны)

Один и тот же набор полей записывается в каждый из 7-13 FINAL-рендеров этой фотки (480/800/1200 в форматах JPG/WebP/AVIF + og-image). Поэтому в любую социалку, любую CMS, или прямо в <img>-тег на сайте — фотка приходит с готовым SEO-контекстом.

7 · Структура БД — что где хранится

Это важно для следующей фазы — векторного поиска и собственной выдачи. Сейчас в проде 9 таблиц:

ТаблицаЧто хранитЗаписей
sitesзарегистрированные сайты-доноры (WP)1
articlesстатьи с сайта-донора; добавлены country и city (text-first геолокация)10
photosуникальные фотки (дедуп по sha256); сами байты живут на диске350
article_photosсвязка статья↔фотка с позицией и контекстом (subheaderPath, surroundingText)350
photo_pre_moderationрезультат pre-moderation: страна/город/POI350
photo_visual_screen NEWрезультат визуального фильтра: verdict + список конкретных issues с confidence191
photo_labelsручная разметка (UI /label); теперь опциональна, остаётся как fallback0
photo_variantsвсе рендеры фотки: REGEN_RAW / UPSCALE_RAW NEW / FINAL (с тегом рендера, размером, sha)1290
photo_metadataфинальные SEO-поля (alt, og, caption, keywords, filenameStem, creator, copyright)159

Ключевые связи

sites (1) ─── (N) articles ─── (N) article_photos ─── (1) photos
                                       │
                                       ├── (1) photo_pre_moderation     [country/city/POI]
                                       ├── (1) photo_visual_screen      [verdict/issues]
                                       ├── (0..1) photo_labels          [манул, опционально]
                                       ├── (N) photo_variants           [REGEN_RAW/UPSCALE_RAW/FINAL]
                                       └── (1) photo_metadata           [SEO поля]

Что важно для поиска (Phase 4)

  1. article_photos.surroundingText уже хранит ~500 chars текста перед фоткой в статье — это хорошее семантическое описание.
  2. photo_metadata.altText, caption, keywords — кандидаты для эмбеддинга в Qdrant.
  3. photo_metadata.llmRawJson хранит сырой ответ vision-модели — можно достать визуальные дескрипторы.
  4. article.country + article.city + photo_pre_moderation.poi — структурированная гео-фасетка для фильтрации.
  5. photos.sha256 — primary key для дедупа на уровне байт между разными статьями/сайтами.

8 · Время и стоимость прогона

ЭтапМодельВызововСтоимость (≈)
pre-moderate · текст-call на статьюgemini-2.5-flash5$0.01
pre-moderate · vision на POIgemini-2.5-flash191$0.30
screen-photo · visiongemini-2.5-flash191$0.30
regen-photo · image-to-imagefal-ai/flux-2-pro/edit120$3.60
upscale-photo · stub (passthrough)39$0.00
enrich-photo · vision + валидацияgemini-2.5-flash159$0.30
Итого~700≈ $4.50

Реальный апскейл (когда выберем модель) добавит ~$0.05–0.10 на фотку, то есть ещё ~$2 на этот объём. Total budget на статью при таких настройках: $0.40–0.70.

Время на полный прогон 5 новых статей (191 фотка): ~36 минут от crawl до последнего FINAL. Бутылочные горлышки — concurrency на pre-mod и enrich (по 2 параллельно), их можно поднять, если упрёмся в throughput.

9 · Что дальше

  1. Выбрать модель для апскейла. Сейчас 39 фоток ушли в UPSCALE-ветку и обогатились, но реального апскейла не было (stub). Кандидаты: fal-ai/clarity-upscaler, fal-ai/real-esrgan.
  2. Решить что делать с ENHANCE-фотками. 7 фоток сейчас в подвешенном состоянии (запись в photo_visual_screen есть, но дальше не пошли).
  3. Перенести байты на R2. Сейчас всё на VPS диске — это узкое место. Архитектура уже готова к driver-абстракции.
  4. Phase 4 · Qdrant. Структура БД к этому готова — поля для эмбеддинга в photo_metadata и article_photos.surroundingText уже наполнены.
  5. Phase 5 · админ-UI. Сейчас всё через CLI и БД, нужен веб для команды (поиск, ревью вердиктов, переразметка).