sdcpp-job-status.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * @node sdcpp-job-status
  3. * @name SD.cpp Job Status
  4. * @category sdcpp
  5. * @version 1.0.0
  6. * @description Read the current state of one sdcpp-restapi job without waiting for it
  7. * @icon activity
  8. */
  9. const configSchema = {
  10. type: 'object',
  11. properties: {
  12. serverUrl: {
  13. type: 'string',
  14. title: 'Server URL',
  15. description: 'Base address of the sdcpp-restapi server',
  16. default: 'http://localhost:8077'
  17. },
  18. credentialId: {
  19. type: 'string',
  20. title: 'Credential',
  21. description: 'A basic credential holding the sdcpp-restapi username and password',
  22. dynamicOptions: {
  23. source: 'credentials',
  24. filter: { type: ['basic'] }
  25. }
  26. },
  27. jobId: {
  28. type: 'string',
  29. title: 'Job ID',
  30. description: 'Id of the job to inspect, normally {{$json.jobId}} from an SD.cpp Generate node'
  31. },
  32. failOnJobFailure: {
  33. type: 'boolean',
  34. title: 'Fail If The Job Failed',
  35. description: 'Throw when the job is in the failed state, instead of returning it as data. Leave off to branch on the status yourself',
  36. default: false
  37. },
  38. timeout: {
  39. type: 'number',
  40. title: 'Timeout (ms)',
  41. default: 30000
  42. }
  43. },
  44. required: ['credentialId']
  45. };
  46. const inputSchema = {
  47. type: 'object',
  48. properties: {
  49. data: { type: 'any' }
  50. }
  51. };
  52. const outputSchema = {
  53. type: 'object',
  54. properties: {
  55. jobId: { type: 'string' },
  56. status: { type: 'string', description: 'pending, processing, completed, failed, cancelled or deleted' },
  57. type: { type: 'string', description: 'txt2img, img2img, txt2vid, upscale, convert, model_download or model_hash' },
  58. done: { type: 'boolean', description: 'True once the job reached a state it cannot leave on its own' },
  59. succeeded: { type: 'boolean', description: 'True only for a completed job' },
  60. step: { type: 'number', description: 'Sampling steps finished so far' },
  61. totalSteps: { type: 'number' },
  62. percent: { type: 'number', description: 'Progress from 0 to 100, or -1 when the server reports no total' },
  63. outputs: { type: 'array', description: 'Relative output paths, empty until the job completes' },
  64. urls: { type: 'array', description: 'The same outputs as full URLs on this server' },
  65. error: { type: 'string', description: 'Failure reason when the job failed' },
  66. createdAt: { type: 'string' },
  67. startedAt: { type: 'string' },
  68. completedAt: { type: 'string' },
  69. params: { type: 'object', description: 'Generation parameters the job actually ran with, including the resolved seed' },
  70. modelSettings: { type: 'object', description: 'Which model and components produced it' }
  71. }
  72. };
  73. function normalizeServer(url) {
  74. const value = String(url || '').trim();
  75. if (!value) {
  76. throw new Error('SD.cpp: a server URL is required, such as http://localhost:8077');
  77. }
  78. return value.replace(/\/+$/, '');
  79. }
  80. function readCredential(credentialId) {
  81. const auth = smartbotic.credentials.get(credentialId);
  82. if (!auth || auth.success !== true) {
  83. throw new Error('SD.cpp: could not read the credential: ' +
  84. ((auth && auth.error) || 'unknown error'));
  85. }
  86. const value = auth.headerValue || '';
  87. if (value.indexOf('Basic ') !== 0) {
  88. throw new Error('SD.cpp: the credential must be a basic one, holding the sdcpp-restapi ' +
  89. 'username and password');
  90. }
  91. const decoded = smartbotic.utils.base64Decode(value.substring(6));
  92. const separator = decoded.indexOf(':');
  93. if (separator < 1) {
  94. throw new Error('SD.cpp: the credential is malformed, expected a username and a password');
  95. }
  96. return {
  97. username: decoded.substring(0, separator),
  98. password: decoded.substring(separator + 1)
  99. };
  100. }
  101. function call(options) {
  102. const response = smartbotic.http.request(options);
  103. let body = response.data;
  104. if (typeof body === 'string' && body.length > 0) {
  105. try {
  106. body = JSON.parse(body);
  107. } catch (e) {
  108. const snippet = body.substring(0, 200).replace(/\s+/g, ' ');
  109. throw new Error('SD.cpp: ' + options.what + ' returned HTTP ' + response.status +
  110. ' with a body that is not JSON: ' + snippet);
  111. }
  112. }
  113. if (response.status < 200 || response.status >= 300) {
  114. const detail = (body && (body.message || body.error)) || ('HTTP ' + response.status);
  115. throw new Error('SD.cpp: ' + options.what + ' failed: ' + detail);
  116. }
  117. return body || {};
  118. }
  119. function login(server, credential, timeout) {
  120. const session = call({
  121. method: 'POST',
  122. url: server + '/auth/login',
  123. headers: { 'Content-Type': 'application/json' },
  124. body: JSON.stringify({
  125. username: credential.username,
  126. password: credential.password
  127. }),
  128. timeout: timeout,
  129. what: 'signing in'
  130. });
  131. if (!session.token) {
  132. throw new Error('SD.cpp: the server accepted the login but returned no token');
  133. }
  134. return session.token;
  135. }
  136. // A job the worker will not move again on its own. "deleted" counts: a job in
  137. // the recycle bin is not coming back without someone restoring it, so a wait
  138. // that treated it as pending would spin until its timeout.
  139. const TERMINAL = ['completed', 'failed', 'cancelled', 'deleted'];
  140. function shapeJob(job, server) {
  141. const status = job.status || 'unknown';
  142. const progress = job.progress || {};
  143. const step = progress.step !== undefined ? progress.step : 0;
  144. const totalSteps = progress.total_steps !== undefined ? progress.total_steps : 0;
  145. const outputs = job.outputs || [];
  146. const urls = [];
  147. for (let i = 0; i < outputs.length; i++) {
  148. urls.push(server + '/output/' + outputs[i]);
  149. }
  150. return {
  151. jobId: job.job_id || '',
  152. status: status,
  153. type: job.type || '',
  154. done: TERMINAL.indexOf(status) !== -1,
  155. succeeded: status === 'completed',
  156. step: step,
  157. totalSteps: totalSteps,
  158. percent: totalSteps > 0 ? Math.round((step / totalSteps) * 100) : -1,
  159. outputs: outputs,
  160. urls: urls,
  161. error: job.error || '',
  162. createdAt: job.created_at || '',
  163. startedAt: job.started_at || '',
  164. completedAt: job.completed_at || '',
  165. params: job.params || {},
  166. modelSettings: job.model_settings || {}
  167. };
  168. }
  169. async function execute(config, input, context) {
  170. const server = normalizeServer(config.serverUrl);
  171. const timeout = config.timeout || 30000;
  172. const jobId = String(config.jobId || '').trim();
  173. if (!jobId) {
  174. throw new Error('SD.cpp: a job id is required');
  175. }
  176. const credential = readCredential(config.credentialId);
  177. const token = login(server, credential, timeout);
  178. const job = call({
  179. method: 'GET',
  180. url: server + '/queue/' + encodeURIComponent(jobId),
  181. headers: { 'Authorization': 'Bearer ' + token },
  182. timeout: timeout,
  183. what: 'reading job ' + jobId
  184. });
  185. const result = shapeJob(job, server);
  186. if (config.failOnJobFailure && result.status === 'failed') {
  187. throw new Error('SD.cpp: job ' + jobId + ' failed: ' + (result.error || 'no reason given'));
  188. }
  189. return result;
  190. }
  191. module.exports = { configSchema, inputSchema, outputSchema, execute };