{ "name": "AI Stock Image Metadata Generator", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "stock-image-meta", "responseMode": "responseNode", "options": {} }, "id": "webhook-1", "name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ 0, 0 ] }, { "parameters": { "jsCode": "// Accept either { imageUrl } or { imageBase64 } (data URL) in the webhook body\nconst raw = $('Webhook').first().json.body || $('Webhook').first().json || {};\nlet imageRef = raw.imageUrl || raw.image_url || raw.image || null;\nif (!imageRef && raw.imageBase64) {\n // Allow raw base64 without the data: prefix - wrap it\n imageRef = raw.imageBase64.startsWith('data:') ? raw.imageBase64 : 'data:image/jpeg;base64,' + raw.imageBase64;\n}\nif (!imageRef) {\n throw new Error('No image provided. Send JSON with \"imageUrl\" (public URL) or \"imageBase64\" (data URL).');\n}\nreturn [{ json: { imageRef } }];\n" }, "id": "parse-input-1", "name": "Parse Input", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 220, 0 ] }, { "parameters": { "jsCode": "// Build the OpenAI vision request body. Prompt: stock metadata (title, description, 20-40 keywords).\nconst imageRef = $json.imageRef;\nconst body = {\n model: 'gpt-4o-mini',\n messages: [\n {\n role: 'system',\n content: 'You are a professional stock-photo metadata writer. You write metadata that makes images SELL on stock marketplaces (Adobe Stock, Shutterstock, Getty).'\n },\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: 'Analyze this image and return ONLY raw JSON (no markdown, no code fences) with exactly these fields: {\"title\": \"compelling stock title under 12 words\", \"description\": \"2-3 sentence description with subject, setting, mood, use cases\", \"keywords\": [\"20-40 lowercase keywords\", \"comma-separated array\"]}. Keywords: mix of subject, style, concept, mood, and usage keywords a buyer would search.'\n },\n { type: 'image_url', image_url: { url: imageRef } }\n ]\n }\n ],\n temperature: 0.3,\n max_tokens: 800\n};\nreturn [{ json: { requestBody: JSON.stringify(body) } }];\n" }, "id": "build-request-1", "name": "Build Vision Request", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 440, 0 ] }, { "parameters": { "method": "POST", "url": "https://api.openai.com/v1/chat/completions", "authentication": "none", "sendHeaders": true, "specifyHeaders": "keypair", "headerParameters": { "parameters": [ { "name": "Authorization", "value": "Bearer YOUR_OPENAI_API_KEY" }, { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "contentType": "raw", "rawContentType": "application/json", "body": "={{ $json.requestBody }}", "options": { "timeout": 120000 } }, "id": "vision-llm-1", "name": "Vision LLM", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 660, 0 ] }, { "parameters": { "jsCode": "// Parse the LLM response; tolerate code fences and whitespace\nlet content = $json.choices && $json.choices[0] && $json.choices[0].message\n ? $json.choices[0].message.content : String($json.content || '');\ncontent = content.replace(/```json\\s*/gi, '').replace(/```/g, '').trim();\nlet meta;\ntry {\n meta = JSON.parse(content);\n} catch (e) {\n // Fallback: extract first {...} block\n const m = content.match(/\\{[\\s\\S]*\\}/);\n meta = m ? JSON.parse(m[0]) : {};\n}\nconst title = (meta.title || 'Untitled').trim();\nconst description = (meta.description || '').trim();\nconst keywords = Array.isArray(meta.keywords) ? meta.keywords.map(k => String(k).trim()).filter(Boolean) : [];\nif (!title || !description || keywords.length === 0) {\n throw new Error('Bad LLM output: missing title/description/keywords. Got: ' + content.slice(0, 200));\n}\nreturn [{ json: { title, description, keywords, keyword_count: keywords.length } }];\n" }, "id": "parse-meta-1", "name": "Parse Metadata", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 880, 0 ] }, { "parameters": { "jsCode": "// Emit the metadata row as CSV (RFC-4180-ish quoting) + a JSON view for the response\nconst t = $json.title.replace(/\"/g, '\"\"');\nconst d = $json.description.replace(/\"/g, '\"\"');\nconst kw = $json.keywords.join('; ').replace(/\"/g, '\"\"');\nconst csvLine = `\"${t}\",\"${d}\",\"${kw}\"`;\nconst csvHeader = '\"title\",\"description\",\"keywords\"';\nreturn [{\n json: {\n title: $json.title,\n description: $json.description,\n keywords: $json.keywords,\n keyword_count: $json.keyword_count,\n csv_row: csvLine,\n csv: csvHeader + '\\n' + csvLine\n }\n}];\n" }, "id": "build-csv-1", "name": "Build CSV Row", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1100, 0 ] }, { "parameters": { "respondWith": "allIncomingItems", "responseBody": "={{ JSON.stringify({ success: true, title: $json.title, description: $json.description, keywords: $json.keywords, keyword_count: $json.keyword_count, csv: $json.csv }) }}", "options": {} }, "id": "respond-1", "name": "Respond to Webhook", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 1320, 0 ] } ], "connections": { "Webhook": { "main": [ [ { "node": "Parse Input", "type": "main", "index": 0 } ] ] }, "Parse Input": { "main": [ [ { "node": "Build Vision Request", "type": "main", "index": 0 } ] ] }, "Build Vision Request": { "main": [ [ { "node": "Vision LLM", "type": "main", "index": 0 } ] ] }, "Vision LLM": { "main": [ [ { "node": "Parse Metadata", "type": "main", "index": 0 } ] ] }, "Parse Metadata": { "main": [ [ { "node": "Build CSV Row", "type": "main", "index": 0 } ] ] }, "Build CSV Row": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "callerPolicy": "workflowsFromSameOwner", "availableInMCP": false }, "active": false }