sdcpp-upscaler-load.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /**
  2. * @node sdcpp-upscaler-load
  3. * @name SD.cpp Load Upscaler
  4. * @category sdcpp
  5. * @version 1.0.0
  6. * @description Make sure an upscaler is loaded, without reloading one that already is
  7. * @icon maximize
  8. */
  9. const configSchema = {
  10. type: 'object',
  11. properties: {
  12. serverUrl: {
  13. type: 'string', title: 'Server URL',
  14. description: 'Base address of the sdcpp-restapi server',
  15. default: 'http://localhost:8077'
  16. },
  17. credentialId: {
  18. type: 'string', title: 'Credential',
  19. description: 'A basic credential holding the sdcpp-restapi username and password',
  20. dynamicOptions: { source: 'credentials', filter: { type: ['basic'] } }
  21. },
  22. modelName: {
  23. type: 'string', title: 'Upscaler',
  24. description: 'File name of the ESRGAN-family upscaler. Browse lists what the server has',
  25. dynamicOptions: {
  26. source: 'node',
  27. node: 'sdcpp-model',
  28. config: { listOnly: true, modelType: 'esrgan' },
  29. itemsPath: 'models',
  30. valueKey: 'name',
  31. labelKey: 'name',
  32. needs: ['serverUrl', 'credentialId']
  33. }
  34. },
  35. force: {
  36. type: 'boolean', title: 'Force Reload',
  37. default: false,
  38. description: 'Load again even when this upscaler is already loaded'
  39. },
  40. timeout: { type: 'number', title: 'Timeout (ms)', default: 120000 }
  41. },
  42. required: []
  43. };
  44. const inputSchema = { type: 'object', properties: { data: { type: 'any' } } };
  45. const outputSchema = {
  46. type: 'object',
  47. properties: {
  48. upscalerName: { type: 'string' },
  49. loaded: { type: 'boolean', description: 'True when this node performed a load' },
  50. alreadyLoaded: { type: 'boolean' },
  51. previousUpscaler: { type: 'string' },
  52. elapsedMs: { type: 'number' }
  53. }
  54. };
  55. function normalizeServer(url) {
  56. const value = String(url || '').trim();
  57. if (!value) {
  58. throw new Error('SD.cpp: a server URL is required, such as http://localhost:8077');
  59. }
  60. return value.replace(/\/+$/, '');
  61. }
  62. function readCredential(credentialId) {
  63. const auth = smartbotic.credentials.get(credentialId);
  64. if (!auth || auth.success !== true) {
  65. throw new Error('SD.cpp: could not read the credential: ' +
  66. ((auth && auth.error) || 'unknown error'));
  67. }
  68. const value = auth.headerValue || '';
  69. if (value.indexOf('Basic ') !== 0) {
  70. throw new Error('SD.cpp: the credential must be a basic one, holding the sdcpp-restapi ' +
  71. 'username and password');
  72. }
  73. const decoded = smartbotic.utils.base64Decode(value.substring(6));
  74. const separator = decoded.indexOf(':');
  75. if (separator < 1) {
  76. throw new Error('SD.cpp: the credential is malformed, expected a username and a password');
  77. }
  78. return {
  79. username: decoded.substring(0, separator),
  80. password: decoded.substring(separator + 1)
  81. };
  82. }
  83. function call(options) {
  84. const response = smartbotic.http.request(options);
  85. let body = response.data;
  86. if (typeof body === 'string' && body.length > 0) {
  87. try {
  88. body = JSON.parse(body);
  89. } catch (e) {
  90. const snippet = body.substring(0, 200).replace(/\s+/g, ' ');
  91. throw new Error('SD.cpp: ' + options.what + ' returned HTTP ' + response.status +
  92. ' with a body that is not JSON: ' + snippet);
  93. }
  94. }
  95. if (response.status < 200 || response.status >= 300) {
  96. const detail = (body && (body.message || body.error)) || ('HTTP ' + response.status);
  97. throw new Error('SD.cpp: ' + options.what + ' failed: ' + detail);
  98. }
  99. return body || {};
  100. }
  101. function login(server, credential, timeout) {
  102. const session = call({
  103. method: 'POST',
  104. url: server + '/auth/login',
  105. headers: { 'Content-Type': 'application/json' },
  106. body: JSON.stringify({
  107. username: credential.username,
  108. password: credential.password
  109. }),
  110. timeout: timeout,
  111. what: 'signing in'
  112. });
  113. if (!session.token) {
  114. throw new Error('SD.cpp: the server accepted the login but returned no token');
  115. }
  116. return session.token;
  117. }
  118. // /health is unauthenticated, and it is the only way to find out what is
  119. // already loaded without asking for a token first.
  120. function readHealth(server, timeout) {
  121. return call({
  122. method: 'GET',
  123. url: server + '/health',
  124. timeout: timeout,
  125. what: 'reading server health'
  126. });
  127. }
  128. function putIfSet(target, key, value) {
  129. if (value === undefined || value === null || value === '') {
  130. return;
  131. }
  132. target[key] = value;
  133. }
  134. async function execute(config, input, context) {
  135. const server = normalizeServer(config.serverUrl);
  136. const timeout = config.timeout > 0 ? config.timeout : 120000;
  137. const modelName = String(config.modelName || '').trim();
  138. if (!modelName) {
  139. throw new Error('SD.cpp: an upscaler name is required. Connect an SD.cpp Model node ' +
  140. 'set to type esrgan, or type the file name');
  141. }
  142. const startedAt = Date.now();
  143. const health = readHealth(server, Math.min(timeout, 15000));
  144. const current = health.upscaler_name || '';
  145. // The upscaler lives in its own slot, independent of the main model, so
  146. // loading one does not disturb whatever is generating.
  147. if (current === modelName && config.force !== true) {
  148. smartbotic.log.info('SD.cpp: upscaler ' + modelName + ' is already loaded');
  149. return {
  150. upscalerName: current,
  151. loaded: false,
  152. alreadyLoaded: true,
  153. previousUpscaler: '',
  154. elapsedMs: Date.now() - startedAt
  155. };
  156. }
  157. const credential = readCredential(config.credentialId);
  158. const token = login(server, credential, Math.min(timeout, 30000));
  159. call({
  160. method: 'POST',
  161. url: server + '/upscaler/load',
  162. headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
  163. body: JSON.stringify({ model_name: modelName }),
  164. timeout: timeout,
  165. what: 'loading upscaler ' + modelName
  166. });
  167. smartbotic.log.info('SD.cpp: loaded upscaler ' + modelName);
  168. return {
  169. upscalerName: modelName,
  170. loaded: true,
  171. alreadyLoaded: false,
  172. previousUpscaler: current === modelName ? '' : current,
  173. elapsedMs: Date.now() - startedAt
  174. };
  175. }
  176. module.exports = { configSchema, inputSchema, outputSchema, execute };