|
@@ -0,0 +1,261 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * @node sdcpp-architecture
|
|
|
|
|
+ * @name SD.cpp Architecture Preset
|
|
|
|
|
+ * @category sdcpp
|
|
|
|
|
+ * @version 1.0.0
|
|
|
|
|
+ * @description Read the generation defaults SD.cpp uses for a model's architecture
|
|
|
|
|
+ * @icon sliders-horizontal
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+const credentialTypes = [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'sdcpp',
|
|
|
|
|
+ label: 'SD.cpp Server',
|
|
|
|
|
+ baseType: 'basic',
|
|
|
|
|
+ description: 'The username and password you sign in to sdcpp-restapi with. The node exchanges them for a token before every call',
|
|
|
|
|
+ usernameLabel: 'Username',
|
|
|
|
|
+ passwordLabel: 'Password'
|
|
|
|
|
+ }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const configSchema = {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ uiGroups: [
|
|
|
|
|
+ { title: 'Server', fields: ['serverUrl', 'credentialId'] },
|
|
|
|
|
+ { title: 'Model', fields: ['modelName', 'timeout'] }
|
|
|
|
|
+ ],
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ serverUrl: {
|
|
|
|
|
+ type: 'string', title: 'Server URL',
|
|
|
|
|
+ description: 'Base address of the sdcpp-restapi server',
|
|
|
|
|
+ default: 'http://localhost:8077'
|
|
|
|
|
+ },
|
|
|
|
|
+ credentialId: {
|
|
|
|
|
+ type: 'string', title: 'Credential',
|
|
|
|
|
+ description: 'A basic credential holding the sdcpp-restapi username and password',
|
|
|
|
|
+ dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
|
|
|
|
|
+ },
|
|
|
|
|
+ modelName: {
|
|
|
|
|
+ type: 'string', title: 'Model',
|
|
|
|
|
+ description: 'Which model to look up. Leave empty to use whatever the server currently has loaded',
|
|
|
|
|
+ dynamicOptions: {
|
|
|
|
|
+ source: 'node',
|
|
|
|
|
+ node: 'sdcpp-model',
|
|
|
|
|
+ config: { listOnly: true },
|
|
|
|
|
+ itemsPath: 'models',
|
|
|
|
|
+ valueKey: 'name',
|
|
|
|
|
+ labelKey: 'name',
|
|
|
|
|
+ needs: ['serverUrl', 'credentialId']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ timeout: {
|
|
|
|
|
+ type: 'number', title: 'Timeout (ms)',
|
|
|
|
|
+ description: 'How long to wait for the server to answer',
|
|
|
|
|
+ default: 30000
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['credentialId']
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const inputSchema = { type: 'object', properties: { data: { type: 'any' } } };
|
|
|
|
|
+
|
|
|
|
|
+const outputSchema = {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ found: { type: 'boolean', description: 'False when the server publishes no preset for this model' },
|
|
|
|
|
+ architecture: { type: 'string', description: 'Architecture id, such as Z-Image or Flux Schnell' },
|
|
|
|
|
+ architectureName: { type: 'string', description: 'Its display name' },
|
|
|
|
|
+ modelName: { type: 'string', description: 'The model this describes' },
|
|
|
|
|
+ description: { type: 'string', description: 'What the server says about this architecture' },
|
|
|
|
|
+ imageEditMode: { type: 'string', description: 'How this architecture takes an image to edit, when it can' },
|
|
|
|
|
+ requiredComponents: { type: 'object', description: 'VAE, LLM and so on that this architecture needs loaded' },
|
|
|
|
|
+ defaults: { type: 'object', description: 'The generation defaults, named the way the generation nodes name their settings' },
|
|
|
|
|
+ preset: { type: 'object', description: 'The preset exactly as the server describes it' }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+function normalizeServer(url) {
|
|
|
|
|
+ const value = String(url || '').trim();
|
|
|
|
|
+ if (!value) {
|
|
|
|
|
+ throw new Error('SD.cpp: a server URL is required, such as http://localhost:8077');
|
|
|
|
|
+ }
|
|
|
|
|
+ return value.replace(/\/+$/, '');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readCredential(credentialId) {
|
|
|
|
|
+ const auth = smartbotic.credentials.get(credentialId);
|
|
|
|
|
+ if (!auth || auth.success !== true) {
|
|
|
|
|
+ throw new Error('SD.cpp: could not read the credential: ' +
|
|
|
|
|
+ ((auth && auth.error) || 'unknown error'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const value = auth.headerValue || '';
|
|
|
|
|
+ if (value.indexOf('Basic ') !== 0) {
|
|
|
|
|
+ throw new Error('SD.cpp: the credential must be a basic one, holding the sdcpp-restapi ' +
|
|
|
|
|
+ 'username and password');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const decoded = smartbotic.utils.base64Decode(value.substring(6));
|
|
|
|
|
+ const separator = decoded.indexOf(':');
|
|
|
|
|
+ if (separator < 1) {
|
|
|
|
|
+ throw new Error('SD.cpp: the credential is malformed, expected a username and a password');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ username: decoded.substring(0, separator),
|
|
|
|
|
+ password: decoded.substring(separator + 1)
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function call(options) {
|
|
|
|
|
+ const response = smartbotic.http.request(options);
|
|
|
|
|
+
|
|
|
|
|
+ let body = response.data;
|
|
|
|
|
+ if (typeof body === 'string' && body.length > 0) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ body = JSON.parse(body);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ const snippet = body.substring(0, 200).replace(/\s+/g, ' ');
|
|
|
|
|
+ throw new Error('SD.cpp: ' + options.what + ' returned HTTP ' + response.status +
|
|
|
|
|
+ ' with a body that is not JSON: ' + snippet);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (response.status < 200 || response.status >= 300) {
|
|
|
|
|
+ const detail = (body && (body.message || body.error)) || ('HTTP ' + response.status);
|
|
|
|
|
+ throw new Error('SD.cpp: ' + options.what + ' failed: ' + detail);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return body || {};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function login(server, credential, timeout) {
|
|
|
|
|
+ const session = call({
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ url: server + '/auth/login',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ username: credential.username, password: credential.password }),
|
|
|
|
|
+ timeout: timeout,
|
|
|
|
|
+ what: 'signing in'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!session.token) {
|
|
|
|
|
+ throw new Error('SD.cpp: the server accepted the login but returned no token');
|
|
|
|
|
+ }
|
|
|
|
|
+ return session.token;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// The server names preset fields the way the generation endpoints do -
|
|
|
|
|
+// cfg_scale, cache_mode - while the nodes name their settings the way the rest
|
|
|
|
|
+// of the editor does. One rule converts between them, so a preset that gains a
|
|
|
|
|
+// field arrives without this node being touched.
|
|
|
|
|
+function toSetting(name) {
|
|
|
|
|
+ const parts = String(name).split('_');
|
|
|
|
|
+ let out = parts[0];
|
|
|
|
|
+ for (let i = 1; i < parts.length; i++) {
|
|
|
|
|
+ out += parts[i].charAt(0).toUpperCase() + parts[i].slice(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Presets are keyed by their id, but a model can be known by a name or an
|
|
|
|
|
+// alias - "ZImage" and "Z Image" both mean Z-Image - so all three are tried.
|
|
|
|
|
+function findPreset(architectures, wanted) {
|
|
|
|
|
+ const target = String(wanted || '').trim().toLowerCase();
|
|
|
|
|
+ if (!target) { return null; }
|
|
|
|
|
+
|
|
|
|
|
+ const keys = Object.keys(architectures || {});
|
|
|
|
|
+ for (let i = 0; i < keys.length; i++) {
|
|
|
|
|
+ const preset = architectures[keys[i]] || {};
|
|
|
|
|
+ const names = [keys[i], preset.id, preset.name];
|
|
|
|
|
+ const aliases = preset.aliases;
|
|
|
|
|
+ if (aliases && typeof aliases.length === 'number') {
|
|
|
|
|
+ for (let j = 0; j < aliases.length; j++) { names.push(aliases[j]); }
|
|
|
|
|
+ }
|
|
|
|
|
+ for (let j = 0; j < names.length; j++) {
|
|
|
|
|
+ if (String(names[j] || '').trim().toLowerCase() === target) {
|
|
|
|
|
+ return preset;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function execute(config, input, context) {
|
|
|
|
|
+ const server = normalizeServer(config.serverUrl);
|
|
|
|
|
+ const timeout = config.timeout || 30000;
|
|
|
|
|
+
|
|
|
|
|
+ const credential = readCredential(config.credentialId);
|
|
|
|
|
+ const token = login(server, credential, timeout);
|
|
|
|
|
+ const authHeader = { 'Authorization': 'Bearer ' + token };
|
|
|
|
|
+
|
|
|
|
|
+ let modelName = String(config.modelName || '').trim();
|
|
|
|
|
+ let architecture = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (!modelName) {
|
|
|
|
|
+ // /health needs no token, and for a model that is already loaded it
|
|
|
|
|
+ // names the architecture outright - which detection does not always
|
|
|
|
|
+ // manage from a filename alone.
|
|
|
|
|
+ const health = call({
|
|
|
|
|
+ method: 'GET', url: server + '/health', timeout: timeout,
|
|
|
|
|
+ what: 'reading server health'
|
|
|
|
|
+ });
|
|
|
|
|
+ modelName = String(health.model_name || health.loading_model_name || '').trim();
|
|
|
|
|
+ architecture = String(health.model_architecture || '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!modelName && !architecture) {
|
|
|
|
|
+ throw new Error('SD.cpp: no model given and the server has none loaded, so there is no ' +
|
|
|
|
|
+ 'architecture to look up');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!architecture && modelName) {
|
|
|
|
|
+ const detection = call({
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ url: server + '/architectures/detect?model=' + encodeURIComponent(modelName),
|
|
|
|
|
+ headers: authHeader,
|
|
|
|
|
+ timeout: timeout,
|
|
|
|
|
+ what: 'detecting the architecture of "' + modelName + '"'
|
|
|
|
|
+ });
|
|
|
|
|
+ const detected = detection.architecture || {};
|
|
|
|
|
+ architecture = String(detected.id || detected.name || '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const listing = call({
|
|
|
|
|
+ method: 'GET', url: server + '/architectures', headers: authHeader,
|
|
|
|
|
+ timeout: timeout, what: 'reading the architecture presets'
|
|
|
|
|
+ });
|
|
|
|
|
+ const preset = findPreset(listing.architectures, architecture) || {};
|
|
|
|
|
+
|
|
|
|
|
+ // The preset's generation defaults, under the names the generation nodes use
|
|
|
|
|
+ // for them. This is what the Fill-in button on those nodes writes into the
|
|
|
|
|
+ // form.
|
|
|
|
|
+ const defaults = {};
|
|
|
|
|
+ const raw = preset.generationDefaults || {};
|
|
|
|
|
+ const keys = Object.keys(raw);
|
|
|
|
|
+ for (let i = 0; i < keys.length; i++) {
|
|
|
|
|
+ defaults[toSetting(keys[i])] = raw[keys[i]];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!preset.id) {
|
|
|
|
|
+ smartbotic.log.warn('SD.cpp: no preset for "' + (architecture || modelName) +
|
|
|
|
|
+ '" - nothing to fill in from');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ smartbotic.log.info('SD.cpp: ' + modelName + ' is ' + preset.id + ', ' +
|
|
|
|
|
+ keys.length + ' generation default(s)');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ found: !!preset.id,
|
|
|
|
|
+ architecture: String(preset.id || architecture || ''),
|
|
|
|
|
+ architectureName: String(preset.name || ''),
|
|
|
|
|
+ modelName: modelName,
|
|
|
|
|
+ description: String(preset.description || ''),
|
|
|
|
|
+ imageEditMode: String(preset.imageEditMode || ''),
|
|
|
|
|
+ requiredComponents: preset.requiredComponents || {},
|
|
|
|
|
+ defaults: defaults,
|
|
|
|
|
+ preset: preset
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+module.exports = { credentialTypes, configSchema, inputSchema, outputSchema, execute };
|