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:
- 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.
- 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. - 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 reason | User-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 |
OTHER | The reviewer’s note, verbatim and standing alone |
unknown / legacy NULL | Generic “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/approvedCharacterDatasetImagegained additive fieldsrejection_reason(user-facing enumCharacterLikenessRejectionReason) andrejection_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 toUNSPECIFIEDwithout erroring.- Outcome derivation (
deriveLikenessReviewOutcome) groups non-SUPERSEDEDimages per slot within one slot set, takes the newest row per slot (API returns rows newest-first), and resolves the overall state: any slot inREVIEW⇒ pending; otherwise zero/all/some rejected ⇒ approved/rejected/mixed. Host steps pass the slot set they display (sfw-ref-uploadis alwayssfw); this matters because VI/proof flows run the review on SFW source slots even for NSFW-enabled characters, so deriving the set fromcharacter.isNsfwalone would miss them. - After a user resubmits replacements,
use-manual-review-submitinvalidatesgetCharacterso 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-editdataset flow (superseded by the character images modal).