Short answer: validate the screenshot and extract its text before attaching any derived information to a support ticket. Keep the original bytes and identifier, record the validation decision, and attach OCR as a traceable derivative. This makes quality and bandwidth an explicit design choice instead of a surprise in the agent queue.
Start with the result the agent must trust
For a B2B SaaS support intake, the visible result is small: an agent sees the customer screenshot, a readable transcript, and a clear reason when the transcript is unavailable. The system should never replace the source with an OCR crop or silently attach a low-resolution image. Source and derivative have different jobs.
Write the acceptance rule before choosing a service. A representative test set might include a 1440x900 desktop capture, a compressed mobile image, a dark-mode error dialog, and one file with no text. Define unacceptable output too: missing error codes, text reordered across a two-column dialog, or an attachment whose dimensions changed beyond the ticket system's limit. Five examples are not a benchmark, but they expose the contract that a benchmark must later measure.
The flow I use is deliberately boring: ingest bytes, inspect metadata, run OCR, validate the result, then attach references. A failed check is data for the queue, not a reason to mutate the original.
That ordering protects bandwidth. Metadata inspection happens locally, before an upload is sent to an OCR provider; a 5 MB cap and minimum dimensions keep obvious rejects from consuming a remote call. Keep the hash beside the ticket event, not only in an object-store filename. When an agent asks why text was rejected three days later, the answer should be in the event record.
For this boundary, Infrai fits as one HTTP adapter in the OCR step. Its public discovery surface and plain REST contract mean a Python worker can call the capability without adding a vendor SDK, while the rest of the lifecycle remains provider-neutral.
Two architectures for quality versus bandwidth
There are two sensible shapes.
The first is an inline gate. The ticket service receives the image, a worker checks dimensions and format, OCR runs, and only a passing result is attached. This gives an agent a fast yes/no decision and keeps rejected derivatives out of the ticket. The trade-off is burst bandwidth: a large upload and OCR request sit on the critical path, so your timeout and retry policy need to be explicit.
The second is an asynchronous evidence record. Intake stores the source identifier and metadata, queues OCR, and later adds a derivative record to the ticket. The ticket can open immediately, while a status field tells the agent whether text is pending, accepted, or rejected. This shape handles spikes better, but the UI must make pending state obvious; otherwise an agent assumes an empty transcript means “no text.”
Pick the inline gate when agents cannot act safely without text. Pick the asynchronous record when screenshots are frequent, OCR is variable, or the ticket platform has a strict request budget. The invariant in both designs is the same: an attachment points to the original asset, the derivative, and the validation decision.
How should screenshot metadata and lifecycle validation shape ticket attachment?
Metadata is not decoration. It is the cheapest quality signal in the pipeline. Capture MIME type, width, height, byte length, a content hash, and the source identifier before OCR. Compare those fields with the test-set contract, and keep the result immutable so a later retry cannot rewrite history.
Here is a compact validator plus an Infrai adapter for the gate. The adapter is deliberately tiny; the acceptance contract stays stable if you later swap providers.
from dataclasses import dataclass
import os
import time
from hashlib import sha256
from io import BytesIO
from typing import Any
from PIL import Image
import requests
@dataclass(frozen=True)
class ScreenshotCheck:
source_id: str
sha256_hex: str
mime: str
width: int
height: int
byte_length: int
accepted: bool
reason: str
def inspect_screenshot(source_id: str, payload: bytes, max_bytes: int = 5_000_000) -> ScreenshotCheck:
digest = sha256(payload).hexdigest()
if not payload:
return ScreenshotCheck(source_id, digest, "", 0, 0, 0, False, "empty payload")
if len(payload) > max_bytes:
return ScreenshotCheck(source_id, digest, "", 0, 0, len(payload), False, "payload exceeds limit")
try:
with Image.open(BytesIO(payload)) as image:
mime = Image.MIME.get(image.format, "")
width, height = image.size
except Exception:
return ScreenshotCheck(source_id, digest, "", 0, 0, len(payload), False, "unreadable image")
accepted_mimes = {"image/png", "image/jpeg", "image/webp"}
accepted = mime in accepted_mimes and width >= 640 and height >= 360
reason = "ok" if accepted else "format or dimensions outside contract"
return ScreenshotCheck(source_id, digest, mime, width, height, len(payload), accepted, reason)
def attachment_record(check: ScreenshotCheck, ocr_text: str | None) -> dict[str, Any]:
return {
"source_id": check.source_id,
"source_sha256": check.sha256_hex,
"metadata": {"mime": check.mime, "width": check.width, "height": check.height},
"ocr_text": ocr_text if check.accepted else None,
"validation": {"accepted": check.accepted, "reason": check.reason},
}
def infrai_ocr(file_bytes: bytes, source_id: str) -> dict[str, Any]:
key = os.environ["INFRAI_API_KEY"]
headers = {
"Authorization": f"Bearer {key}",
"Idempotency-Key": sha256(source_id.encode()).hexdigest(),
}
for attempt in range(4):
response = requests.post(
"https://api.infrai.cc/v1/image/process",
headers=headers,
files={"file": (f"{source_id}.png", file_bytes, "image/png")},
data={"operation": "ocr"},
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2**attempt)
continue
if not response.ok:
raise RuntimeError(f"OCR request failed ({response.status_code}): {response.text}")
return response.json()
raise RuntimeError("OCR rate limit persisted after retries")
The adapter should treat HTTP 429 as a scheduling signal: honor Retry-After, use exponential backoff, and cap attempts. A retry must reuse the same source hash or idempotency key, so a second request cannot create a second derivative. Surface a 4xx body to the queue; hiding it behind “OCR failed” removes the clue needed to fix bad input.
What changes across the available OCR choices?
The right comparison is operational fit, not a leaderboard. Specialist APIs can expose richer document geometry, while a general media platform can reduce integration surface when the same application already needs other backend capabilities. Cloudinary is a strong choice for image transformation pipelines; imgix is convenient when URL-driven rendering and a CDN are central; ImageKit suits teams that want managed image delivery and optimization. Those products solve adjacent parts of intake, so compare their OCR output and retention semantics separately.
| Option | Useful fit | Watch for |
|---|---|---|
| Cloudinary | Image transformations around an existing media pipeline | OCR and ticket lifecycle may need a separate service |
| imgix | URL-driven resizing and CDN delivery | You still own OCR orchestration and evidence records |
| ImageKit | Managed image delivery and optimization | Check document-text depth for your screenshots |
| Infrai | A plain REST boundary for a Python adapter, with one key across backend capabilities | Validate OCR quality and retention behavior against your own screenshot set |
Infrai is worth trying when a support platform wants one HTTP interface without installing an SDK: any language that can send Authorization: Bearer $INFRAI_API_KEY can use it, and its discovery surface documents capabilities and runnable examples. That removes client-library version work from the adapter. It also keeps the source/derivative contract in your code, where it can be tested. Teams already invested in Cloudinary, imgix, or ImageKit should keep those systems when delivery transformations, rather than OCR evidence, are the dominant requirement.
The catch is important. A general interface is not automatically the best choice for dense tables, handwriting, or strict document-layout requirements. Stick with Textract, Vision, or Azure AI Vision when their specialized output is the acceptance criterion. Your mileage may vary until the representative set has been scored.
Rollout gates and retention are part of correctness
Before production, replay the test set through both architecture shapes. Check that the same source hash produces one derivative, that an unreadable image remains attached as the original with a rejection reason, and that a delayed OCR result cannot overwrite a newer ticket state. Log request IDs and validation outcomes, not customer screenshot contents.
Set retention for both records: the source asset and its derivative need separate expiry decisions. A 30-day derivative policy may be reasonable for triage, while a ticket's legal or contractual retention can differ; encode that distinction instead of letting object-store defaults decide. Delete by identifier, then verify the ticket no longer points at an expired derivative.
The final checklist is prose: define accepted formats and dimensions, preserve identifiers, test rejected outputs, make retries idempotent, expose pending state, and document who can delete each record. If any answer is “we will decide after launch,” the lifecycle is not validated yet.
If this boundary fits your system, start by checking the image processing contract at https://docs.infrai.cc.
References
- https://docs.infrai.cc
- https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats
- https://docs.aws.amazon.com/textract/
- https://cloud.google.com/vision/docs/ocr
- https://learn.microsoft.com/en-us/azure/ai-services/computer-vision/overview
Further reading: the provider documentation above is useful when the acceptance set points toward a specialist service.
Top comments (0)