# Ocr Images

> ---

- Skill: `carlsonchik/ocr-images` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add carlsonchik/ocr-images`
- Raw SKILL.md: https://api.skillmd.com/api/skills/carlsonchik/ocr-images/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: carlsonchik (https://skillmd.com/u/carlsonchik)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/carlsonchik/ocr-images

---

---
name: ocr-images
description: Распознавание текста с изображений, скриншотов и сканов, включая текст внутри docx/pdf. Загружать, когда нужно вытащить текст с картинок, когда документ состоит из изображений, при работе с выгрузками-сканами или когда пользователь спрашивает про OCR.
---

# OCR: как читать текст с картинок

Проверено 15.09.2026 на 51 скриншоте (2340×1080, тёмная тема, окна PowerPoint).

## Три движка, у каждого своя роль

| Движок | Когда | Скорость | Качество |
|---|---|---|---|
| **macOS Vision** (Swift) | основной проход по печатному тексту | 1.8 с на кадр | хорошее, но рвёт строки и врёт на стилизованном тексте |
| **vision-модель** (`deepseek/deepseek-v4-flash-vision-exp`) | сложные кадры, окна программ, рукописное/стилизованное | ~10–30 с на кадр через субагента | заметно полнее: на тесте **+27% текста** |
| **tesseract 5.5.3** | почти никогда | 0.2 с | на тёмных скриншотах вернул **0 строк** |

Вывод: tesseract на скриншотах бесполезен, нативный Vision — рабочая лошадка, vision-модель —
второе мнение и спасение на сложных кадрах.

## Нативный Vision: собрать бинарь один раз

`swift script.swift` компилирует при каждом запуске (в тесте это давало 52 с вместо 1.8 с) — поэтому
собрать бинарник и вызывать его. Скрипт лежит в `scripts/ocr.swift`, сборка:

```bash
swiftc -O scripts/ocr.swift -o /tmp/ocrx
/tmp/ocrx image.jpeg "ru-RU,en-US" 1 2.0
# аргументы: файл, языки, языковая коррекция (0/1), масштаб
```

Полезные детали скрипта: сортировка результатов по координатам (иначе строки идут вразнобой),
вывод `уверенность<TAB>текст`, поддержка увеличения изображения вдвое (на мелком тексте помогает:
«красмаая» → «красивая»).

## Проверка второго проходом

Не доверять одному проходу: прогнать кадр в двух-трёх вариантах (с коррекцией / без / увеличенным)
и сравнить строки. В тесте это дало 51 исправленную строку и 7 строк, которые первый проход не увидел
вовсе («неудовлстворенных импульсов … плохого опоа» → «неудовлетворенных … плохого опыта»).
Сливались варианты по собственной уверенности модели.

## Vision-модель как второй движок

Запускать субагентом: `model: "deepseek/deepseek-v4-flash-vision-exp"`, промпт просит прочитать файл
по пути и вернуть текст дословно, сохраняя переносы, с `[?]` на неразборчивом. Работает и пачкой:
в промпт давать список «кадр N: путь», модель возвращает блоки `### кадр N`.

Модель сама отделяет интерфейс программы от содержимого и честно помечает неуверенное — это удобно,
но **её вывод надо считать вторым мнением, а не истиной**: она может «поправить» стиль и пунктуацию.
Проверять по расхождениям с OCR.

## Чистка служебного — отдельный обязательный этап

После распознавания всегда идёт чистка обвязки, **единая для обоих движков**. Нельзя полагаться на то,
что vision-модель сама пометит интерфейс: она помечает не всегда, и в тесте после неё осталось 94
служебные строки в 14 кадрах. Что убирать:

1. **Строки из логов субагентов** (`subagent_type=...`) — если собираешь расшифровки из логов задач,
   эта метка легко попадает в текст (в тесте — семь раз).
2. **Примечания модели в скобках** («поверх слайда водяной знак», «в правом верхнем углу»).
3. **Панель миниатюр**: перечни слайдов вида «36 Первичные отношения 37 Борец и Герой…» — это дублирование.
4. **Лента и статусная строка** PowerPoint: «Вставить | Вырезать | Копировать…», «Слайд N из M», «88%».
5. **Заголовок окна** («…[Автосохраненный] - PowerPoint»).

Правило «доля интерфейсных слов ≥ 60%» плюс словарь из ~60 слов ленты работает надёжно.

## Грабли фильтров: каждая из них съела настоящий текст

- **`len(строка) <= 6` → мусор** выбросило слайд с одним словом «Либидо».
  Условие должно быть «короткая И без единой кириллической буквы».
- **`^(С|С текущего)\s`** (задумано для «С начала / С текущего слайда») совпало со строкой стихотворения
  «С тех пор все тянутся предо мною». Правило с якорем на одну букву — всегда опасность; писать полные
  строки интерфейса.
- **«Повторяется в ≥3 кадрах → колонтитул»** выбросило заголовок слайда «Развитие». Повторяться может
  именно заголовок. Такие строки — только в журнал кандидатов, из текста не удалять.
- **Римские цифры I/II** с диаграмм не считать мусором: исключение `^[IVX]+$`.

Вывод: любое правило фильтра проверять на настоящем тексте, и держать **журнал удалённого** — файл, где
каждая убранная строка перечислена с причиной. Тогда потеря видна сразу, а не через неделю.

## Проверка эталоном

Если пользователь вычитал часть текста руками — это идеальный тест: сравнить свой автоматический
результат с его файлом **по словам** (difflib по списку слов, а не по строкам: разбивка на строки у всех
разная). Совпадение 93% при ручной вычитке — хороший показатель; расхождения в остатке обычно артефакты
выравнивания, но каждое надо посмотреть.

## Грабли

- **docx может не содержать текста вообще.** Проверять сразу: `unzip -p file.docx word/document.xml | grep -c "<w:t"`,
  и смотреть `word/media/`. В разобранном файле было 51 изображение и ноль `<w:t>`.
- **Порядок картинок** брать из `document.xml` по `r:embed` + `word/_rels/document.xml.rels`, а не по именам файлов.
- **Не выбрасывать короткие строки по длине**: фильтр «меньше 25 символов — мусор» выбросил «Либидо»,
  подписи диаграмм и части фраз. Надёжное правило: выбрасывать только то, где нет ни одной кириллической
  буквы, и явные надписи интерфейса. Всё отброшенное — писать в отдельный файл «что не вошло».
- **Латинские двойники**: OCR путает `от`/`OT`, `по`/`ПO`. Чинить переводом знаков-двойников, если в
  слове уже есть кириллица, или если всё слово состоит только из двойников.
- **Служебные слова капсом** посреди фразы (`ПО какой-либо причине`) — приводить к нижнему регистру
  после склейки абзацев, а не построчно (в разбитых строках слово стоит первым и правило не срабатывает).

