Просмотр исходного кода

fix: rate a blog post from the picture it publishes, not from the source photo

Every post came out 18+. The flag was read from the vision analysis of
the source photograph, and the pipeline only redraws photographs the
safety gate marked NSFW - so that value is explicit every time, whatever
the anime redraw actually looks like.

A Rate The Picture node now analyses the image that is about to be
published, sitting between the download and the publish so it judges the
same file in the same chain. Tested in both directions before it was
wired in, because a rater that only ever answers "safe" would be worse
than the blanket 1 it replaces: a landscape came back none, a flagged
source came back partial with "bare midriff, visible underwear", and
another came back explicit with "visible nipples".

The nine existing posts were re-rated against their own published covers.
All ten patches returned 200 and the results agreed with what was already
set - eight do contain nudity, one does not. Nothing visibly changed;
the difference is that each flag now comes from its own picture rather
than from a rule that answered 1 for everything.

I had also claimed the one post flagged 0 was under-rated by a staleness
bug. It was not: it was set deliberately, the image has no nudity, and
the rater independently agreed. I inferred a fault from a value that was
simply correct.

The staleness it *would* have been is real elsewhere, and worth knowing:
$node[...] inside a loop holds whichever iteration finished last rather
than the one being worked on, which is why the tag flow needed its own
capture node. The rating avoids it by living in the same chain as the
file.

Also here: the three workflows of this pipeline are exported as
definitions. They live in the database, so until now a change made in the
editor left no trace in the repository and could not be read in a diff.
They are documentation, not a restore path - that is the tested database
backup on zeus. Nextcloud conversation tokens are redacted; they identify
a private room and do not belong in a repository even though they are not
passwords.
fszontagh 1 месяц назад
Родитель
Сommit
6cdc9c23d1

Разница между файлами не показана из-за своего большого размера
+ 273 - 0
deploy/workflows/35photo2anime-sdcpp.json


+ 45 - 0
deploy/workflows/README.md

@@ -0,0 +1,45 @@
+# The workflows, as definitions
+
+Workflows live in the database, not in this repository, so a change made in the
+editor leaves no trace here and cannot be reviewed in a diff. These are exports
+of the three that make up the blog pipeline, kept so the shape of them is
+readable and reviewable.
+
+They are **not a restore mechanism**. Only the definition is exported - name,
+description, active flag, nodes, connections, settings - because version numbers
+and audit stamps are the database's business and would churn on every autosave.
+Restoring a workflow means the database backup on zeus, which has been tested.
+
+**Nextcloud conversation tokens are redacted.** They identify a private chat
+room, and while they are not passwords they are not something to keep in a
+repository either. Re-enter them if you ever rebuild from these.
+
+Credential ids appear as ids. The secrets themselves are encrypted in the
+database and never leave it except to a runner.
+
+## What is here
+
+- `35photo2anime-sdcpp.json` - the pipeline. Reads two 35photo feeds, judges each
+  image, redraws the ones the gate selects, writes the post and publishes it.
+- `swf-sdccp-v2-generate-image.json` - generation, called as a sub-workflow.
+  `storeInDatabase` is an input, defaulting to true; the pipeline passes false
+  because the picture is already on the SD.cpp server and goes to the blog from
+  there.
+- `swf-zsebhoki-publish-anime-post.json` - uploads the image, creates the post,
+  then creates or reuses each tag and links it.
+
+## The content rating
+
+`is_adult` is decided by **Rate The Picture**, which analyses the image that is
+about to be published. It used to come from the analysis of the source
+photograph, and since the pipeline only redraws sources the safety gate marked
+NSFW, that value was explicit every time - so every post was flagged 18+ whatever
+the redraw actually looked like.
+
+The node sits between the download and the publish so it judges the same file in
+the same chain. That also avoids a trap worth knowing about: `$node[...]` inside
+a loop holds whichever iteration finished last, not the one being worked on, so
+anything read across nodes in a loop body has to be captured in that iteration.
+
+An unreadable rating still counts as adult. The blog requires the flag on every
+post, and under-rating on a site with an 18+ gate is the harmful direction.

+ 355 - 0
deploy/workflows/swf-sdccp-v2-generate-image.json

@@ -0,0 +1,355 @@
+{
+  "active": true,
+  "connections": [
+    {
+      "sourceNodeId": "node_1785999268136",
+      "sourceOutput": "main",
+      "targetInput": "config",
+      "targetNodeId": "node_1786002941621"
+    },
+    {
+      "sourceNodeId": "node_1786002941621",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786004423373"
+    },
+    {
+      "sourceNodeId": "node_1785999268136",
+      "sourceOutput": "main",
+      "targetInput": "config",
+      "targetNodeId": "node_1786004423373"
+    },
+    {
+      "sourceNodeId": "node_1785999216336",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786002941621"
+    },
+    {
+      "sourceNodeId": "node_1785999221583",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786002941621"
+    },
+    {
+      "sourceNodeId": "node_1785999268136",
+      "sourceOutput": "main",
+      "targetInput": "config",
+      "targetNodeId": "node_1786011832521"
+    },
+    {
+      "sourceNodeId": "node_1786004423373",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786011832521"
+    },
+    {
+      "sourceNodeId": "node_1785999268136",
+      "sourceOutput": "main",
+      "targetInput": "config",
+      "targetNodeId": "node_1786013716017"
+    },
+    {
+      "sourceNodeId": "node_1786011832521",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786029193764_8510"
+    },
+    {
+      "sourceNodeId": "node_1786029193764_8510",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786013716017"
+    },
+    {
+      "sourceNodeId": "node_1786013716017",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "node_1786003128448"
+    }
+  ],
+  "name": "[SWF] SDCCP v2 generate image",
+  "nodes": [
+    {
+      "config": {
+        "allowExtra": false,
+        "fields": [
+          {
+            "defaultValue": "",
+            "description": "What to generate",
+            "name": "prompt",
+            "required": true
+          },
+          {
+            "defaultValue": "1024",
+            "description": "Target width",
+            "name": "width",
+            "required": true
+          },
+          {
+            "defaultValue": "1024",
+            "description": "Target height",
+            "name": "height",
+            "required": true
+          },
+          {
+            "defaultValue": "<lora:ZImage/zimage_alexandrabotez_v1:0.7>",
+            "description": "Appended to the prompt. Empty for none",
+            "name": "loraTag",
+            "required": false
+          },
+          {
+            "defaultValue": "reddit/r/selfie",
+            "description": "Job title, so the queue says which workflow asked",
+            "name": "title",
+            "required": false
+          },
+          {
+            "defaultValue": "sdcpp_outputs",
+            "description": "Where the finished image is stored",
+            "name": "collection",
+            "required": false
+          },
+          {
+            "defaultValue": "2",
+            "description": "How many images",
+            "name": "batchCount",
+            "required": false
+          },
+          {
+            "defaultValue": "24",
+            "description": "How long the stored image lives. 0 keeps it for good",
+            "name": "keepForHours",
+            "required": false
+          },
+          {
+            "defaultValue": true,
+            "description": "Keep a copy of the generated image in the database. The image is already on the SD.cpp server and is fetched from there by URL, so a caller that publishes it elsewhere does not need a second copy.",
+            "name": "storeInDatabase",
+            "required": false
+          }
+        ]
+      },
+      "disabled": false,
+      "id": "node_1785999216336",
+      "name": "Workflow Input",
+      "position": {
+        "x": 320,
+        "y": 0
+      },
+      "type": "workflow-input"
+    },
+    {
+      "config": {
+        "label": "Execute"
+      },
+      "disabled": false,
+      "id": "node_1785999221583",
+      "name": "Click Trigger",
+      "position": {
+        "x": 620,
+        "y": 0
+      },
+      "type": "click-trigger"
+    },
+    {
+      "config": {
+        "settings": [
+          {
+            "name": "credentialId",
+            "value": "cred_62748b40-e9d1-4659-b175-6dbc691cc0b1"
+          },
+          {
+            "name": "serverUrl",
+            "value": "http://mulan:8077"
+          }
+        ]
+      },
+      "disabled": false,
+      "id": "node_1785999268136",
+      "name": "Configurator",
+      "position": {
+        "x": -300,
+        "y": 180
+      },
+      "type": "configurator"
+    },
+    {
+      "config": {
+        "backend": "",
+        "diffusionConvDirect": false,
+        "diffusionFlashAttn": true,
+        "eagerLoad": true,
+        "enableMmap": false,
+        "flashAttn": true,
+        "force": false,
+        "forceSdxlVaeConvScale": false,
+        "llm": "Huihui-Qwen3-4B-abliterated-v2.f16.gguf",
+        "loadWaitMs": 900000,
+        "loraApplyMode": "",
+        "maxVram": -1,
+        "modelArgs": "",
+        "modelName": "z_image_turbo_bf16.safetensors",
+        "modelType": "diffusion",
+        "nThreads": 10,
+        "options": {},
+        "paramsBackend": "*=cpu",
+        "prediction": "",
+        "rngType": "",
+        "rpcServers": "",
+        "samplerRngType": "",
+        "serverUrl": "http://localhost:8077",
+        "streamLayers": true,
+        "taePreviewOnly": false,
+        "timeout": 600000,
+        "vae": "ae.safetensors",
+        "vaeConvDirect": false,
+        "vaeFormat": "auto",
+        "weightType": "",
+        "whenDifferent": "load"
+      },
+      "disabled": false,
+      "id": "node_1786002941621",
+      "name": "SD.cpp Load Model",
+      "position": {
+        "x": 360,
+        "y": 180
+      },
+      "type": "sdcpp-model-load"
+    },
+    {
+      "config": {
+        "fields": [
+          {
+            "name": "jobId",
+            "value": "{{$node['SD.cpp Wait For Job'].jobId}}"
+          },
+          {
+            "name": "succeeded",
+            "value": "{{$node['SD.cpp Wait For Job'].succeeded}}"
+          },
+          {
+            "name": "outputs",
+            "value": "{{$node['SD.cpp Wait For Job'].outputs}}"
+          },
+          {
+            "name": "urls",
+            "value": "{{$node['SD.cpp Wait For Job'].urls}}"
+          },
+          {
+            "name": "count",
+            "value": "{{$node['SD.cpp Fetch Output'].count}}"
+          },
+          {
+            "name": "stored",
+            "value": "{{$node['SD.cpp Fetch Output'].storage}}"
+          },
+          {
+            "name": "error",
+            "value": "{{$node['SD.cpp Wait For Job'].error}}"
+          }
+        ],
+        "source": "fields"
+      },
+      "disabled": false,
+      "id": "node_1786003128448",
+      "name": "Workflow Output",
+      "position": {
+        "x": 0,
+        "y": 1260
+      },
+      "type": "workflow-output"
+    },
+    {
+      "config": {
+        "batchCount": "{{$node[\"Workflow Input\"].batchCount}}",
+        "cacheMode": "off",
+        "cfgScale": 1,
+        "clipSkip": -1,
+        "height": "{{$node[\"Workflow Input\"].height}}",
+        "prompt": "{{$node[\"Workflow Input\"].prompt}} {{$node[\"Workflow Input\"].loraTag}}",
+        "sampler": "euler",
+        "scheduler": "smoothstep",
+        "seed": 42,
+        "serverUrl": "http://localhost:8077",
+        "steps": 9,
+        "timeout": 30000,
+        "title": "{{$node[\"Workflow Input\"].title}}",
+        "width": "{{$node[\"Workflow Input\"].width}}"
+      },
+      "disabled": false,
+      "id": "node_1786004423373",
+      "name": "SD.cpp Text to Image",
+      "position": {
+        "x": 360,
+        "y": 360
+      },
+      "type": "sdcpp-txt2img"
+    },
+    {
+      "config": {
+        "failOnJobFailure": false,
+        "failOnTimeout": false,
+        "jobId": "{{$node[\"SD.cpp Text to Image\"].jobId}}",
+        "maxWait": 600000,
+        "pollInterval": 2000,
+        "serverUrl": "http://localhost:8077",
+        "timeout": 30000
+      },
+      "disabled": false,
+      "id": "node_1786011832521",
+      "name": "SD.cpp Wait For Job",
+      "position": {
+        "x": 140,
+        "y": 540
+      },
+      "type": "sdcpp-job-wait"
+    },
+    {
+      "config": {
+        "collection": "{{$node[\"Workflow Input\"].collection}}",
+        "limit": 0,
+        "paths": [
+          "{{$node[\"SD.cpp Wait For Job\"].outputs}}"
+        ],
+        "serverUrl": "http://localhost:8077",
+        "storageTtlHours": "{{$node[\"Workflow Input\"].keepForHours}}",
+        "storeInDatabase": "{{String($node['Workflow Input'].storeInDatabase) !== 'false'}}",
+        "timeout": 120000
+      },
+      "disabled": false,
+      "id": "node_1786013716017",
+      "name": "SD.cpp Fetch Output",
+      "position": {
+        "x": 0,
+        "y": 900
+      },
+      "type": "sdcpp-fetch-output"
+    },
+    {
+      "config": {
+        "duration": 500,
+        "unit": "milliseconds"
+      },
+      "disabled": false,
+      "id": "node_1786029193764_8510",
+      "name": "Wait",
+      "position": {
+        "x": 140,
+        "y": 720
+      },
+      "type": "wait"
+    }
+  ],
+  "settings": {
+    "errorWorkflowId": "wf_766fcbcb-6345-4e9c-b7f8-f219505ee45c",
+    "storagePermissions": {
+      "collections": {
+        "anime_images": "read-write",
+        "files": "read-write",
+        "sdcpp_outputs": "read-write"
+      },
+      "defaultAccess": "none"
+    }
+  }
+}

+ 459 - 0
deploy/workflows/swf-zsebhoki-publish-anime-post.json

@@ -0,0 +1,459 @@
+{
+  "active": true,
+  "connections": [
+    {
+      "sourceNodeId": "input",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "prep"
+    },
+    {
+      "sourceNodeId": "trigger",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "prep"
+    },
+    {
+      "sourceNodeId": "prep",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "upload"
+    },
+    {
+      "sourceNodeId": "upload",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "post"
+    },
+    {
+      "sourceNodeId": "post",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "tags_prep"
+    },
+    {
+      "sourceNodeId": "tags_prep",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "tags_loop"
+    },
+    {
+      "sourceNodeId": "tags_loop",
+      "sourceOutput": "loop",
+      "targetInput": "data",
+      "targetNodeId": "tag_item"
+    },
+    {
+      "sourceNodeId": "tag_item",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "tag_create"
+    },
+    {
+      "sourceNodeId": "tag_create",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "tag_find"
+    },
+    {
+      "sourceNodeId": "tag_find",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "tag_link_old"
+    },
+    {
+      "sourceNodeId": "tag_link_old",
+      "sourceOutput": "main",
+      "targetInput": "data",
+      "targetNodeId": "tags_loop"
+    },
+    {
+      "sourceNodeId": "tags_loop",
+      "sourceOutput": "done",
+      "targetInput": "data",
+      "targetNodeId": "out"
+    }
+  ],
+  "name": "[SWF] zsebhoki - publish anime post",
+  "nodes": [
+    {
+      "config": {
+        "allowExtra": true,
+        "fields": [
+          {
+            "description": "The image itself, as the binary object an image node or a download produces",
+            "name": "file",
+            "required": true
+          },
+          {
+            "description": "Hungarian title",
+            "name": "title_hu",
+            "required": true
+          },
+          {
+            "description": "English title",
+            "name": "title_en",
+            "required": true
+          },
+          {
+            "description": "Hungarian body, markdown",
+            "name": "content_hu",
+            "required": true
+          },
+          {
+            "description": "English body, markdown",
+            "name": "content_en",
+            "required": true
+          },
+          {
+            "defaultValue": "",
+            "description": "Left empty it is made from the English title. Must be unique on the site",
+            "name": "slug",
+            "required": false
+          },
+          {
+            "defaultValue": "draft",
+            "description": "draft or published. Draft by default - publishing needs both languages written",
+            "name": "status",
+            "required": false
+          },
+          {
+            "defaultValue": "",
+            "name": "cover_alt_hu",
+            "required": false
+          },
+          {
+            "defaultValue": "",
+            "name": "cover_alt_en",
+            "required": false
+          },
+          {
+            "defaultValue": "https://8xlu0ewmcsjhdu7y.picobaas.eu",
+            "description": "The site. Change to https://zsebhoki.hu once its DNS answers",
+            "name": "baseUrl",
+            "required": false
+          },
+          {
+            "defaultValue": "png",
+            "description": "Extension for the stored image - png or jpg",
+            "name": "imageExtension",
+            "required": false
+          },
+          {
+            "defaultValue": [],
+            "description": "Tags as [{slug, name_hu, name_en}]. Each is reused if the slug already exists, then linked to the post.",
+            "name": "tags",
+            "required": false
+          },
+          {
+            "defaultValue": 0,
+            "description": "1 for 18+ content, 0 for general audience. Required by the blog on every post; it records what the content is, whatever the site-wide check is set to.",
+            "name": "is_adult",
+            "required": false
+          }
+        ]
+      },
+      "disabled": false,
+      "id": "input",
+      "name": "Workflow Input",
+      "position": {
+        "x": 0,
+        "y": 0
+      },
+      "type": "workflow-input"
+    },
+    {
+      "config": {
+        "label": "Execute"
+      },
+      "disabled": false,
+      "id": "trigger",
+      "name": "Click Trigger",
+      "position": {
+        "x": 300,
+        "y": 0
+      },
+      "type": "click-trigger"
+    },
+    {
+      "config": {
+        "fields": [
+          {
+            "name": "slug",
+            "value": "{{($node['Workflow Input'].slug || $node['Workflow Input'].title_en || 'post').toString().toLowerCase().normalize('NFD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80)}}"
+          },
+          {
+            "name": "mediaPath",
+            "value": "{{'anime/' + new Date().getFullYear() + '/' + (($node['Workflow Input'].slug || $node['Workflow Input'].title_en || 'post').toString().toLowerCase().normalize('NFD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60)) + '-' + Date.now() + '.' + ($node['Workflow Input'].imageExtension || 'png')}}"
+          },
+          {
+            "name": "contentType",
+            "value": "{{($node['Workflow Input'].imageExtension || 'png') === 'jpg' ? 'image/jpeg' : 'image/png'}}"
+          },
+          {
+            "name": "baseUrl",
+            "value": "{{$node['Workflow Input'].baseUrl}}"
+          }
+        ],
+        "mode": "keep"
+      },
+      "disabled": false,
+      "id": "prep",
+      "name": "Prepare",
+      "position": {
+        "x": 160,
+        "y": 180
+      },
+      "type": "set-fields"
+    },
+    {
+      "config": {
+        "authType": "credential",
+        "bodyFrom": "file",
+        "contentType": "other",
+        "contentTypeCustom": "{{$node['Prepare'].contentType}}",
+        "credentialId": "cred_c6b24bca-c3e9-4be5-a266-25ddc8538f36",
+        "downloadCollection": "downloads",
+        "downloadTtlHours": 24,
+        "followRedirects": true,
+        "method": "POST",
+        "responseMode": "json",
+        "retries": 2,
+        "retryDelayMs": 2000,
+        "retryMaxDelayMs": 30000,
+        "storeDownload": false,
+        "timeout": 120000,
+        "url": "{{$node['Prepare'].baseUrl}}/api/storage/media/{{$node['Prepare'].mediaPath}}?content_type={{$node['Prepare'].contentType}}"
+      },
+      "disabled": false,
+      "id": "upload",
+      "name": "Upload Image",
+      "position": {
+        "x": 160,
+        "y": 360
+      },
+      "type": "http-request"
+    },
+    {
+      "config": {
+        "authType": "credential",
+        "body": "{\n  \"slug\": \"{{$node['Prepare'].slug}}\",\n  \"is_adult\": {{Number($node['Workflow Input'].is_adult) === 1 ? 1 : 0}},\n  \"title_hu\": \"{{$node['Workflow Input'].title_hu}}\",\n  \"title_en\": \"{{$node['Workflow Input'].title_en}}\",\n  \"content_hu\": \"{{$node['Workflow Input'].content_hu}}\",\n  \"content_en\": \"{{$node['Workflow Input'].content_en}}\",\n  \"status\": \"{{$node['Workflow Input'].status || 'draft'}}\",\n  \"cover_url\": \"{{$node['Prepare'].baseUrl}}/storage/media/{{$node['Prepare'].mediaPath}}\",\n  \"cover_alt_hu\": \"{{$node['Workflow Input'].cover_alt_hu || $node['Workflow Input'].title_hu}}\",\n  \"cover_alt_en\": \"{{$node['Workflow Input'].cover_alt_en || $node['Workflow Input'].title_en}}\"\n}",
+        "contentType": "application/json",
+        "credentialId": "cred_c6b24bca-c3e9-4be5-a266-25ddc8538f36",
+        "downloadCollection": "downloads",
+        "downloadTtlHours": 24,
+        "followRedirects": true,
+        "method": "POST",
+        "responseMode": "json",
+        "retries": 1,
+        "retryDelayMs": 1000,
+        "retryMaxDelayMs": 30000,
+        "storeDownload": false,
+        "timeout": 60000,
+        "url": "{{$node['Prepare'].baseUrl}}/api/posts"
+      },
+      "disabled": false,
+      "id": "post",
+      "name": "Create Post",
+      "position": {
+        "x": 160,
+        "y": 540
+      },
+      "type": "http-request"
+    },
+    {
+      "config": {
+        "fields": [
+          {
+            "name": "ok",
+            "value": "{{$node['Create Post'].ok}}"
+          },
+          {
+            "name": "status",
+            "value": "{{$node['Create Post'].statusCode}}"
+          },
+          {
+            "name": "postId",
+            "value": "{{$node['Create Post'].body.id || ($node['Create Post'].body.data && $node['Create Post'].body.data.id) || ''}}"
+          },
+          {
+            "name": "slug",
+            "value": "{{$node['Prepare'].slug}}"
+          },
+          {
+            "name": "coverUrl",
+            "value": "{{$node['Prepare'].baseUrl}}/storage/media/{{$node['Prepare'].mediaPath}}"
+          },
+          {
+            "name": "url",
+            "value": "{{$node['Prepare'].baseUrl}}/hu/post/{{$node['Prepare'].slug}}"
+          },
+          {
+            "name": "uploadStatus",
+            "value": "{{$node['Upload Image'].statusCode}}"
+          },
+          {
+            "name": "error",
+            "value": "{{$node['Create Post'].body.error || $node['Create Post'].body.message || ''}}"
+          }
+        ],
+        "source": "fields"
+      },
+      "disabled": false,
+      "id": "out",
+      "name": "Workflow Output",
+      "position": {
+        "x": 160,
+        "y": 1800
+      },
+      "type": "workflow-output"
+    },
+    {
+      "config": {
+        "continueOnError": true,
+        "indexVariableName": "index",
+        "inputField": "data.tags",
+        "itemVariableName": "item",
+        "maxIterations": 20,
+        "outputField": "results"
+      },
+      "disabled": false,
+      "id": "tags_loop",
+      "name": "Each Tag",
+      "position": {
+        "x": 160,
+        "y": 900
+      },
+      "type": "loop"
+    },
+    {
+      "config": {
+        "authType": "credential",
+        "contentType": "application/json",
+        "credentialId": "cred_c6b24bca-c3e9-4be5-a266-25ddc8538f36",
+        "downloadCollection": "downloads",
+        "downloadTtlHours": 24,
+        "followRedirects": true,
+        "method": "GET",
+        "responseMode": "json",
+        "retries": 1,
+        "retryDelayMs": 1000,
+        "retryMaxDelayMs": 30000,
+        "storeDownload": false,
+        "timeout": 30000,
+        "url": "{{$node['Prepare'].baseUrl}}/api/tags?filter[slug]=eq.{{$node['This Tag'].slug}}"
+      },
+      "disabled": false,
+      "id": "tag_find",
+      "name": "Read Tag Back",
+      "position": {
+        "x": 200,
+        "y": 1340
+      },
+      "type": "http-request"
+    },
+    {
+      "config": {
+        "authType": "credential",
+        "body": "{\n  \"slug\": \"{{$node['This Tag'].slug}}\",\n  \"name_hu\": \"{{$node['This Tag'].name_hu}}\",\n  \"name_en\": \"{{$node['This Tag'].name_en}}\"\n}",
+        "contentType": "application/json",
+        "credentialId": "cred_c6b24bca-c3e9-4be5-a266-25ddc8538f36",
+        "downloadCollection": "downloads",
+        "downloadTtlHours": 24,
+        "failOnErrorStatus": false,
+        "followRedirects": true,
+        "method": "POST",
+        "responseMode": "json",
+        "retries": 1,
+        "retryDelayMs": 1000,
+        "retryMaxDelayMs": 30000,
+        "storeDownload": false,
+        "timeout": 30000,
+        "url": "{{$node['Prepare'].baseUrl}}/api/tags"
+      },
+      "disabled": false,
+      "id": "tag_create",
+      "name": "Create Tag",
+      "position": {
+        "x": 200,
+        "y": 1220
+      },
+      "type": "http-request"
+    },
+    {
+      "config": {
+        "authType": "credential",
+        "body": "{\n  \"post_id\": \"{{$node['Create Post'].body.id || ($node['Create Post'].body.data && $node['Create Post'].body.data.id)}}\",\n  \"tag_id\": \"{{$node['Read Tag Back'].body.data[0].id}}\"\n}",
+        "contentType": "application/json",
+        "credentialId": "cred_c6b24bca-c3e9-4be5-a266-25ddc8538f36",
+        "downloadCollection": "downloads",
+        "downloadTtlHours": 24,
+        "failOnErrorStatus": false,
+        "followRedirects": true,
+        "method": "POST",
+        "responseMode": "json",
+        "retries": 1,
+        "retryDelayMs": 1000,
+        "retryMaxDelayMs": 30000,
+        "storeDownload": false,
+        "timeout": 30000,
+        "url": "{{$node['Prepare'].baseUrl}}/api/post_tags"
+      },
+      "disabled": false,
+      "id": "tag_link_old",
+      "name": "Link Tag",
+      "position": {
+        "x": 200,
+        "y": 1480
+      },
+      "type": "http-request"
+    },
+    {
+      "config": {
+        "fields": [
+          {
+            "name": "tags",
+            "value": "{{$node['Workflow Input'].tags || []}}"
+          }
+        ],
+        "mode": "keep"
+      },
+      "disabled": false,
+      "id": "tags_prep",
+      "name": "Tags To Do",
+      "position": {
+        "x": 160,
+        "y": 720
+      },
+      "type": "set-fields"
+    },
+    {
+      "config": {
+        "fields": [
+          {
+            "name": "slug",
+            "value": "{{data.loop.item.slug}}"
+          },
+          {
+            "name": "name_hu",
+            "value": "{{data.loop.item.name_hu}}"
+          },
+          {
+            "name": "name_en",
+            "value": "{{data.loop.item.name_en}}"
+          }
+        ],
+        "mode": "set"
+      },
+      "disabled": false,
+      "id": "tag_item",
+      "name": "This Tag",
+      "position": {
+        "x": 200,
+        "y": 1080
+      },
+      "type": "set-fields"
+    }
+  ],
+  "settings": {}
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов