{
  "name": "SpicyAPI \u00b7 character photo set",
  "nodes": [
    {
      "id": "04988b37-75f4-40fd-a213-9c727a81d9fc",
      "name": "Setup and limits",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Character photo set: 4, 8 or 12 scenes\n\nSelf-hosted n8n 2.42 + @spicyapi/n8n-nodes-spicyapi 0.1.1.\n\n1. Select a SpicyAPI credential on Generate Scene.\n2. Settings: add your public HTTPS referenceImageUrl, choose photoCount (4/8/12), review maxCostUsd, then set enabled=true.\n3. Execute manually. Each scene uses the original reference, not the previous generated image.\n\nOne quote and one image task per scene. The maximum for this execution is photoCount \u00d7 maxCostUsd (default $0.40 for 4). The plugin stops any item over its cap and waits up to 300 seconds. If a task needs_review, retrieve that task rather than resubmitting it.\n\nPhoto Set Results includes binary.data for each downloaded image and the task ID. resultUrl is temporary; download the binary or archive it in your own storage. Portrait fidelity can vary with the scene.\n\nSelect the same SpicyAPI credential in Get Scene Result. Generate submits once and stores its task ID; Get Scene Result retrieves that task and waits/downloads without generating again. Any result that is not ready becomes needs_review. Accepted task IDs remain in the Generate node and result output even if fetching fails. If no ID was returned, inspect the saved execution and your console before creating any new task.",
        "width": 780,
        "height": 520
      },
      "position": [
        0,
        -420
      ]
    },
    {
      "id": "51456372-095a-449c-a06e-a3af6155765d",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        0
      ]
    },
    {
      "id": "62502765-a79b-4627-8df6-1b1ee1dc4003",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a8a30ef1-963d-4609-bd59-a07cee6f678c",
              "name": "enabled",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "d443be33-f88f-4c47-9730-258ca3a465e5",
              "name": "referenceImageUrl",
              "value": "",
              "type": "string"
            },
            {
              "id": "4b98e364-91f6-48da-9a13-2eddc4b42dbc",
              "name": "photoCount",
              "value": 4,
              "type": "number"
            },
            {
              "id": "5b448251-6806-4f40-8182-0e56fcd4b0f4",
              "name": "maxCostUsd",
              "value": 0.1,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "position": [
        220,
        0
      ]
    },
    {
      "id": "e886c1fe-a5fc-4652-9f68-df03a0b2cdcc",
      "name": "Build Scenes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function publicImage(value) {\n  const url = String(value || '').trim();\n  const match = url.match(/^https:\\/\\/([^/?#]+)(?:[/?#]|$)/i);\n  const host = match ? match[1].toLowerCase() : '';\n  const hostname = host.split(':')[0];\n  if (!host || host.includes('@') || host.startsWith('[') || !/^[a-z0-9.-]+(?::443)?$/.test(host) || /^(localhost|127\\.|0\\.|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.)/i.test(hostname)) throw new Error('Set a public HTTPS referenceImageUrl in Settings, without URL credentials.');\n  return url;\n}\nfunction costLimit(value) {\n  const limit = Number(value);\n  if (!Number.isFinite(limit) || limit <= 0 || limit > 0.10) throw new Error('maxCostUsd must be above 0 and at most 0.10 per image.');\n  return limit;\n}\n\nconst c = $input.first().json;\nif (c.enabled !== true) throw new Error('Configure Settings, select your SpicyAPI credential, then set enabled to true.');\nconst referenceImageUrl = publicImage(c.referenceImageUrl);\nconst maxCostUsd = costLimit(c.maxCostUsd);\nconst count = Number(c.photoCount);\nif (![4, 8, 12].includes(count)) throw new Error('photoCount must be 4, 8 or 12.');\nconst scenes = [[\"Rooftop at dusk\", \"On a rooftop at dusk, crimson satin evening dress, warm string lights and a glowing skyline.\"], [\"Soft window light\", \"In a quiet bedroom by the window, fully covered silk robe, soft morning light, relaxed seated pose.\"], [\"Studio evening portrait\", \"In a graphite photography studio, black evening dress, full-length portrait, soft rim light.\"], [\"Neon street\", \"On a neon-lit city street, dark leather jacket over a simple top, rain reflections.\"], [\"Rainy city\", \"Under a clear umbrella on a rainy street, belted trench coat, blue hour city lights.\"], [\"Cafe portrait\", \"At a small cafe table, cashmere sweater, warm coffee-shop light, candid expression.\"], [\"Athletic editorial\", \"In a modern fitness studio, fitted sportswear, confident standing pose, daylight.\"], [\"Art gallery\", \"In a bright art gallery, white blazer and tailored trousers, natural walking pose.\"], [\"Resort terrace\", \"On a resort terrace, a modest two-piece swimsuit with a light wrap, warm sunset.\"], [\"Moonlit garden\", \"In a moonlit garden, deep-green velvet evening dress, cinematic soft light.\"], [\"Hotel corridor\", \"In a warmly lit hotel corridor, tailored blazer dress, fashion editorial composition.\"], [\"Piano lounge\", \"Beside a piano in a quiet lounge, burgundy cocktail dress, amber lamps and soft shadows.\"]];\nreturn scenes.slice(0, count).map(([sceneName, scene], i) => ({json: {sceneNumber: i + 1, sceneName, referenceImageUrl, maxCostUsd, batchMaxCostUsd: Number((count * maxCostUsd).toFixed(4)), prompt: 'Use the same adult fictional character as the reference. Keep the face, hairstyle and overall build recognisable; change the setting and outfit. ' + scene + ' Fashion photography, complete clothing, natural anatomy.'}, pairedItem: {item: 0}}));"
      },
      "position": [
        450,
        0
      ]
    },
    {
      "id": "48c9c09b-fa69-40ea-8c0f-e6e96bd8f5df",
      "name": "Generate Scene",
      "type": "@spicyapi/n8n-nodes-spicyapi.spicyapiAi",
      "typeVersion": 1,
      "parameters": {
        "resource": "image",
        "operation": "generate",
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "alibaba/qwen-image-2.1/edit"
        },
        "prompt": "={{ $json.prompt }}",
        "mediaInputs": {
          "input": [
            {
              "field": "image_urls",
              "source": "url",
              "url": "={{ $json.referenceImageUrl }}"
            }
          ]
        },
        "modelParameters": {
          "mappingMode": "defineBelow",
          "value": {
            "aspect_ratio": "3:4",
            "output_format": "png",
            "resolution": "1k",
            "enable_prompt_expansion": false,
            "background": "opaque"
          },
          "schema": []
        },
        "additionalParameters": "{}",
        "maxCost": "={{ $('Settings').first().json.maxCostUsd }}",
        "generateOptions": {
          "waitForCompletion": false,
          "downloadOutput": false
        }
      },
      "position": [
        700,
        0
      ],
      "credentials": {
        "spicyapiAiApi": {
          "id": "",
          "name": "Select your SpicyAPI credential"
        }
      },
      "continueOnFail": true
    },
    {
      "id": "9f16a918-e231-44a9-83be-9e27cf2b42d5",
      "name": "Get Scene Result",
      "type": "@spicyapi/n8n-nodes-spicyapi.spicyapiAi",
      "typeVersion": 1,
      "parameters": {
        "resource": "task",
        "operation": "get",
        "taskId": "={{ $json.taskId || '' }}",
        "taskOptions": {
          "waitForCompletion": true,
          "maxWaitTime": 300,
          "downloadOutput": true,
          "binaryPropertyName": "data"
        }
      },
      "position": [
        930,
        0
      ],
      "credentials": {
        "spicyapiAiApi": {
          "id": "",
          "name": "Select your SpicyAPI credential"
        }
      },
      "continueOnFail": true
    },
    {
      "id": "4b72b340-6581-4d2e-b93d-bbd762db821a",
      "name": "Photo Set Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const inputs = $('Build Scenes').all();\nreturn $input.all().map((item, index) => {\n  const accepted = $('Generate Scene').all()[index]?.json || {};\n  const t = item.json;\n  const taskId = t.taskId || accepted.taskId || '';\n  const failure = String(t.error || t.errorMessage || accepted.error || 'Retrieve this task before creating another image.');\n  const ready = t.state === 'succeeded' && !!item.binary?.data;\n  return {json: {sceneNumber: inputs[index].json.sceneNumber, sceneName: inputs[index].json.sceneName, status: ready ? 'done' : 'needs_review', taskId, resultUrl: t.output?.assets?.find(a => a.url && String(a.mime || '').startsWith('image/'))?.url || '', cost: t.cost || '', error: ready ? '' : failure + (taskId ? '' : ' Inspect the saved execution and console before creating another task.'), batchMaxCostUsd: inputs[index].json.batchMaxCostUsd}, ...(item.binary ? {binary: item.binary} : {}), pairedItem: {item: index}};\n});"
      },
      "position": [
        1190,
        0
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Build Scenes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Scenes": {
      "main": [
        [
          {
            "node": "Generate Scene",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Scene": {
      "main": [
        [
          {
            "node": "Get Scene Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Scene Result": {
      "main": [
        [
          {
            "node": "Photo Set Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 4200
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
