swf-anime-process-one-image.json 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. {
  2. "active": true,
  3. "connections": [
  4. {
  5. "sourceNodeId": "node_input",
  6. "sourceOutput": "main",
  7. "targetInput": "data",
  8. "targetNodeId": "node_ollama_vision"
  9. },
  10. {
  11. "sourceNodeId": "node_ollama_vision",
  12. "sourceOutput": "main",
  13. "targetInput": "data",
  14. "targetNodeId": "node_safety_gate"
  15. },
  16. {
  17. "sourceNodeId": "node_safety_gate",
  18. "sourceOutput": "main",
  19. "targetInput": "data",
  20. "targetNodeId": "node_build_doc"
  21. },
  22. {
  23. "sourceNodeId": "node_safety_gate",
  24. "sourceOutput": "main",
  25. "targetInput": "data",
  26. "targetNodeId": "node_prep_gen"
  27. },
  28. {
  29. "sourceNodeId": "node_prep_gen",
  30. "sourceOutput": "main",
  31. "targetInput": "data",
  32. "targetNodeId": "node_if_gen"
  33. },
  34. {
  35. "sourceNodeId": "node_if_gen",
  36. "sourceOutput": "true",
  37. "targetInput": "data",
  38. "targetNodeId": "node_call_gen"
  39. },
  40. {
  41. "sourceNodeId": "node_if_gen",
  42. "sourceOutput": "false",
  43. "targetInput": "data",
  44. "targetNodeId": "node_output"
  45. },
  46. {
  47. "sourceNodeId": "node_call_gen",
  48. "sourceOutput": "main",
  49. "targetInput": "data",
  50. "targetNodeId": "node_gen_ok"
  51. },
  52. {
  53. "sourceNodeId": "node_gen_ok",
  54. "sourceOutput": "true",
  55. "targetInput": "data",
  56. "targetNodeId": "node_blog_text"
  57. },
  58. {
  59. "sourceNodeId": "node_gen_ok",
  60. "sourceOutput": "false",
  61. "targetInput": "data",
  62. "targetNodeId": "node_output"
  63. },
  64. {
  65. "sourceNodeId": "node_blog_text",
  66. "sourceOutput": "main",
  67. "targetInput": "data",
  68. "targetNodeId": "node_blog_image"
  69. },
  70. {
  71. "sourceNodeId": "node_blog_image",
  72. "sourceOutput": "main",
  73. "targetInput": "data",
  74. "targetNodeId": "node_blog_rate"
  75. },
  76. {
  77. "sourceNodeId": "node_blog_rate",
  78. "sourceOutput": "main",
  79. "targetInput": "data",
  80. "targetNodeId": "node_blog_post"
  81. },
  82. {
  83. "sourceNodeId": "node_blog_post",
  84. "sourceOutput": "main",
  85. "targetInput": "data",
  86. "targetNodeId": "node_output"
  87. }
  88. ],
  89. "description": "The ten-node chain that turns one image into a redraw and a published post: Vision Analyse through Publish To Blog. Called by the RSS pipeline (35photo2anime - sdcpp) per item and by the upload form, so both run exactly the same steps. Input: file (binary).",
  90. "name": "[SWF] anime: process one image",
  91. "nodes": [
  92. {
  93. "config": {
  94. "allowExtra": true,
  95. "fields": [
  96. {
  97. "description": "The image, as the binary object a download or an upload produces",
  98. "name": "file",
  99. "required": true
  100. }
  101. ]
  102. },
  103. "disabled": false,
  104. "id": "node_input",
  105. "name": "Workflow Input",
  106. "position": {
  107. "x": 140,
  108. "y": 0
  109. },
  110. "type": "workflow-input"
  111. },
  112. {
  113. "config": {
  114. "authType": "credential",
  115. "baseUrl": "https://ollama.com",
  116. "credentialId": "cred_b01319ae-e31c-4545-86ee-ff655a7cc1e4",
  117. "imageField": "",
  118. "imageMode": "auto",
  119. "model": "minimax-m3:cloud",
  120. "passthroughImage": true,
  121. "responseFormat": "json",
  122. "retryCount": 3,
  123. "retryDelayMs": 3000,
  124. "retryMaxDelayMs": 30000,
  125. "skipOnError": true,
  126. "systemPrompt": "You are an image analysis service. You respond ONLY with a single raw JSON object and nothing else - no markdown fences, no explanation.\n\nRespond in exactly this JSON shape:\n{\"humanCount\": 2, \"people\": [{\"index\": 1, \"gender\": \"female\", \"apparentAge\": 24, \"isMinor\": false, \"nsfw\": true, \"exposedParts\": [\"breasts\"]}, {\"index\": 2, \"gender\": \"male\", \"apparentAge\": 30, \"isMinor\": false, \"nsfw\": false, \"exposedParts\": []}], \"nsfw\": true, \"nsfwLevel\": \"partial\", \"nsfwReason\": \"one subject is topless\", \"description\": \"a woman standing by a tall window with rain running down it, wearing an open linen shirt, one hand resting on the frame, quiet room behind her\"}\n\nRules:\n- humanCount: integer, number of visible people.\n- people: exactly humanCount entries, ordered left to right. index starts at 1.\n- people[].gender: exactly \"male\", \"female\" or \"unknown\".\n- people[].nsfw: true if THAT person shows nudity.\n- people[].exposedParts: any of \"breasts\", \"buttocks\", \"genitals\", \"torso\", \"legs\", \"underwear\". Empty array if fully clothed.\n- nsfw: true if ANY person is nsfw.\n- nsfwLevel: \"none\" if nobody is nsfw, \"partial\" if partial nudity or underwear only, \"explicit\" if genitals are visible or sexual activity is shown.\n- nsfwReason: short justification.\n- people[].apparentAge: integer best estimate of age in years.\n- people[].isMinor: true if the person appears under 18. When uncertain, answer true.\n- description: a single flowing prompt for redrawing this scene, describing subject, appearance, clothing, pose, setting and composition. Do NOT describe the medium: no \"photograph\", no \"black and white\", no camera, lens, film, exposure or depth-of-field terms, and do not name the lighting as photographic. The style is decided elsewhere and those words fight it. Plain natural language, no negatives, no lists.\n\nAlso return these keys, the same ones the description pipeline records:\n- person_count: (integer) number of people in the image. Same number as humanCount.\n- genders: (array of strings) the detected genders, one entry per person.\n- eroticism_level: (string) one of \"none\", \"low\", \"medium\", \"high\", \"explicit\".\n- is_black_and_white: (boolean)\n- face_visible: (boolean) true if any face is visible.\n- described_image_content: (string) objective description of the image for moderation logs. If a female is detected, include the phrase '18-year-old' somewhere in this string.\n",
  127. "temperature": 0,
  128. "timeoutMs": 120000,
  129. "userPrompt": "Analyse this image and return the JSON object."
  130. },
  131. "disabled": false,
  132. "id": "node_ollama_vision",
  133. "name": "Vision Analyse",
  134. "position": {
  135. "x": 140,
  136. "y": 140
  137. },
  138. "type": "ollama-chat"
  139. },
  140. {
  141. "config": {
  142. "code": "const src = input.data || input;\nconst a = (src && src.json) || {};\n\nconst DESCRIPTOR = '18-year-old';\nconst MINOR_WORDS = ['child','children','kid','kids','toddler','baby','infant','boy','girl','teen','teenager','teenage','adolescent','schoolgirl','schoolboy','minor','underage','youngster','juvenile'];\nconst MINOR_MAP = {child:'adult',children:'adults',kid:'adult',kids:'adults',toddler:'adult',baby:'adult',infant:'adult',boy:'man',girl:'woman',teen:'adult',teenager:'adult',teenage:'adult',adolescent:'adult',schoolgirl:'woman',schoolboy:'man',minor:'adult',underage:'adult',youngster:'adult',juvenile:'adult'};\nconst PERSON_NOUNS = ['woman','man','female','male','lady','gentleman','model','person','adult','figure','subject','women','men','people','persons','figures'];\n\nfunction articleFor(word) {\n return /^(8|11|18|[aeiou])/i.test(String(word)) ? 'an' : 'a';\n}\n\nfunction tidy(text) {\n let out = String(text);\n out = out.replace(new RegExp('\\\\badult\\\\s+(' + DESCRIPTOR + ')\\\\b', 'gi'), '$1');\n out = out.replace(new RegExp('\\\\b(' + DESCRIPTOR + ')\\\\s+adult\\\\b', 'gi'), '$1');\n out = out.replace(/\\s{2,}/g, ' ');\n out = out.replace(/\\b(a|an|A|An)\\s+(\\S+)/g, function (m, art, next) {\n const correct = articleFor(next);\n return (art.charAt(0) === 'A' ? correct.charAt(0).toUpperCase() + correct.slice(1) : correct) + ' ' + next;\n });\n return out;\n}\n\nfunction enforceAdult(text, humanCount) {\n if (!text) { return ''; }\n let out = String(text);\n out = out.replace(/\\b(\\d{1,2})[\\s-]*(?:year|yr)s?[\\s-]*old\\b/gi, DESCRIPTOR);\n for (let i = 0; i < MINOR_WORDS.length; i++) {\n out = out.replace(new RegExp('\\\\b' + MINOR_WORDS[i] + '\\\\b', 'gi'), MINOR_MAP[MINOR_WORDS[i]] || 'adult');\n }\n if (humanCount < 1 || out.toLowerCase().indexOf(DESCRIPTOR.toLowerCase()) !== -1) {\n return tidy(out);\n }\n for (let i = 0; i < PERSON_NOUNS.length; i++) {\n const withArticle = new RegExp('\\\\b(an?)\\\\s+(' + PERSON_NOUNS[i] + ')\\\\b', 'i');\n if (withArticle.test(out)) {\n return tidy(out.replace(withArticle, articleFor(DESCRIPTOR) + ' ' + DESCRIPTOR + ' $2'));\n }\n const bare = new RegExp('\\\\b(' + PERSON_NOUNS[i] + ')\\\\b', 'i');\n if (bare.test(out)) {\n return tidy(out.replace(bare, DESCRIPTOR + ' $1'));\n }\n }\n return tidy(out);\n}\n\nconst people = [];\nlet containsMinor = false;\nconst rawPeople = a.people && typeof a.people.length === 'number' ? a.people : [];\nfor (let i = 0; i < rawPeople.length; i++) {\n const p = rawPeople[i] || {};\n let age = Number(p.apparentAge);\n if (!(age > 0)) { age = 0; }\n let isMinor = p.isMinor === true;\n if (age > 0 && age < 18) { isMinor = true; }\n if (isMinor) { containsMinor = true; }\n const parts = [];\n if (p.exposedParts && typeof p.exposedParts.length === 'number') {\n for (let j = 0; j < p.exposedParts.length; j++) { parts.push(String(p.exposedParts[j])); }\n }\n let g = String(p.gender || '').toLowerCase();\n if (g !== 'male' && g !== 'female') { g = 'unknown'; }\n people.push({ index: Number(p.index) || i + 1, gender: g, apparentAge: age, isMinor: isMinor, nsfw: p.nsfw === true, exposedParts: parts });\n}\n\nlet humanCount = Number(a.humanCount);\nif (!(humanCount >= 0)) { humanCount = people.length; }\n\nconst analysed = src && src.success === true;\nconst blocked = containsMinor;\nconst nsfw = a.nsfw === true;\nconst description = blocked ? '' : String(a.description || '');\nconst prompt = blocked ? '' : enforceAdult(description, humanCount);\n\nreturn {\n analysed: analysed,\n humanCount: humanCount,\n people: people,\n containsMinor: containsMinor,\n blocked: blocked,\n blockReason: blocked ? 'suspected minor present' : '',\n nsfw: nsfw,\n nsfwLevel: String(a.nsfwLevel || (nsfw ? 'partial' : 'none')),\n nsfwReason: String(a.nsfwReason || ''),\n storeEligible: analysed && nsfw && !blocked,\n description: description,\n prompt: prompt,\n imageBase64: blocked ? '' : String(src.imageBase64 || ''),\n mimeType: String(src.mimeType || ''),\n sourceUrl: String(src.sourceUrl || ''),\n\n // The same fields the description pipeline records, so an image analysed\n // here and one analysed there can be compared without translating between\n // two shapes.\n person_count: Number(a.person_count) >= 0 ? Number(a.person_count) : humanCount,\n genders: a.genders && typeof a.genders.length === 'number'\n ? a.genders.map(function (g) { return String(g); })\n : people.map(function (p) { return p.gender; }),\n eroticism_level: String(a.eroticism_level || (nsfw ? 'medium' : 'none')),\n is_black_and_white: a.is_black_and_white === true,\n face_visible: a.face_visible === true,\n described_image_content: String(a.described_image_content || a.description || ''),\n\n // The reply exactly as it arrived. The named fields above are what the rest\n // of the flow reads; this is what stops the next prompt change from\n // silently dropping whatever it adds.\n analysis: a\n};\n",
  143. "timeout": 30
  144. },
  145. "disabled": false,
  146. "id": "node_safety_gate",
  147. "name": "Safety Gate + Prompt",
  148. "position": {
  149. "x": 140,
  150. "y": 260
  151. },
  152. "type": "code"
  153. },
  154. {
  155. "config": {
  156. "code": "function findDedupe(node, depth) {\n if (!node || typeof node !== 'object' || depth > 6) { return null; }\n if (Object.prototype.hasOwnProperty.call(node, 'fileId') &&\n Object.prototype.hasOwnProperty.call(node, 'checksum')) { return node; }\n const keys = Object.keys(node);\n for (let i = 0; i < keys.length; i++) {\n const found = findDedupe(node[keys[i]], depth + 1);\n if (found) { return found; }\n }\n return null;\n}\n\nfunction findAnalysis(node, depth) {\n if (!node || typeof node !== 'object' || depth > 6) { return null; }\n if (Object.prototype.hasOwnProperty.call(node, 'storeEligible')) { return node; }\n const keys = Object.keys(node);\n for (let i = 0; i < keys.length; i++) {\n const found = findAnalysis(node[keys[i]], depth + 1);\n if (found) { return found; }\n }\n return null;\n}\n\nconst doc = findAnalysis(input, 0);\nif (!doc) { throw new Error('No analysis payload found in input'); }\n\nif (!doc.storeEligible) {\n return { stored: false, reason: doc.blocked ? doc.blockReason : 'not nsfw' };\n}\n\n// The image already lives in the file store, put there once by the dedupe step\n// and deduplicated by checksum, so the record references it rather than carrying\n// a second copy of the same bytes.\nconst source = findDedupe(input, 0);\n\nconst record = {\n sourceUrl: doc.sourceUrl,\n mimeType: doc.mimeType,\n fileId: source ? source.fileId : '',\n checksum: source ? source.checksum : '',\n humanCount: doc.humanCount,\n people: doc.people,\n nsfw: doc.nsfw,\n nsfwLevel: doc.nsfwLevel,\n nsfwReason: doc.nsfwReason,\n description: doc.description,\n prompt: doc.prompt,\n\n // Everything the description pipeline stores, under the same names.\n person_count: doc.person_count,\n genders: doc.genders,\n eroticism_level: doc.eroticism_level,\n is_black_and_white: doc.is_black_and_white,\n face_visible: doc.face_visible,\n described_image_content: doc.described_image_content,\n analysis: doc.analysis,\n\n detectedAt: Date.now()\n};\n\nconst res = smartbotic.storage.insert('nsfw_images', record, null, 0);\nif (!res.success) {\n throw new Error('Failed to store NSFW record: ' + res.error);\n}\nreturn { stored: true, id: res.id, sourceUrl: doc.sourceUrl };\n",
  157. "timeout": 30
  158. },
  159. "disabled": false,
  160. "id": "node_build_doc",
  161. "name": "Store If NSFW",
  162. "position": {
  163. "x": 0,
  164. "y": 400
  165. },
  166. "type": "code"
  167. },
  168. {
  169. "config": {
  170. "code": "function findAnalysis(node, depth) {\n if (!node || typeof node !== 'object' || depth > 6) { return null; }\n if (Object.prototype.hasOwnProperty.call(node, 'storeEligible')) { return node; }\n const keys = Object.keys(node);\n for (let i = 0; i < keys.length; i++) {\n const found = findAnalysis(node[keys[i]], depth + 1);\n if (found) { return found; }\n }\n return null;\n}\n\n// One resolution, transposed to match the source orientation. Both dimensions\n// sit near 1024 and are multiples of 64, which is what Z-Image expects; the only\n// thing that varies is which way round they go.\nconst LANDSCAPE = { width: 1152, height: 896 };\nconst PORTRAIT = { width: 896, height: 1152 };\nconst SQUARE = { width: 1024, height: 1024 };\n\nfunction pickSize(srcWidth, srcHeight) {\n if (!(srcWidth > 0) || !(srcHeight > 0)) {\n return SQUARE;\n }\n if (srcWidth > srcHeight) {\n return LANDSCAPE;\n }\n if (srcHeight > srcWidth) {\n return PORTRAIT;\n }\n return SQUARE;\n}\n\nfunction measure(base64) {\n if (!base64) { return { width: 0, height: 0 }; }\n\n const path = '/tmp/smartbotic-measure-' + smartbotic.utils.uuid() + '.img';\n const written = smartbotic.fs.writeFile(path, base64, 'base64');\n if (!written || written.success === false) {\n smartbotic.log.warn('Could not write the image for measuring; falling back to square');\n return { width: 0, height: 0 };\n }\n\n let size = { width: 0, height: 0 };\n const result = smartbotic.process.exec('identify -format \"%w %h\" \"' + path + '\"', { timeout: 15000 });\n if (result && result.success && result.stdout) {\n const parts = String(result.stdout).trim().split(/\\s+/);\n size = { width: Number(parts[0]) || 0, height: Number(parts[1]) || 0 };\n } else {\n smartbotic.log.warn('identify failed: ' + ((result && result.stderr) || 'no output'));\n }\n\n smartbotic.fs.unlink(path);\n return size;\n}\n\nconst doc = findAnalysis(input, 0);\nif (!doc) { throw new Error('No analysis payload found in input'); }\n\nif (!doc.storeEligible) {\n return { generate: false, reason: doc.blocked ? doc.blockReason : 'not nsfw',\n prompt: '', seed: 0, width: 1024, height: 1024, sourceUrl: doc.sourceUrl || '' };\n}\n\nconst source = measure(doc.imageBase64);\nconst target = pickSize(source.width, source.height);\nsmartbotic.log.info('Source ' + source.width + 'x' + source.height +\n ' -> generating ' + target.width + 'x' + target.height);\n\nreturn {\n generate: true,\n reason: '',\n prompt: 'high-quality anime illustration, cel shaded, clean line art, vibrant colours, 2D, clean lineart, ' + String(doc.prompt || ''),\n // A fixed seed, so the same photo and the same prompt give the same\n // picture. A random one made every run unrepeatable, which meant a\n // result could never be compared against a change to the prompt or the\n // sampler - the seed moved at the same time as everything else.\n seed: 42,\n width: target.width,\n height: target.height,\n sourceWidth: source.width,\n sourceHeight: source.height,\n sourceUrl: doc.sourceUrl || ''\n};\n",
  171. "timeout": 30
  172. },
  173. "disabled": false,
  174. "id": "node_prep_gen",
  175. "name": "Prepare Generation",
  176. "position": {
  177. "x": 260,
  178. "y": 400
  179. },
  180. "type": "code"
  181. },
  182. {
  183. "config": {
  184. "combineWith": "and",
  185. "conditions": [
  186. {
  187. "field": "data.result.generate",
  188. "operator": "is_true",
  189. "value": ""
  190. }
  191. ]
  192. },
  193. "disabled": false,
  194. "id": "node_if_gen",
  195. "name": "Should Generate",
  196. "position": {
  197. "x": 260,
  198. "y": 520
  199. },
  200. "type": "if-condition"
  201. },
  202. {
  203. "config": {
  204. "fields": [
  205. {
  206. "name": "prompt",
  207. "value": "{{$node[\"Prepare Generation\"].result.prompt}}"
  208. },
  209. {
  210. "name": "width",
  211. "value": "{{$node[\"Prepare Generation\"].result.width}}"
  212. },
  213. {
  214. "name": "height",
  215. "value": "{{$node[\"Prepare Generation\"].result.height}}"
  216. },
  217. {
  218. "name": "loraTag",
  219. "value": ""
  220. },
  221. {
  222. "name": "title",
  223. "value": "35photo2anime"
  224. },
  225. {
  226. "name": "batchCount",
  227. "value": "1"
  228. },
  229. {
  230. "name": "keepForHours",
  231. "value": "0"
  232. },
  233. {
  234. "name": "storeInDatabase",
  235. "value": "false"
  236. }
  237. ],
  238. "inputSource": "fields",
  239. "workflowId": "wf_f7c8244a-2f8a-4a66-a3a4-62b8c63381dc"
  240. },
  241. "disabled": false,
  242. "id": "node_call_gen",
  243. "name": "Generate Image",
  244. "position": {
  245. "x": 260,
  246. "y": 660
  247. },
  248. "type": "call-workflow"
  249. },
  250. {
  251. "config": {
  252. "authType": "none",
  253. "contentType": "",
  254. "downloadCollection": "downloads",
  255. "downloadTtlHours": 24,
  256. "failOnErrorStatus": true,
  257. "followRedirects": true,
  258. "method": "GET",
  259. "responseMode": "binary",
  260. "retries": 2,
  261. "retryDelayMs": 2000,
  262. "retryMaxDelayMs": 30000,
  263. "storeDownload": false,
  264. "timeout": 60000,
  265. "url": "{{$node['Generate Image'].result.urls[0]}}"
  266. },
  267. "disabled": false,
  268. "id": "node_blog_image",
  269. "name": "Fetch Generated Image",
  270. "position": {
  271. "x": 400,
  272. "y": 1040
  273. },
  274. "type": "http-request"
  275. },
  276. {
  277. "config": {
  278. "authType": "credential",
  279. "baseUrl": "https://ollama.com",
  280. "credentialId": "cred_b01319ae-e31c-4545-86ee-ff655a7cc1e4",
  281. "imageField": "",
  282. "imageMode": "none",
  283. "model": "glm-5.2:cloud",
  284. "passthroughImage": false,
  285. "responseFormat": "json",
  286. "retryCount": 2,
  287. "retryDelayMs": 3000,
  288. "retryMaxDelayMs": 30000,
  289. "skipOnError": false,
  290. "systemPrompt": "You write short blog posts about an illustration, in Hungarian and English.\nYou are given the scene it shows. Reply with ONE raw JSON object and nothing else - no markdown fences, no commentary.\n\nShape:\n{\"title_hu\":\"...\",\"title_en\":\"...\",\"content_hu\":\"...\",\"content_en\":\"...\",\"tags\":[{\"slug\":\"...\",\"name_hu\":\"...\",\"name_en\":\"...\"}]}\n\nRules:\n- The Hungarian is the point. Write it as Hungarian, not as a word-for-word rendering of the English - natural word order, natural phrasing.\n- title: at most 60 characters, no full stop, no quotation marks.\n- content: two or three sentences of markdown about what the picture shows and its mood. Do not mention prompts, models, generation or that it was made by a machine.\n- tags: two to four, drawn from what is actually in the picture (subject, setting, mood). slug is lowercase letters, digits and hyphens only, ASCII, no accents - it is a URL. name_hu and name_en are how the tag is shown, so those keep their accents.\n- Never leave a field empty.",
  291. "temperature": 0.4,
  292. "timeoutMs": 120000,
  293. "userPrompt": "The illustration shows: {{$node['Vision Analyse'].json.description}}"
  294. },
  295. "disabled": false,
  296. "id": "node_blog_text",
  297. "name": "Write The Post",
  298. "position": {
  299. "x": 400,
  300. "y": 920
  301. },
  302. "type": "ollama-chat"
  303. },
  304. {
  305. "config": {
  306. "authType": "credential",
  307. "baseUrl": "https://ollama.com",
  308. "credentialId": "cred_b01319ae-e31c-4545-86ee-ff655a7cc1e4",
  309. "imageField": "",
  310. "imageMode": "auto",
  311. "model": "minimax-m3:cloud",
  312. "passthroughImage": true,
  313. "responseFormat": "json",
  314. "retryCount": 2,
  315. "retryDelayMs": 3000,
  316. "retryMaxDelayMs": 30000,
  317. "skipOnError": false,
  318. "systemPrompt": "You report what is visible in one illustration, so that another step can decide whether it needs an 18+ warning. Reply with ONE raw JSON object and nothing else - no markdown fences, no commentary.\n\n{\"nudity\": \"none\", \"visible\": [], \"sexualActivity\": false, \"underwear\": false, \"reason\": \"woman in a blazer, cleavage visible, nothing bare\"}\n\nReport what is drawn, not what it suggests. You are not deciding the warning, so do not lean towards caution - a wrong \"yes\" is as much an error here as a wrong \"no\".\n\n- nudity: \"full\" when a person is naked or all but naked; \"partial\" when one of the parts listed under `visible` is bare; \"none\" otherwise, however tight, short, thin or low-cut the clothing is.\n- visible: any of \"nipples\", \"genitals\", \"buttocks\", \"pubic_area\" - bare skin only, never shape or outline through fabric. Empty array when none are bare.\n- sexualActivity: true only when a sexual act is depicted.\n- underwear: true when underwear or lingerie is the visible outer layer. Recorded, but on its own it is not nudity.\n- reason: a short phrase naming what you actually saw.\n\nNone of the following is nudity. Each one gives nudity \"none\" and an empty `visible`: cleavage, a low or open neckline, a bare midriff or navel, bare arms, bare legs, bare thighs, bare shoulders, a bare back, swimwear, a leotard or bodysuit, tight or thin-looking clothing, and any pose, expression or camera angle however suggestive it seems.\n\nA stylised or cartoon drawing is reported exactly like any other image.",
  319. "temperature": 0,
  320. "timeoutMs": 120000,
  321. "userPrompt": "Report what is visible in this illustration and return the JSON object."
  322. },
  323. "disabled": false,
  324. "id": "node_blog_rate",
  325. "name": "Rate The Picture",
  326. "position": {
  327. "x": 400,
  328. "y": 1180
  329. },
  330. "type": "ollama-chat"
  331. },
  332. {
  333. "config": {
  334. "fields": [
  335. {
  336. "name": "file",
  337. "value": "{{$node['Fetch Generated Image'].file}}"
  338. },
  339. {
  340. "name": "imageExtension",
  341. "value": "png"
  342. },
  343. {
  344. "name": "title_hu",
  345. "value": "{{$node['Write The Post'].json.title_hu}}"
  346. },
  347. {
  348. "name": "title_en",
  349. "value": "{{$node['Write The Post'].json.title_en}}"
  350. },
  351. {
  352. "name": "content_hu",
  353. "value": "{{$node['Write The Post'].json.content_hu}}"
  354. },
  355. {
  356. "name": "content_en",
  357. "value": "{{$node['Write The Post'].json.content_en}}"
  358. },
  359. {
  360. "name": "tags",
  361. "value": "{{$node['Write The Post'].json.tags || []}}"
  362. },
  363. {
  364. "name": "slug",
  365. "value": "{{(($node['Write The Post'].json.title_en || 'anime').toString().toLowerCase().normalize('NFD').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,60)) + '-' + ($node['Generate Image'].result.jobId || Date.now()).toString().slice(0,8)}}"
  366. },
  367. {
  368. "name": "status",
  369. "value": "published"
  370. },
  371. {
  372. "name": "is_adult",
  373. "value": "{{(function(r){if(!r) return 1;if(r.sexualActivity===true) return 1;var bare=['nipples','genitals','buttocks','pubic_area'];var v=(r.visible||[]).map(function(x){return (x||'').toString().toLowerCase();});for(var i=0;i<v.length;i++) if(bare.indexOf(v[i])>=0) return 1;if((r.nudity||'').toString().toLowerCase()==='full') return 1;if((r.nsfwLevel||'').toString().toLowerCase()==='explicit') return 1;return 0;})($node['Rate The Picture'].json)}}"
  374. }
  375. ],
  376. "inputSource": "fields",
  377. "workflowId": "wf_234820e9-807a-44a9-b8cf-d8830ea8257c"
  378. },
  379. "disabled": false,
  380. "id": "node_blog_post",
  381. "name": "Publish To Blog",
  382. "position": {
  383. "x": 400,
  384. "y": 1300
  385. },
  386. "type": "call-workflow"
  387. },
  388. {
  389. "config": {
  390. "combineWith": "and",
  391. "conditions": [
  392. {
  393. "field": "data.result.succeeded",
  394. "operator": "is_true",
  395. "value": ""
  396. }
  397. ]
  398. },
  399. "disabled": false,
  400. "id": "node_gen_ok",
  401. "name": "Did The Image Get Made",
  402. "position": {
  403. "x": 260,
  404. "y": 780
  405. },
  406. "type": "if-condition"
  407. },
  408. {
  409. "config": {
  410. "fields": [
  411. {
  412. "name": "succeeded",
  413. "value": "{{ !$node['Generate Image'] || ($node['Generate Image'].result.succeeded === true && !!($node['Publish To Blog'] && $node['Publish To Blog'].result && $node['Publish To Blog'].result.ok)) }}"
  414. },
  415. {
  416. "name": "skipped",
  417. "value": "{{ !$node['Generate Image'] }}"
  418. },
  419. {
  420. "name": "error",
  421. "value": "{{ $node['Generate Image'] ? $node['Generate Image'].result.error : '' }}"
  422. },
  423. {
  424. "name": "url",
  425. "value": "{{ ($node['Publish To Blog'] && $node['Publish To Blog'].result) ? $node['Publish To Blog'].result.url : '' }}"
  426. },
  427. {
  428. "name": "postId",
  429. "value": "{{ ($node['Publish To Blog'] && $node['Publish To Blog'].result) ? $node['Publish To Blog'].result.postId : '' }}"
  430. }
  431. ],
  432. "source": "fields"
  433. },
  434. "disabled": false,
  435. "id": "node_output",
  436. "name": "Workflow Output",
  437. "position": {
  438. "x": 260,
  439. "y": 1560
  440. },
  441. "type": "workflow-output"
  442. }
  443. ],
  444. "settings": {
  445. "continueOnError": false,
  446. "errorWorkflowId": "wf_766fcbcb-6345-4e9c-b7f8-f219505ee45c",
  447. "storagePermissions": {
  448. "collections": {
  449. "files": "read-write",
  450. "nsfw_images": "read-write"
  451. },
  452. "defaultAccess": "none"
  453. }
  454. }
  455. }