{ "active": true, "connections": [ { "sourceNodeId": "node_1785733293322", "sourceOutput": "loop", "targetInput": "data", "targetNodeId": "node_1785733345099" }, { "sourceNodeId": "node_ollama_vision", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_safety_gate" }, { "sourceNodeId": "node_safety_gate", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_build_doc" }, { "sourceNodeId": "node_prep_gen", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_if_gen" }, { "sourceNodeId": "node_1785733345099", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_dedupe" }, { "sourceNodeId": "node_dedupe", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_if_new" }, { "sourceNodeId": "node_if_new", "sourceOutput": "true", "targetInput": "data", "targetNodeId": "node_ollama_vision" }, { "sourceNodeId": "node_safety_gate", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_prep_gen" }, { "sourceNodeId": "node_1785761774374", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785732919652" }, { "sourceNodeId": "node_1785733004181", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785732919652" }, { "sourceNodeId": "node_if_new", "sourceOutput": "false", "targetInput": "data", "targetNodeId": "node_1785733293322" }, { "sourceNodeId": "node_if_gen", "sourceOutput": "false", "targetInput": "data", "targetNodeId": "node_1785733293322" }, { "sourceNodeId": "node_1785832349865", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785733293322" }, { "sourceNodeId": "node_1785909802844", "sourceOutput": "false", "targetInput": "data", "targetNodeId": "node_gen_failed" }, { "sourceNodeId": "node_gen_failed", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785733293322" }, { "sourceNodeId": "node_1785732919652", "sourceOutput": "main", "targetInput": "input1", "targetNodeId": "node_1786088830206_58567" }, { "sourceNodeId": "node_1786088853294_0", "sourceOutput": "main", "targetInput": "input2", "targetNodeId": "node_1786088830206_58567" }, { "sourceNodeId": "node_1785733004181", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1786088853294_0" }, { "sourceNodeId": "node_1785761774374", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1786088853294_0" }, { "sourceNodeId": "node_1786088830206_58567", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785733293322" }, { "sourceNodeId": "node_1785733293322", "sourceOutput": "done", "targetInput": "data", "targetNodeId": "node_no_feed" }, { "sourceNodeId": "node_no_feed", "sourceOutput": "true", "targetInput": "data", "targetNodeId": "node_feeds_down" }, { "sourceNodeId": "node_if_gen", "sourceOutput": "true", "targetInput": "data", "targetNodeId": "node_call_gen" }, { "sourceNodeId": "node_call_gen", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785909802844" }, { "sourceNodeId": "node_blog_text", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_blog_image" }, { "sourceNodeId": "node_blog_post", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_1785832349865" }, { "sourceNodeId": "node_1785909802844", "sourceOutput": "true", "targetInput": "data", "targetNodeId": "node_blog_text" }, { "sourceNodeId": "node_blog_image", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_blog_rate" }, { "sourceNodeId": "node_blog_rate", "sourceOutput": "main", "targetInput": "data", "targetNodeId": "node_blog_post" } ], "description": "", "name": "35photo2anime - sdcpp", "nodes": [ { "config": { "_customLabel": "35photo pro", "detectNewItems": false, "maxItems": 0, "retries": 3, "retryDelayMs": 6000, "retryMaxDelayMs": 30000, "skipOnError": true, "timeout": 30000, "url": "https://35photo.pro/rss/photo_new.xml", "userAgent": "smartbotic-rss/1.0 (+https://smartbotics.ai)" }, "disabled": false, "id": "node_1785732919652", "name": "35photo pro", "position": { "x": 3460, "y": 180 }, "type": "rss-reader" }, { "config": { "label": "Execute" }, "disabled": false, "id": "node_1785733004181", "name": "Click Trigger", "position": { "x": 3440, "y": 0 }, "type": "click-trigger" }, { "config": { "continueOnError": true, "indexVariableName": "index", "inputField": "data.merged", "itemVariableName": "item", "maxIterations": 10, "outputField": "results" }, "disabled": false, "id": "node_1785733293322", "name": "Loop", "position": { "x": 3600, "y": 540 }, "type": "loop" }, { "config": { "authType": "none", "contentType": "", "downloadCollection": "nsfw_temp", "downloadTtlHours": 24, "followRedirects": true, "method": "GET", "responseMode": "binary", "retries": 0, "retryDelayMs": 1000, "retryMaxDelayMs": 30000, "storeDownload": false, "timeout": 30000, "url": "{{data.loop.item.enclosure.url}}" }, "disabled": false, "id": "node_1785733345099", "name": "Fetch Image", "position": { "x": 3580, "y": 720 }, "type": "http-request" }, { "config": { "authType": "credential", "baseUrl": "https://ollama.com", "credentialId": "cred_b01319ae-e31c-4545-86ee-ff655a7cc1e4", "imageField": "", "imageMode": "auto", "model": "minimax-m3:cloud", "passthroughImage": true, "responseFormat": "json", "retryCount": 3, "retryDelayMs": 3000, "retryMaxDelayMs": 30000, "skipOnError": true, "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", "temperature": 0, "timeoutMs": 120000, "userPrompt": "Analyse this image and return the JSON object." }, "disabled": false, "id": "node_ollama_vision", "name": "Vision Analyse", "position": { "x": 3580, "y": 1120 }, "type": "ollama-chat" }, { "config": { "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", "timeout": 30 }, "disabled": false, "id": "node_safety_gate", "name": "Safety Gate + Prompt", "position": { "x": 3580, "y": 1240 }, "type": "code" }, { "config": { "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", "timeout": 30 }, "disabled": false, "id": "node_build_doc", "name": "Store If NSFW", "position": { "x": 3440, "y": 1380 }, "type": "code" }, { "config": { "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: 'anime style illustration, cel shaded, clean line art, vibrant colours, ' + 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", "timeout": 30 }, "disabled": false, "id": "node_prep_gen", "name": "Prepare Generation", "position": { "x": 3700, "y": 1380 }, "type": "code" }, { "config": { "combineWith": "and", "conditions": [ { "field": "data.result.generate", "operator": "is_true", "value": "" } ] }, "disabled": false, "id": "node_if_gen", "name": "Should Generate", "position": { "x": 3700, "y": 1500 }, "type": "if-condition" }, { "config": { "cronExpression": "0 * * * *", "maxConcurrent": 1, "maxRunMinutes": 0, "mode": "interval", "overlapPolicy": "skip", "pollInterval": 15, "timezone": "Europe/Budapest", "triggerName": "35photo every 5m" }, "disabled": false, "id": "node_1785761774374", "name": "Schedule Trigger", "position": { "x": 3760, "y": 0 }, "type": "schedule-trigger" }, { "config": { "code": "function findFetch(node, depth) {\n if (!node || typeof node !== 'object' || depth > 6) { return null; }\n if (node.file && typeof node.file.data === 'string' && node.file.data.length > 0) { return node; }\n const keys = Object.keys(node);\n for (let i = 0; i < keys.length; i++) {\n const found = findFetch(node[keys[i]], depth + 1);\n if (found) { return found; }\n }\n return null;\n}\n\nconst fetched = findFetch(input, 0);\nif (!fetched) { throw new Error('No fetched image found in input'); }\n\nconst hash = fetched.checksum || (fetched.file && fetched.file.checksum) ||\n smartbotic.utils.sha256(fetched.file.data);\nconst url = fetched.url || '';\nconst now = Date.now();\n\nconst existing = smartbotic.storage.get('image_hashes', hash);\n\nif (existing && existing.found && existing.document) {\n const doc = existing.document;\n const urls = doc.urls && typeof doc.urls.length === 'number' ? doc.urls : [];\n if (url && urls.indexOf(url) === -1) { urls.push(url); }\n\n smartbotic.storage.update('image_hashes', hash, {\n lastSeenAt: now,\n seenCount: (Number(doc.seenCount) || 1) + 1,\n urls: urls\n }, 0, true);\n\n smartbotic.log.info('Duplicate image skipped: ' + hash.substring(0, 16) + ' seen ' +\n ((Number(doc.seenCount) || 1) + 1) + ' times');\n\n return {\n isNew: false,\n checksum: hash,\n url: url,\n fileId: doc.fileId || '',\n seenCount: (Number(doc.seenCount) || 1) + 1\n };\n}\n\n// A feed can carry the same image twice in one run, so two iterations reach here\n// having both seen \"not found\". The id is the content hash, so the second insert\n// collides. Treat that as the duplicate it is rather than letting it fail the run.\n// Guarded both ways because the runtime reports a rejected insert as success\n// false, while the underlying database error can also surface as a throw.\nlet inserted = null;\nlet insertError = '';\ntry {\n inserted = smartbotic.storage.insert('image_hashes', {\n firstSeenAt: now,\n lastSeenAt: now,\n seenCount: 1,\n urls: url ? [url] : [],\n mimeType: fetched.file.mimeType || '',\n size: fetched.file.size || 0\n }, hash, 0);\n} catch (e) {\n insertError = e && e.message ? e.message : String(e);\n}\n\nif (insertError || !inserted || inserted.success === false) {\n const detail = insertError || (inserted && inserted.error) || 'unknown';\n smartbotic.log.info('Image already recorded in this run: ' + hash.substring(0, 16) + ' (' + detail + ')');\n return { isNew: false, checksum: hash, url: url, seenCount: 2 };\n}\n\nconst stored = smartbotic.storage.uploadFile(fetched.file.data, {\n name: 'source-' + hash.substring(0, 16) + '.img',\n mimeType: fetched.file.mimeType || 'image/jpeg',\n fileType: 'document',\n relatedId: hash\n});\nif (!stored.success) {\n throw new Error('Could not store the source image: ' + stored.error);\n}\n\nsmartbotic.storage.update('image_hashes', hash, { fileId: stored.id }, 0, true);\n\nreturn {\n fileId: stored.id,\n isNew: true,\n checksum: hash,\n url: url,\n seenCount: 1,\n file: fetched.file\n};\n", "timeout": 30 }, "disabled": false, "id": "node_dedupe", "name": "Dedupe Check", "position": { "x": 3580, "y": 860 }, "type": "code" }, { "config": { "combineWith": "and", "conditions": [ { "field": "data.result.isNew", "operator": "is_true", "value": "" } ] }, "disabled": false, "id": "node_if_new", "name": "Is New Image", "position": { "x": 3580, "y": 980 }, "type": "if-condition" }, { "config": { "action": "send", "baseUrl": "https://cloud.fsociety.hu", "credentialId": "cred_c3c55bce-82b4-41ef-a061-bc679b386579", "limit": 20, "message": "Image is done.\n**Original** URL: {{$node[\"Fetch Image\"].url}}\n**NEW**:\nPrompt: __{{$node[\"Safety Gate + Prompt\"].result.description}}__", "retries": 1, "silent": false, "skipOnError": false, "timeoutMs": 30000, "token": "REDACTED-conversation-token" }, "disabled": true, "id": "node_1785832349865", "name": "Nextcloud Talk", "position": { "x": 3840, "y": 2420 }, "type": "nextcloud-talk" }, { "config": { "_customLabel": "Did the image get made", "combineWith": "and", "conditions": [ { "field": "data.result.succeeded", "operator": "is_true", "value": "" } ] }, "disabled": false, "id": "node_1785909802844", "name": "IF Condition", "position": { "x": 3700, "y": 1760 }, "type": "if-condition" }, { "config": { "action": "send", "baseUrl": "https://cloud.fsociety.hu", "credentialId": "cred_c3c55bce-82b4-41ef-a061-bc679b386579", "limit": 20, "message": "SD.cpp did not produce an image, moving on to the next photo.\n**Original** URL: {{$node[\"Fetch Image\"].url}}\n**Reason**: {{$node[\"Generate Image\"].result.error}}", "retries": 1, "silent": true, "skipOnError": true, "timeoutMs": 30000, "token": "REDACTED-conversation-token" }, "disabled": false, "id": "node_gen_failed", "name": "Say It Failed", "position": { "x": 3580, "y": 1900 }, "type": "nextcloud-talk" }, { "config": { "key": "enclosure.url", "mode": "combine-by-key", "path1": "items", "path2": "items" }, "disabled": false, "id": "node_1786088830206_58567", "name": "Merge", "position": { "x": 3600, "y": 360 }, "type": "merge" }, { "config": { "_customLabel": "35photo ru", "detectNewItems": false, "maxItems": 0, "retries": 3, "retryDelayMs": 6000, "retryMaxDelayMs": 30000, "skipOnError": true, "timeout": 30000, "url": "https://35photo.ru/rss/photo_new.xml", "userAgent": "smartbotic-rss/1.0 (+https://smartbotics.ai)" }, "disabled": false, "id": "node_1786088853294_0", "name": "35photo ru", "position": { "x": 3760, "y": 180 }, "type": "rss-reader" }, { "config": { "_customLabel": "Did any feed answer", "combineWith": "and", "conditions": [ { "field": "{{$node[\"Merge\"].count}}", "operator": "equals", "value": "0" } ] }, "disabled": false, "id": "node_no_feed", "name": "Did Any Feed Answer", "position": { "x": 3600, "y": 3240 }, "type": "if-condition" }, { "config": { "action": "send", "baseUrl": "https://cloud.fsociety.hu", "credentialId": "cred_c3c55bce-82b4-41ef-a061-bc679b386579", "limit": 20, "message": "35photo2anime found no photos at all this run - both feeds came back empty or unreachable. Nothing was generated.", "retries": 1, "silent": true, "skipOnError": true, "timeoutMs": 30000, "token": "REDACTED-conversation-token" }, "disabled": false, "id": "node_feeds_down", "name": "Say No Feed Answered", "position": { "x": 3600, "y": 3420 }, "type": "nextcloud-talk" }, { "config": { "fields": [ { "name": "prompt", "value": "{{$node[\"Prepare Generation\"].result.prompt}}" }, { "name": "width", "value": "{{$node[\"Prepare Generation\"].result.width}}" }, { "name": "height", "value": "{{$node[\"Prepare Generation\"].result.height}}" }, { "name": "loraTag", "value": "" }, { "name": "title", "value": "35photo2anime" }, { "name": "batchCount", "value": "1" }, { "name": "keepForHours", "value": "0" }, { "name": "storeInDatabase", "value": "false" } ], "inputSource": "fields", "workflowId": "wf_f7c8244a-2f8a-4a66-a3a4-62b8c63381dc" }, "disabled": false, "id": "node_call_gen", "name": "Generate Image", "position": { "x": 3700, "y": 1640 }, "type": "call-workflow" }, { "config": { "authType": "credential", "baseUrl": "https://ollama.com", "credentialId": "cred_b01319ae-e31c-4545-86ee-ff655a7cc1e4", "imageField": "", "imageMode": "none", "model": "glm-5.2:cloud", "passthroughImage": false, "responseFormat": "json", "retryCount": 2, "retryDelayMs": 3000, "retryMaxDelayMs": 30000, "skipOnError": false, "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.", "temperature": 0.4, "timeoutMs": 120000, "userPrompt": "The illustration shows: {{$node['Vision Analyse'].json.description}}" }, "disabled": false, "id": "node_blog_text", "name": "Write The Post", "position": { "x": 3840, "y": 2020 }, "type": "ollama-chat" }, { "config": { "authType": "none", "downloadCollection": "downloads", "downloadTtlHours": 24, "followRedirects": true, "method": "GET", "responseMode": "binary", "retries": 2, "retryDelayMs": 2000, "retryMaxDelayMs": 30000, "storeDownload": false, "timeout": 60000, "url": "{{$node['Generate Image'].result.urls[0]}}" }, "disabled": false, "id": "node_blog_image", "name": "Fetch Generated Image", "position": { "x": 3840, "y": 2160 }, "type": "http-request" }, { "config": { "fields": [ { "name": "file", "value": "{{$node['Fetch Generated Image'].file}}" }, { "name": "imageExtension", "value": "png" }, { "name": "title_hu", "value": "{{$node['Write The Post'].json.title_hu}}" }, { "name": "title_en", "value": "{{$node['Write The Post'].json.title_en}}" }, { "name": "content_hu", "value": "{{$node['Write The Post'].json.content_hu}}" }, { "name": "content_en", "value": "{{$node['Write The Post'].json.content_en}}" }, { "name": "tags", "value": "{{$node['Write The Post'].json.tags || []}}" }, { "name": "slug", "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)}}" }, { "name": "status", "value": "published" }, { "name": "is_adult", "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=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)}}" } ], "inputSource": "fields", "workflowId": "wf_234820e9-807a-44a9-b8cf-d8830ea8257c" }, "disabled": false, "id": "node_blog_post", "name": "Publish To Blog", "position": { "x": 3840, "y": 2280 }, "type": "call-workflow" }, { "config": { "authType": "credential", "baseUrl": "https://ollama.com", "credentialId": "cred_b01319ae-e31c-4545-86ee-ff655a7cc1e4", "imageField": "", "imageMode": "auto", "model": "minimax-m3:cloud", "passthroughImage": true, "responseFormat": "json", "retryCount": 2, "retryDelayMs": 3000, "retryMaxDelayMs": 30000, "skipOnError": false, "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.", "temperature": 0, "timeoutMs": 120000, "userPrompt": "Report what is visible in this illustration and return the JSON object." }, "id": "node_blog_rate", "name": "Rate The Picture", "position": { "x": 600, "y": 450 }, "type": "ollama-chat" } ], "settings": { "continueOnError": false, "errorWorkflowId": "wf_766fcbcb-6345-4e9c-b7f8-f219505ee45c", "storagePermissions": { "collections": { "anime_images": "read-write", "files": "read-write", "image_hashes": "read-write", "nsfw_images": "read-write", "nsfw_temp": "read-write" }, "defaultAccess": "none" } } }