# Интеграция OCR API

API для пакетного распознавания изображений и PDF-документов. Распознавание
выполняется моделью через chat-completions с tool-calling: в запрос уходит
изображение (base64) и JSON Schema ответа — модель возвращает данные строго
по этой схеме. PDF-документы разбиваются на страницы (изображения), каждая
страница распознаётся отдельным запросом.

**Оба endpoint принимают массив источников и всегда возвращают массив
результатов** (по элементу на каждый входной источник) — можно обрабатывать
пачками.

## Общие сведения

- **Base URL:** `{{BASE_URL}}` (например `https://ai.wormsoft.ru`)
- **Аутентификация:** заголовок `Authorization: Bearer <API_KEY>`

- **Контент:** `Content-Type: application/json`
- **Тарификация:** по токенам (input/output) всех страниц всех документов,
  списывается как операция `ocr` в кредитах. Итоговая стоимость возвращается
  в `usage.credits`. Если кредитов не хватает — часть уходит в кредитный долг,
  ошибка списания НЕ блокирует ответ.

### Формат входных данных

Каждый элемент массива `sources` (или `documents`) — объект:

| Поле      | Формат                                                              |
|-----------|---------------------------------------------------------------------|
| `image`   | base64-строка (с опциональным префиксом `data:image/...;base64,`)   |
| `pdfFile` | base64-строка PDF-файла                                             |

Ровно **один** из `image` / `pdfFile` должен быть передан в каждом элементе.
---
## Распознавание через MCP

OCR-инструмент доступен через **MCP** (`POST /api/mcp`, метод `tools/call`).
Инструмент называется **`recognize_image_content`** — тот же распознаватель,
что и здесь, но вызывается как MCP-инструмент.

### Параметры

```json
{
  "sources": [
    { "image": "data:image/png;base64,..." },
    { "pdfFile": "<base64 PDF>" }
  ],
  "prompt": "Распознай текст на изображении",
  "responseSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "text": { "type": "string", "description": "Распознанный текст" }
    },
    "required": ["text"]
  },
  "model": "wormsoft/vision/medium"
}
```

- `sources` — массив источников (изображения и/или PDF); в каждом элементе
  ровно один из `image` / `pdfFile` (base64).
- `prompt` — уточняющая инструкция (добавляется в запрос к модели).
- `responseSchema` — JSON Schema ответа (передается в tool-calling модели).
- `model` — vision-модель провайдера с поддержкой tool-calling.

### Ответ

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"model\": \"wormsoft/vision/medium\",
         \"documents\": [...],
         \"usage\": { \"credits\": 100 }\n}"
      }
    ]
  }
}
```

`result.content[0].text` — JSON-объект распознавания (см. раздел «Общий формат
ответа» выше): `documents[]` (всегда массив, по элементу на каждый источник)
и итоговое `usage.credits`.

> Тарификация через MCP та же — списание по токенам как операция `ocr`.
> Доступно только на платных подписках.

### Лимиты

| Ограничение                              | Значение    |
|------------------------------------------|-------------|
| Максимум источников за один запрос       | 20          |
| Размер одного `image`/`pdfFile`          | 25 МБ       |
| Максимум страниц в одном PDF             | 50 страниц  |

### Обработчики

| Метод | URL               | Назначение                                                        |
|-------|-------------------|-------------------------------------------------------------------|
| POST  | `/api/ocr`        | Пакетное распознавание с собственной схемой и промптом          |
| POST  | `/api/ocr/document` | «Под ключ» распознавание текстовых документов (массив файлов)   |

Работают и алиасы с префиксом v1: `/api/ocr/v1` и `/api/ocr/v1/document`
(контроллер зарегистрирован на обоих префиксах).

### Общий формат ответа

```json
{
  "documents": [
    {
      "documentIndex": 0,
      "pageNumbers": [1, 2],
      "pages": [
        {
          "pageNumber": 1,
          "data": { ... },
          "usage": { "inputTokens": 1234, "outputTokens": 567, "credits": 100 }
        }
      ]
    }
  ],
  "usage": { "inputTokens": 3468, "outputTokens": 1334, "credits": 350 }
}
```

- `documents` — **всегда массив**, по элементу на каждый входной источник
  (для изображения — одна страница, `pageNumbers: [1]`; для PDF — по одной
  записи на страницу).
- `data` — объект, возвращённый моделью (соответствует `responseSchema`).
- `usage` — токены и стоимость: на страницу, на документ (сумма страниц)
  и итог по операции.

---

## Кейс 1. Фиксированное распознавание текста PDF/JPG-документа (пачка)

**`POST /api/ocr/document`** — метод «под ключ»: промпт, response schema
(`{ "text": string }`) и модель зашиты в сервисе. На вход — массив документов
(PDF и/или изображения), на выходе — массив распознаваний по страницам
для каждого документа.

### Запрос

```json
{
  "documents": [
    { "pdfFile": "<base64 PDF>" },
    { "image": "data:image/png;base64,<base64 JPG/PNG>" },
    { "image": "<base64>" }
  ]
}
```

Каждый элемент массива содержит **либо** `pdfFile`, **либо** `image`.
Количество элементов ограничено лимитом источников на запрос (20).
Порядок документов сохраняется в ответе.

### Ответ

```json
{
  "documents": [
    {
      "documentIndex": 0,
      "pageNumbers": [1, 2],
      "pages": [
        { "pageNumber": 1, "data": { "text": "Текст страницы 1..." },
          "usage": { "inputTokens": 1234, "outputTokens": 567, "credits": 100 } },
        { "pageNumber": 2, "data": { "text": "Текст страницы 2..." },
          "usage": { "inputTokens": 1234, "outputTokens": 567, "credits": 100 } }
      ]
    },
    {
      "documentIndex": 1,
      "pageNumbers": [1],
      "pages": [
        { "pageNumber": 1, "data": { "text": "..." },
          "usage": { "inputTokens": 1000, "outputTokens": 200, "credits": 50 } }
      ]
    }
  ],
  "usage": { "inputTokens": 3468, "outputTokens": 1334, "credits": 350 }
}
```

### Пример (JS)

```js
const fs = require('fs');
const path = require('path');

const BASE_URL = 'https://ai.wormsoft.ru'; // вставь свой URL
const TOKEN = 'PASTE_TOKEN_HERE'; // вставь свой API-ключ

async function parseTextDocument() {
  const pdfBase64 = fs.readFileSync(path.join(__dirname, 'document.pdf')).toString('base64');

  const body = {
    documents: [
      { pdfFile: pdfBase64 },
      { image: 'data:image/jpeg;base64,...' }, // можно смешивать и передавать пачкой
    ],
  };

  const response = await fetch(`${BASE_URL}/api/ocr/document`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${TOKEN}`,
    },
    body: JSON.stringify(body),
  });

  const json = await response.json();

  // Текст страниц: documents[i].pages[j].data.text
  for (const doc of json.documents) {
    for (const page of doc.pages) {
      console.log(`doc ${doc.documentIndex}, page ${page.pageNumber}: ${page.data.text.slice(0, 100)}`);
    }
  }
  console.log('Total credits:', json.usage.credits);
}
```

---

## Кейс 2. Пакетное распознавание с промптом и своей схемой

**`POST /api/ocr`** — агент сам задаёт `prompt` (уточнение задачи),
`responseSchema` (JSON Schema результата — передаётся напрямую в
tool-calling) и `model`. На вход — **массив** `sources` (изображения и/или
PDF), ответ — **массив** `documents`.

### Запрос

```json
{
  "sources": [
    { "image": "<base64, с опциональным data-url префиксом>" },
    { "pdfFile": "<base64 PDF>" }
  ],
  "prompt": "Дополнительная инструкция к распознаванию",
  "responseSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "text": { "type": "string", "description": "Распознанный текст" }
    },
    "required": ["text"]
  },
  "model": "wormsoft/vision/medium"
}
```

- `sources` — массив, 1..20 элементов; один и тот же `prompt`/
  `responseSchema` применяются ко всем источникам.
- `prompt` добавляется в общий запрос к модели (уточняет распознавание).
- `responseSchema` — произвольная JSON Schema; модель обязана вернуть объект
  ровно по ней (для каждой страницы PDF).
- `model` — любая vision-модель провайдера с поддержкой tool-calling.

### Ответ

```json
{
  "model": "wormsoft/vision/medium",
  "documents": [
    {
      "documentIndex": 0,
      "pageNumbers": [1],
      "pages": [
        {
          "pageNumber": 1,
          "data": { "text": "..." },
          "usage": { "inputTokens": 1234, "outputTokens": 567, "credits": 100 }
        }
      ]
    },
    {
      "documentIndex": 1,
      "pageNumbers": [1, 2],
      "pages": [
        { "pageNumber": 1, "data": { "text": "..." },
          "usage": { "inputTokens": 1234, "outputTokens": 567, "credits": 100 } },
        { "pageNumber": 2, "data": { "text": "..." },
          "usage": { "inputTokens": 1234, "outputTokens": 567, "credits": 100 } }
      ]
    }
  ],
  "usage": { "inputTokens": 4936, "outputTokens": 2268, "credits": 400 }
}
```

### Пример: простое распознавание текста пачкой (test-ocr-request.js)

```js
const fs = require('fs');
const path = require('path');

const BASE_URL = 'https://ai.wormsoft.ru'; // вставь свой URL
const TOKEN = 'PASTE_TOKEN_HERE'; // вставь свой API-ключ
const MODEL = 'wormsoft/vision/medium';

async function ocr() {
  const imageBase64 = fs.readFileSync(path.join(__dirname, 'image.png')).toString('base64');

  const body = {
    sources: [{ image: imageBase64 }],
    prompt: '',
    responseSchema: {
      type: 'object',
      additionalProperties: false,
      properties: {
        text: { type: 'string', description: 'Распознанный текст' },
      },
      required: ['text'],
    },
    model: MODEL,
  };

  const response = await fetch(`${BASE_URL}/api/ocr`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${TOKEN}`,
    },
    body: JSON.stringify(body),
  });

  const json = await response.json();

  // documents — всегда массив: по элементу на каждый входной источник
  for (const doc of json.documents) {
    for (const page of doc.pages) {
      console.log(`doc ${doc.documentIndex}, page ${page.pageNumber}:`, page.data);
    }
  }
  console.log('credits:', json.usage.credits);
}
```

### Пример: подсчёт людей на фото (test-photo-people-count.js)

```js
const body = {
  sources: [{ image: imageBase64 }],
  prompt:
    'Посчитай количество людей, видимых на фотографии. Считай только ' +
    'людей (без манекенов, портретов на одежде, изображений на экранах и т.п.).',
  responseSchema: {
    type: 'object',
    additionalProperties: false,
    properties: {
      peopleCount: { type: 'integer', description: 'Количество людей на фотографии' },
    },
    required: ['peopleCount'],
  },
  model: 'wormsoft/vision/medium',
};
// json.documents[0].pages[0].data → { peopleCount: 3 }
```

### Пример: структурированное распознавание (своя схема)

Схема под конкретный кейс агента, например — таблица:

```js
const body = {
  sources: [{ image: imageBase64 }],
  prompt: 'Это таблица: распознай её как табличные данные',
  responseSchema: {
    type: 'object',
    additionalProperties: false,
    properties: {
      headers: { type: 'array', items: { type: 'string' } },
      rows: { type: 'array', items: { type: 'array', items: { type: 'string' } } },
    },
    required: ['headers', 'rows'],
  },
  model: 'wormsoft/vision/medium',
};
// json.documents[0].pages[0].data → { headers: [...], rows: [[...], ...] }
```

---

## Кейс 3. Распознавание PDF с промптом и своей схемой

**`POST /api/ocr`** с `pdfFile` в `sources`. PDF разбивается на страницы,
каждая страница распознаётся отдельным tool-calling запросом —
**одна и та же** `responseSchema` применяется к каждой странице.

### Пример (test-document-pdf.js)

```js
const pdfBase64 = fs.readFileSync(path.join(__dirname, 'document.pdf')).toString('base64');

const body = {
  sources: [{ pdfFile: pdfBase64 }],
  prompt: '',
  responseSchema: {
    type: 'object',
    additionalProperties: false,
    properties: {
      text: { type: 'string', description: 'Распознанный текст страницы' },
    },
    required: ['text'],
  },
  model: 'wormsoft/vision/medium',
};

const json = await (
  await fetch(`${BASE_URL}/api/ocr`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${TOKEN}`,
    },
    body: JSON.stringify(body),
  })
).json();

// documents[0].pages — по одной записи на страницу:
const doc = json.documents[0];
for (const page of doc.pages) {
  console.log(`--- Страница ${page.pageNumber} ---`);
  console.log(page.data.text);
}
```

Если нужен общий структурированный результат по всему PDF (а не постранично),
собирайте поля из `documents[0].pages[i].data` на стороне агента.

---

## Сколько стоит один элемент

Фиксированной цены за «один элемент» нет: стоимость одного источника
(изображения или страницы PDF) считается от его реальных токенов:

```
стоимость = input_tokens × 1 / 1000 + output_tokens × 4000 / 1000
```

- Тариф: **1 кредит за 1K входных токенов** (1 000 000 за 1M) и
  **4000 кредитов за 1K выходных токенов** (4 000 000 за 1M).
- Входные токены страницы — это «визуальные» токены изображения + токены
  промпта/схемы. Размер и детализация изображения влияют на их количество.
- Выходные токены — размер JSON-ответа по вашей `responseSchema`. Основная
  часть стоимости — именно выходные токены.

Ориентиры по цене одного элемента:

| Элемент | Вх. токены (пример) | Вых. токены (пример) | Цена (пример) |
|---------|--------------------|-----------------------|----------------|
| Изображение с коротким текстом (фото документа) | 1 500–3 000 | 200–500 | ≈ 800–2 000 кредитов |
| Страница PDF с плотным текстом | 5 000–10 000 | 2 000–3 000 | ≈ 8 000–12 000 кредитов |
| Текстовый документ (схема `{text}`) | зависит от изображения | = длине распознанного текста | растёт с объёмом текста |

Точную стоимость конкретного распознавания смотрите в `usage.credits`
ответа (итог) и `documents[i].pages[j].usage.credits` (по страницам) —
она рассчитывается по фактическим токенам запроса.

---

## Рекомендации для агента

1. **Обычный текст документов** (сканы, фото документов, PDF) пачкой —
   `POST /api/ocr/document`: минимум настроек, постраничный текст «из коробки».
2. **Структурированное извлечение** (таблицы, договоры, чеки, формы,
   подсчёт объектов) — `POST /api/ocr` со своей `responseSchema` и `prompt`.
   В `prompt` формулируйте задачу именно как инструкцию модели.
3. **Схемы:**
   - всегда указывайте `additionalProperties: false` и `required` — это
     дисциплинирует модель;
   - для неопределившихся полей используйте типы-union `["string", "null"]`,
     чтобы модель не выдумывала значения;
   - описывайте каждое поле в `description` — модель следует описаниям.
4. **Токены/стоимость:** input-токены включают «визуальные» токены
   изображений, поэтому стоимость страницы зависит от её размера. Следите
   за `usage.credits` в ответе (на страницу / на документ / итог).
5. **Пачки:** отправляйте несколько источников одним запросом
   (`sources`/`documents` — массив), это дешевле по оверхеду и атомарно
   тарифицируется одним списанием.
6. **Ошибки:**
   - `400` — неверный вход (пустой массив, переданы оба image и pdfFile,
     невалидный base64, PDF больше лимита по страницам, превышен лимит
     источников на запрос);
   - `502`/`503` — проблемы с моделью (сервис уже делает ретраи при 5xx);
   - `401` — невалидный API-ключ; `403` — требуется платная подписка.

## Файлы для локального тестирования

В `http/` лежат готовые примеры:

| Файл                        | Кейс                                     |
|-----------------------------|------------------------------------------|
| `ocr.http`                  | http-запросы (VS Code REST Client), включая смешанную пачку |
| `test-ocr-request.js`       | изображение, простая схема `{text}`      |
| `test-photo-people-count.js`| фото, схема `{peopleCount}` + prompt     |
| `test-document-pdf.js`      | PDF, простая схема `{text}`              |

Скрипты читают `image.png` / `photo.jpg` / `document.pdf` из своей папки;
URL и TOKEN подставляются в начале каждого скрипта.
