Текст — это просто: пайплайн, который иллюстрирует статьи, написанные ИИ

Генерация текста — решённая задача. Его иллюстрирование — нет. Сквозной пайплайн: промпт-роутер, поиск фото, диаграммы Mermaid, проверенные графики и alt-текст, указание авторства и разметка ImageObject, которые превращают всё это в SEO.

Поделиться
Программист печатает за столом с двумя мониторами, полными кода, в свете цветных ламп.
Фото с Unsplash

Вы разработчик с контентной целью: десятки статей в месяц, а может, и сотни. Модель, пишущая текст, справляется с черновиком, планом, мета-описанием, внутренними ссылками. А затем конвейер упирается в единственный шаг без собственной модели — изображения — и останавливается. Не потому, что картинки трудно достать, а потому, что ничто в стеке не знает, какая именно картинка нужна, откуда она, с какой лицензией и как её описать.

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

Текст решён. Иллюстрация — вот где всё останавливается.

Проведите честный аудит автоматизированного конвейера статей. План: решено. Черновик: решено. Заголовок, мета-описание, схема, внутренние ссылки, перевод: решено, всё одной и той же моделью, всё в тексте. А дальше:

Шаг конвейера Статус Что реально блокирует
План и черновик Решено Один вызов модели, один промпт
Заголовки, мета, схема, ссылки Решено Текст на входе, текст на выходе
Главное изображение Заблокировано Нужен реальный файл, лицензия, размеры и alt-текст — ничего из этого текстовая модель произвести не может
Изображения разделов Заблокировано От трёх до пяти на статью, каждое своё, ни одного повтора по сайту
Графики и диаграммы Заблокировано Должны быть точными — единственный визуал, который поиск не может вернуть, а генератор не должен придумывать

Проблема не эстетическая, а структурная: статья публикуется с заглушкой из стокового банка, или с тем же фото, что и в предыдущих двенадцати, или со сгенерированным изображением, шестипалая рука на котором — первое, что видит читатель. И картинка — не украшение: собственная документация Google по изображениям прямо говорит, что alt-текст «является наиболее важным атрибутом с точки зрения предоставления метаданных для изображения», и рекомендует использовать настоящие элементы <img> с содержательным alt, а не фоновые изображения CSS, чтобы изображение вообще можно было найти и понять.1

Четыре типа визуала, четыре типа модели

Самая крупная ошибка проектирования — считать «изображение» одной задачей с одним поставщиком. На деле это четыре разные задачи, а маршрутизатор между ними — строка промпта, а не сервис:

Разделу нужна… Произвести с помощью Почему не остальными способами
Сцена из реального мираглавное изображение, ситуации с людьми, места, объекты, жесты Семантический поиск фото (Pexafy) Генератор выдумывает детали; графику нечего отображать
Реально имеющиеся у вас числабенчмарки, цены, результаты опросов, задержка Модель, пишущая код для построения графиков, запускаемый в песочнице Модели генерации изображений нельзя доверить значение; фото не может нести данные
Структура или потокархитектура, последовательность, конечный автомат Модель, пишущая на Mermaid / Graphviz, отрисованные детерминированно В бесплатных фотобанках нет диаграммы вашей системы
Ваш продукт на экранедокументация, changelog, туториалы Скриптовый скриншот браузера (Playwright) Ничто другое не покажет интерфейс, существующий только в вашей сборке

А сгенерированная иллюстрация? За ней остаётся одна честная роль: сцена, которую нельзя сфотографировать и которая не является данными — абстрактный механизм, продукт, которого ещё не существует, фирменный иллюстративный стиль. (Полный аргумент в пользу настоящей фотографии против генерации — скорость при объёме, точность, проблема одинаковости — приведён здесь.) Оценивая направление движения: с 2 августа 2026 года статья 50 Закона ЕС об искусственном интеллекте требует, чтобы поставщики генеративных систем маркировали синтетические результаты в машиночитаемом формате.2 Это обязанность поставщиков и операторов ИИ, а не правило о том, что может публиковать блог — но именно поэтому происхождение изображения в начале статьи всё чаще становится тем, что читатель может проверить, а не принять на веру.

Конвейер целиком

Пять этапов. Только этап 3 обращается к API изображений, и только этап 4 опционален:

Общая схема — на входе одна статья, на выходе — готовая к публикации
┌─ 1. WRITE ────────────────────────────────────────────────────┐
   topic ──▶ LLM ──▶ draft.md  (h2 sections, front-matter)
└───────────────────────────────┬───────────────────────────────┘
┌─ 2. BRIEF ────────────────────┴───────────────────────────────┐
   draft.md ──▶ LLM ──▶ { hero: {...}, sections: [ {...} ] }
                     one JSON object: per slot, a kind +
                     either a camera brief or a data/diagram spec
└──────────┬──────────────────────────────────┬─────────────────┘
           │ kind = "photo"                │ kind = "chart" | "diagram"
           ▼                               ▼
┌─ 3. SEARCH ───────────────┐   ┌─ 4. DRAW (optional) ─────────┐
   GET /search/photos          LLM ──▶ mermaid | plotting code
   ← credited photo +           ──▶ sandbox ──▶ .svg / .png
     w/h, blur_hash, alt,       (deterministic render, no
     licence, source URL         invented numbers)
└──────────┬────────────────┘   └──────────────┬───────────────┘
           └───────────────┬──────────────────┘
┌─ 5. ASSEMBLE ─────────────┴───────────────────────────────────┐
   <img> with width/height + fetchpriority | loading
   alt written for a human · visible credit · ImageObject JSON-LD
   photo_id stored so no two pages share a hero
└───────────────────────────────────────────────────────────────┘

Важнее самой схемы два свойства. Этап 2 — это маршрутизатор: для каждого слота он решает, какой генератор запустится, поэтому вы никогда не запросите у фотобанка столбчатую диаграмму. А этап 5 — это место, где живёт SEO: здесь выдаётся всё, что документирует Google об изображениях (содержательный alt, метаданные лицензии, безопасный для LCP главный баннер), из полей, уже полученных в ответе поиска.

Этап 2: превратить черновик в задания для визуалов

Один вызов модели на готовом черновике — и то, что она возвращает, это план, а не запрос. Как написать одно фотографическое задание — почему заголовок статьи — худший возможный вход, как выглядит камерное задание из 12–25 слов и в чём оно может подвести — подробно изложено в руководстве по иллюстрированию одной статьи и здесь не повторяется. Что добавляет конвейер — это маршрутизацию: тот же вызов должен решить, слот за слотом, какой производитель запускается — а запись для графика несёт данные там, где запись для фото несёт сцену.

Промпт-маршрутизатор — скопируйте как есть
# системный промпт — запускается один раз на готовый черновик
You are the art director of a technical publication. Read the article and
return the visual plan: one entry for the hero, one per H2 section.

For each entry choose exactly one kind:
  "photo"    a real scene: someone doing something somewhere, a place,
              an object, a gesture. The default for heroes.
  "chart"    the section states numbers that are IN the article. Never
              invent values: copy them into data, verbatim.
  "diagram"  the section describes a structure, a flow or a sequence.
  "none"     the section is short, or already carries a code block.

Rules for "photo" entries — the field is query:
   Напишите краткое описание кадра: сцена, которую мог бы снять фотоаппарат, от 12 до 25 слов,
   на английском языке, соответствующая настроению раздела. Назовите то, что находится в кадре,
   но никогда не саму тему. Никакого текста, логотипов, брендов или знаменитостей; никаких
   невидимых метафор. (Полные правила, с примерами и случаями неудач:
   pexafy.com/blog/illustrate-blog-articles-at-scale/)

Do NOT write the alt text of a photo entry: the picture you get back is the
closest match to the brief, not the scene you described, so its alt has to be
written from the chosen photo. Chart and diagram entries DO carry an alt —
there you control exactly what is rendered.

Return JSON only:
{
  "hero": { "kind": "photo", "query": "…", "orientation": "landscape" },
  "sections": [
    { "h2": "…", "kind": "photo",   "query": "…" },
    { "h2": "…", "kind": "chart",   "title": "…", "unit": "ms",
      "data": [ {"label": "…", "value": 0} ], "alt": "…" },
    { "h2": "…", "kind": "diagram", "spec": "flowchart LR; …", "alt": "…" }
  ]
}

Строка kind делает большую часть работы, а инструкция data — остальное: запись для графика может содержать только числа, которые уже присутствуют в черновике, поэтому модель переписывает, а не изобретает. Вот маршрутизатор на трёх реальных разделах статьи для разработчиков:

Раздел kind Что вернул маршрутизатор
Главное изображение — «Почему наша ночная сборка занимает 40 минут» photo «разработчик, работающий поздно вечером за столом с двумя мониторами и механической клавиатурой в тёмной комнате, освещённой экранами»
«На что реально уходит время» chart data скопированы из абзаца: установка 480 с, компиляция 1080 с, тесты 720 с, загрузка 120 с
«Как мы разбили граф» diagram flowchart LR графа зависимостей задач
«Что мы изменили и что сделали бы снова» photo «два инженера стоят у доски, покрытой диаграммами, вместе разбирая проблему»

Этап 3: фотографии возвращаются с указанием авторства

Каждый элемент с kind: "photo" — это один запрос. Приведённое выше задание для главного изображения, отправленное в публичный API, вернуло результат за 147 мс:

GET /search/photos — задание для главного изображения · «разработчик, работающий поздно вечером за столом с двумя мониторами…» · 147 мс
Движок ранжирует по смыслу, поэтому развёрнутое предложение сужает выборку, а не опустошает её. Запустить этот же поиск →

Задание для последнего раздела, совершенно другая сцена, за 144 мс:

GET /search/photos — задание для раздела · «два инженера стоят у доски, покрытой диаграммами…» · 144 мс
Та же статья, тот же запуск, сцена, которую никто не спутает с главным изображением — потому что задание было написано для каждого раздела отдельно, а не для статьи в целом. Запустить и этот тоже →

То, что возвращается по каждому фото, и делает возможным этап 5 — это не просто файл:

Один результат, сокращённый до полей, которые использует сборщик
{
  "photo_id":  "019e1c7f-0063-759e-b498-33ce1714e6c9",   // сохраните: без повторов
  "urls": { "small": "…?w=400", "regular": "…?w=1080",
             "large": "…?w=1920" },
  "width": 3000, "height": 1688,          // → без сдвига макета
  "blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na",   // → настоящая заглушка
  "alt_description": "Person types on keyboard in front of dual monitors…",
  "photographer_full_name": "Jakub Żerdzicki",   // → ImageObject.creator
  "source": "Unsplash", "license_type": "free",
  "source_image_url": "https://unsplash.com/photos/…",
  "attribution": { "html": "<span…>Photo by …</span>",
                    "plain": "Photo by Jakub Żerdzicki on Unsplash (…)" }
}

Этап 4: то, что фотография сказать не может

Два слота в плане не поддаются поиску, и именно здесь вторая модель находит своё применение — не для того, чтобы нарисовать картинку, а чтобы написать код, который её нарисует. Разница важна: код можно проверить, он детерминирован и не может галлюцинировать высоту столбца.

Диаграммы: текст на входе, SVG на выходе

Mermaid отрисовывает диаграммы из текстового описания в формате, похожем на Markdown,3 что делает его самой безопасной целью для модели: результат можно проинспектировать, сравнить через diff в git, и он отрисовывается одинаково при каждом запуске. Маршрутизатор уже вернул спецификацию.

diagram.sh — модель написала спецификацию, CLI её отрисовывает
# поле "spec" элемента типа diagram, записанное в build/graph.mmd
cat build/graph.mmd
flowchart LR
  install["install deps · 480s"] --> compile["compile · 1080s"]
  compile --> test["test suite · 720s"]
  compile --> upload["upload artifacts · 120s"]

npx -y @mermaid-js/mermaid-cli -i build/graph.mmd -o static/img/graph.svg
# → SVG, который можно посмотреть в PR, а не картинка, которой нужно доверять

Графики: только числа, уже содержащиеся в статье

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

chart.py — отрисовать переписанные данные, затем проверить их
import re, matplotlib
matplotlib.use("Agg")                # без интерфейса: без отображения в CI
import matplotlib.pyplot as plt

def assert_in_draft(value, draft: str) -> None:
    """Число на графике должно присутствовать в статье именно как число."""
    # Ловушка здесь — сравнение подстрок: "120" есть внутри "1200" и
    # внутри "?w=1200" — наивный `str(v) in draft` пройдёт на чём угодно.
    # Сверяем по границам слов, принимаем формы 1 234 / 1,234 / 1234.
    body = re.sub(r"(?<=\d)[  ,](?=\d{3}\b)", "", draft)   # убираем разделители
    if not re.search(rf"(?<![\d.]){re.escape(str(value))}(?![\d.])", body):
        raise ValueError(f"{value} is not stated in the article — refusing to plot")

def render_chart(entry: dict, draft: str, out: str) -> str:
    labels = [d["label"] for d in entry["data"]]
    values = [d["value"] for d in entry["data"]]
    for v in values:
        assert_in_draft(v, draft)       # галлюцинированное значение → без графика

    fig, ax = plt.subplots(figsize=(8, 4.5), dpi=160)
    ax.barh(labels, values)
    ax.set_xlabel(entry["unit"])
    ax.set_title(entry["title"])
    fig.tight_layout()
    fig.savefig(out)                    # детерминированный, проверяемый артефакт
    return out

Защита короткая, но писать её нужно аккуратно: проверка по подстроке не работает. Выражение "120" in draft истинно и для статьи, содержащей 1200 или ?w=1200, поэтому наивная версия пропускает всё подряд и ничего не защищает. Привязывайтесь к границам цифр, нормализуйте разделители тысяч — и класс ошибки, из-за которой техническую статью разберут по косточкам в комментариях (график, противоречащий собственному абзацу), не сможет попасть в продакшн. Скриншоты подчиняются тому же принципу: скриптовый вызов page.screenshot() для вашей реальной сборки — единственный источник истины о вашем собственном интерфейсе, и он остаётся верным по мере изменения интерфейса.

Этап 5: сборка — вот где живёт SEO

Всё, что было до этого, производило файлы и поля. На этом этапе они превращаются в разметку — и здесь стоит быть точным, потому что в этих нескольких строках решаются три задокументированных поведения.

Главное изображение, выдано из ответа поиска — ничего не выдумано
<!-- Байты приходят с другого источника: заплатите за handshake заранее -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>

<!-- Элемент LCP: никогда не lazy, всегда высокий приоритет -->
<figure>
  <img src="{urls.regular}"
       width="{width}" height="{height}"        <!-- убивает сдвиг макета -->
       alt="{alt}"                                <!-- написан ПОСЛЕ выбора -->
       fetchpriority="high" decoding="async"
       style="background:{color_hex}">   <!-- доминирующий цвет, 1 поле -->
  <figcaption>{attribution.html}</figcaption>
</figure>

<!-- Изображения разделов, ниже первого экрана: обратные настройки -->
<img src="{urls.regular}" width="{width}" height="{height}"
     alt="{alt}" loading="lazy" decoding="async">

Hotlink или перезалить к себе? Фрагмент выше делает hotlink — это самый быстрый способ доставки и причина использования preconnect: удалённое главное изображение стоит DNS-запроса и TLS-рукопожатия на критическом пути, и это может съесть выигрыш, который вы только что купили с помощью fetchpriority. Перезалив к себе полностью убирает сторонний источник, позволяет отдавать AVIF/WebP на собственных контрольных точках и переживает изменение внешнего URL — ценой хранения, дополнительного шага загрузки в конвейере и собственного счёта за CDN. Что бы вы ни выбрали, color_hex даёт заглушку одним полем (blur hash красивее, но его сначала нужно декодировать в data URI — это не цвет CSS). Скопировать грубое приближение главного изображения прямо в background: — вот где большинство конвейеров незаметно выдают пустой серый прямоугольник.

  1. Никогда не используйте ленивую загрузку для главного изображения. web.dev однозначен: «Никогда не используйте lazy-load для вашего LCP-изображения, поскольку это всегда приведёт к ненужной задержке загрузки ресурса и негативно скажется на LCP», и рекомендует fetchpriority="high" для элемента, который вероятно станет LCP, — применяя это экономно, к одному изображению.4 Конвейер, ставящий loading="lazy" на каждое изображение, включая главное, — самая распространённая самонанесённая рана Core Web Vitals в автоматизированных публикациях.
  2. Всегда выдавайте width и height — они возвращаются в ответе, так что отговорок нет; именно эта пара атрибутов позволяет браузеру зарезервировать место и не даёт макету прыгать. Используйте blur_hash как заглушку, пока файл загружается.
  3. Пишите alt после выбора, никогда до. Это тонкий момент. Поле alt в плане описывает сцену, которую вы запросили; полученное фото — ближайшее совпадение, а не та сцена. Выдать текст задания как alt — это именно та ошибка доступности, которую этот конвейер призван предотвратить: описание картинки, которой нет на странице. Формируйте alt из alt_description выбранного фото, доработанного с учётом абзаца, в котором оно находится. Рекомендация Google — «сосредоточиться на создании полезного, содержательного контента, который использует ключевые слова уместно и в контексте содержимого страницы», и предупреждает, что переполнение атрибутов alt ключевыми словами «приводит к негативному пользовательскому опыту и может привести к тому, что ваш сайт будет воспринят как спам».1

То, что почти никто не автоматизирует: метаданные лицензии

Google поддерживает структурированные данные ImageObject для лицензирования изображений. Требуется contentUrl плюс хотя бы одно из creator, creditText, copyrightNotice или license, рекомендуется acquireLicensePage, и изображения с информацией о лицензии становятся кандидатами на значок Licensable в Google Images.5 Каждое из этих полей уже присутствует в ответе поиска — так что его выдача это шаблон, а не проект:

ImageObject JSON-LD, заполненный из ответа API
<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "ImageObject",
  "contentUrl": "{urls.large}",                 // обязательно
  "creator": { "@type": "Person",
                "name": "{photographer_full_name}" },
  "creditText": "{photographer_full_name} on {source}",
  "license": "{LICENSE_URL[source]}",           // собственная страница библиотеки
  "acquireLicensePage": "{source_image_url}"     // страница фото
}
</script>

# LICENSE_URL сопоставляет поле `source` с лицензией, которая реально регулирует
# использование фото — unsplash.com/license, pexels.com/license, pixabay.com/…

Одна деталь, которую стоит сделать правильно: license должен указывать на лицензию, регулирующую именно это фото — на собственную страницу лицензии исходной библиотеки, а не на сводную страницу на вашем домене. Google читает это поле, чтобы решить, подходит ли изображение для значка, а самореферентный URL и как сигнал слабее, и трудно защитить его иначе, чем как ссылку на самого себя. Держите собственную сводку лицензий как внутреннюю страницу для читателей; в разметку помещайте каноническую.

И раз уж вы в сборщике: дайте файлу короткое содержательное имя вместо IMG_0042.jpg и добавьте изображение в карту сайта — формат карты сайта для изображений от Google принимает до 1000 изображений на URL страницы.6 Оба пункта — по одной строке в конвейере, и ни один из них никогда не делается вручную.

Два конвейера, которые стоит перенять

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

1 · Блог разработчика в CI — Markdown в репозитории

Статьи хранятся как Markdown, изображения коммитятся рядом с ними, всё это запускается при push. Детерминированно, проверяемо в PR, без зависимости от какого-либо API во время выполнения:

.github/workflows/illustrate.yml
on: { pull_request: { paths: ["content/**.md"] } }

jobs:
  illustrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: python plan_to_pr.py $(git diff --name-only origin/main -- 'content/*.md')
        env:
          PEXAFY_API_KEY: ${{ secrets.PEXAFY_API_KEY }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      - uses: peter-evans/create-pull-request@v6   # изображения попадают в PR
        with: { commit-message: "chore(content): illustrate" }

Человек всё равно утверждает PR, и в этом суть: конвейер предлагает, ревьюер решает, а написанный им front-matter доступен для diff.

Половина, отвечающая за поиск, — это один GET-запрос — сам запрос, его score_threshold и возвращаемые поля построчно расписаны в руководстве по одной статье, так что здесь он импортируется, а не переписывается заново. Что добавляет этот файл — это всё, что нужно плану и не нужно одному фото: повторную попытку при задании, которое ничего не вернуло, закрепление за photo_id, чтобы ни одно изображение не совпадало на двух страницах, и alt-текст, написанный по фото, которое вернулось.

plan_to_pr.py — связующий код: визуальный план на входе, фронт-маттер на выходе
import frontmatter
from photo_search import search   # один GET /search/photos; пропускает id из `used`

def find_photo(entry: dict, used: set) -> dict | None:
    """Ищем; если задание было слишком узким, расширяем один раз, затем сдаёмся."""
    orientation = entry.get("orientation", "landscape")
    for query in (entry["query"], widen(entry["query"])):
        photo = search(query, orientation, used)
        if photo:
            used.add(photo["photo_id"])   # закрепляем: без повторов по всему сайту
            return photo
    return None                        # вызывающий код решает: пропустить слот или упасть

def widen(query: str) -> str:
    """Отбрасываем последнее уточнение — обычно оно и слишком специфично."""
    return query.rsplit(" in ", 1)[0] if " in " in query else query

def illustrate(path: str, plan: dict, used: set) -> None:
    post = frontmatter.load(path)
    hero = find_photo(plan["hero"], used)
    if hero is None:                    # отсутствие главного изображения лучше плохого
        raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")

    post["hero"] = {                    # всё, что нужно шаблону
        "src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
        # alt описывает фото, которое мы ПОЛУЧИЛИ, а не сцену, которую запросили.
        "alt": alt_for(hero, section=post.get("title", "")),
        "bg": hero["color_hex"], "credit": hero["attribution"]["html"],
        "creator": hero["photographer_full_name"], "id": hero["photo_id"],
        "source": hero["source"],
        "source_url": hero["source_image_url"],   # → acquireLicensePage
    }
    open(path, "w").write(frontmatter.dumps(post))

def alt_for(photo: dict, section: str) -> str:
    """alt_description как основа, обрезанная до ~125 символов для скринридеров.
    Прогоните заново через модель вместе с абзацем, если нужен результат лучше."""
    base = photo.get("alt_description") or photo.get("description", "")
    return base[:125].rstrip(" ,;")

2 · Документация и changelog — сначала скриншоты, фото в последнюю очередь

Инвертируйте настройки маршрутизатора по умолчанию. В документации продукта честный визуал — почти всегда ваш собственный интерфейс: скрипт Playwright, открывающий реальную сборку, устанавливающий фиксированный viewport и захватывающий именно то состояние, которое описывает абзац. Диаграммы покрывают страницы архитектуры, а фотографии появляются только на концептуальных и посадочных страницах — там, где скриншот ничего бы не сказал.

shots.py — скриншот генерируется, а не описывается
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900},
                            device_scale_factor=2)   # чёткость на retina
    page.goto("http://localhost:3000/dashboard")
    page.get_by_role("button", name="New API key").click()
    page.screenshot(path="static/img/docs/new-api-key.png")
    browser.close()
# Выполняется в той же задаче CI, что и сборка документации → скриншот никогда не
# сможет описывать версию интерфейса, которой уже не существует.

Два варианта стоит упомянуть, хотя они не заслуживают отдельного рецепта. Программное SEO переворачивает цикл: с сотнями страниц, генерируемых из базы данных, вы не ищете на каждую страницу отдельно — один запрос возвращает до 100 фотографий, поэтому вы ищете по кластеру тем и распределяете из пула, а уникальность обеспечивает ограничение на дедупликацию (эта архитектура целиком). А рассылкам и карточкам для соцсетей нужно одно и то же фото в четырёх кадрах: сохраняйте photo_id, запрашивайте нужный размер из urls и ищите заново только если кадрирование действительно не подошло.

Тот же конвейер как один агент

Если черновик уже пишет модель, кратчайший путь — дать ей инструмент поиска напрямую, вместо того чтобы перекладывать JSON между процессами. Pexafy размещает хостинг-сервер Model Context Protocol по адресу mcp.pexafy.com/mcp. Настройка коннектора и инструменты, которые он предоставляет, описаны в других материалах — установка для десктопа и редактора в руководстве по одной статье, безголовый вариант для CI-раннера в статье о контенте в масштабе, а как выглядит более широкий рынок коннекторов — кто предлагает MCP-сервер изображений и на каких условиях — в исследовании инфраструктуры поиска изображений для AI-агентов. Здесь стоит показать, что происходит с этим конвейером, когда агент получает инструменты в руки: он перестаёт быть пятью этапами и становится одной инструкцией.

Одна инструкция — выполнен весь план
Вы  Вот черновик. Построй визуальный план, проиллюстрируй его и открой PR.
     Графики — только из чисел, уже присутствующих в тексте.

Агент  → план: hero=photo · §2=chart · §3=diagram · §4=photo
      → search_photos(q="a developer working late at a desk with two
         monitors and a mechanical keyboard in a dark room…")
      ← 16 фото · 147 мс · выбрано #1, 3000×1688, с указанием автора
      → mermaid-cli build/graph.mmd → static/img/graph.svg
      → chart.py §2 → 4 значения сверены с черновиком ✓
      → search_photos(q="two engineers standing at a whiteboard…")
      ← 16 фото · 144 мс · выбрано #1

      ✓ заполнено 4 слота · 2 вызова API · написаны alt + credit + ImageObject
      ⚠ §5 не вернул ничего выше 0.5 — задание слишком абстрактно, переписано как
        "человек за кухонным столом проверяет цифры на ноутбуке"

Последняя строка и объясняет, зачем нужен агент в этом цикле, а не чистый скрипт: сценарий сбоя автоматизированной иллюстрации — плохое задание, а переписывание плохого задания — именно то, для чего нужна языковая модель. Детерминированные части — распределение, дедупликацию, числовую проверку — держите в коде.

Во что обходится одна проиллюстрированная статья

Три слота фото на статью означают три поисковых запроса, поэтому бесплатный план (5,000 запросов/месяц) покрывает 1,666 статей в месяц, прежде чем вообще встанет вопрос об оплате — а если объединять поиск по кластерам тем вместо отдельных статей, этот потолок сдвигается ещё на порядок. Разбивка по планам, а также архитектура объединения, которая делает вопрос неактуальным, описаны в сопутствующей статье об иллюстрировании контента в масштабе.

Два вызова модели на статью — один для черновика, один для визуального плана — это несколько тысяч токенов, и они окажутся самой дешёвой строкой в конвейере; рендеринг Mermaid и matplotlib ничего не стоит, кроме секунд CI. Строка, которая удивляет людей — та, что не дешёвая: генерация четырёх изображений на статью, четырёхсот изображений в месяц, плюс попытки, не прошедшие отбор — а результат всё равно не несёт ни фотографа, ни даты, ни URL источника.

Чего этот конвейер не исправляет

Честный раздел, потому что предотвращаемая им ошибка обходится дорого. Хорошо проиллюстрированная статья остаётся статьёй: реальная фотография, точные графики и корректная разметка улучшают страницу, которая заслуживает существования. Они не заставляют ранжироваться тонкий, массово произведённый контент. Политика Google в отношении спама называет злоупотребление масштабированным контентом — генерацию множества страниц прежде всего для манипуляции ранжированием и предоставление читателям малой ценности, независимо от того, применялась ли автоматизация, — и качество иллюстраций не является фактором в этом суждении.7

Поэтому верна такая формулировка: этот конвейер — контролируемый вами порог качества, применяемый к страницам, у которых уже есть причина быть опубликованными. Где он доказуемо окупается:

  • Происхождение, которое читатель может проверить. Строка с указанием авторства с реальным фотографом и URL источника — это утверждение, которое можно проверить, и те же поля наполняют разметку ImageObject, которую читает Google.
  • Доступность и Core Web Vitals. Настоящий alt-текст, размеры у каждого изображения, главное изображение, которое никогда не грузится лениво. Умножьте на каждую публикуемую статью — и это и есть история качества изображений сайта.
  • Точность там, где её можно проверить. График, чьи числа проверяются по статье, скриншот, сгенерированный из работающей сборки, диаграмма, проверяемая как текст в PR — три визуала, которые не могут разойтись с истиной без падения теста.

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

С чего начать

  1. Добавьте промпт-маршрутизатор к тому, что уже пишет ваши черновики, и выведите JSON, не действуя по нему. Прочитайте десять планов. Если задания называют темы вместо сцен, исправьте промпт до того, как писать какую-либо интеграцию.
  2. Подключите только слоты фото. Один GET /search/photos на задание, и сохраняйте photo_id с первого дня — дедупликация, добавленная задним числом на 3000 живых страниц, это миграция, а не колонка.
  3. Выдавайте width, height и alt в том же коммите. Это самая дешёвая работа по Core Web Vitals, которую вы когда-либо сделаете.
  4. Затем добавьте этап 4, диаграммы раньше графиков — Mermaid это текст, так что у него нет сценария сбоя, кроме синтаксической ошибки.
  5. Добавьте числовую проверку до того, как первый график дойдёт до читателя, а не после.

Источники и сноски

1 Google Search Central, Рекомендации по SEO для изображений: alt-текст — «наиболее важный атрибут с точки зрения предоставления метаданных для изображения»; рекомендация — создавать «полезный, содержательный контент, который использует ключевые слова уместно и в контексте содержимого страницы», избегать переполнения атрибутов alt ключевыми словами, использовать HTML-элементы <img> вместо изображений CSS и давать файлам короткие, но содержательные имена.

2 Закон ЕС об ИИ, статья 50 — обязательства по прозрачности, применимые с 2 августа 2026 года: поставщики систем, генерирующих синтетические изображения, аудио, видео или текст, должны маркировать результаты в машиночитаемом формате и делать их распознаваемыми как искусственно сгенерированные. Обязательство касается поставщиков и операторов ИИ; это не правило о том, какие изображения может публиковать сайт.

3 Mermaid отрисовывает диаграммы и графики из текстовых описаний в стиле Markdown, что делает результат проверяемым и детерминированным.

4 web.dev, Оптимизация Largest Contentful Paint: «Хорошая идея — установить fetchpriority="high" на элементе <img>, если вы считаете, что он, вероятно, станет элементом LCP вашей страницы», применяя это экономно; и «Никогда не используйте lazy-load для вашего LCP-изображения, поскольку это всегда приведёт к ненужной задержке загрузки ресурса и негативно скажется на LCP».

5 Google Search Central, Метаданные изображений (структурированные данные): ImageObject требует contentUrl плюс хотя бы одно из creator, creditText, copyrightNotice или license; acquireLicensePage рекомендуется, и изображения с информацией о лицензии могут стать кандидатами на значок Licensable в Google Images.

6 Google Search Central, Карты сайта для изображений: карты сайта для изображений информируют Google об изображениях на сайте, включая найденные через JavaScript, и принимают до 1000 изображений на URL страницы.

7 Политика Google в отношении спама в поиске — злоупотребление масштабированным контентом: генерация множества страниц прежде всего для манипуляции ранжированием и предоставление читателям малой ценности, независимо от того, создан ли контент с помощью автоматизации, ручного труда или их сочетания.

Часто задаваемые вопросы

Как автоматически иллюстрировать статьи, написанные LLM?
Добавьте один вызов модели между написанием и публикацией: попросите её вернуть визуальный план — по одной записи на каждый слот изображения, с пометкой «фото», «график», «диаграмма» или «ничего». Записи с фото содержат описание сцены на 12–25 слов, которую мог бы снять фотоаппарат; это описание отправляется в API семантического поиска изображений (GET /api/v1/search/photos, примерно 150 мс). Записи с графиками и диаграммами передаются модели, которая пишет код построения графиков или Mermaid-разметку, рендерящуюся детерминированно. Никогда не подавайте заголовок статьи в поиск изображений: заголовки абстрактны, и ни одна фотография им не соответствует.
Должен ли сайт с документацией использовать скриншоты или стоковые фото?
Переверните настройки роутера по умолчанию: в документации к продукту честной иллюстрацией почти всегда является ваш собственный интерфейс. Скрипт на Playwright, который открывает реальную сборку, фиксирует область просмотра и захватывает именно то состояние, которое описывает абзац, запускается в той же CI-задаче, что и сборка документации, поэтому скриншот никогда не сможет показать версию интерфейса, которой уже не существует. Диаграммы несут смысловую нагрузку страниц архитектуры, а фотографии появляются только на концептуальных и посадочных страницах, где скриншот не сказал бы ничего.
Как не допустить, чтобы ИИ-пайплайн вставлял неверные числа в график?
Заставьте план не изобретать, а переписывать: роутеру разрешено копировать в поле data записи графика только те значения, которые уже присутствуют в черновике. Затем проверяйте это в коде перед рендерингом — для каждого значения убеждайтесь, что его строковое представление встречается в статье, иначе выбрасывайте исключение. Девять строк кода — и график, противоречащий собственному абзацу, никогда не дойдёт до читателя.
Какую разметку изображений должен генерировать автоматизированный пайплайн для SEO?
Три вещи, и все они берутся из полей, которые уже есть в ответе поиска. Описательный alt, написанный с учётом контекста абзаца — Google называет alt-текст самым важным метаданным изображения и предостерегает от переспама ключевыми словами. width и height на каждом изображении, с fetchpriority="high" на hero-изображении и без loading="lazy" на нём, потому что LCP-изображение не должно загружаться лениво. И структурированные данные ImageObject с contentUrl, creator, creditText и license — именно это делает изображение подходящим для значка Licensable в Google Images.
Помогает ли иллюстрирование ИИ-статей их ранжированию?
Само по себе — нет, и здесь важна точность. Политики Google в отношении спама определяют масштабное злоупотребление контентом как создание большого количества страниц в первую очередь для манипуляции ранжированием с минимальной пользой для пользователей, независимо от того, задействована ли автоматизация; иллюстрации не меняют эту оценку. Хороший пайплайн даёт «пол качества» для страниц, которые и так заслуживают существования: проверяемое происхождение изображений, доступный alt-текст, показатели Core Web Vitals, выдерживающие автоматизацию, а также графики и скриншоты, которые не могут разойтись с истиной.
Как запустить этап иллюстрирования в CI, не теряя редакторский контроль?
Запускайте задачу при pull request, затрагивающем ваши файлы контента, позвольте ей сгенерировать изображения и front-matter, и пусть она открывает pull request, а не коммитит напрямую в ветку — конвейер предлагает, человек одобряет, и каждое написанное им поле можно сравнить построчно (diff). Оставьте три вещи детерминированными в коде, а не в модели: дедупликацию по photo_id, проверку того, что каждое число на графике присутствует в статье, и жёсткий отказ, если ни одно фото не преодолевает порог оценки. Лучше отсутствие главного изображения, чем неверное.

Хватит подбирать ключевые слова. Опишите, что вы имеете в виду.

Ищите среди 9M+ бесплатных изображений по смыслу — на любом языке, менее чем за 100 мс.