Skip to content

Likeness Review User Outcomes

Likeness Review User Outcomes

This flow extends Contested Likeness Review with the user-facing half: end users now see whether their likeness review is pending, approved, rejected, or mixed — and why an image was rejected — instead of retrying blind.

Reviewers pick a structured rejection reason in Strip (stored per image by the Likeness Review History work). This flow carries that reason to the Brisket UI.

These surfaces are always on (an earlier feature-flag gate was removed before release); rollback is a revert of the PR.

What the user sees

Three Brisket surfaces, all fed by the same character.getCharacter query:

  1. Outcome panel (features/likeness-review-outcome/) at the top of the upload steps (sfw-ref-upload, photos-upload). Rejection-focused by design — approved photos are already visible on the slots themselves, so the panel never enumerates them:
    • Pending — “Your photos are being reviewed”; already-rejected slots show their reason immediately.
    • Rejected / mixed — “N photo(s) were rejected in review”, listing only the non-approved slots with the reason and a next-step hint.
    • All approved — no panel (the slots and the continuing flow communicate it). Never-reviewed characters see no panel either.
  2. Slot failure line — the rejected slot’s existing “Reason for failure” line (example-preview) shows the specific reason instead of the generic face-match copy.
  3. Character images modal — rejected tiles get a reason-neutral Rejected badge and a tooltip with the reason (replacing the hardcoded “Manual review was rejected…” text and the sometimes-wrong “Face mismatch” label).

Reason copy

The enum→copy map lives in features/likeness-review-outcome/constants.ts (FE-owned so copy can change without a backend deploy):

Stored reasonUser-facing message
NEEDS_PROOF_OF_CREATION”We couldn’t confirm this character was created by you” + upload-proof hint
REAL_IMAGES_OF_SOMEONE_ELSE”These photos appear to show a real person who isn’t you” + use-own/fictional hint
UNUSABLE_FOR_GENERATION”This photo won’t work for content generation” + photo-guidelines hint
OTHERThe reviewer’s note, verbatim and standing alone
unknown / legacy NULLGeneric “This photo couldn’t be approved” fallback (never crashes on future enum values)

Reviewer note visibility policy

The reviewer’s free-text note is shown to users only when the reason is OTHER. This is enforced in sirloin (referenceDatasetImageRejection in services/characters/rejectionreason.go), not in the frontend — notes attached to standard reasons never leave the backend. Strip’s reject dialog shows “This note will be shown to the user.” under the note field while OTHER is selected, so reviewers write accordingly.

Architecture

sequenceDiagram
participant Strip as Strip (reject w/ reason)
participant DB as characters.reference_dataset_images
participant Sirloin as Sirloin ListCharacters
participant Brisket as Brisket getCharacter
participant UI as Panel / slot / modal tile
Strip->>DB: rejection_reason (strip enum name), rejection_note
Brisket->>Sirloin: getCharacter (existing query, no new endpoint)
Sirloin->>DB: load dataset images (newest-first)
Sirloin->>Brisket: CharacterDatasetImage{status, rejection_reason, rejection_note*}
Note over Sirloin: *note only when reason = OTHER,<br/>fields only when status = REJECTED
Brisket->>UI: deriveLikenessReviewOutcome → pending/rejected/mixed/approved
  • CharacterDatasetImage gained additive fields rejection_reason (user-facing enum CharacterLikenessRejectionReason) and rejection_note. The user-facing enum deliberately mirrors but does not reuse Strip’s admin enum; sirloin maps the stored admin-enum name strings and degrades unknown values to UNSPECIFIED without erroring.
  • Outcome derivation (deriveLikenessReviewOutcome) groups non-SUPERSEDED images per slot within one slot set, takes the newest row per slot (API returns rows newest-first), and resolves the overall state: any slot in REVIEW ⇒ pending; otherwise zero/all/some rejected ⇒ approved/rejected/mixed. Host steps pass the slot set they display (sfw-ref-upload is always sfw); this matters because VI/proof flows run the review on SFW source slots even for NSFW-enabled characters, so deriving the set from character.isNsfw alone would miss them.
  • After a user resubmits replacements, use-manual-review-submit invalidates getCharacter so the panel flips to pending immediately instead of serving the 5-minute stale cache.

Analytics

One PostHog event, Likeness Review Outcome Viewed, fires when the panel mounts with a resolved outcome (rejected, mixed, or approved — never pending/none), carrying the state and the rejected slots’ reason enum names only (no notes, no free text). A new review round re-arms tracking so a repeat rejection is counted — the event exists to measure whether blind-retry loops drop.

Out of scope (follow-ups)

  • Rejection reasons in outcome emails (emails stay generic; CS handles edge cases manually).
  • Automatic slot reopen when nudify fitness fails (separate issue).
  • The deprecated image-edit dataset flow (superseded by the character images modal).