Обзор
API версионирован под /v1/. Все запросы и ответы — JSON, кроме загрузки изображения (multipart/form-data) и скачивания результата (бинарник).
Три шага:
- Создать задачу —
POST /v1/jobsс файлом. - Дождаться — long-poll
?wait=60либо опросGET /v1/jobs/{id}. - Скачать —
GET /v1/jobs/{id}/result(или поresult_urlиз ответа).
Результаты хранятся 30 минут после завершения, затем удаляются. Базовый URL обозначается как BASE (например https://api.example.com).
Аутентификация
Необязательная. Если сервер запущен с переменной API_TOKEN, каждый запрос к /v1/* должен содержать:
Authorization: Bearer YOUR_TOKEN
Без токена — 401 unauthorized. /health всегда публичный.
Лимиты
| Параметр | Значение |
|---|---|
| Максимальный размер файла | 3 MiB |
| Максимальное разрешение входа | должно уместиться в 1920×1080 либо 1080×1920 |
| Разрешение результата | всегда Full HD (1920×1080 в границах) |
| Таймаут обработки | 2 минуты на задачу |
| TTL результата | 30 минут после succeeded |
| Переполнение очереди | 503 queue_full с Retry-After |
Формат ошибок
Все ошибки приходят в одном формате:
{
"error": {
"code": "invalid_image",
"message": "resolution 4000x3000 exceeds Full HD (1920x1080 or 1080x1920)"
}
}
| HTTP | code | Когда |
|---|---|---|
400 | invalid_image, invalid_parameter, image_required | битый файл, слишком большой/крупный, неверный параметр |
401 | unauthorized | нет/неверный Bearer |
404 | job_not_found | задача удалена по TTL |
410 | job_failed | обработка упала |
503 | queue_full | повторить через Retry-After секунд |
Каждый ответ содержит заголовок X-Request-Id — прикладывайте его в баг-репортах.
POST /v1/jobs — создать задачу
Content-Type: multipart/form-data
| Поле | Где | Обязательно | Описание |
|---|---|---|---|
image | form | да | файл (JPEG, PNG, GIF, BMP, TIFF, WebP) |
task | form / query | нет | upscale (по умолчанию), remove-bg или restore |
mode | form / query | нет | только для upscale: fit (по умолчанию), fill, stretch |
format | form / query | нет | только для upscale: jpeg (по умолчанию) или png. remove-bg всегда возвращает PNG. |
quality | form / query | нет | JPEG quality 1–100 (92) — только для upscale |
wait | query | нет | long-poll: ждать до N секунд (макс. 60) |
Задачи task
upscale— апскейл через Real-ESRGAN и приведение к Full HD поmode.remove-bg— удаление фона (U²-Net). Результат всегдаimage/pngс прозрачностью.mode/qualityигнорируются.restore— реставрация старых/нечётких фото: восстановление лиц (GFPGAN v1.4) + 2× апскейл. Результат —image/png. Принимает любое разрешение до 3 MiB. Для сканов, мягких портретов, слегка повреждённых снимков.
Режимы mode
fit— вписать в 1920×1080 с сохранением пропорций.fill— заполнить ровно 1920×1080, центральная обрезка.stretch— растянуть до 1920×1080 без пропорций.
Ответ
202 Accepted (или 200 OK, если wait успел). Заголовок Location указывает на запись задачи.
{
"id": "7b1f…",
"status": "queued",
"mode": "fit",
"format": "jpeg",
"created_at": "2026-04-22T10:20:30Z",
"original_width": 1280,
"original_height": 720
}
GET /v1/jobs/{id} — статус задачи
| Query | Описание |
|---|---|
wait | long-poll: держит коннект до терминального статуса (макс. 60 с) |
Статусы: queued, processing, succeeded, failed.
Ответ при succeeded:
{
"id": "7b1f…",
"status": "succeeded",
"mode": "fit",
"format": "jpeg",
"created_at": "2026-04-22T10:20:30Z",
"started_at": "2026-04-22T10:20:30Z",
"finished_at": "2026-04-22T10:20:42Z",
"result_url": "https://api.example.com/v1/jobs/7b1f…/result",
"expires_at": "2026-04-22T10:50:42Z",
"original_width": 1280, "original_height": 720,
"output_width": 1920, "output_height": 1080
}
GET /v1/jobs/{id}/result — скачать результат
Возвращает байты изображения (image/jpeg или image/png).
- 202 — задача ещё в работе (есть
Retry-After). - 410 — задача упала.
- 404 — не найдена или истёк TTL.
- Query
?download=1— добавляетContent-Disposition: attachment.
POST /v1/cull — отбор лучшего кадра из серии
Загрузите 2–20 изображений одним multipart-запросом. Сервис группирует похожие кадры через perceptual hash, оценивает резкость (Laplacian variance), экспозицию и насыщенность, возвращает JSON с лучшим кадром в каждой группе и причинами отклонения для остальных.
Content-Type: multipart/form-data. Поле images повторяется для каждого файла.
| Лимит | Значение |
|---|---|
| Файлов за запрос | 2–20 |
| Размер одного файла | 3 MiB |
| Суммарный upload | 60 MiB |
| Rate limit | считается как 1 запрос из квоты 30/час |
Пример:
curl -X POST https://api.printgpt.ru/v1/cull \
-F "images=@IMG_001.jpg" \
-F "images=@IMG_002.jpg" \
-F "images=@IMG_003.jpg"
Ответ (сокращённо):
{
"images": [
{ "index": 0, "filename": "IMG_001.jpg", "group_id": 0, "is_best": true,
"metrics": { "score": 9.15, "blur": 1.0, "exposure": 1.0,
"saturation": 0.43, "blur_raw": 333.8,
"width": 800, "height": 600 } },
{ "index": 1, "filename": "IMG_002.jpg", "group_id": 0, "is_best": false,
"metrics": { "score": 3.18, "blur": 0.01, "exposure": 1.0,
"saturation": 0.42, "blur_raw": 5.7,
"width": 800, "height": 600,
"reasons": ["blurry"] } }
],
"groups": [ { "group_id": 0, "best_index": 0, "members": [0, 1] } ]
}
Поля:
group_id— кластер близких дублей (Hamming distance pHash ≤ 10).is_best—trueу кадра с максимальным score в группе.metrics.score— композитный 0–10 (60% резкость + 25% экспозиция + 15% насыщенность).metrics.blur— 0–1, нормализовано min-max по пакету;blur_raw— сырая дисперсия Laplacian.metrics.exposure— 0–1,1 − доля_клиппированных_пикселей.metrics.reasons— подмножество["blurry", "clipped", "flat"]; отсутствует если кадр чистый.
GET /health
{ "status": "ok", "version": "2.0.0", "api": "v1" }
Быстрый старт — curl
curl -X POST "$BASE/v1/jobs?wait=45" \
-H "Authorization: Bearer $TOKEN" \
-F "image=@photo.jpg" -F "mode=fit" -F "format=jpeg"
Если wait успел — статус уже succeeded, в ответе есть result_url:
curl -L -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/jobs/$ID/result" -o result.jpg
Long-polling цикл
# Создаём задачу
ID=$(curl -sS -X POST "$BASE/v1/jobs" -F "image=@photo.jpg" | jq -r .id)
# Ждём до 30 секунд на запрос; повторяем до терминального статуса
while :; do
RESP=$(curl -sS "$BASE/v1/jobs/$ID?wait=30")
STATUS=$(echo "$RESP" | jq -r .status)
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && { echo "$RESP"; exit 1; }
done
curl -sSL "$BASE/v1/jobs/$ID/result" -o result.jpg
PHP
<?php
$base = 'https://api.example.com';
$token = getenv('API_TOKEN');
$ch = curl_init("$base/v1/jobs?wait=45");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
CURLOPT_POSTFIELDS => [
'image' => new CURLFile('/path/to/photo.jpg'),
'mode' => 'fit',
'format' => 'jpeg',
],
]);
$job = json_decode(curl_exec($ch), true);
curl_close($ch);
if (($job['status'] ?? '') !== 'succeeded') {
throw new RuntimeException('job not ready: ' . json_encode($job));
}
file_put_contents('/tmp/result.jpg',
file_get_contents($job['result_url'], false, stream_context_create([
'http' => ['header' => "Authorization: Bearer $token"]
]))
);
Node.js (fetch, Node 18+)
import fs from 'node:fs';
const base = 'https://api.example.com';
const token = process.env.API_TOKEN;
const form = new FormData();
form.set('image', new Blob([fs.readFileSync('photo.jpg')]), 'photo.jpg');
form.set('mode', 'fit');
const r = await fetch(`${base}/v1/jobs?wait=45`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: form,
});
const job = await r.json();
if (job.status !== 'succeeded') throw new Error(JSON.stringify(job));
const img = await fetch(job.result_url, {
headers: { Authorization: `Bearer ${token}` },
}).then(x => x.arrayBuffer());
fs.writeFileSync('result.jpg', Buffer.from(img));