AutomationFlowsAI & RAG › Gemini Mockup Generator V1.1

Gemini Mockup Generator V1.1

Gemini Mockup Generator v1.1. Uses httpRequest, googleGemini, microsoftOutlook. Scheduled trigger; 98 nodes.

Cron / scheduled trigger★★★★★ complexityAI-powered98 nodesHTTP RequestGoogle GeminiMicrosoft Outlook
AI & RAG Trigger: Cron / scheduled Nodes: 98 Complexity: ★★★★★ AI nodes: yes Added:
Gemini Mockup Generator V1.1 — n8n workflow card showing HTTP Request, Google Gemini, Microsoft Outlook integration

This workflow follows the Googlegemini → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "Rmv4Ht895SiIgUOC",
  "name": "Gemini Mockup Generator v1.1",
  "active": true,
  "isArchived": false,
  "tags": [],
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Berlin",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "none",
    "executionTimeout": 600,
    "errorWorkflow": "2gTu1lSGwsONtPSH",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "createdAt": "2026-04-16T12:16:09.400Z",
  "updatedAt": "2026-07-21T16:53:37.862Z",
  "activeVersionId": "e4cba1cf-9988-48d7-a26f-7fff19c13fc5",
  "versionCreatedAt": "2026-07-21T16:53:37.868Z",
  "versionName": null,
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      },
      "id": "n01",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        1080
      ]
    },
    {
      "parameters": {
        "url": "https://api.trello.com/1/lists/659ffc4e5f8bffd67fe38265/cards",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "customFieldItems",
              "value": "true"
            },
            {
              "name": "attachments",
              "value": "true"
            },
            {
              "name": "attachment_fields",
              "value": "id,name,mimeType,date,url"
            },
            {
              "name": "fields",
              "value": "id,name,url,labels,desc,idBoard"
            }
          ]
        },
        "options": {}
      },
      "id": "n02",
      "name": "Get Quote Ready Cards",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        224,
        1080
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 10000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const cards = $input.all().map(item => item.json);\n\nconst PROCESSING_LABEL_ID = '69e72e4817206131923d4fcc';\nconst UPLOADED_LABEL_ID = '69eb6348d13470a412a6ab9b';\nconst MANUAL_REVIEW_LABEL_ID = '69ecda6ec3c70150828ccc15';\nconst DESIGN_ISSUE_LABEL_ID = '69ecfa31a36fbbc35b69e217';\nconst QC_VERIFIED_LABEL_ID = '69471d255442a0100e28a93f';\nconst QC_FAILED_LABEL_ID = '69471d337a802712e1c8d56c';\nconst MAX_RETRIES = 3;\nconst SOURCE_MOCKUP_MAX_AGE_MS = 24 * 60 * 60 * 1000;\nconst UPDATE_SOURCE_BATCH_MAX_AGE_MS = 30 * 60 * 1000;\nconst SOURCE_MOCKUP_CLOCK_SKEW_MS = 5 * 60 * 1000;\n\nfunction attachmentCreatedAtMs(attachment) {\n  const explicitDate = Date.parse(String((attachment && attachment.date) || ''));\n  if (Number.isFinite(explicitDate)) return explicitDate;\n  const id = String((attachment && attachment.id) || '');\n  if (!/^[0-9a-f]{24}$/i.test(id)) return null;\n  const objectIdTimestamp = parseInt(id.slice(0, 8), 16) * 1000;\n  return Number.isFinite(objectIdTimestamp) ? objectIdTimestamp : null;\n}\n\nfunction isSourceMockupInLatestBatch(attachment, newestSourceCreatedAtMs, maxAgeMs) {\n  const createdAtMs = attachmentCreatedAtMs(attachment);\n  const effectiveMaxAgeMs = Number.isFinite(maxAgeMs) ? maxAgeMs : SOURCE_MOCKUP_MAX_AGE_MS;\n  if (!Number.isFinite(createdAtMs) || !Number.isFinite(newestSourceCreatedAtMs)) return false;\n  return createdAtMs >= newestSourceCreatedAtMs - effectiveMaxAgeMs\n    && createdAtMs <= newestSourceCreatedAtMs + SOURCE_MOCKUP_CLOCK_SKEW_MS;\n}\nconst MAX_ACTIVE_PROCESSING_CARDS = 10;\nconst GENERATED_REGEX = /^Mockup[\\d_.]+_ai_\\d+\\.jpe?g$/i;\nconst TABLETOP_REGEX = /^(?:Tabletop|Tischgeraet|Tischger\u00e4t)(?:\\d+)?(?:_ai_\\d+)?\\.(?:jpe?g|png|webp)$/i;\nconst BROKEN_REGEX    = /^Mockup(undefined|NaN|null)\\.jpg$/i;\n// Source-Mockups vom Browser: mockup3969.jpg, mockup_1234.jpeg, mockup_0512.1916.png (mit Punkten/Underscores)\nconst SOURCE_REGEX    = /^mockup[\\d_.]+\\.(?:jpe?g|png|webp)$/i;\nconst MODERN_3D_SOURCE_VISUALIZATION_REGEX = /^mockup_\\d{4}_\\d{4}\\.jpe?g$/i;\nconst SINGLE_SOURCE_MOCKUPS = 3;\nconst MULTI_SOURCE_MOCKUPS_PER_SOURCE = 2;\nconst DEFAULT_MAX_SOURCE_MOCKUPS = 4;\nconst THREE_D_MAX_SOURCE_MOCKUPS = 12;\nconst LARGE_REQUEST_SOURCE_THRESHOLD = 4;\nconst LARGE_REQUEST_MAX_SOURCE_MOCKUPS = 40;\nconst MAX_PER_RUN = MULTI_SOURCE_MOCKUPS_PER_SOURCE * LARGE_REQUEST_MAX_SOURCE_MOCKUPS;\nconst HARD_LIMIT_TOTAL = MAX_PER_RUN;\nconst BACKBOARD_FIELD_IDS = {\n  1: '67ab333359b643271da233e1',\n  2: '67ab333c476749520e9e31ec',\n  3: '67f4fac25f71afb659744bce',\n  4: '67f4fb1601e710fe13ee8f23',\n};\nconst LIGHT_COLOR_FIELD_IDS = {\n  1: '67989cac3cb7e28e19d3dbfe',\n  2: '67989cb3c85879336601b578',\n  3: '67f4fab7590a76e3d66734ff',\n  4: '67f4fb0c593bcdebe2a99775',\n};\n\nfunction generatedSlotNumber(name) {\n  const m = String(name || '').match(/^Mockup[\\d_.]+_ai_(\\d+)\\.jpe?g$/i);\n  return m ? parseInt(m[1], 10) : null;\n}\n\nfunction isTargetGeneratedMockup(attachment) {\n  return GENERATED_REGEX.test(String((attachment && attachment.name) || ''));\n}\n\nfunction extractSourceNum(name) {\n  const m = (name || '').match(/^mockup([\\d_.]+)\\.(?:jpe?g|png|webp)$/i);\n  if (!m) return -1;\n  const digits = m[1].replace(/[^\\d]/g, '');\n  return digits ? parseInt(digits, 10) : -1;\n}\n\nfunction sourceBaseName(name) {\n  const raw = String(name || '').trim().replace(/\\.[^.]+$/i, '');\n  const match = raw.match(/^mockup([\\d_.]+)$/i);\n  if (!match) return 'Mockup' + Date.now();\n  return 'Mockup' + match[1].replace(/[^\\d_.]/g, '');\n}\n\nfunction aiMockupFileName(sourceName, variantSlot) {\n  return sourceBaseName(sourceName) + '_ai_' + String(variantSlot) + '.jpg';\n}\n\nfunction aiMockupPublicUrl(requestId, fileName) {\n  return 'https://klibiejfisijpagzkxls.supabase.co/storage/v1/object/public/pandadoc-mockups/' + encodeURIComponent(requestId || '') + '/' + encodeURIComponent(fileName);\n}\n\nfunction aiMockupNameParts(name) {\n  // Must accept every base emitted by sourceBaseName(), including dot/underscore Trello source names.\n  const m = String(name || '').match(/^(Mockup[\\d_.]+)_ai_(\\d+)\\.(?:jpe?g|png|webp)$/i);\n  if (!m) return null;\n  const index = parseInt(m[2], 10);\n  if (!Number.isFinite(index) || index < 1) return null;\n  return { base: m[1], baseKey: m[1].toLowerCase(), index };\n}\n\nfunction aiMockupNumbersBySourceBase(attachments) {\n  const map = new Map();\n  for (const attachment of attachments || []) {\n    const parts = aiMockupNameParts(attachment && attachment.name);\n    if (!parts) continue;\n    if (!map.has(parts.baseKey)) map.set(parts.baseKey, new Set());\n    map.get(parts.baseKey).add(parts.index);\n  }\n  return map;\n}\n\nfunction nextAiMockupFileNameForSource(sourceName, usedByBase, reservedByBase) {\n  const base = sourceBaseName(sourceName);\n  const baseKey = base.toLowerCase();\n  const used = usedByBase.get(baseKey) || new Set();\n  const reserved = reservedByBase.get(baseKey) || new Set();\n  let next = 1;\n  while (used.has(next) || reserved.has(next)) next++;\n  reserved.add(next);\n  reservedByBase.set(baseKey, reserved);\n  return base + '_ai_' + String(next) + '.jpg';\n}\n\nfunction canonicalSignType(text) {\n  const lower = (text || '').toLowerCase();\n  // NEONTRIP PRODUCT CONTRACT v1: an explicit non-lit title must win over generic neon wording elsewhere.\n  if (/\\b(?:3d\\s*)?(?:non[-\\s]?lit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(?:beleuchtung|licht))\\b/.test(lower)) return '3D Non-Lit';\n  if (/\\b3d\\s*frontlit\\b/.test(lower)) return '3D Frontlit';\n  if (/\\b3d\\s*backlit\\b/.test(lower)) return '3D Backlit';\n  if (/\\b3d\\s*halo\\b/.test(lower)) return '3D Halo';\n  if (/\\b3d\\b/.test(lower)) return '3D Leuchtbuchstaben';\n  if (/\\bhalo\\s*lit\\b/.test(lower) || /\\bhalo\\b/.test(lower)) return 'Halo Lit';\n  if (/\\bdouble\\s*sided\\b/.test(lower) || /\\bnasenschild\\b/.test(lower)) return 'Double Sided Lightbox';\n  if (/\\blight\\s*box\\b/.test(lower) || /\\blightbox\\b/.test(lower) || /\\bleuchtkasten\\b/.test(lower)) return 'Lightbox';\n  if (/\\bacrylic\\b/.test(lower)) return 'Acrylic LED';\n  if (/\\bfull\\s*glow\\b/.test(lower)) return 'Full Glow LED Flex';\n  if (/\\bfront\\s*glow\\b/.test(lower)) return 'Front Glow LED Flex';\n  if (/\\bback\\s*glow\\b/.test(lower)) return 'Back Glow LED Flex';\n  if (/\\bled\\s*flex\\b/.test(lower) || /\\bneon\\s*flex\\b/.test(lower)) return 'LED Flex';\n  if (/\\bled\\b/.test(lower) || /\\bneon\\b/.test(lower)) return 'LED Neon';\n  return null;\n}\n\nfunction extractModifiers(text) {\n  const lower = (text || '').toLowerCase();\n  const mods = [];\n  if (/\\buv[-\\s]?print\\b/.test(lower)) mods.push('UV_PRINT');\n  if (/\\bcut\\s*to\\s*shape\\b/.test(lower)) mods.push('CUT_TO_SHAPE');\n  if (/\\bloose\\s*letters?\\b/.test(lower) || /\\blose\\s*buchstaben\\b/.test(lower)) mods.push('LOOSE_LETTERS');\n  if (/\\bon\\s*backboard\\b/.test(lower) || /\\bauf\\s*backboard\\b/.test(lower)) mods.push('BACKBOARD');\n  return mods;\n}\n\nfunction customFieldTextById(customFields, id) {\n  const field = (customFields || []).find(item => item && item.idCustomField === id);\n  return field && field.value ? String(field.value.text || field.value.name || field.value.value || '').trim() : '';\n}\n\nfunction customFieldTextValues(customFields) {\n  return (customFields || [])\n    .map(field => field && field.value ? (field.value.text || field.value.name || field.value.value || '') : '')\n    .map(value => String(value || '').trim())\n    .filter(Boolean);\n}\n\nfunction normalizeColorText(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00df/g, 'ss')\n    .replace(/[_-]+/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nfunction colorFromTitle(value) {\n  const text = normalizeColorText(value);\n  if (/\\b(kaltweiss|cool white|cold white|6000\\s*k)\\b/i.test(text)) return 'Kaltwei\u00df';\n  if (/\\b(warmweiss|warm white|3000\\s*k)\\b/i.test(text)) return 'Warmwei\u00df';\n  if (/\\b(goldgelb|golden yellow)\\b/i.test(text)) return 'Goldgelb';\n  if (/\\b(zitronengelb|lemon yellow)\\b/i.test(text)) return 'Zitronengelb';\n  if (/\\b(gelb|yellow)\\b/i.test(text)) return 'Gelb';\n  if (/\\b(color|colour|farbe)\\s+as\\s+(logo|design)\\b/i.test(text)) return 'Wie im Logo';\n  return '';\n}\n\nfunction buildColorLockPromptBlock(value, signType) {\n  const label = String(value || '').trim();\n  if (!label) return '';\n  const color = normalizeColorText(label);\n  const product = normalizeColorText(signType);\n  const common = [\n    'COLOR LOCK - AUTHORITATIVE TRELLO VALUE: ' + label + '.',\n    'This explicit Trello color value overrides any color inferred from the source image, wall, ambient light, reflections, camera white balance or artistic color grading.',\n    'Keep this exact sign color stable in every generated view. Never shift its hue or color temperature because of the surrounding scene.',\n    'Use neutral photographic white balance so warm room lighting or sunset light does not recolor the sign.'\n  ];\n  if (/non\\s*lit|nonlit|unbeleuchtet/.test(product)) {\n    return common.concat([\n      'This product is non-illuminated: apply the requested value only as the opaque surface/paint color.',\n      'No LEDs, no glow, no halo and no emitted light.'\n    ]).join('\\n');\n  }\n  if (/kaltweiss|cool white|cold white|6000\\s*k/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as KALTWEISS / COOL WHITE at approximately 6000 Kelvin: crisp neutral-to-cool white.',\n      'Forbidden: warm white, cream, beige, yellow, amber, orange or golden light. Do not add a blue or cyan tint either.'\n    ]).join('\\n');\n  }\n  if (/warmweiss|warm white|3000\\s*k/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as WARMWEISS / WARM WHITE at approximately 3000 Kelvin: a controlled warm white light.',\n      'Forbidden: cool white, icy white, blue, cyan or neutral 6000K light. It must remain white light, not saturated yellow or orange.'\n    ]).join('\\n');\n  }\n  if (/zitronengelb|lemon yellow/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as clearly saturated bright LEMON YELLOW.',\n      'Forbidden: warm white, cream, beige, white, amber or orange. Yellow must remain visibly yellow.'\n    ]).join('\\n');\n  }\n  if (/goldgelb|golden yellow/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as clearly saturated GOLDEN YELLOW.',\n      'Forbidden: warm white, cream, beige, plain white or orange. Golden yellow must remain visibly yellow, not white light.'\n    ]).join('\\n');\n  }\n  if (/\\bgelb\\b|\\byellow\\b/.test(color)) {\n    return common.concat([\n      'Render all illuminated parts as clearly saturated YELLOW.',\n      'Forbidden: warm white, cream, beige, plain white, amber or orange. Yellow must remain visibly yellow.'\n    ]).join('\\n');\n  }\n  if (/wie im logo|wie im design|as logo|as design|color as|colour as|farbe wie/.test(color)) {\n    return common.concat([\n      'Preserve the exact single- or multi-color mapping from the supplied logo/design.',\n      'Each original colored element must keep its own hue and position; never collapse yellow into warm white or swap warm and cool white.'\n    ]).join('\\n');\n  }\n  return common.concat([\n    'Render the illuminated parts exactly in the requested color: ' + label + '.',\n    'For multi-color values, preserve every listed color and its original element-to-color mapping; do not merge or substitute colors.'\n  ]).join('\\n');\n}\n\nfunction normalizeBackboardText(value) {\n  return String(value || '')\n    .toLowerCase()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u00df/g, 'ss')\n    .trim();\n}\n\nfunction extractBackboardValue(card, customFields) {\n  const values = customFieldTextValues(customFields);\n  const explicit = values.find(value => /einzelne\\s+buchstaben|loose\\s+letters?|individual\\s+letters?|ohne\\s+(?:rueckplatte|ruckplatte|backboard|platte)|no\\s+(?:backboard|backing\\s*plate|backplate)|feinschnitt|formzuschnitt|rechteck|rectang|square|quadratisch|viereck|kontur|contour|cut\\s*to\\s*shape|cut-to-shape|shape\\s*cut|rueckplatte|ruckplatte|backboard|backing\\s*plate|backplate|platte/i.test(value));\n  if (explicit) return explicit;\n  const desc = String((card && card.desc) || '');\n  const match = desc.match(/(?:Rueckplatte|R\u00fcckplatte|Backboard|Zuschnitt)\\s*:\\s*([^\\n]+)/i);\n  return match ? String(match[1] || '').trim() : '';\n}\n\nfunction buildBackboardPromptBlock(backboardValue) {\n  const normalized = normalizeBackboardText(backboardValue);\n  if (/uv[-\\s]?print|uv\\s*druck|mit\\s+uv\\s*druck/i.test(normalized)) {\n    return 'NEONTRIP UV-PRINT BACKBOARD CONTRACT v2 - AUTHORITATIVE:\\n'\n      + 'The explicit UV-Print request overrides every generic clear/transparent acrylic rule.\\n'\n      + 'Preserve the exact source backboard silhouette, material, opacity and color.\\n'\n      + 'The UV-printed backing is one continuous solid opaque panel, never transparent or clear acrylic.\\n'\n      + 'If the source panel is black, keep the complete area inside its outer contour solid opaque black; never show the wall, window or scene through it.\\n'\n      + 'Only separate LED/neon lines mounted above the UV print may glow. The print and backing panel stay flat and non-illuminated.';\n  }\n  if (!normalized || /einzelne\\s+buchstaben|loose\\s+letters?|individual\\s+letters?|ohne\\s+(?:ruckplatte|rueckplatte|backboard|platte)|no\\s+(?:backboard|backing\\s*plate|backplate)/i.test(normalized)) {\n    return '3D BACKBOARD / RUECKPLATTE - EINZELNE BUCHSTABEN (DEFAULT):\\n'\n      + 'For 3D frontlit, backlit, halo and non-lit signs the default is individual loose letters without any backing plate.\\n'\n      + 'If Backboard is empty or says Einzelne Buchstaben / loose letters, render separate stainless steel/acrylic letters mounted directly to the wall.\\n'\n      + 'Do not render a rectangular plate, contour-cut acrylic panel, shared transparent sheet, white panel or any visible backboard.\\n'\n      + 'The wall must remain visible in the gaps between all letters and logo elements; only discreet hidden mounting or tiny standoffs are allowed.';\n  }\n  if (/feinschnitt|fine\\s*cut|minimal\\s*(?:acryl|acrylic)/i.test(normalized)) {\n    return 'BACKBOARD CONSTRUCTION - FEINSCHNITT:\\n'\n      + 'Do not show a large acrylic backing plate.\\n'\n      + 'Do not show a contour-cut acrylic board around the full logo.\\n'\n      + 'The neon tubes should have only minimal transparent acrylic directly behind the tubes.\\n'\n      + 'If separate letters or logo parts need support, connect them with very thin, nearly invisible transparent acrylic bridges.\\n'\n      + 'The result must still be one manufacturable sign, but with as little visible acrylic as technically possible.';\n  }\n  if (/rechteck|rectang|square|quadratisch|viereck/i.test(normalized)) {\n    return 'BACKBOARD CONSTRUCTION - RECTANGULAR ACRYLIC:\\n'\n      + 'Show the complete design mounted on one clear rectangular or square acrylic glass plate.\\n'\n      + 'Use straight clean edges and proportions that fit the design.\\n'\n      + 'Do not make the acrylic backing follow the logo contour.';\n  }\n  if (/formzuschnitt|form\\s*zuschnitt|kontur|contour|cut\\s*to\\s*shape|shape\\s*cut|cut-to-shape/i.test(normalized)) {\n    return 'BACKBOARD CONSTRUCTION - FORMZUSCHNITT / CONTOUR CUT:\\n'\n      + 'Show a transparent acrylic backing plate that roughly follows the outside contour of the logo.\\n'\n      + 'The contour-cut acrylic may be visible, but it must look clean, premium and intentionally shaped.\\n'\n      + 'Do not turn this into a large rectangular plate unless the request explicitly says rectangular backing.';\n  }\n  return '';\n}\n\nfunction extractDesignText(name) {\n  const text = String(name || '');\n  const matches = [...text.matchAll(/\u00b7\\s*Design:\\s*([^\u00b7]+)/giu)]\n    .map(match => String(match[1] || '').trim())\n    .filter(Boolean);\n  return matches.length ? matches[matches.length - 1].slice(0, 300) : '';\n}\n\nfunction stripTitlePrefixes(name) {\n  let cur = name || '';\n  let prev = null;\n  while (cur !== prev) {\n    prev = cur;\n    // Step 1: ALLE trailing \"\u00b7 Design: \u2026\" Suffixe ZUERST entfernen (auch Doppel-Append).\n    cur = cur.replace(/\\s*\u00b7\\s*Design:\\s*[^\u00b7]+$/u, '').replace(/\\s+$/, '');\n    // Step 2: Leading Status-Marker\n    cur = cur.replace(/^[\\s\\u{1F501}\\u{2705}\\u{FE0F}]+/u, '');\n    cur = cur.replace(/^\\s*AI MOCKUP FEHLER\\s*(\\[\\d+\\/\\d+\\])?[^\u00b7\\-]*[\u00b7\\-]\\s*/i, '');\n    cur = cur.replace(/^\\s*\u274c\\s*(\\[\\d+\\/\\d+\\])?[^\u00b7]*\u00b7\\s*/, '');\n    cur = cur.replace(/^\\s*\u23f8\ufe0f[^\u00b7]*\u00b7\\s*/, '');\n    cur = cur.replace(/^\\s*\u26a0\ufe0f\\s*MOCKUP\\s*PR\u00dcFEN[^\u00b7]*\u00b7\\s*/i, '');\n    cur = cur.replace(/^\\s*\ud83c\udfa8\\s*DESIGN\\s*ABWEICHUNG[^\u00b7]*\u00b7\\s*/i, '');\n  }\n  return cur;\n}\n\nconst activeProcessingCards = cards.filter(card =>\n  (card.labels || []).some(label => label.id === PROCESSING_LABEL_ID)\n);\nif (activeProcessingCards.length >= MAX_ACTIVE_PROCESSING_CARDS) {\n  return [];\n}\n\nconst validCards = [];\nlet firstInvalid = null;\n\nfor (const card of cards) {\n  const labels = card.labels || [];\n  const labelIds = labels.map(l => l.id);\n  const labelNames = labels.map(l => (l.name || '').toLowerCase());\n  const hasRetryLabel = labelNames.includes('retry') || labelNames.some(n => n.includes('video generieren'));\n\n  const attachments = card.attachments || [];\n  const cardContext = [card.name, card.desc].filter(Boolean).join(' ').toLowerCase();\n  const twoSourceMockupProduct = /\\b3d\\b|ultra\\s*thin|ultrathin|double\\s*sided|nasenschild|light\\s*box|lightbox|leuchtkasten|acrylic|full\\s*glow|front\\s*glow|back\\s*glow|non\\s*lit|nonlit|unbeleuchtet/.test(cardContext);\n  const modernSourceVisualizationCount = attachments.filter(a =>\n    a.name && a.mimeType && a.mimeType.startsWith('image/') &&\n    MODERN_3D_SOURCE_VISUALIZATION_REGEX.test(String(a.name))\n  ).length;\n\n  // 3D/Ultra-Thin/Non-Lit cards intentionally have two source visualizations per design.\n  // They still need AI mockups; do not skip them just because the two source images exist.\n  // Existing generated attachments are filtered later so re-entry does not duplicate completed slots.\n\n  const TABLETOP_TARGET_COUNT = 1;\n  const existingTabletopMockups = attachments.filter(a => a.name && TABLETOP_REGEX.test(a.name));\n  const existingTabletopNames = new Set(existingTabletopMockups.map(a => String(a.name || '').toLowerCase()));\n  const missingTabletopNumbers = [];\n  for (let tabletopNumber = 1; tabletopNumber <= TABLETOP_TARGET_COUNT; tabletopNumber++) {\n    const expectedName = 'Tabletop' + String(tabletopNumber).padStart(2, '0') + '.png';\n    if (!existingTabletopNames.has(expectedName.toLowerCase())) missingTabletopNumbers.push(tabletopNumber);\n  }\n  const needsTabletopMockup = missingTabletopNumbers.length > 0;\n  const hasMockupUploadedLabel = labelIds.includes(UPLOADED_LABEL_ID);\n\n  // Only the explicit \"Mockup Uploaded\" label is terminal for mockup generation.\n  // Other labels such as manual review, design issue or QC labels must not block Quote Ready mockups.\n  // To rerun a completed card, remove \"Mockup Uploaded\" first.\n  if (labelIds.includes(PROCESSING_LABEL_ID)) continue;\n  if (hasMockupUploadedLabel) continue;\n\n  if (attachments.some(a => a.name && BROKEN_REGEX.test(a.name))) {\n    if (!firstInvalid) {\n      firstInvalid = { json: {\n        valid: false, needsAI: false,\n        cardId: card.id, cardName: card.name, cardUrl: card.url,\n        errorReason: 'Mockupundefined.jpg gefunden \u2014 manuell aufr\u00e4umen n\u00f6tig'\n      } };\n    }\n    continue;\n  }\n\n  const allMockupFiles = attachments.filter(a =>\n    a.name && a.mimeType && a.mimeType.startsWith('image/') && SOURCE_REGEX.test(a.name)\n  );\n  const isUpdateCard = /\\bupdate\\b/i.test(String(card.name || ''));\n  const sourceBatchMaxAgeMs = isUpdateCard\n    ? UPDATE_SOURCE_BATCH_MAX_AGE_MS\n    : SOURCE_MOCKUP_MAX_AGE_MS;\n  const sourceTimestamps = allMockupFiles\n    .map(attachment => attachmentCreatedAtMs(attachment))\n    .filter(value => Number.isFinite(value));\n  const newestSourceCreatedAtMs = sourceTimestamps.length ? Math.max(...sourceTimestamps) : null;\n  const sourceBatchMockups = Number.isFinite(newestSourceCreatedAtMs)\n    ? allMockupFiles.filter(attachment =>\n        isSourceMockupInLatestBatch(attachment, newestSourceCreatedAtMs, sourceBatchMaxAgeMs)\n      )\n    : [];\n  const sourceBatchIds = new Set(sourceBatchMockups.map(attachment => attachment.id));\n  const staleSourceMockups = allMockupFiles.filter(attachment => !sourceBatchIds.has(attachment.id));\n  const totalMockupFiles = sourceBatchMockups.length;\n  const largeRequestCandidate = totalMockupFiles > LARGE_REQUEST_SOURCE_THRESHOLD;\n\n  const generatedMockups = attachments.filter(a => a.name && GENERATED_REGEX.test(a.name));\n  const existingAiNumbersByBase = aiMockupNumbersBySourceBase(generatedMockups);\n  const reservedAiNumbersByBase = new Map();\n  function existingAiCountForSource(sourceName) {\n    const baseKey = sourceBaseName(sourceName).toLowerCase();\n    return (existingAiNumbersByBase.get(baseKey) || new Set()).size;\n  }\n  let sourceMockups = sourceBatchMockups\n    .filter(a => hasRetryLabel || !GENERATED_REGEX.test(a.name))\n    .sort((a, b) => {\n      const diff = extractSourceNum(a.name) - extractSourceNum(b.name);\n      if (diff !== 0) return diff;\n      return (b.id || '').localeCompare(a.id || '');\n    });\n\n  const titleRaw = card.name || '';\n  const designText = extractDesignText(titleRaw);\n  const titleStripped = titleRaw.replace(/^[\\s\\u{1F501}\\u{2705}\\u{FE0F}]+/u, '');\n  const hasErrorMarker = titleStripped.includes('AI MOCKUP FEHLER') || /^\\s*\u274c/.test(titleStripped);\n  const hasWaitingMarker = /^\\s*\u23f8\ufe0f/.test(titleStripped);\n\n  const retryMatch = titleRaw.match(/\\[(\\d+)\\/\\d+\\]/);\n  const retryCount = retryMatch ? parseInt(retryMatch[1], 10) : (hasErrorMarker ? 1 : 0);\n  if (!hasRetryLabel && retryCount >= MAX_RETRIES) continue;\n\n  if ((hasErrorMarker || hasWaitingMarker) && !hasRetryLabel) continue;\n\n  let cardName = stripTitlePrefixes(titleRaw);\n  cardName = cardName.replace(/[\\u{1F501}]\\s*/gu, '').trim();\n  if (!cardName) cardName = titleRaw;\n\n  const cardDesc = card.desc || '';\n\n  let signType = canonicalSignType(cardName) || 'LED Neon Sign';\n  const modifiers = extractModifiers(cardName);\n\n  const promptHints = [];\n  if (signType === '3D Non-Lit') promptHints.push(\"NON-LIT / UNBELEUCHTET - HIGHEST PRIORITY PRODUCT CONTRACT: This sign has no lighting components and must remain completely unilluminated in every generated variation. No LED or neon-tube appearance, no emitted light, no halo, no glow, no backlighting and no illuminated face. Show only solid 3D material, natural daylight reflections and physically plausible contact shadows. Even if the source rendering looks bright, never interpret brightness as illumination.\");\n  if (modifiers.includes('UV_PRINT')) promptHints.push(\"NEONTRIP UV-PRINT BACKBOARD CONTRACT v2 - HIGHEST PRIORITY: The explicit UV-Print request overrides every generic transparent-acrylic or cut-to-shape rule that appears anywhere else in the prompt. Preserve the exact backboard silhouette, material, opacity and color visible in the source mockup. The UV-printed backing is one continuous solid opaque panel, not clear acrylic and not an open frame. If the source backboard is black, every pixel inside its visible outer contour that is black in the source must remain solid opaque black in every generated view; the wall or scene must never be visible through that black area. Only the separate LED/neon lines mounted above the print may emit light. UV-printed artwork and the black panel remain flat, non-transparent and non-illuminated.\");\n  if (modifiers.includes('CUT_TO_SHAPE')) promptHints.push('CUT-TO-SHAPE: Das Schild ist entlang der Design-Kontur ausgeschnitten (keine rechteckige Platte, die Aussenkontur folgt dem Design). Behalte die Schnittkante wie im Originalbild.');\n  if (modifiers.includes('LOOSE_LETTERS')) promptHints.push('LOOSE LETTERS: Einzelne Buchstaben, keine Backboard/Platte dahinter. Zwischen den Buchstaben sieht man die Wand. Abst\u00e4nde wie im Originalbild erhalten.');\n  if (modifiers.includes('BACKBOARD')) promptHints.push('BACKBOARD: Das Schild hat eine sichtbare R\u00fcckplatte / Tr\u00e4gerplatte. Form und Gr\u00f6sse wie im Originalbild.');\n\n  if (sourceMockups.length === 0) continue;\n\n  const isLargeRequest = sourceMockups.length > LARGE_REQUEST_SOURCE_THRESHOLD;\n  if (isLargeRequest && sourceMockups.length > LARGE_REQUEST_MAX_SOURCE_MOCKUPS) {\n    if (!firstInvalid) firstInvalid = { json: {\n      valid: false, needsAI: false,\n      cardId: card.id, cardName, cardUrl: card.url,\n      errorReason: sourceMockups.length + ' Ausgangsmockups erkannt; Sicherheitsgrenze ist ' + LARGE_REQUEST_MAX_SOURCE_MOCKUPS + '. Bitte Sonderfall pr\u00fcfen.'\n    } };\n    continue;\n  }\n  const maxSourceMockupsForCard = isLargeRequest\n    ? LARGE_REQUEST_MAX_SOURCE_MOCKUPS\n    : (twoSourceMockupProduct ? THREE_D_MAX_SOURCE_MOCKUPS : DEFAULT_MAX_SOURCE_MOCKUPS);\n  const selectedSources = sourceMockups.slice(0, maxSourceMockupsForCard);\n  const isStandardNeonFlex = !twoSourceMockupProduct && /(?:LED Flex|LED Neon|Front Glow LED Flex|Back Glow LED Flex)/i.test(signType) && !/full\\s*glow/i.test(cardContext);\n  const mockupsPerSelectedSource = isLargeRequest\n    ? MULTI_SOURCE_MOCKUPS_PER_SOURCE\n    : isStandardNeonFlex\n      ? 4\n      : selectedSources.length > 1\n        ? MULTI_SOURCE_MOCKUPS_PER_SOURCE\n        : SINGLE_SOURCE_MOCKUPS;\n  const currentTotal = generatedMockups.length;\n  const slotsToProcess = [];\n  let globalSlot = 1;\n\n  for (let sourceIndex = 0; sourceIndex < selectedSources.length; sourceIndex++) {\n    const source = selectedSources[sourceIndex];\n    const existingForSource = existingAiCountForSource(source.name);\n    const variantsToCreate = Math.max(0, mockupsPerSelectedSource - existingForSource);\n    for (let variantOffset = 0; variantOffset < variantsToCreate; variantOffset++) {\n      const variantSlot = (variantOffset % mockupsPerSelectedSource) + 1;\n      slotsToProcess.push({\n        slot: globalSlot++,\n        variantSlot,\n        sourceIndex: sourceIndex + 1,\n        linkedItemIndex: Math.floor(sourceIndex / (twoSourceMockupProduct ? 2 : 1)),\n        linkedItemTitle: 'Leuchtschild Design ' + String(Math.floor(sourceIndex / (twoSourceMockupProduct ? 2 : 1)) + 1),\n        source\n      });\n    }\n  }\n\n  const numToGenerate = slotsToProcess.length;\n  const finalizationOnly = slotsToProcess.length === 0 && !needsTabletopMockup;\n  if (finalizationOnly) {\n    validCards.push({ json: {\n      valid: true,\n      needsAI: false,\n      finalizationOnly: true,\n      cardId: card.id,\n      boardId: card.idBoard,\n      cardName: card.name,\n      cardUrl: card.url,\n      generatedMockupCount: generatedMockups.length,\n      existingTabletopMockupCount: existingTabletopMockups.length\n    } });\n    continue;\n  }\n\n  const ioMatch = cardDesc.match(/Indoor\\/Outdoor:\\s*(Indoor|Outdoor)/i);\n  let parsedUsage = '';\n  if (ioMatch) parsedUsage = ioMatch[1].toLowerCase() === 'indoor' ? 'Innen' : 'Au\u00dfen';\n\n  const customFields = card.customFieldItems || [];\n  const requestIdField = customFields.find(f => f.idCustomField === '67a200bd23b1ffd304e63335');\n  const requestId = requestIdField && requestIdField.value ? (requestIdField.value.text || '') : '';\n  const usageField = customFields.find(f => f.idCustomField === '67b714e5a7b5e806684eb8be');\n  const descField = customFields.find(f => f.idCustomField === '699d986489ebd38030a0d1eb');\n  const backboard = extractBackboardValue(card, customFields);\n  const backboardPromptBlock = buildBackboardPromptBlock(backboard);\n  const backboardsByIndex = {};\n  for (const [index, fieldId] of Object.entries(BACKBOARD_FIELD_IDS)) {\n    const value = customFieldTextById(customFields, fieldId);\n    if (value) backboardsByIndex[Number(index)] = value;\n  }\n  const lightColorsByIndex = {};\n  for (const [index, fieldId] of Object.entries(LIGHT_COLOR_FIELD_IDS)) {\n    const value = customFieldTextById(customFields, fieldId);\n    if (value) lightColorsByIndex[Number(index)] = value;\n  }\n  const titleColor = colorFromTitle(cardName);\n\n  let usage = (usageField && usageField.value ? usageField.value.text : '') || parsedUsage;\n  const usageNorm0 = usage.toLowerCase().trim();\n  if (usageNorm0 === 'indoor') usage = 'Innen';\n  else if (usageNorm0 === 'outdoor') usage = 'Au\u00dfen';\n  let description = descField && descField.value ? (descField.value.text || '') : '';\n\n  const errors = [];\n  const usageLower = usage.toLowerCase().trim();\n  if (!usageLower || (usageLower !== 'innen' && usageLower !== 'au\u00dfen' && usageLower !== 'aussen')) errors.push('Usage fehlt');\n  const descriptionMissing = !description || description.trim().length < 3;\n\n  if (descriptionMissing) {\n    errors.push('Description fehlt');\n  }\n  if (errors.length > 0) {\n    if (!firstInvalid) firstInvalid = { json: { valid: false, needsAI: false, cardId: card.id, cardName, cardUrl: card.url, errorReason: errors.join('; ') } };\n    continue;\n  }\n\n  let slotPlans = slotsToProcess.map(p => {\n    const fn = p.source.fileName || p.source.name;\n    const fileName = nextAiMockupFileNameForSource(p.source.name || fn, existingAiNumbersByBase, reservedAiNumbersByBase);\n    const planBackboard = backboardsByIndex[p.linkedItemIndex + 1] || backboard;\n    const planBackboardPromptBlock = buildBackboardPromptBlock(planBackboard);\n    const requestedLightColor = lightColorsByIndex[p.linkedItemIndex + 1] || titleColor;\n    const colorBlock = buildColorLockPromptBlock(requestedLightColor, signType);\n    return {\n      slot: p.slot,\n      variantSlot: p.variantSlot,\n      sourceIndex: p.sourceIndex,\n      linkedItemIndex: p.linkedItemIndex,\n      linkedItemTitle: p.linkedItemTitle,\n      requestedLightColor,\n      colorBlock,\n      backboard: planBackboard,\n      backboardPromptBlock: planBackboardPromptBlock,\n      promptHints: [promptHints.join('\\\\n\\\\n'), planBackboardPromptBlock].filter(Boolean).join('\\\\n\\\\n'),\n      sourceDownloadUrl: 'https://api.trello.com/1/cards/' + card.id + '/attachments/' + p.source.id + '/download/' + encodeURIComponent(fn),\n      sourceName: p.source.name,\n      sourceId: p.source.id,\n      sourceBaseName: sourceBaseName(p.source.name || fn),\n      aiMockupFileName: fileName,\n      aiMockupPublicUrl: aiMockupPublicUrl(requestId, fileName),\n    };\n  });\n\n  if (needsTabletopMockup) {\n    const source = selectedSources[0];\n    const fn = source.fileName || source.name;\n    for (const tabletopNumber of missingTabletopNumbers) {\n      const tabletopFileName = 'Tabletop' + String(tabletopNumber).padStart(2, '0') + '.png';\n      slotPlans.push({\n        slot: 9000 + tabletopNumber,\n        variantSlot: 98 + tabletopNumber,\n        sourceIndex: 1,\n        tabletopDevice: true,\n        outputFileName: tabletopFileName,\n        linkedItemIndex: null,\n        linkedItemTitle: 'Acryl LED Tischger\u00e4t',\n        sourceDownloadUrl: 'https://api.trello.com/1/cards/' + card.id + '/attachments/' + source.id + '/download/' + encodeURIComponent(fn),\n        sourceName: source.name,\n        sourceId: source.id,\n        sourceBaseName: 'Tabletop',\n        aiMockupFileName: tabletopFileName,\n        aiMockupPublicUrl: aiMockupPublicUrl(requestId, tabletopFileName),\n      });\n    }\n  }\n\n  validCards.push({ json: {\n    valid: true,\n    cardId: card.id,\n    boardId: card.idBoard,\n    cardName,\n    designText,\n    cardUrl: card.url,\n    cardDesc,\n    usage: usage.trim(),\n    description: description.trim(),\n    backboard,\n    backboardPromptBlock,\n    lightColorsByIndex,\n    titleColor,\n    signType,\n    modifiers,\n    promptHints: [promptHints.join('\\n\\n'), backboardPromptBlock].filter(Boolean).join('\\n\\n'),\n    mockupSlots: slotsToProcess.map(p => p.slot),\n    slotPlans,\n    totalMockupsNeeded: slotPlans.filter(plan => plan && plan.tabletopDevice !== true).length,\n    totalSlotsToProcess: slotPlans.length,\n    maxNormalMockupSlots: isLargeRequest ? selectedSources.length * mockupsPerSelectedSource : (isStandardNeonFlex ? 16 : 8),\n    isLargeRequest,\n    mockupsPerSource: mockupsPerSelectedSource,\n    sourceMockupCount: sourceMockups.length,\n    staleSourceMockupCount: staleSourceMockups.length,\n    sourceMockupFreshnessHours: sourceBatchMaxAgeMs / (60 * 60 * 1000),\n    sourceSelectionMode: isUpdateCard ? 'update_latest_batch' : 'latest_24h_batch',\n    processedSourceMockupCount: selectedSources.length,\n    totalMockupFilesOnCard: currentTotal,\n    hasRetryLabel,\n    requestId,\n    existingMockups: [],\n    existingTabletopMockups: existingTabletopMockups.map(a => ({ id: a.id, name: a.name })),\n    needsTabletopMockup,\n    isRetry: hasErrorMarker || hasWaitingMarker,\n    retryCount: hasRetryLabel ? 0 : retryCount\n  } });\n}\n\nif (validCards.length > 0) {\n  validCards.sort((a, b) => {\n    const aLarge = Boolean(a && a.json && a.json.isLargeRequest);\n    const bLarge = Boolean(b && b.json && b.json.isLargeRequest);\n    if (aLarge !== bLarge) return aLarge ? 1 : -1;\n\n    const aRetry = Boolean(a && a.json && a.json.hasRetryLabel);\n    const bRetry = Boolean(b && b.json && b.json.hasRetryLabel);\n    if (aRetry !== bRetry) return aRetry ? -1 : 1;\n\n    const aSlots = Number((a && a.json && a.json.totalSlotsToProcess) || 0);\n    const bSlots = Number((b && b.json && b.json.totalSlotsToProcess) || 0);\n    return aSlots - bSlots;\n  });\n  return [validCards[0]];\n}\nif (firstInvalid) return [firstInvalid];\nreturn [];"
      },
      "id": "n03",
      "name": "Extract & Validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        1080
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond1",
              "leftValue": "={{ $json.valid }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "n04",
      "name": "IF Valid",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        672,
        1080
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.sourceDownloadUrl }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": false,
        "options": {
          "redirect": {
            "redirect": {
              "maxRedirects": 5
            }
          },
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "n05",
      "name": "Download Mockup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1792,
        864
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "edit",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3-pro-image"
        },
        "prompt": "=NEONTRIP DESIGN LOCK v2 - HIGHEST PRIORITY:\nTreat only the actual customer sign/logo artwork in the source as an immutable design layer.\nDo not redraw, reinterpret, beautify, simplify, vectorize, typeset, correct, complete, or recreate it from memory.\nPreserve every glyph outline, counter/opening, curve, corner, serif, stroke path, endpoint, line break, kerning, spacing, icon position, component count, and relative proportion exactly.\nOnly a single uniform scale plus physically correct camera perspective may be applied to the complete design. Never deform individual letters, symbols, lines, or logo parts.\nMaterial depth, mounting, scene, shadows, and glow may be added around the locked front-face design, but must not alter its geometry.\nTechnical measurements, arrows, prices, labels, tables, and page backgrounds are not part of the design and must still be removed.\nIf exact preservation is not possible, fail without an image instead of inventing or approximating any part of the design.\n\nWICHTIGSTE REGEL:\nDas Leuchtschild aus dem Input-Bild MUSS UNVER\u00c4NDERT bleiben.\n\u00c4ndere AUSSCHLIESSLICH die Umgebung um das Schild herum.\n\nNiemals Text, Buchstaben, Schriftart, Logo, Form, Proportionen oder Design-Element des Schildes ver\u00e4ndern.\nNiemals eigenen Text hinzuf\u00fcgen oder bestehenden Text ersetzen.\nWelche Teile leuchten bzw. nicht leuchten, bleibt EXAKT wie im Input.\n\n\nWICHTIG BEI TECHNISCHEN REFERENZBILDERN:\nDas Input-Bild ist eine technische Vorlage.\nAlle Ma\u00dfe, Ziffern, Ma\u00dflinien, Pfeile, Gr\u00f6\u00dfenangaben, Preise, Codes, Labels, Raster, wei\u00dfen Zeichenfl\u00e4chen oder Zusatztexte geh\u00f6ren NICHT zum finalen Foto und m\u00fcssen vollst\u00e4ndig entfernt werden.\n\u00dcbernimm niemals die technische Zeichnung im unteren Bereich, keine Zahlen, keine cm-Angaben, keine Skizzen, keine Blueprint-Optik.\n\nNur die tats\u00e4chliche Schildform, das Logo/Design und die echte Leucht-/Materialwirkung d\u00fcrfen \u00fcbernommen werden.\n\nWenn das Input mehrere Gr\u00f6ssen-Varianten desselben Schildes nebeneinander zeigt:\nRendere NUR EIN Schild im finalen Mockup (das gr\u00f6sste / prominenteste).\nNiemals mehrere Gr\u00f6ssen nebeneinander darstellen.\n\n\nAUFGABE:\nPlatziere das Schild in einer realistischen {{ $('Extract & Validate').item.json.usage }} Umgebung\n(Ort: {{ $('Extract & Validate').item.json.description }}).\nDas Ergebnis soll wie ein hochwertiges, fotorealistisches Werbebild aussehen.\n\nSZENARIO JE MOCKUP:\n{{ (() => {\n  const slot = Number($('Loop Over Slots').item.json.variantSlot || $('Loop Over Slots').item.json.mockupSlot || $json.variantSlot || $json.mockupSlot || 1);\n  const photoAnchor = `\n- echtes hochwertiges Werbefoto, keine 3D-Visualisierung, kein CGI, kein Konzeptbild, kein Render-Look\n- Schild gross, nah und klar lesbar; die Umgebung ist nur ruhiger Kontext\n- keine Totale, keine Weitwinkelaufnahme, keine Menschenmenge, keine Banner, keine erfundenen Texte`;\n  const rawUsageTop = String($('Extract & Validate').item.json.usage || '').toLowerCase();\n  const rawDescriptionTop = String($('Extract & Validate').item.json.description || '').toLowerCase();\n  const rawSignTypeTop = String($('Extract & Validate').item.json.signType || '').toLowerCase();\n  const rawCardNameTop = String($('Extract & Validate').item.json.cardName || '').toLowerCase();\n  const neonContextTop = [rawUsageTop, rawDescriptionTop, rawSignTypeTop, rawCardNameTop].join(' ');\n  const isOutdoorTop = /(au\u00dfen|aussen|outdoor|outside|exterior|fassade|ladenfront|storefront|au\u00dfenbereich|aussenbereich)/i.test(neonContextTop);\n  const isExcludedProductTop = /\\b3d\\b|frontlit|backlit|halo|full\\s*glow|ultra\\s*thin|ultrathin|light\\s*box|lightbox|leuchtkasten|double\\s*sided|nasenschild|non\\s*lit|nonlit|unbeleuchtet/i.test(neonContextTop);\n  const isStandardNeonFlexTop = !isExcludedProductTop && /(led\\s*flex|neon\\s*flex|led\\s*neon|neonschild|neon\\s*sign|front\\s*glow|back\\s*glow)/i.test(neonContextTop);\n  if (isStandardNeonFlexTop) {\n    const indoorScenarios = {\n      1: `MOCKUP 01 - WANDMONTAGE IM SETTING:\\n${photoAnchor}\\n- Description und Usage bestimmen den Ort, z.B. Indoor-Surfhalle, Restaurant, Studio, Praxis oder Shop\\n- Schild ist sauber an einer passenden Innenwand montiert, nicht freischwebend, keine Kabel sichtbar\\n- reales hochwertiges Foto, Schild vollst\u00e4ndig sichtbar und klar lesbar`,\n      2: `MOCKUP 02 - ABGEH\u00c4NGT VOR FENSTER/GLAS:\\n${photoAnchor}\\n- im selben Setting aus der Description, aber mit Drahtseilen sauber vor einem Fenster, Glasbereich oder heller Raum\u00f6ffnung abgeh\u00e4ngt\\n- Drahtseile wirken realistisch und dezent; keine Kabel, kein Netzteil, kein Chaos\\n- Schild vollst\u00e4ndig sichtbar, nicht zu seitlich, hochwertiges echtes Foto`,\n      3: `MOCKUP 03 - FRONTALE ANSICHT:\\n${photoAnchor}\\n- relativ frontal, nur minimale Perspektive, damit Logo/Text maximal klar lesbar bleibt\\n- Setting weiterhin aus Description und Usage ableiten\\n- keine extreme Seitenansicht, kein schr\u00e4ger Winkel bei dem man das Design kaum erkennt`,\n      4: `MOCKUP 04 - MOOSWAND:\\n${photoAnchor}\\n- hochwertige echte Indoor-Naturmooswand oder Pflanzenwand, aber nur als ruhiger Hintergrund\\n- Schild fest auf der Mooswand montiert, nicht schwebend, keine Kabel sichtbar\\n- realistisches Premium-Foto, kein k\u00fcnstlicher Render-Look`\n    };\n    const outdoorScenarios = {\n      1: `MOCKUP 01 - AUSSEN AN HAUSWAND/FASSADE:\\n${photoAnchor}\\n- Usage Outdoor/Au\u00dfen hat Vorrang: echte Au\u00dfenfassade, Hauswand, Ladenfront, Eingang oder Geb\u00e4udewand\\n- Schild wetterfest und glaubw\u00fcrdig an der Wand montiert, keine Mooswand, kein Innenraum\\n- reale Tageslicht- oder Abendstimmung, klare Lesbarkeit`,\n      2: `MOCKUP 02 - AUSSEN \u00dcBER LADENLOKAL/EINGANG:\\n${photoAnchor}\\n- passend zur Description als Au\u00dfenwerbung \u00fcber T\u00fcr, Schaufenster, Eingang oder Ladenfront\\n- Schild ist vollst\u00e4ndig sichtbar, prominent und kaufentscheidend inszeniert\\n- keine Menschenmenge, keine erfundenen Banner, keine zus\u00e4tzlichen Texte`,\n      3: `MOCKUP 03 - FRONTALE AUSSENANSICHT:\\n${photoAnchor}\\n- relativ frontal auf Au\u00dfenwand oder Fassade, damit Logo/Text maximal klar lesbar bleibt\\n- keine extreme Seitenansicht, keine Perspektive bei der man das Design kaum erkennt\\n- reale Schatten, Wandkontakt und wetterfeste Montage`,\n      4: `MOCKUP 04 - OUTDOOR-EVENT / AUSSENKONTEXT:\\n${photoAnchor}\\n- wenn die Description Event, Festival, Terrasse, Outdoor-Bar oder Veranstaltung nahelegt: hochwertiger Outdoor-Event-Kontext\\n- sonst hochwertige Au\u00dfenwand/Fassade als sichere Alternative\\n- keine Mooswand, kein Innenraum, keine Menschenmenge; Schild bleibt Hauptmotiv`\n    };\n    const selectedScenarios = isOutdoorTop ? outdoorScenarios : indoorScenarios;\n    return selectedScenarios[slot] || selectedScenarios[4];\n  }\n  const isDoubleSidedTop = /double\\s*sided|nasenschild/i.test(neonContextTop);\n  const isUltraThinTop = /ultra\\s*thin|ultrathin|acrylic\\s*lightbox|acryl\\s*lightbox/i.test(neonContextTop);\n  const isLightboxTop = !isDoubleSidedTop && !isUltraThinTop && /light\\s*box|lightbox|leuchtkasten/i.test(neonContextTop);\n  const isFullGlowTop = /full\\s*glow/i.test(neonContextTop);\n  const is3dTop = /\\b3d\\b|leuchtbuchstaben|buchstaben/i.test(neonContextTop);\n  const isNonLitTop = /non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(beleuchtung|licht)/i.test(neonContextTop);\n  const isBacklitTop = /backlit|r\u00fcckbeleuchtet|rueckbeleuchtet|halo/i.test(neonContextTop);\n  const isFrontlitTop = /frontlit|frontbeleuchtet/i.test(neonContextTop);\n  if (isDoubleSidedTop) {\n    const scenes = {\n      1: `DOUBLE SIDED LIGHTBOX - AUSSEN NASENSCHILD FRONT: ${photoAnchor}\\n- echtes beidseitiges Nasenschild au\u00dfen an einer Ladenfront, seitliche stabile Wandhalterung sichtbar\\n- immer Au\u00dfenkontext, niemals Mooswand, niemals Drahtseile, niemals Innenraum\\n- Schild vollst\u00e4ndig lesbar, frontal genug, echte Schatten und wetterfeste Montage`,\n      2: `DOUBLE SIDED LIGHTBOX - SEITLICHE HALTERUNG: ${photoAnchor}\\n- Kamera leicht seitlich, damit die Wandhalterung und beide leuchtenden Seiten plausibel sichtbar sind\\n- Au\u00dfenfassade, Ladenlokal oder Eingang, keine frei schwebende Box\\n- keine Zusatztexte, keine Banner, kein CGI`,\n      3: `DOUBLE SIDED LIGHTBOX - FRONTALE AUSSENANSICHT: ${photoAnchor}\\n- relativ frontal an Au\u00dfenwand oder Ladenfront, sehr klare Lesbarkeit\\n- seitliche Halterung darf sichtbar sein, Schild h\u00e4ngt physisch korrekt an der Wand\\n- keine Mooswand, keine Drahtseile`,\n      4: `DOUBLE SIDED LIGHTBOX - ABEND AUSSEN: ${photoAnchor}\\n- Au\u00dfenfassade bei D\u00e4mmerung/Abend, gleichm\u00e4\u00dfig beleuchtete beidseitige Lightbox\\n- reale Wandhalterung, kein Schweben, keine Kabel oder Netzteile`\n    };\n    return scenes[slot] || scenes[3];\n  }\n  if (isUltraThinTop || isLightboxTop || isFullGlowTop) {\n    const productName = isUltraThinTop ? 'ULTRA THIN ACRYLIC LIGHTBOX' : (isFullGlowTop ? 'FULL GLOW LIGHTBOX' : 'LIGHTBOX');\n    const scenes = {\n      1: `${productName} - FLACH AN WAND/FASSADE: ${photoAnchor}\\n- d\u00fcnner, gleichm\u00e4\u00dfig leuchtender Lichtk\u00f6rper flach an einer durchgehenden Wand oder Fassade montiert\\n- keine Drahtseile, keine Mooswand, kein frei schwebendes Schild\\n- saubere Kanten, echte Schatten, hochwertige reale Fotografie`,\n      2: `${productName} - \u00dcBER EINGANG/SHOPFRONT: ${photoAnchor}\\n- als hochwertige Au\u00dfen- oder Innenwerbung \u00fcber Eingang, Empfang oder Shopfront\\n- gleichm\u00e4\u00dfige Lichtfl\u00e4che, keine Neonr\u00f6hren, keine sichtbaren Kabel\\n- physisch plausibel befestigt`,\n      3: `${productName} - FRONTALE LESBARE ANSICHT: ${photoAnchor}\\n- relativ frontal, minimale Verzerrung, Logo/Text maximal klar lesbar\\n- vollst\u00e4ndiger Lichtkasten sichtbar, keine abgeschnittenen R\u00e4nder\\n- echtes Foto, kein Render`,\n      4: `${productName} - PREMIUM DETAILWINKEL: ${photoAnchor}\\n- milder Seitenwinkel, damit d\u00fcnne Materialtiefe sichtbar wird, aber Logo lesbar bleibt\\n- Wandkontakt und Schatten plausibel, keine Drahtseile, keine Mooswand`\n    };\n    return scenes[slot] || scenes[3];\n  }\n  if (is3dTop) {\n    const family = isNonLitTop ? '3D NON-LIT / UNBELEUCHTET' : (isBacklitTop ? '3D BACKLIT / HALO' : (isFrontlitTop ? '3D FRONTLIT' : '3D LEUCHTBUCHSTABEN'));\n    const wallMaterial = isOutdoorTop ? 'Au\u00dfenfassade, Ladenfront, Stein, Putz, Beton oder Metallfassade' : 'hochwertige Innenwand aus Putz, Beton, Stein, Holz oder Metall';\n    const scenes = {\n      1: `${family} - FESTE WANDMONTAGE: ${photoAnchor}\\n- ${wallMaterial}; alle Buchstaben physisch fest an EINER durchgehenden Wand montiert\\n- niemals Drahtseile, niemals Deckenabh\u00e4ngung, niemals freischwebend, niemals vor einer Wandkante\\n- ${isNonLitTop ? 'kein Leuchten, kein Glow, nur echte Materialoberfl\u00e4che und nat\u00fcrliche Schatten' : isBacklitTop ? 'Halo-Licht strahlt indirekt nach hinten auf die Wand, Front bleibt materialgerecht' : 'Licht strahlt nach vorne, Materialtiefe bleibt sichtbar'}`,\n      2: `${family} - MATERIALTIEFE SICHTBAR: ${photoAnchor}\\n- milder 15-25\u00b0 Seitenwinkel, Tiefe und Wandabstand sichtbar, aber Schrift/Logo klar lesbar\\n- reale Kontakt-Schatten, keine Kabel, keine Netzteile\\n- keine Mooswand au\u00dfer bei eindeutig passendem Indoor-Wellness/Spa-Kontext`,\n      3: `${family} - FRONTALE ANSICHT: ${photoAnchor}\\n- straight-on frontal view, minimale Perspektivverzerrung, vollst\u00e4ndig lesbar\\n- alle Elemente sauber an der Wand befestigt, nichts schwebt\\n- ${isNonLitTop ? 'absolut keine Leuchteffekte' : 'korrektes Lichtverhalten passend zu frontlit/backlit'}`,\n      4: `${family} - PREMIUM KONTEXT: ${photoAnchor}\\n- ${isOutdoorTop ? 'hochwertiger Au\u00dfenkontext an Fassade oder Eingang, keine Mooswand' : (isBacklitTop ? 'optional hochwertige Mooswand nur wenn es realistisch zum Segment passt, sonst Premium-Innenwand' : 'Premium-Innenwand passend zur Description, keine Drahtseile')}\\n- Foto wirkt wie echte fertige Installation, keine KI-Optik, keine erfundenen Elemente`\n    };\n    return scenes[slot] || scenes[3];\n  }\n  const scenarios = {\n    1: `MOCKUP 01 - WIE BISHER:\n- nutze die Kartenbeschreibung und Usage als wichtigste Umgebungsvorgabe\n- keine neue Sonder-Szene erzwingen, insbesondere keine Messe/Booth/Event-Szene ohne ausdr\u00fccklichen Hinweis in der Beschreibung\n- maximal fotorealistisch, nah, hochwertig, vollst\u00e4ndig sichtbar und klar lesbar`,\n    2: (() => {\n      const rawUsage = String($('Extract & Validate').item.json.usage || '').toLowerCase();\n      const rawDescription = String($('Extract & Validate').item.json.description || '').toLowerCase();\n      const rawSignType = String($('Extract & Validate').item.json.signType || '').toLowerCase();\n      const rawCardName = String($('Extract & Validate').item.json.cardName || '').toLowerCase();\n      const context = [rawUsage, rawDescription, rawSignType, rawCardName].join(' ');\n      const isOutdoor = /(au\u00dfen|aussen|outdoor|outside|exterior|fassade|ladenfront|storefront)/i.test(context);\n      const is3D = /\\b3d\\b|frontlit|backlit|halo|leuchtbuchstaben|buchstaben/i.test(context);\n      const isNonLit = /(non\\s*lit|nonlit|unbeleuchtet|nicht\\s*beleuchtet|ohne\\s*(beleuchtung|licht))/i.test(context);\n      const wellnessIndoor = /(spa|wellness|beauty|kosmetik|hotel|lobby|praxis|medical|physio|physiotherapie|salon)/i.test(context) && !isOutdoor;\n      const mossAllowed = wellnessIndoor && !isNonLit;\n\n      if (isOutdoor) {\n        return `MOCKUP 02 - OUTDOOR / FASSADE:\n${photoAnchor}\n- Usage hat Vorrang: Au\u00dfen/Outdoor bedeutet echte Au\u00dfenfassade, Ladenfront, Eingangswand, Stein-, Beton-, Putz- oder Metallfassade\n- niemals Naturmooswand, Pflanzenwand, Spa-Interior, Sofa, Showroom oder Innenraum verwenden\n- Schild ist fest und glaubw\u00fcrdig an EINER durchgehenden vertikalen Au\u00dfenwand montiert, nicht vor der Wand, nicht freischwebend\n- reale Tageslicht- oder Abend-Fassadenstimmung, wetterfeste Installation, echte Schatten und Wandkontakt\n- kein CGI, keine Architekturvisualisierung, keine erfundenen Zusatztexte`;\n      }\n\n      if (is3D && !mossAllowed) {\n        return `MOCKUP 02 - PREMIUM WANDINSTALLATION:\n${photoAnchor}\n- deutlich anderes Motiv als Mockup 01: hochwertige reale Wand aus Putz, Beton, Stein, Holz oder Metall, passend zu Ort und Branche\n- keine Naturmooswand, keine Pflanzenwand, kein gr\u00fcner Fake-Hintergrund, au\u00dfer der Kontext ist eindeutig Indoor-Wellness/Praxis/Spa\n- 3D-Elemente sind physisch fest an EINER durchgehenden vertikalen Wand montiert, mit Kontakt-Schatten, Tiefe und realem Wandabstand\n- niemals schwebend, niemals vor einer Wandkante, niemals \u00fcber T\u00fcr-/Fensterkante, niemals in offener Luft oder auf unsichtbarer Halterung\n- mild seitlicher 15-25\u00b0 Fotowinkel: Tiefe sichtbar, aber Logo/Text bleibt klar lesbar`;\n      }\n\n      return `MOCKUP 02 - PREMIUM INTERIOR:\n${photoAnchor}\n- hochwertige reale Innenwand passend zur Beschreibung; Naturmooswand nur wenn es wie eine echte, dichte, flache, vollfl\u00e4chige Indoor-Mooswand wirkt\n- Schild ist sauber und fest an einer durchgehenden Wand montiert, nicht abgeh\u00e4ngt und nicht freischwebend\n- keine Drahtseile, keine Stahlseile, keine Deckenabh\u00e4ngung, keine sichtbaren Aufh\u00e4ngeseile, keine Saugn\u00e4pfe, keine Glasmontage\n- nat\u00fcrliche Wandstruktur, hochwertige ruhige Lichtstimmung, Schild bleibt gross, nah und klar lesbar\n- keine erfundenen \u00d6ffnungszeiten, keine Preisaufkleber, keine Zusatztexte`;\n    })(),\n    3: `MOCKUP 03 - WEITERE PREMIUM-ANSICHT:\n${photoAnchor}\n- Beschreibung und Usage haben Vorrang: dieselbe Branche, derselbe Ort und dieselbe Nutzung wie aus der Kartenbeschreibung ableiten\n- keine automatische Messe-, Booth-, Event- oder Showroom-Szene; nur wenn die Kartenbeschreibung ausdr\u00fccklich Messe/Messestand/Exhibition/Booth nennt\n- hochwertiger realistischer Wand-/Raum-/Fassadenkontext passend zur Beschreibung; Schild bleibt gro\u00df, nah, vollst\u00e4ndig sichtbar und klar lesbar`\n  };\n  return scenarios[slot] || scenarios[3];\n})() }}\n\nQUALIT\u00c4T F\u00dcR DIESES EINE MOCKUP:\n- Mockup 01 ist der Qualit\u00e4tsanker: jedes weitere Mockup muss genauso realistisch, nah, hochwertig und fotografisch wirken.\n- Erzeuge ein echtes Premium-Werbefoto mit realistischer Linse, nat\u00fcrlicher Tiefensch\u00e4rfe, echten Oberfl\u00e4chen, echten Schatten und echten Lichtreflexionen.\n- Niemals 3D-Rendering, Architekturvisualisierung, Produktvisualisierung, KI-Concept-Art, flaches CGI, cartoonartige Szene oder generisches Stockfoto.\n- Die Szene nicht zu gross erz\u00e4hlen. Das Schild bleibt Hauptmotiv; Umgebung bleibt sekund\u00e4r.\n- Keine zweite Szene, keine Mehrfachansicht, kein Raster, keine Collage, keine Zusatzlogos und keine erfundenen Texte.\n\n\nKAMERA \u2014 VARIANT A:\n- milder 15\u201325\u00b0 Drei-Viertel-Winkel von LINKS\n- nat\u00fcrliche Augenh\u00f6he, realistische 35mm- oder 50mm-Fotoperspektive\n- sichtbare Tiefe (Kantendicke, Wandabstand), aber Logo/Text bleibt klar lesbar\n- realistischer Kontakt-Schatten direkt auf der Wand\n- nicht extrem seitlich, nicht gekippt, keine Perspektive bei der man das Design kaum lesen kann\n- Schild muss wie ein echtes Foto einer fertigen Installation aussehen\n\n\nUMGEBUNG & LICHT:\n- realistisches Setting passend zu {{ $('Extract & Validate').item.json.description }}\n- kein Studio-Freisteller\n- nat\u00fcrliche Raumbeleuchtung der Umgebung\n- Glow des Schildes wirkt realistisch auf Wand und Umgebung\n- leichte Tiefensch\u00e4rfe: Schild scharf, Hintergrund weich\n\n\n{{ $('Loop Over Slots').item.json.colorBlock || $('Parse Color Result').first().json.colorBlock }}\n\n\n{{ $('Loop Over Slots').item.json.promptHints || $('Extract & Validate').item.json.promptHints || '' }}\n\n\n{{ $('Loop Over Slots').item.json.backboardPromptBlock || $('Extract & Validate').item.json.backboardPromptBlock || '' }}\n\n\nNEGATIVREGELN:\nKeine Ma\u00dfzeichnung, keine cm-Zahlen, keine Pfeile, keine technischen Linien, keine wei\u00dfe Konstruktionsfl\u00e4che, keine Preis-/Code-Texte, keine Skizze aus dem unteren Referenzbereich.\n\nMONTAGE:\nSchild realistisch fest an EINER durchgehenden Wandfl\u00e4che befestigt.\nAlle Buchstaben/Logo-Elemente haben Wandkontakt, reale Kontakt-Schatten und glaubw\u00fcrdige Tiefe.\nKeine Kabel sichtbar.\nNicht schwebend, nicht freistehend, nicht vor der Wand, nicht auf unsichtbarer Halterung.\nNicht auf Glas.",
        "images": {
          "values": [
            {}
          ]
        },
        "options": {
          "binaryPropertyOutput": "mockupImage"
        }
      },
      "id": "n06",
      "name": "Gemini Image Edit (Variant A)",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1.1,
      "position": [
        2464,
        592
      ],
      "retryOnFail": false,
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond2",
              "leftValue": "={{ $json.error }}",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "n07",
      "name": "Check Gemini Result",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2688,
        672
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}/attachments",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "trelloApi",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $('Loop Over Slots').item.json.aiMockupPublicUrl }}"
            },
            {
              "name": "name",
              "value": "={{ $('Loop Over Slots').item.json.aiMockupFileName }}"
            }
          ]
        }
      },
      "id": "n10",
      "name": "Upload Mockup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2912,
        432
      ],
      "retryOnFail": true,
      "maxTries": 6,
      "waitBetweenTries": 10000,
      "credentials": {
        "trelloApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.trello.com/1/cards/{{ $('Extract & Validate').item.json.cardId }}",
        "authentication": "prede

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Gemini Mockup Generator v1.1. Uses httpRequest, googleGemini, microsoftOutlook. Scheduled trigger; 98 nodes.

Source: https://github.com/thenextmovement/daniel-AI-stuff/blob/31126ce4de99ad72e50951378d494ea707e29de3/workflows/loop-agent-hardening/backups/2026-07-21/Rmv4Ht895SiIgUOC.published-active.pre-deactivation.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets

Google Sheets, XML, HTTP Request +3
AI & RAG

Overview This is a production-grade, fully automated stock analysis system built entirely in n8n. It combines institutional-level financial analysis, dual AI model consensus, and a self-improving back

Google Sheets, XML, HTTP Request +3
AI & RAG

This workflow is a complete outbound automation system that discovers local businesses, extracts contact emails, generates personalized cold emails using AI, and runs a multi-step follow-up sequence —

Stop And Error, Google Sheets, HTTP Request +2
AI & RAG

A professional AI equity analysis automation built on n8n that transforms structured financial data and real-time news into disciplined, risk-adjusted price targets and actionable BUY/HOLD/SELL signal

Google Sheets, OpenAI, XML +3
AI & RAG

This workflow runs on a weekday evening schedule to generate tomorrow’s multi-platform social post using Google Gemini, stores the draft in NocoDB, generates a branded image, uploads it to Cloudinary,

Noco Db, Google Gemini, N8N Nodes Cloudinary +1