Проблема
Снимок экрана в контексте весит 1050–1840 токенов против 450–500 у своего описания — это замер на моих снимках; предел тарифа высокого разрешения (2576 точек по длинной стороне) — до 4784 токенов на картинку, так что на крупных снимках разница больше. Но главное не разовая цена: вложение остаётся в истории разговора и отправляется модели заново в каждом следующем запросе сессии. Десяток снимков за долгую беседу — и заметная часть окна занята картинками, которые давно разобраны.
Штатных рычагов нет:
- данные вставленного изображения не отдаются ни хукам, ни расширениям — anthropics/claude-code#16592, открыт;
- отключить обработку вставки нельзя — #43194, открыт.
Почему не получилось хуком
Очевидный план: хук UserPromptSubmit ловит сообщение, находит вложение в транскрипте, распознаёт, возвращает decision: "block" и подкладывает описание через additionalContext. Логика была написана и на подготовленной копии транскрипта работает. В бою не срабатывает никогда.
Причина: запись сообщения пользователя ложится в транскрипт позже, чем отрабатывают хуки. Метка времени внутри записи ставится по моменту отправки и потому выглядит более ранней, чем время работы хука — это и вводит в заблуждение.
Замеры на Claude Code 2.1.220:
| Прогон | Что видел хук |
|---|---|
| 16:40 | запись с картинкой имеет метку 13:40:20.878Z; хук ждал её с 13:40:21.2 до 13:40:23.97 — 2,80 с опроса каждые 50 мс. Результат: не найдена |
| 16:42 | хук закончил ожидание в 16:42:22.56 и снял состояние файла: 204 записи, последняя пользовательская — 13:41:44.480Z, блоки ['tool_result']. Записи с картинкой нет. Постфактум она нашлась с меткой 13:42:19.461Z — то есть легла на диск после 16:42:22,5 |
Ждать дольше бессмысленно: запись произойдёт только после того, как хук вернёт управление. Это не гонка, которую можно выиграть таймаутом, а порядок операций.
Где живёт вставленная картинка
Разобрано по коду расширения .vscode/extensions/anthropic.claude-code-2.1.220-win32-x64/webview/index.js:
- Буфер обмена Windows — до нажатия
Ctrl+V. Единственное звено, доступное снаружи. Ctrl+V→ окно чата.FileReader.readAsDataURL(file)превращает картинку в строкуdata:image/png;base64,..., она складывается в массив вложений в состоянии поля ввода. На диск не пишется ничего — проверено сканированием%TEMP%,~/.claudeи хранилищ VS Code за период вставок.- Отправка. Строка режется по запятой, собирается блок
{type: "image", source: ...}. - Транскрипт
.jsonl— пишется последним, уже после хуков.
Вывод: вмешаться после Ctrl+V невозможно, значит, надо вмешаться до.
Решение
- Низкоуровневый перехватчик клавиатуры видит
Ctrl+Vво всей системе. - Три быстрые проверки: активное окно принадлежит нужной программе, в буфере есть картинка (
CF_DIB,CF_DIBV5илиCF_BITMAP), не зажат Shift. - Совпало — нажатие поглощается, картинка сохраняется файлом, показывается окно-индикатор со счётчиком секунд, идёт распознавание.
- В буфер кладётся текст (описание плюс путь к оригиналу), служба сама посылает окну
Ctrl+V.
Ctrl+Shift+V не перехватывается — путь «вставить как есть», когда изображение действительно нужно показать модели целиком.
Тонкости перехвата
Обработчик обязан отвечать за миллисекунды. Windows отключает низкоуровневый перехватчик клавиатуры, который думает дольше примерно 300 мс, а распознавание идёт секунды. В самом обработчике — только проверки, работа уходит в отдельный поток через канал:
if is_target && clipboard_has_image() {
if BUSY.load(Ordering::SeqCst) { return LRESULT(1); }
BUSY.store(true, Ordering::SeqCst);
if let Some(tx) = TX.get() { let _ = tx.send(()); }
return LRESULT(1); // нажатие поглощено
}
Не поймать собственную вставку. Служба сама посылает Ctrl+V, и её же перехватчик это видит. Спасает флаг LLKHF_INJECTED: у программно порождённых событий он взведён, такие нажатия пропускаются дальше.
Флаг занятости, а не очередь. Пока идёт распознавание, пользователь жмёт Ctrl+V ещё раз. Отложенные нажатия сработали бы после успеха, когда в буфере уже лежит текст, и вставили бы его повторно. Поэтому лишние нажатия поглощаются.
Дождаться отпускания Ctrl. Свою вставку нельзя посылать, пока клавиша зажата физически: она смешается с посланной и выйдет другое сочетание.
Любая ошибка = поведение как раньше. Буфер не трогается до успеха. Модель недоступна, вышло время, ответ пустой — в буфере всё ещё исходная картинка, служба просто посылает Ctrl+V. Сломанная служба не должна мешать работать.
Задание модели пришлось переписывать дважды
Первая версия не замечала пометок, дорисованных поверх снимка, — а ведь обычно именно они и есть причина, по которой снимок сделан. Вторая ударилась в другую крайность: за пометки принималось оформление интерфейса — подсветка кода, заливка под текстом, рамки таблиц.
Рабочая формулировка задаёт границу явно: пометка — только дорисованное вручную (рамка, обводка, стрелка, подчёркивание, зачёркивание, мазок маркером); собственные элементы программы пометками не считаются; ничего не дорисовано — писать строго «пометок нет». Ответ разбит на три части: пометки, дословный текст, одна-две фразы про обстановку. Отдельно оговорено: длинные наборы букв и цифр переписывать посимвольно, при неуверенности ставить ? вместо того чтобы придумать.
Кто распознаёт — замеры
Все распознаватели проверены на одном снимке (1399×564, окно с текстом и таблицей) с одинаковым заданием. Считалось посимвольное совпадение пяти трудных строк:
b04o4i5o0 · 3e0d4a2d-7b81-4a9f-ac1e-73ac282e7d60 · c--CraftSkill · 14.12s · b8lolxzl0
Последняя — мелкий шрифт внутри снятой таблицы, «снимок снимка». Плюс три русские строки на проверку кириллицы.
| Модель зрения | Время | Точность | Пометки |
|---|---|---|---|
| Qwen3-VL-30B-A3B локально, RTX 3090 | 4,5–8 с | 4/5 | описывает |
| Qwen3-VL-32B локально, плотная | 24,3 с | 4/5 | описывает |
Haiku 4.5 через claude -p |
20,7–32 с | 3/5 | описывает |
Codex gpt-5.6-luna, подписка ChatGPT |
32 с | 4/5 | описывает |
bytedance-seed/seed-2.0-mini, OpenRouter |
12–18 с | 5/5 (3 прогона из 3) | описывает |
qwen/qwen3-vl-32b-instruct, OpenRouter |
7,8 с | 1/5 | — |
qwen/qwen3.7-flash, OpenRouter |
105 с | 3/5 | — |
meta-llama/llama-4-scout, OpenRouter |
6–40 с | 0/5, 0/5, 3/5 — неустойчива | — |
Снимок через seed-2.0-mini стоит около $0,0008.
| Распознаватель текста | Русский | Латинские идентификаторы | Время |
|---|---|---|---|
| RapidOCR, штатная китайско-английская модель | не читает | 3/5 — берёт мелкую строку | 4,5 с |
RapidOCR + кириллическая PP-OCRv5 |
отлично | 2/5 — портит латиницу | 4,2 с |
| EasyOCR (ru+en, torch) | отлично | 0/5 | 1,2 с на RTX 2060 |
| PaddleOCR-VL-1.6 (0,9B, 1,8 ГБ) | отлично | 4/5 | 1,4–6,7 с |
Характерная порча у кириллической модели: c--CraftSkill → c--Craftski11, 3e0d4a20 → Зе0d4а20. Русский она читает отлично ровно до тех пор, пока в строке не появляется латиница.
Выбрана локальная Qwen3-VL-30B-A3B: 4,5–8 с, 4/5, описывает и пометки, и смысл, не расходует лимиты подписок. Для мелкого шрифта в конфиге есть режим openrouter с seed-2.0-mini, по умолчанию выключен.
Обязательное условие для семейства Qwen-VL — запуск с --image-min-tokens 2048. Без него модель уверенно врёт в длинных наборах букв и цифр: путает o и 0, l и 1, а обрезанные краем снимка пути достраивает выдуманными.
Сколько места это освобождает
Освобождается разница «картинка минус описание»: 575 токенов на обычном снимке, 1365 на верхней границе замера, до 4309 на полноразмерном снимке в тарифе высокого разрешения.
| Снимков за сессию | Обычный снимок | Верхняя граница | Полное разрешение |
|---|---|---|---|
| 5 | 2,9 тыс. токенов | 6,8 тыс. | 21,5 тыс. |
| 10 | 5,8 тыс. | 13,7 тыс. | 43 тыс. |
| 20 | 11,5 тыс. | 27,3 тыс. | 86 тыс. |
| 30 | 17,3 тыс. | 41 тыс. | 129 тыс. |
Если считать рабочее окно в 200 тысяч токенов, картина такая:
- Обычная работа, пять-десять снимков за сессию — освобождается 3–7% окна. Заметно, но ничего не решает: момент уплотнения контекста отодвигается ровно на эти проценты.
- Отладка интерфейса или разбор чужих экранов, двадцать-тридцать снимков — от 14% до двух третей окна. Здесь это уже разница между «дошли до конца задачи» и «контекст уплотнился на середине».
То есть выигрыш не постоянный, а режимный: на снимкоёмких сессиях он решающий, на обычных — приятный, но не более.
Отдельно про деньги, чтобы закрыть вопрос об облачном режиме. Повторное чтение истории идёт по цене чтения из кэша — десятая часть входной цены. Десять снимков на двухстах запросах — это от 1,1 до 2,7 миллиона лишних токенов чтения, порядка одного-двух долларов за сессию. Распознавание тех же десяти снимков через seed-2.0-mini стоит $0,008. Облачный режим окупается с запасом в сотни раз, так что его цена аргументом против не является.
Что проверено и отвергнуто
Двойной проход: OCR даёт точный текст, модель зрения — смысл. Ни один распознаватель текста не даёт одновременно русский и латинские идентификаторы: силён в одном — слаб в другом. Выигрыш — одна строка из пяти, цена — зависимость от Python в службе на Rust и склейка двух ответов.
PaddleOCR-VL вместо модели зрения. Текст читает не хуже (4/5, отличный русский) при 1,8 ГБ против 20. Но обучена только выписывать содержимое документа: на составное задание из трёх частей вернула пустой ответ, пометки описывать не умеет.
Плотная 32B точнее разреженной 30B. Та же точность при впятеро большем времени: 24,3 с против 4,5–8. Зрительная часть у них общая, языковая на чтение букв не влияет.
Больше токенов на изображение — точнее. Только до 2048. Значение 4096 вдвое дольше и точности не добавляет.
Облако точнее локальной модели. Не подтвердилось ни по одному показателю, кроме единственной мелкой строки.
Экономия контекста огромна и постоянна. Не постоянна. На снимок это два-четыре раза, а в долях окна выигрыш зависит от режима работы — от трёх процентов до двух третей, разбивка выше.
Ограничения
- Только Windows: перехват построен на низкоуровневом хуке клавиатуры и буфере обмена Windows.
- Перетаскивание картинки мышью и вставка через меню не перехватываются.
- Текст мельче примерно 12 точек внутри снятого скриншота путается в похожих знаках у всех проверенных локальных моделей.
Грабли
pkill -f <шаблон>на ВМ убивает собственную SSH-сессию — шаблон попадает в командную строку оболочки, которая эту команду и выполняет. Безопасно:pgrep -x <имя>плюс проверка/proc/PID/cmdline.rapidocr_onnxruntime1.4.4 принимает толькоrec_model_path=/rec_keys_path=; точечная записьRec.model_pathмолча игнорируется, распознавание идёт штатной моделью — заметно лишь по содержимому ответа.- Модели PaddleX с HuggingFace не содержат встроенного словаря символов, его надо доставать из
inference.ymlи передавать отдельным файлом. - Задача планировщика под учётной записью пользователя показывает консольное окно;
-WindowStyle Hiddenне помогает. Работает обёрткаwscript.exe+.vbs, запускающий.bat. settings.jsonв Claude Code перечитывается без перезапуска сессии — по крайней мере секция хуков в 2.1.220: правка в 16:11 подхватилась той же сессией к 16:20.- Хук, вызывающий
claude -p, рекурсивно запускает сам себя в порождённой сессии. sys.stdin.read()на Windows портит русский текст, читать надоsys.stdin.buffer.read().decode("utf-8").- Модель зрения без модуля
mmprojне работает: llama.cpp ответит «image input is not supported».
Установка
git clone https://github.com/Regsorm/clip-image-describe
cd clip-image-describe
cargo build --release
copy target\release\clip-image-describe.exe .
copy config.example.toml config.toml
В config.toml указывается как минимум адрес своей модели зрения. Проверка с разбором каждого нажатия в консоли — переменная окружения CLIP_DEBUG=1. Постоянная работа — задачей планировщика при входе в систему через wscript + .vbs, команда регистрации есть в README.
Годится любой llama.cpp с моделью зрения, загруженной вместе с модулем mmproj.
Исходники: https://github.com/Regsorm/clip-image-describe (MIT).
Вступайте в нашу телеграмм-группу Инфостарт