Explorar el Código

Merge branch 'rss-ua-and-follow-loaded-model'

fszontagh hace 1 mes
padre
commit
d33b257f31
Se han modificado 3 ficheros con 115 adiciones y 8 borrados
  1. 25 2
      nodes/rss/rss-reader.js
  2. 70 6
      nodes/sdcpp/sdcpp-model.js
  3. 20 0
      tests/nodes/sdcpp-model-follow-loaded.json

+ 25 - 2
nodes/rss/rss-reader.js

@@ -31,6 +31,12 @@ const configSchema = {
             description: 'Maximum number of items to return (0 = unlimited)',
             default: 0
         },
+        userAgent: {
+            type: 'string',
+            title: 'User-Agent',
+            description: 'Sent with the request. Several feeds refuse an unnamed client outright - Reddit answers 403 - and some rate-limit a browser string harder than a named one. Identify yourself here',
+            default: 'smartbotic-rss/1.0 (+https://smartbotics.ai)'
+        },
         timeout: {
             type: 'number',
             title: 'Timeout (ms)',
@@ -699,6 +705,11 @@ async function execute(config, input) {
         throw new Error('State collection is required when "Detect New Items" is enabled. Please select a read-write collection in the workflow storage settings.');
     }
 
+    // No User-Agent at all is what several feeds reject, Reddit among them, so
+    // there is always one.
+    var userAgent = String(config.userAgent || '').trim() ||
+                    'smartbotic-rss/1.0 (+https://smartbotics.ai)';
+
     smartbotic.log.info('Fetching RSS/Atom feed from ' + url);
 
     var response = smartbotic.http.request({
@@ -706,12 +717,24 @@ async function execute(config, input) {
         url: url,
         timeout: timeout,
         headers: {
-            'Accept': 'application/rss+xml, application/atom+xml, application/xml, text/xml, */*'
+            'Accept': 'application/rss+xml, application/atom+xml, application/xml, text/xml, */*',
+            'User-Agent': userAgent
         }
     });
 
     if (response.status < 200 || response.status >= 300) {
-        throw new Error('Failed to fetch feed: HTTP ' + response.status);
+        var hint = '';
+        if (response.status === 403) {
+            hint = '. The feed refused this client - most feeds that do this want a descriptive ' +
+                   'User-Agent, which is the User-Agent setting on this node';
+        } else if (response.status === 429) {
+            hint = '. The feed is rate limiting this address - poll it less often, or from ' +
+                   'somewhere else. Reddit throttles hard and counts every request, including ' +
+                   'failed ones';
+        } else if (response.status === 404) {
+            hint = '. Check the address: ' + url;
+        }
+        throw new Error('Failed to fetch feed: HTTP ' + response.status + hint);
     }
 
     var xml = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);

+ 70 - 6
nodes/sdcpp/sdcpp-model.js

@@ -36,15 +36,30 @@ const configSchema = {
             description: 'A basic credential holding the sdcpp-restapi username and password. Listing models needs one',
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
         },
+        source: {
+            type: 'string', title: 'Model',
+            enum: ['choose', 'loaded'],
+            enumLabels: ['Pick one from the server', 'Whatever is loaded right now'],
+            default: 'choose',
+            description: 'Pick one, or take the model the server currently has loaded. Taking the loaded one keeps a workflow portable between servers, and means it never spends minutes swapping a model just to match what a node was set to'
+        },
+        includeComponents: {
+            type: 'boolean', title: 'Include Components',
+            default: false,
+            showWhen: { field: 'source', value: 'loaded' },
+            description: 'Also supply the VAE, CLIP, T5, LLM and TAESD the server has loaded, not only the model name. Only useful for a diffusion model, which loads its parts separately'
+        },
         modelType: {
             type: 'string', title: 'Kind',
+            showWhen: { field: 'source', value: 'choose' },
             enum: ['checkpoint', 'diffusion', 'vae', 'lora', 'clip', 't5', 'embedding',
                    'controlnet', 'llm', 'esrgan', 'taesd', 'motion_module', 'adetailer'],
             default: 'checkpoint',
             description: 'Which kind of model to choose from. esrgan is the upscaler kind'
         },
         modelName: {
-            type: 'string', title: 'Model',
+            type: 'string', title: 'Choose',
+            showWhen: { field: 'source', value: 'choose' },
             description: 'Chosen from what the server has. Change the Kind above to list a different sort',
             dynamicOptions: {
                 source: 'node',
@@ -73,6 +88,8 @@ const outputSchema = {
         _config: { type: 'object', description: 'The setting this supplies, as the engine consumes it' },
         modelName: { type: 'string', description: 'The chosen model' },
         modelType: { type: 'string' },
+        architecture: { type: 'string', description: 'What the server detected, when following the loaded model' },
+        loadedComponents: { type: 'object', description: 'The components the server has loaded, when following it' },
         setting: { type: 'string', description: 'Which setting it was supplied as' },
         models: { type: 'array', description: 'Everything of this kind the server has. Only filled when listing' }
     }
@@ -98,6 +115,19 @@ const SETTING_FOR_KIND = {
     adetailer: 'adetailer'
 };
 
+// What the server calls each loaded component, and the setting it fills in on a
+// node that loads one. Reported under several spellings across versions, so each
+// setting lists the ones seen.
+const COMPONENT_SETTINGS = [
+    { setting: 'vae', keys: ['vae', 'vae_model'] },
+    { setting: 'clipL', keys: ['clip_l', 'clipL'] },
+    { setting: 'clipG', keys: ['clip_g', 'clipG'] },
+    { setting: 't5xxl', keys: ['t5xxl', 't5'] },
+    { setting: 'llm', keys: ['llm'] },
+    { setting: 'taesd', keys: ['taesd'] },
+    { setting: 'controlnet', keys: ['controlnet', 'controlnet_model'] }
+];
+
 // The server groups models by kind under its own key names.
 const GROUP_FOR_KIND = {
     checkpoint: 'checkpoints',
@@ -256,16 +286,48 @@ async function execute(config, input, context) {
         };
     }
 
-    const modelName = String(config.modelName || '').trim();
-    if (!modelName) {
-        throw new Error('SD.cpp Model: choose a model. Pick one from the list, which is read ' +
-            'from the server named above');
+    let modelName;
+    let architecture = '';
+    let loadedComponents = {};
+    const values = {};
+
+    if ((config.source || 'choose') === 'loaded') {
+        // /health is unauthenticated, so this works before any credential is
+        // set up - useful when the point is to follow whatever the server has.
+        const health = readHealth(server, Math.min(timeout, 15000));
+        if (health.model_loaded !== true || !health.model_name) {
+            throw new Error('SD.cpp Model: this node is set to use the model the server has ' +
+                'loaded, but ' + server + ' has none. Load one first, or choose a model here');
+        }
+        modelName = health.model_name;
+        architecture = health.model_architecture || '';
+        loadedComponents = health.loaded_components || {};
+
+        if (config.includeComponents === true) {
+            for (let i = 0; i < COMPONENT_SETTINGS.length; i++) {
+                const entry = COMPONENT_SETTINGS[i];
+                for (let k = 0; k < entry.keys.length; k++) {
+                    const found = loadedComponents[entry.keys[k]];
+                    // A component the server does not have comes back empty or
+                    // absent; supplying that would blank the target's own value.
+                    if (found) {
+                        values[entry.setting] = found;
+                        break;
+                    }
+                }
+            }
+        }
+    } else {
+        modelName = String(config.modelName || '').trim();
+        if (!modelName) {
+            throw new Error('SD.cpp Model: choose a model. Pick one from the list, which is read ' +
+                'from the server named above');
+        }
     }
 
     const setting = String(config.setting || '').trim() ||
                     SETTING_FOR_KIND[modelType] || 'modelName';
 
-    const values = {};
     values[setting] = modelName;
 
     smartbotic.log.info('SD.cpp Model: supplying ' + setting + ' = ' + modelName);
@@ -276,6 +338,8 @@ async function execute(config, input, context) {
         _config: values,
         modelName: modelName,
         modelType: modelType,
+        architecture: architecture,
+        loadedComponents: loadedComponents,
         setting: setting,
         models: []
     };

+ 20 - 0
tests/nodes/sdcpp-model-follow-loaded.json

@@ -0,0 +1,20 @@
+{
+  "name": "verify-sdcpp-model-follow-loaded",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "pick", "name": "Model", "type": "sdcpp-model", "position": {"x": -200, "y": 100},
+     "config": {"serverUrl": "http://mulan:8077", "source": "loaded"}},
+    {"id": "load", "name": "Load", "type": "sdcpp-model-load", "position": {"x": 0, "y": 250},
+     "config": {"serverUrl": "http://mulan:8077",
+                "credentialId": "cred_62748b40-e9d1-4659-b175-6dbc691cc0b1"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "data"},
+    {"sourceNodeId": "pick", "sourceOutput": "main", "targetNodeId": "load", "targetInput": "config"}
+  ],
+  "expectStatus": "completed",
+  "expect": {
+    "pick": {"status": "completed", "output": {"modelName": "SD1x/AnythingXL_v50.safetensors", "architecture": "SD 1.x"}},
+    "load": {"status": "completed", "output": {"alreadyLoaded": true, "loaded": false}}
+  }
+}