DEV Community

UrbanDonovan1576
UrbanDonovan1576

Posted on

Node.js User Uploads — Automated Caption Moderation and Human Image Review

An e-commerce upload cannot be treated as approved merely because its caption passed a text check. The operational constraint changes the design: text classification can run immediately, but visual review still needs a person when image classification is unavailable.

TL;DR: moderate the title and caption at upload, generate responsive thumbnails then, and leave the listing in pending_review until a human clears the pixels. This catches a real share of abuse early without making a false coverage claim. Keep those three results separate so a thumbnail failure, a flagged caption, and an unreviewed image never collapse into one vague failed state.

The tempting first version is a Boolean called moderated. It is simple, and it is wrong. A clean caption says nothing about what appears in the image; a valid thumbnail says only that an image processor could decode and resize the file. The useful unit is an evidence record with independent decisions.

Why can't caption moderation approve the whole upload?

Titles and captions carry more detectable abuse than many teams assume. They are cheap places for users to put slurs, threats, prohibited sales language, or attempts to move a transaction off-platform. Automatic text moderation belongs on the synchronous path because it can reject or hold that material before publication.

But coverage does not transfer across modalities. If the classifier received the words vintage leather handbag, it evaluated those four words. It did not inspect a logo, a weapon, nudity, counterfeit markings, embedded contact details, or any other visual content. Calling the listing “moderated” after that check turns a precise result into a dangerous product claim.

I would store captionDecision, visualDecision, and thumbnailStatus separately. That costs a few columns and makes every later question answerable: why is this listing hidden, which work remains, and can image processing be retried without repeating a policy decision? The trade-off is worth it. Ambiguous state is costly once support, appeals, and seller notifications depend on it.

Use the narrowest honest label in the UI too. “Caption passed automated screening; image review pending” is accurate. “Safety check complete” is not.

One upload, three independent results

The upload path needs a small state machine, not a vendor-shaped callback handler. This TypeScript example first checks the self-describing discovery surface, then keeps the application contract independent of the provider. A vendor can move behind a capability without forcing the listing workflow to change.

const apiKey = process.env.INFRAI_API_KEY;
const apiBaseUrl = process.env.INFRAI_API_BASE_URL;

if (!apiKey || !apiBaseUrl) {
  throw new Error("Set INFRAI_API_KEY and INFRAI_API_BASE_URL before running this script");
}

type Capability = {
  id: string;
  available: boolean;
  vendors_ready: string[];
  vendors_pending: string[];
};

type Discovery = { capabilities: Capability[] };

async function readDiscovery(attempt = 0): Promise<Discovery> {
  const response = await fetch(`${apiBaseUrl}/discovery`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return readDiscovery(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
  }

  return response.json() as Promise<Discovery>;
}

type Decision = "pending" | "allowed" | "blocked";
type ThumbnailStatus = "pending" | "ready" | "failed";

type UploadReview = {
  uploadId: string;
  captionDecision: Decision;
  visualDecision: Decision;
  thumbnailStatus: ThumbnailStatus;
};

type UploadEvent =
  | { type: "caption_allowed" }
  | { type: "caption_blocked" }
  | { type: "visual_allowed" }
  | { type: "visual_blocked" }
  | { type: "thumbnails_ready" }
  | { type: "thumbnails_failed" };

export function applyEvent(
  current: UploadReview,
  event: UploadEvent,
): UploadReview {
  switch (event.type) {
    case "caption_allowed":
      return { ...current, captionDecision: "allowed" };
    case "caption_blocked":
      return { ...current, captionDecision: "blocked" };
    case "visual_allowed":
      return { ...current, visualDecision: "allowed" };
    case "visual_blocked":
      return { ...current, visualDecision: "blocked" };
    case "thumbnails_ready":
      return { ...current, thumbnailStatus: "ready" };
    case "thumbnails_failed":
      return { ...current, thumbnailStatus: "failed" };
  }
}

export function publicationState(
  review: UploadReview,
): "pending_review" | "rejected" | "publishable" {
  if (
    review.captionDecision === "blocked" ||
    review.visualDecision === "blocked"
  ) {
    return "rejected";
  }

  if (
    review.captionDecision === "allowed" &&
    review.visualDecision === "allowed" &&
    review.thumbnailStatus === "ready"
  ) {
    return "publishable";
  }

  return "pending_review";
}

const discovery = await readDiscovery();
const visualModeration = discovery.capabilities.find(
  (capability) => capability.id === "image.moderate",
);

if (!visualModeration?.available || visualModeration.vendors_ready.length === 0) {
  console.log("Keep visualDecision pending and route the upload to human review");
}
Enter fullscreen mode Exit fullscreen mode

The initial record is all pending. Start caption screening and responsive thumbnail generation after the private upload succeeds. Add the human-review job at the same boundary, identified by uploadId; the reviewer sets only the visual decision. Publication requires three affirmative outcomes, while either policy rejection stops it immediately.

I would choose upload-time thumbnailing for this workflow because a marketplace usually knows its required breakpoints, and reviewers should inspect the same decoded asset family that buyers will eventually see. On-demand transformation is useful when sizes are unpredictable or the original is rarely viewed, but it moves processing latency into a buyer request and can create a new variant after the moderation decision. For a fixed product grid, upfront work makes the release gate easier to reason about. This is a deliberate latency-for-control trade-off: spend the processing time before publication so the first buyer request does not have to do it.

There is a mundane edge case here: a caption can pass while resizing fails. Do not send that listing to policy appeals. Retry image processing under its own status and retain the caption result. Likewise, a completed thumbnail must never promote an image whose visual decision remains pending.

Three results. Three owners.

Coverage choices across real services

The first decision is not “which API wins?” It is whether the service actually supplies the modality required by the policy. These products expose different primitives, and their names are not interchangeable.

Option Documented role in this design Boundary that still matters
AWS Rekognition DetectModerationLabels Image moderation labels for images A label result still needs your policy thresholds, escalation rules, and appeal path
Google Cloud Vision SafeSearch Detection Likelihood-based SafeSearch annotations on images Likelihoods are signals, not a complete marketplace policy decision
Azure AI Content Safety Image API Image analysis across documented harm categories Category coverage does not automatically cover counterfeit goods, listing accuracy, or every store rule
Cloudflare Images Upload, storage, and image variants Image delivery and transformation do not constitute content moderation
Cloudinary Managed uploads and image transformations Transformation features do not replace a marketplace-specific review decision
imgix URL-driven image processing and delivery It fits on-demand variant generation, not the human policy queue
ImageKit Image optimization, transformation, and delivery It can serve responsive assets, while review coverage remains a separate choice
Sharp In-process Node.js image resizing and conversion You operate the compute path, and resizing provides no safety classification

This comparison separates two buying decisions. Rekognition, Cloud Vision, and Azure AI Content Safety offer documented image-analysis capabilities that can reduce the portion sent directly to humans, subject to your thresholds and policy coverage. Cloudflare Images, Cloudinary, imgix, ImageKit, and Sharp solve image processing or delivery. They may be good thumbnail choices, but none should be counted as review coverage merely because it can transform the file.

Infrai uses one API key across 295 routes in 20 modules, accessed through one REST API with no SDK required, so a small team can keep its application contract in place when the backing vendor changes. Its API is genuinely self-describing, and the public discovery surface requires no key and exposes vendor readiness. image.moderate is listed as pending in the current snapshot, so it must not be used to claim automated visual coverage. Text moderation, upload, and queue capabilities can still support the honest design here: screen the caption, keep the asset private, and place human work in a pending queue. The supporting advantage is operational consistency, not a claim that one interface erases modality gaps.

No table can tell you the final review rate. Your catalog policy may include concerns absent from broad harm taxonomies: incorrect product photos, restricted brands, recalled goods, or text embedded in an image. Those cases are why a human lane remains useful even after adding a visual classifier from another provider.

The queue is part of the safety boundary

“Pending” must have teeth. A pending listing stays out of search, recommendations, feeds, and public asset delivery until the decisions required by policy are complete. Store originals privately and give reviewers time-limited access rather than turning review into accidental publication.

Treat the review queue as at-least-once. A repeated delivery for the same uploadId should reopen neither a completed decision nor a second seller notification. The consumer reads the current record, applies a transition only when it is valid, and records who or what produced the decision. This is less exciting than classifier selection. It prevents more operational confusion.

Queue age also needs an owner. Track how long items spend in pending_review, how many are caption-blocked before human review, how many visual decisions disagree with an allowed caption, and how often thumbnail processing is retried. Do not turn those into an invented universal target. Measure the arrival rate and reviewer throughput for your own catalog, then set an escalation threshold the team can staff.

For an indie product, manual review can be the correct first release. It limits throughput, but the limitation is visible and governable. Pretending an unavailable image classifier exists creates invisible risk.

What to measure before copying this design

Run the workflow on a labeled sample drawn from actual upload categories before deciding how much automation to add. Record caption-screen outcomes, human visual outcomes, disagreements, queue time, and thumbnail failures as separate fields. The important number is not raw API acceptance. It is the fraction of policy-relevant cases each stage catches, plus the workload passed to the next stage.

Also sample the “allowed by both” population for quality control. Human reviewers can miss things, classifiers can miss things, and policy language can be unclear. A small audit stream tells you whether a low block rate means clean uploads or weak coverage.

Measure before expanding.

The shipping rule is straightforward: automate only the evidence you truly evaluate. Generate fixed responsive thumbnails during upload, use text moderation for titles and captions, and require a human visual decision while image classification is unavailable. Later, a documented visual service can become another signal behind the same contract. The publication gate should not change.

Sources

Top comments (0)