| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- /**
- * @node sdcpp-job-status
- * @name SD.cpp Job Status
- * @category sdcpp
- * @version 1.0.0
- * @description Read the current state of one sdcpp-restapi job without waiting for it
- * @icon activity
- */
- // The credential this node wants, named so it can be found. It is stored as a
- // plain basic credential - that is what decides how it is encrypted - and this
- // only says which basic credential is the SD.cpp one. Anything that accepts a
- // basic credential still accepts this, and this node still accepts a plain
- // basic credential, because the shape is identical.
- 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',
- 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'] }
- }
- },
- jobId: {
- type: 'string',
- title: 'Job ID',
- description: 'Id of the job to inspect, normally {{$json.jobId}} from an SD.cpp Generate node'
- },
- failOnJobFailure: {
- type: 'boolean',
- title: 'Fail If The Job Failed',
- description: 'Throw when the job is in the failed state, instead of returning it as data. Leave off to branch on the status yourself',
- default: false
- },
- timeout: {
- type: 'number',
- title: 'Timeout (ms)',
- default: 30000
- }
- },
- required: ['credentialId']
- };
- const inputSchema = {
- type: 'object',
- properties: {
- data: { type: 'any' }
- }
- };
- const outputSchema = {
- type: 'object',
- properties: {
- jobId: { type: 'string' },
- status: { type: 'string', description: 'pending, processing, completed, failed, cancelled or deleted' },
- type: { type: 'string', description: 'txt2img, img2img, txt2vid, upscale, convert, model_download or model_hash' },
- done: { type: 'boolean', description: 'True once the job reached a state it cannot leave on its own' },
- succeeded: { type: 'boolean', description: 'True only for a completed job' },
- step: { type: 'number', description: 'Sampling steps finished so far' },
- totalSteps: { type: 'number' },
- percent: { type: 'number', description: 'Progress from 0 to 100, or -1 when the server reports no total' },
- outputs: { type: 'array', description: 'Relative output paths, empty until the job completes' },
- urls: { type: 'array', description: 'The same outputs as full URLs on this server' },
- error: { type: 'string', description: 'Failure reason when the job failed' },
- createdAt: { type: 'string' },
- startedAt: { type: 'string' },
- completedAt: { type: 'string' },
- params: { type: 'object', description: 'Generation parameters the job actually ran with, including the resolved seed' },
- modelSettings: { type: 'object', description: 'Which model and components produced 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;
- }
- // A job the worker will not move again on its own. "deleted" counts: a job in
- // the recycle bin is not coming back without someone restoring it, so a wait
- // that treated it as pending would spin until its timeout.
- const TERMINAL = ['completed', 'failed', 'cancelled', 'deleted'];
- function shapeJob(job, server) {
- const status = job.status || 'unknown';
- const progress = job.progress || {};
- const step = progress.step !== undefined ? progress.step : 0;
- const totalSteps = progress.total_steps !== undefined ? progress.total_steps : 0;
- const outputs = job.outputs || [];
- const urls = [];
- for (let i = 0; i < outputs.length; i++) {
- urls.push(server + '/output/' + outputs[i]);
- }
- return {
- jobId: job.job_id || '',
- status: status,
- type: job.type || '',
- done: TERMINAL.indexOf(status) !== -1,
- succeeded: status === 'completed',
- step: step,
- totalSteps: totalSteps,
- percent: totalSteps > 0 ? Math.round((step / totalSteps) * 100) : -1,
- outputs: outputs,
- urls: urls,
- error: job.error || '',
- createdAt: job.created_at || '',
- startedAt: job.started_at || '',
- completedAt: job.completed_at || '',
- params: job.params || {},
- modelSettings: job.model_settings || {}
- };
- }
- async function execute(config, input, context) {
- const server = normalizeServer(config.serverUrl);
- const timeout = config.timeout || 30000;
- const jobId = String(config.jobId || '').trim();
- if (!jobId) {
- throw new Error('SD.cpp: a job id is required');
- }
- const credential = readCredential(config.credentialId);
- const token = login(server, credential, timeout);
- const job = call({
- method: 'GET',
- url: server + '/queue/' + encodeURIComponent(jobId),
- headers: { 'Authorization': 'Bearer ' + token },
- timeout: timeout,
- what: 'reading job ' + jobId
- });
- const result = shapeJob(job, server);
- if (config.failOnJobFailure && result.status === 'failed') {
- throw new Error('SD.cpp: job ' + jobId + ' failed: ' + (result.error || 'no reason given'));
- }
- return result;
- }
- module.exports = { configSchema, inputSchema, outputSchema, execute };
|