{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "model-swap-job",
  "title": "Model swap job",
  "description": "One product photo, one model, one reviewed edit. Mirrors skills/model-swap/scripts/identity-swap.py and body-swap.py (reference implementation v2.6.0). All free-text fields are written in words: no digits, because the image model renders digits as text.",
  "type": "object",
  "required": ["product_photo", "model", "garment", "size_check"],
  "properties": {
    "product_photo": {
      "type": "object",
      "required": ["file"],
      "properties": {
        "file": {"type": "string", "description": "The brand's original photo, full resolution. Its pixel size and aspect ratio define the deliverable."},
        "view": {"type": "string", "enum": ["front", "back"], "default": "front", "description": "back when the face is not visible; changes the CHANGE block of the prompt."},
        "pose": {"type": "string", "description": "The pose in words, e.g. 'facing the camera with one hand on her hip'. Goes into the FRAME AND POSE block."},
        "photographed_size": {"type": "string", "description": "The size the photo was shot in (from the page, the brand, or the variant image), e.g. 'S'."}
      }
    },
    "model": {
      "type": "object",
      "required": ["face_reference", "body_reference", "look"],
      "properties": {
        "id": {"type": "string"},
        "kind": {"type": "string", "enum": ["fictional", "brand_own"], "description": "brand_own drops the 'fictional person' sentence and requires rights_confirmed."},
        "face_reference": {"type": "string", "description": "PNG: the sheet's portrait panel (fictional) or refset/front.png (brand's own)."},
        "body_reference": {"type": "string", "description": "PNG: the sheet's studio front panel or refset/full-length.png; a plain base layer so the build reads."},
        "look": {"type": "string", "description": "What the face reference shows, skin tone included, e.g. 'platinum blunt bob with heavy bangs, pale skin tone, mid-forties with fine crow's feet, matte red lip, small earrings'. Used in the prompt and handed to the reviewer as the model's own look, so her jewellery and glasses are not counted as added accessories."},
        "build": {"type": "string", "description": "Required for a refit, in words: 'full-figured and curvy with a strong frame'."},
        "measurements_cm": {"type": "object", "properties": {"height": {"type": "number"}, "bust": {"type": "number"}, "waist": {"type": "number"}, "hip": {"type": "number"}, "size": {"type": "string"}}},
        "rights_confirmed": {"type": "boolean", "description": "brand_own only: the brand's written confirmation that it holds AI-likeness rights. Generation is refused without it."}
      }
    },
    "garment": {
      "type": "string",
      "description": "Every visible piece, named: 'the zebra-print short-sleeve blouse with its crew neck, draped side tie and long asymmetrical cascading hem, layered over a black top, and the light-wash jeans'. Describe layering neutrally; 'semi-sheer' and 'bralette' have failed moderation."
    },
    "size_check": {
      "type": "object",
      "required": ["mode"],
      "properties": {
        "size_chart": {"type": "string", "description": "Reference to the product's size chart record."},
        "model_size": {"type": "string"},
        "mode": {"type": "string", "enum": ["identity", "refit", "no_swap"], "description": "identity when the model's size equals the photographed size; refit when it differs but lies inside the chart; no_swap outside the chart (ask the brand for a fit-model shoot)."}
      }
    },
    "generation": {
      "type": "object",
      "properties": {
        "image_models": {"type": "array", "items": {"type": "string"}, "default": ["gpt-image-2.5-sunburst", "gemini-nano-banana-2.1"], "description": "Tried in order; a model the account cannot use is skipped; if neither is available the job stops. No other model is substituted."},
        "quality": {"type": "string", "enum": ["1440p", "2160p"], "default": "1440p", "description": "2160p only on an explicit request."},
        "detail_level": {"type": "string", "default": "high", "description": "GPT Image models only."},
        "aspect_ratio": {"type": "string", "description": "Derived: the nearest supported ratio that is at least as wide as the photo (3:4 for a 5:7 photo); never narrower."},
        "count": {"type": "integer", "default": 1, "description": "One job per photo."},
        "max_attempts": {"type": "integer", "default": 3, "description": "A failed review is confirmed by a second review, then resubmitted with the reviewer's hint appended to the prompt."},
        "idempotency_key": {"type": "string", "description": "<job>-a<attempt>-<model>; the backend dedupes by key."}
      }
    },
    "review": {
      "type": "object",
      "properties": {
        "enabled": {"type": "boolean", "default": true},
        "models": {"type": "array", "items": {"type": "string"}, "default": ["gemini-4-argon", "gemini-3.8-flash"]},
        "pass_rule": {"type": "string", "default": "every critical criterion at 4 or 5 and nothing below 3"}
      }
    }
  },
  "$defs": {
    "result": {
      "title": "Model swap result",
      "type": "object",
      "required": ["status", "master", "attempts"],
      "properties": {
        "status": {"type": "string", "enum": ["pass", "not_passed", "stopped"]},
        "master": {"type": "string", "description": "PNG at the product photo's exact pixel size (or, for a 2K generation smaller than the photo, at its own size with the photo's aspect; never upscaled)."},
        "raw": {"type": "string", "description": "The image model's untouched output for the chosen attempt."},
        "before_after": {"type": "string", "description": "JPEG pair for the eye check."},
        "attempts": {"type": "array", "items": {"type": "object", "properties": {"n": {"type": "integer"}, "image_id": {"type": "string"}, "model": {"type": "string"}, "cost_credits": {"type": "number"}, "review": {"type": "string", "description": "Path to the review record (scores, evidence, worst_detail, resubmit_hint, judge model and route)."}, "verdict": {"type": "string"}}}},
        "claim": {"type": "string", "const": "kept as photographed and checked against your photo", "description": "Plus 'refitted to her' after a refit. Never 'pixel-identical' or 'untouched'."}
      }
    }
  }
}
