{
  "id": "rezGqGc8BNB4IWl0",
  "name": "Smart Portfolio Archiver with AI Vision Analysis",
  "tags": [],
  "nodes": [
    {
      "id": "cfa62816-7d0c-4706-a775-c91a52d16721",
      "name": "Main Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        336
      ],
      "parameters": {
        "width": 550,
        "height": 676,
        "content": "# Smart Portfolio Archiver with AI Vision\n\nAutomate your photography workflow by organizing images using AI vision capabilities. This workflow monitors a Google Drive folder, analyzes new images with GPT-4o, and sorts them while syncing metadata to Notion.\n\n### How it works\n1. **Watch**: Detects new images uploaded to your \"Inbox\" folder.\n2. **Analyze**: GPT-4o Vision checks for NSFW content, determines the category (Portrait vs. Landscape), and generates descriptive tags.\n3. **Sort**: Moves the file to the appropriate folder based on the category.\n4. **Archive**: Creates a Notion database entry with the image details and sends a notification to Slack.\n\n### Setup steps\n1. **Credentials**: Connect Google Drive, OpenAI, Notion, and Slack.\n2. **Config**: Open the **\"Workflow Configuration\"** node and fill in your folder IDs, Notion Database ID, and Slack Channel ID.\n3. **Notion**: Ensure your database has properties for Category (Select), Description (Text), Tags (Text), and Date."
      },
      "typeVersion": 1
    },
    {
      "id": "5483d304-3e13-4195-9037-6663b47e09b1",
      "name": "Group: Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 680,
        "content": "## 1. Setup & Trigger\nConfigure your global IDs (Folders, Database, Channel) and start monitoring for new files."
      },
      "typeVersion": 1
    },
    {
      "id": "b5a94ff3-dd88-4f4c-8b7f-681772edbb71",
      "name": "Group: Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 1116,
        "height": 680,
        "content": "## 2. AI Analysis & Safety\nDownloads the image, analyzes it with GPT-4o, and performs an NSFW safety check before proceeding."
      },
      "typeVersion": 1
    },
    {
      "id": "c7fbebd3-037a-4fac-8a89-01ad4b198ac1",
      "name": "Group: Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2320,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 900,
        "height": 684,
        "content": "## 3. Categorization & Routing\nRoutes the workflow based on image category, generates context-aware tags, and moves files to their respective folders."
      },
      "typeVersion": 1
    },
    {
      "id": "25f16b83-e6d3-4370-8846-ebc20747107e",
      "name": "Group: Archive",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3264,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 1460,
        "height": 680,
        "content": "## 4. Archiving & Notification\nConsolidates metadata, creates a Notion page, updates the Google Drive description, and notifies Slack of the result."
      },
      "typeVersion": 1
    },
    {
      "id": "ea171606-06e2-4f66-a3bd-e98d265e4049",
      "name": "Watch for New Images",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        544,
        608
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "id",
          "value": "1yTbUzOVHIxHBocxY_aSuhJ5KHoTqsn2F"
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "40955cd2-c61e-4939-a504-f7761507133a",
      "name": "Workflow Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        848,
        608
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "portraitsFolderId",
              "type": "string",
              "value": "1UDvMgw8nUgLeH2zTB1uy4AgJpHb-RW6a"
            },
            {
              "id": "id-2",
              "name": "landscapesFolderId",
              "type": "string",
              "value": "1C7Taq4VQK9DFGphGrccEi81JJYVzWUUt"
            },
            {
              "id": "id-3",
              "name": "notionDatabaseId",
              "type": "string",
              "value": "2b4b691abdcc80f7b713cbcde4a65224"
            },
            {
              "id": "id-4",
              "name": "slackChannel",
              "type": "string",
              "value": "C09TPHE5C15"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "5aa8d7bb-198c-4afb-a80d-693afc8ec181",
      "name": "Download Image File",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        1248,
        608
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {
          "binaryPropertyName": "data"
        },
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "f9da398d-bc08-4c70-b894-b7db02aef4f2",
      "name": "AI Vision Analysis",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1472,
        608
      ],
      "parameters": {
        "text": "You are a professional photo archiver. Analyze the image and output ONLY a raw JSON object. Do not use markdown formatting (no ```json).\n\nOutput Fields:\n1. \"is_nsfw\" (boolean): Set to true ONLY if the image contains explicit pornography or severe gore. Swimwear, artistic nudity, or mild glamour should be false.\n2. \"category\" (string): Strictly choose \"Portrait\" or \"Landscape\". Only use \"Other\" if the image is unrecognizable (e.g., a solid black image).\n   - Rule: If a person, animal, or specific object is the main focus, choose \"Portrait\".\n   - Rule: If scenery, architecture, or a wide shot is the main focus, choose \"Landscape\".\n3. \"description\" (string): A concise description of the image in English.\n4. \"main_colors\" (array of strings): List of dominant colors.\n\nEnsure the output is valid JSON.",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "gpt-4o"
        },
        "options": {
          "detail": "auto",
          "maxTokens": 300
        },
        "resource": "image",
        "inputType": "base64",
        "operation": "analyze"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "440d4ea1-603c-43bf-9465-3f80642e47c6",
      "name": "Parse AI Response",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        608
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Retrieve input data\nconst item = $input.item.json;\n\n// Magic function: Deep search for \"AI Response\"\nfunction findAiJsonString(obj) {\n  if (!obj) return null;\n  if (typeof obj === 'string') {\n    if (obj.includes('is_nsfw') || obj.includes('```json')) {\n      return obj;\n    }\n    return null;\n  }\n  if (typeof obj === 'object') {\n    for (const key in obj) {\n      if (Object.prototype.hasOwnProperty.call(obj, key)) {\n        const result = findAiJsonString(obj[key]);\n        if (result) return result;\n      }\n    }\n  }\n  return null;\n}\n\nlet aiText = findAiJsonString(item);\n\nif (!aiText) {\n  return {\n    error: \"Could not find AI response text.\",\n    debug_input: item \n  };\n}\n\nconst cleanJson = aiText\n  .replace(/```json/g, '')\n  .replace(/```/g, '')\n  .trim();\n\ntry {\n  return JSON.parse(cleanJson);\n} catch (e) {\n  return { \n    error: \"Failed to parse JSON.\", \n    raw_text: aiText \n  };\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "7af4a7f1-54ac-4d08-a467-4071b93e0a42",
      "name": "Safety Check (NSFW Filter)",
      "type": "n8n-nodes-base.if",
      "position": [
        1904,
        608
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.is_nsfw }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2acb0242-f229-4fd9-9d3e-02d60714d7d6",
      "name": "Category Router",
      "type": "n8n-nodes-base.switch",
      "position": [
        2384,
        576
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Portrait",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c01e7ce7-b30f-4ae0-babb-0cf0bb950151",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "Portrait"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Landscape",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c239304c-cf92-463f-9f20-816543b47a25",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "Landscape"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Other",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "69b3c802-46fc-4020-9512-2001f5d6c90c",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.category }}",
                    "rightValue": "Other"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "84f6301f-0fb0-421f-8cea-89305a9465d5",
      "name": "Generate Portrait Tags",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2624,
        496
      ],
      "parameters": {
        "modelId": {
          "mode": "id",
          "value": "gpt-4o-mini"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "content": "=Input description: {{ $('Parse AI Response').item.json.description }}\n\nBased on the description above, generate 5 to 10 detailed keywords..."
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "e8d87cf9-789f-488c-8015-6b26587f5915",
      "name": "Move to Portraits Folder",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        2992,
        496
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Download Image File').item.json.id }}"
        },
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow Configuration').item.json.portraitsFolderId }}"
        },
        "operation": "move"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "d9205296-a6fd-46b1-ba73-b313c24583c0",
      "name": "Generate Landscape Tags",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2624,
        752
      ],
      "parameters": {
        "modelId": {
          "mode": "id",
          "value": "gpt-4o-mini"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "content": "=Input description: {{ $('Parse AI Response').item.json.description }}\n\nBased on the description above, generate 5 to 10 detailed keywords..."
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "5a5529cb-3618-451c-9f0a-048470985111",
      "name": "Move to Landscapes Folder",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        2992,
        752
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Download Image File').item.json.id }}"
        },
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow Configuration').item.json.landscapesFolderId }}"
        },
        "operation": "move"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "2aff7f03-d3e3-4345-bd63-8cf94845d4aa",
      "name": "Merge Routes",
      "type": "n8n-nodes-base.merge",
      "position": [
        3328,
        608
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "90af729c-bb95-42aa-b8ed-99ca23440426",
      "name": "Prepare Data for Notion",
      "type": "n8n-nodes-base.code",
      "position": [
        3632,
        608
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Function to automatically search for tag info\nfunction findTagsInNode(nodeName) {\n  try {\n    const nodeData = $(nodeName).last();\n    if (!nodeData || !nodeData.json) return null;\n\n    function searchRecursive(obj) {\n      if (!obj) return null;\n      if (typeof obj === 'string') {\n        if (obj.includes(',')) return obj;\n        return null;\n      }\n      if (Array.isArray(obj)) {\n        for (const item of obj) {\n          const found = searchRecursive(item);\n          if (found) return found;\n        }\n      }\n      if (typeof obj === 'object') {\n        if (obj.text) {\n          const found = searchRecursive(obj.text);\n          if (found) return found;\n        }\n        for (const key in obj) {\n          const found = searchRecursive(obj[key]);\n          if (found) return found;\n        }\n      }\n      return null;\n    }\n    return searchRecursive(nodeData.json);\n  } catch (e) {\n    return null;\n  }\n}\n\nconst portraitTags = findTagsInNode('Generate Portrait Tags');\nconst landscapeTags = findTagsInNode('Generate Landscape Tags');\nconst finalTags = portraitTags || landscapeTags || $('Parse AI Response').item.json.description;\n\nreturn {\n  ...$input.item.json,\n  final_tags: finalTags,\n  final_date: new Date().toISOString()\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "e9180f6c-de2e-4576-83f5-8b458b44fb8f",
      "name": "Save to Notion Database",
      "type": "n8n-nodes-base.notion",
      "position": [
        3920,
        608
      ],
      "parameters": {
        "title": "={{ $('Download Image File').item.json.name }}",
        "options": {},
        "resource": "databasePage",
        "databaseId": {
          "__rl": true,
          "mode": "id",
          "value": "=2b4b691abdcc80f7b713cbcde4a65224"
        },
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Category|select",
              "selectValue": "={{ $('Parse AI Response').item.json.category }}"
            },
            {
              "key": "Description|rich_text",
              "textContent": "={{ $('Parse AI Response').item.json.description }}"
            },
            {
              "key": "Image URL|url",
              "urlValue": "={{ $('Watch for New Images').item.json.webViewLink }}"
            },
            {
              "key": "Date|date",
              "date": "={{ $json.final_date }}",
              "timezone": "Asia/Tokyo"
            },
            {
              "key": "Tags|rich_text",
              "textContent": "={{ $json.final_tags }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "07dc3f85-b4ad-4b84-96f9-e85935e9a29d",
      "name": "Update Description API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4224,
        608
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/drive/v3/files/{{ $('Download Image File').item.json.id }}",
        "method": "PATCH",
        "options": {},
        "sendBody": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "description",
              "value": "={{ $('Parse AI Response').last().json.description }}\n\nTags: {{ $('Prepare Data for Notion').item.json.final_tags }}"
            }
          ]
        },
        "nodeCredentialType": "googleDriveOAuth2Api"
      },
      "typeVersion": 4.3
    },
    {
      "id": "3a275745-6d14-4bce-9357-6c9125ccdd1d",
      "name": "Send Success Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        4528,
        608
      ],
      "parameters": {
        "text": "=\u2705 New image archived successfully!\n\n*File:* {{ $('Download Image File').last().json.name }}\n*Category:* {{ $('Parse AI Response').last().json.category }}\n...",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow Configuration').item.json.slackChannel }}"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "f59aec90-0afa-4069-b154-0c3ea0255167",
      "name": "Send NSFW Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        2384,
        816
      ],
      "parameters": {
        "text": "=\u26a0\ufe0f NSFW CONTENT DETECTED\n\n*File:* {{ $('Download Image File').item.json.name }}\n...",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Workflow Configuration').item.json.slackChannel }}"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "6bbfbcf6-e83e-45c9-83e3-4fa3fda04c2f",
  "connections": {
    "Merge Routes": {
      "main": [
        [
          {
            "node": "Prepare Data for Notion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Category Router": {
      "main": [
        [
          {
            "node": "Generate Portrait Tags",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Landscape Tags",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Landscape Tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Response": {
      "main": [
        [
          {
            "node": "Safety Check (NSFW Filter)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Vision Analysis": {
      "main": [
        [
          {
            "node": "Parse AI Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Image File": {
      "main": [
        [
          {
            "node": "AI Vision Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Watch for New Images": {
      "main": [
        [
          {
            "node": "Workflow Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Portrait Tags": {
      "main": [
        [
          {
            "node": "Move to Portraits Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Description API": {
      "main": [
        [
          {
            "node": "Send Success Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Configuration": {
      "main": [
        [
          {
            "node": "Download Image File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Landscape Tags": {
      "main": [
        [
          {
            "node": "Move to Landscapes Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Data for Notion": {
      "main": [
        [
          {
            "node": "Save to Notion Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Notion Database": {
      "main": [
        [
          {
            "node": "Update Description API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move to Portraits Folder": {
      "main": [
        [
          {
            "node": "Merge Routes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move to Landscapes Folder": {
      "main": [
        [
          {
            "node": "Merge Routes",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Safety Check (NSFW Filter)": {
      "main": [
        [
          {
            "node": "Category Router",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send NSFW Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}