Как нейросеть ищет баги на скриншотах: анализ UI для QA

Playwright снимает экран, модель с поддержкой изображений возвращает JSON с опечатками, наложениями и пропавшими элементами. Код на Python, промпт и честный расчёт стоимости.

4 мин · · разработка · автоматизация

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

Какие модели умеют смотреть на картинки

Изображения принимают модели Claude и GPT. GLM 5.3 и GLM 5.3 Flash работают только с текстом — скриншот им отправлять бессмысленно.

МодельВход / выход, $ за 1M токеновФормат
claude-sonnet-50.3 / 0.3Anthropic
claude-opus-5-50.3 / 0.3Anthropic
gpt-5.6-sol0.3 / 0.3OpenAI
gpt-5.6-luna0.1 / 0.1OpenAI

Набор моделей меняется, актуальный список — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Какая модель лучше находит баги именно в вашем интерфейсе, заранее сказать нельзя: прогоните одни и те же 10–20 скриншотов с заранее известными дефектами через две-три модели и сравните, сколько дефектов каждая нашла и сколько ложных срабатываний выдала.

Что модель замечает, а что нет

Нейросеть хорошо подходит как дополнительный слой проверки, но не заменяет попиксельное сравнение (visual regression) и ручное тестирование. Разумно поручать ей:

  • опечатки и несогласованный текст в кнопках, заголовках, подсказках;
  • явно перекрывающиеся или обрезанные элементы;
  • пропавшие элементы, которые вы перечислили в промпте («на экране входа должны быть поле email, поле пароля и кнопка "Войти"»);
  • плохо читаемый текст: слишком светлый на светлом фоне, слишком мелкий;
  • несоответствие текста состоянию экрана («Загрузка…» при уже показанных данных).

Плохо работает всё, что требует точных измерений: «отступ 15 px вместо 16», «оттенок не совпадает с брендбуком на 3%». Для этого есть инструменты сравнения изображений.

Отправить скриншот через Anthropic SDK

Скриншот кодируется в base64 и передаётся блоком image. Для Anthropic SDK базовый адрес указывается без /v1 — SDK добавляет его сам.

bash
import base64
from anthropic import Anthropic

client = Anthropic(base_url="https://api.zukko.pro", api_key="fetch_ваш_ключ")

with open("screenshot.png", "rb") as f:
    image_data = base64.standard_b64encode(f.read()).decode("utf-8")

message = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=2048,
    messages=[{
        "role": "user",
        "content": [
            {"type": "image", "source": {
                "type": "base64", "media_type": "image/png", "data": image_data}},
            {"type": "text", "text": "Найди на скриншоте баги интерфейса. Ответь JSON."},
        ],
    }],
)
print(message.content[0].text)
print(message.usage.input_tokens, message.usage.output_tokens)

По документации Anthropic поддерживаются форматы PNG, JPEG, GIF и WebP, максимальный размер — 8000×8000 пикселей. Крупные изображения перед обработкой уменьшаются по длинной стороне, поэтому мелкий текст на огромном full-page скриншоте может стать нечитаемым.

Тот же запрос в формате OpenAI

Если в проекте уже используется OpenAI SDK, картинку передают как data URL в блоке image_url. Базовый адрес — с /v1:

bash
import base64
from openai import OpenAI

client = OpenAI(base_url="https://api.zukko.pro/v1", api_key="fetch_ваш_ключ")

with open("screenshot.png", "rb") as f:
    b64 = base64.b64encode(f.read()).decode("utf-8")

resp = client.chat.completions.create(
    model="gpt-5.6-sol",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "Найди на скриншоте баги интерфейса. Ответь JSON."},
            {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
        ],
    }],
)
print(resp.choices[0].message.content)

Шлюз умеет конвертировать форматы, но перед запуском на сотнях экранов проверьте выбранную связку модель + формат на одном скриншоте.

Шаблон промпта для QA

Просьба «найди баги» даёт расплывчатый ответ. Перечислите критерии и ожидаемые элементы экрана, а формат ответа задайте жёстко:

bash
QA_PROMPT = """Ты проверяешь скриншот веб-приложения перед релизом.
Экран: {screen}. На нём обязательно должны быть: {required}.

Ищи только реальные проблемы:
1. Опечатки и ошибки в тексте интерфейса.
2. Перекрывающиеся, обрезанные или вылезающие за край элементы.
3. Отсутствие обязательных элементов из списка выше.
4. Плохо читаемый текст (низкий контраст, слишком мелкий шрифт).
5. Текст, который противоречит состоянию экрана.

Если сомневаешься — не включай пункт. Ответь только JSON без пояснений:
{{"bugs": [{{"type": "typo|overlap|missing|contrast|state",
            "location": "где на экране",
            "description": "что не так",
            "severity": "critical|minor"}}],
  "status": "ok|warn|critical"}}"""

Двойные фигурные скобки нужны, потому что строка заполняется через .format(). Модель иногда оборачивает JSON в блок кода, поэтому разбирайте ответ терпимо:

bash
import json
import re

def parse_json(text: str):
    match = re.search(r"\{.*\}", text, re.S)
    if not match:
        return None
    try:
        return json.loads(match.group(0))
    except json.JSONDecodeError:
        return None

Интеграция с Playwright

Скрипт обходит несколько страниц, снимает каждую и складывает находки в отчёт. Используется асинхронный клиент, чтобы не блокировать цикл событий Playwright.

bash
import asyncio
import base64
import json
from anthropic import AsyncAnthropic
from playwright.async_api import async_playwright

SCREENS = [
    ("https://example.com/login", "вход", "поле email, поле пароля, кнопка «Войти»"),
    ("https://example.com/settings", "настройки", "кнопка «Сохранить»"),
]

async def main():
    client = AsyncAnthropic(base_url="https://api.zukko.pro", api_key="fetch_ваш_ключ")
    report = []

Для мобильной вёрстки задайте узкий viewport (viewport={"width": 375, "height": 812}) или профиль устройства из p.devices. Длинные страницы лучше снимать несколькими экранами, а не одним full_page=True: так текст не потеряет читаемость после уменьшения.

Сколько стоит анализ скриншотов нейросетью

Число токенов на картинку зависит от её размера и модели, поэтому не стоит опираться на чужие оценки — измерьте на своих экранах. В ответе API есть usage.input_tokens (картинка + промпт) и usage.output_tokens. Для Anthropic-формата можно заранее вызвать POST /v1/messages/count_tokens с тем же содержимым.

Формула для claude-sonnet-5:

стоимость = (input_tokens × 0.3 + output_tokens × 0.3) / 1 000 000

Пример: если на вашем экране вышло 2 000 входных и 400 выходных токенов, то (2 000 × 0.3 + 400 × 0.3) / 1 000 000 = $0.00072 за скриншот, а прогон 300 экранов — 300 × 0.00072 = $0.216. Для gpt-5.6-luna ставьте в формулу 0.1 вместо 0.3. Скрипт выше уже сохраняет токены в отчёт, так что расход видно по каждому экрану.

Чтобы отдельный ключ для QA не съел бюджет, задайте ему дневной лимит и список разрешённых моделей в кабинете — подробнее в разделе лимиты ключей. Создать ключ можно на странице API-ключи, а параметры запроса с картинками описаны в справке по Messages API.

Частые вопросы

Можно ли отправлять скриншоты в GLM, чтобы сэкономить?

Нет. GLM 5.3 и GLM 5.3 Flash не принимают изображения. Самый дешёвый вариант с поддержкой картинок — gpt-5.6-luna за $0.1 за 1M токенов; проверьте на своих экранах, хватает ли её для ваших задач.

Что делать с конфиденциальными данными на скриншотах?

Запускайте проверку на тестовых данных и тестовых аккаунтах. Если реальные данные неизбежны, замаскируйте поля перед снимком: у page.screenshot() в Playwright есть параметр mask со списком локаторов.

Почему модель находит «баги», которых нет?

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

Отправлять страницу целиком или частями?

Обычный экран 1280×800 — целиком. Длинную страницу — несколькими снимками по высоте viewport, иначе после уменьшения мелкий текст может стать нечитаемым.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.