Тест новой архитектуры — 5 новых статей, 191 фотка
findbestbeaches.com.Главные цифры
(новая когорта)
(все 100%)
метаданными
(SEO-готовых)
403 + enrich)
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 статьи | Country | City | Фоток |
|---|---|---|---|
| best-beaches-in-koh-phangan | Thailand | Ko Pha Ngan | 45 |
| best-beaches-in-lorca | Spain | Lorca | 35 |
| best-beaches-in-north-rhine-westphalia | Germany | (не определён, регион) | 31 |
| best-beaches-in-ourense | Spain | Ourense | 35 |
| best-beaches-in-pattaya | Thailand | Pattaya | 42 |
2 · Визуальный фильтр — распределение вердиктов
Все 191 фотка новой когорты прошли через фильтр. Распределение:
Почему именно так — по критериям
| Вердикт | Критерий | Что значит | Фоток |
|---|---|---|---|
| UPSCALE | low_quality | низкое разрешение — апскейлим перед regen | 39 |
| ENHANCE | dull_photo | тусклое фото — пока паркуется, ждёт модель | 7 |
| REJECT | faces_visible | лица людей — приватность | 13 |
| REJECT | overexposure | сильные пересветы — спасти нечем | 8 |
| REJECT | construction | стройка / реконструкция | 4 |
| REJECT | low_quality | низкое разрешение + другие проблемы | 5 |
| REJECT | watermark_text | водяные знаки / текст | 1 |
| REJECT | horizon_tilt | сильный наклон горизонта | 1 |
3 · Распределение по статьям
| Статья | Фоток | Clean | Upscale | Enhance | Reject | Обогащено |
|---|---|---|---|---|---|---|
| koh-phangan | 45 | 38 | 1 | 0 | 6 | 39 |
| lorca | 35 | 26 | 8 | 0 | 1 | 34 |
| north-rhine-westphalia | 31 | 6 | 15 | 4 | 6 | 20 |
| ourense | 35 | 21 | 8 | 2 | 4 | 29 |
| pattaya | 42 | 30 | 6 | 1 | 5 | 36 |
Статья north-rhine-westphalia — больше всего UPSCALE (15/31) и ENHANCE (4/31). Это говорит о том, что у сайта-донора по этому региону залиты более слабые фотки. Хороший индикатор для будущих курации источников.
4 · Поток "оригинал → FINAL" на примерах
10 примеров (по 2 на статью): слева — что лежало на сайте-доноре, справа — что получилось после визуального фильтра, регенерации (или апскейла-stub) и обогащения метаданными. Все справа — рендер 800-jpg из набора SEO-рендеров (полный набор: 480/800/1200 в 3 форматах + og-image).




















5 · Ошибки и что они значат
Баг #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: страна/город/POI | 350 |
photo_visual_screen NEW | результат визуального фильтра: verdict + список конкретных issues с confidence | 191 |
photo_labels | ручная разметка (UI /label); теперь опциональна, остаётся как fallback | 0 |
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)
article_photos.surroundingTextуже хранит ~500 chars текста перед фоткой в статье — это хорошее семантическое описание.photo_metadata.altText,caption,keywords— кандидаты для эмбеддинга в Qdrant.photo_metadata.llmRawJsonхранит сырой ответ vision-модели — можно достать визуальные дескрипторы.article.country+article.city+photo_pre_moderation.poi— структурированная гео-фасетка для фильтрации.photos.sha256— primary key для дедупа на уровне байт между разными статьями/сайтами.
8 · Время и стоимость прогона
| Этап | Модель | Вызовов | Стоимость (≈) |
|---|---|---|---|
| pre-moderate · текст-call на статью | gemini-2.5-flash | 5 | $0.01 |
| pre-moderate · vision на POI | gemini-2.5-flash | 191 | $0.30 |
| screen-photo · vision | gemini-2.5-flash | 191 | $0.30 |
| regen-photo · image-to-image | fal-ai/flux-2-pro/edit | 120 | $3.60 |
| upscale-photo · stub (passthrough) | — | 39 | $0.00 |
| enrich-photo · vision + валидация | gemini-2.5-flash | 159 | $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 · Что дальше
- Выбрать модель для апскейла. Сейчас 39 фоток ушли в UPSCALE-ветку и обогатились, но реального апскейла не было (stub). Кандидаты:
fal-ai/clarity-upscaler,fal-ai/real-esrgan. - Решить что делать с ENHANCE-фотками. 7 фоток сейчас в подвешенном состоянии (запись в
photo_visual_screenесть, но дальше не пошли). - Перенести байты на R2. Сейчас всё на VPS диске — это узкое место. Архитектура уже готова к driver-абстракции.
- Phase 4 · Qdrant. Структура БД к этому готова — поля для эмбеддинга в
photo_metadataиarticle_photos.surroundingTextуже наполнены. - Phase 5 · админ-UI. Сейчас всё через CLI и БД, нужен веб для команды (поиск, ревью вердиктов, переразметка).