@civitai/app-sdk 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -3
- package/dist/blocks/index.d.ts +9 -1
- package/dist/blocks/index.d.ts.map +1 -1
- package/dist/blocks/index.js +7 -0
- package/dist/blocks/index.js.map +1 -1
- package/dist/blocks/initFragment.d.ts +89 -0
- package/dist/blocks/initFragment.d.ts.map +1 -0
- package/dist/blocks/initFragment.js +136 -0
- package/dist/blocks/initFragment.js.map +1 -0
- package/dist/blocks/messages.d.ts +67 -3
- package/dist/blocks/messages.d.ts.map +1 -1
- package/dist/blocks/messages.js.map +1 -1
- package/dist/blocks/types.d.ts +436 -41
- package/dist/blocks/types.d.ts.map +1 -1
- package/dist/blocks/types.js +54 -1
- package/dist/blocks/types.js.map +1 -1
- package/dist/orchestrator/index.d.ts +158 -13
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +202 -14
- package/dist/orchestrator/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/app-block/v1.json +1 -1
package/dist/blocks/types.js
CHANGED
|
@@ -4,5 +4,58 @@
|
|
|
4
4
|
* Framework-agnostic. Hooks and transport classes that consume these types
|
|
5
5
|
* live in a separate package so this module stays usable from any runtime.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Narrow a {@link BlockContext} to {@link ModelSlotContext}.
|
|
9
|
+
*
|
|
10
|
+
* A real runtime check, not just a cast: the context arrived over
|
|
11
|
+
* `postMessage`, so its static type is the host's claim about it. Returning
|
|
12
|
+
* `true` here is what lets the block render a useful "wrong slot" state instead
|
|
13
|
+
* of `undefined`-ing its way through a generation body.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 IT CHECKS EVERY FIELD IT ASSERTS, not just `slotId`. A `slotId`-only check
|
|
16
|
+
* is not enough, because {@link UnknownSlotContext} declares `slotId: string` —
|
|
17
|
+
* so a structurally-incomplete known slot (`{ slotId: 'model.sidebar_top' }` and
|
|
18
|
+
* nothing else) is a legal `BlockContext` that COMPILES, passes a `slotId`-only
|
|
19
|
+
* guard, and then hands the block `ctx.modelId` typed `number` and valued
|
|
20
|
+
* `undefined`. The five required fields are asserted individually so the
|
|
21
|
+
* predicate is true exactly when the type it claims is true.
|
|
22
|
+
*
|
|
23
|
+
* No legitimate host payload is rejected by the extra checks. The only
|
|
24
|
+
* production producer of a model slot (civitai/civitai
|
|
25
|
+
* `ModelVersionDetails.tsx`) sets all five unconditionally, and the host's own
|
|
26
|
+
* pinned allowlist test (`__tests__/projectBlockInit.test.ts`) asserts the
|
|
27
|
+
* projected context carries them as an EXACT key set. A payload that somehow
|
|
28
|
+
* lacked one is precisely the case the block must not treat as a model context.
|
|
29
|
+
*/
|
|
30
|
+
export function isModelSlotContext(ctx) {
|
|
31
|
+
if (ctx.slotId !== 'model.sidebar_top' &&
|
|
32
|
+
ctx.slotId !== 'model.below_images' &&
|
|
33
|
+
ctx.slotId !== 'model.actions_extra') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const c = ctx;
|
|
37
|
+
return (typeof c.modelId === 'number' &&
|
|
38
|
+
typeof c.modelVersionId === 'number' &&
|
|
39
|
+
typeof c.modelName === 'string' &&
|
|
40
|
+
typeof c.modelType === 'string' &&
|
|
41
|
+
typeof c.modelNsfwLevel === 'number');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Narrow a {@link BlockContext} to {@link PageSlotContext}. Same shape and same
|
|
45
|
+
* reasoning as {@link isModelSlotContext}: the three fields `PageSlotContext`
|
|
46
|
+
* declares REQUIRED are checked, not just `slotId`.
|
|
47
|
+
*
|
|
48
|
+
* `subPath` is checked as a string, not a NON-EMPTY one — `''` is the real value
|
|
49
|
+
* on an app's own index (`PageBlockHost.buildContext()`), and rejecting it would
|
|
50
|
+
* make every app's landing route fail to narrow. `viewerUserId` is `number |
|
|
51
|
+
* null`, and `null` is the genuine anonymous value.
|
|
52
|
+
*/
|
|
53
|
+
export function isPageSlotContext(ctx) {
|
|
54
|
+
if (ctx.slotId !== 'app.page')
|
|
55
|
+
return false;
|
|
56
|
+
const c = ctx;
|
|
57
|
+
return (typeof c.slug === 'string' &&
|
|
58
|
+
typeof c.subPath === 'string' &&
|
|
59
|
+
(typeof c.viewerUserId === 'number' || c.viewerUserId === null));
|
|
60
|
+
}
|
|
8
61
|
//# sourceMappingURL=types.js.map
|
package/dist/blocks/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/blocks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/blocks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2ZH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAiB;IAClD,IACE,GAAG,CAAC,MAAM,KAAK,mBAAmB;QAClC,GAAG,CAAC,MAAM,KAAK,oBAAoB;QACnC,GAAG,CAAC,MAAM,KAAK,qBAAqB,EACpC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,CAAC,GAAG,GAAgC,CAAC;IAC3C,OAAO,CACL,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAC7B,OAAO,CAAC,CAAC,cAAc,KAAK,QAAQ;QACpC,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;QAC/B,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;QAC/B,OAAO,CAAC,CAAC,cAAc,KAAK,QAAQ,CACrC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAiB;IACjD,IAAI,GAAG,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IAC5C,MAAM,CAAC,GAAG,GAA+B,CAAC;IAC1C,OAAO,CACL,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAC7B,CAAC,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ,IAAI,CAAC,CAAC,YAAY,KAAK,IAAI,CAAC,CAChE,CAAC;AACJ,CAAC"}
|
|
@@ -27,8 +27,30 @@ export type TerminalStatus = (typeof TERMINAL_STATUSES)[number];
|
|
|
27
27
|
* at the matching `build*Body` helper (if one exists) or fall back to
|
|
28
28
|
* {@link callOrchestrator} with a hand-crafted body.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* 🔴 SOURCE OF TRUTH — AND HOW IT IS HELD. The authoritative population is the
|
|
31
|
+
* `WorkflowStepTemplate` **discriminator mapping** in
|
|
32
|
+
* `https://orchestration.civitai.com/openapi/v2-consumers.json` (the submit-side
|
|
33
|
+
* union; `WorkflowStep`'s mapping is identical). That is the *defining* surface —
|
|
34
|
+
* enumerate it, do not sample the spec's type names or a generated client's
|
|
35
|
+
* exports, which include shapes that are not submittable step types.
|
|
36
|
+
*
|
|
37
|
+
* The keys below are pinned EXACTLY against that mapping two ways:
|
|
38
|
+
* - `test/orchestrator.test.ts` compares this catalog's key set to a list
|
|
39
|
+
* transcribed from the spec (hermetic, offline, order-insensitive), and
|
|
40
|
+
* - `scripts/check-orchestrator-catalogs.mjs` re-fetches the LIVE spec and
|
|
41
|
+
* diffs it (network; wired into CI as `pnpm check:catalogs`).
|
|
42
|
+
*
|
|
43
|
+
* The second one is what notices the orchestrator shipping a new step. When it
|
|
44
|
+
* fails, refresh both this catalog and the test's transcribed list in the same
|
|
45
|
+
* PR — the test is a deliberate second copy so a catalog edit cannot mark its
|
|
46
|
+
* own homework.
|
|
47
|
+
*
|
|
48
|
+
* 🔴 A SPOT-CHECK IS NOT A PIN. Until 2026-08 this catalog was guarded only by
|
|
49
|
+
* `expect(keys).toContain('textToImage')`-style assertions over 7 names. It
|
|
50
|
+
* carried a phantom `audioMix` — a `$type` that appears **0 times** in the spec
|
|
51
|
+
* and would have come back a 400 — and was missing 10 real step types, for as
|
|
52
|
+
* long as those assertions existed. An exact-set comparison is the whole
|
|
53
|
+
* difference.
|
|
32
54
|
*/
|
|
33
55
|
export declare const WORKFLOW_STEP_TYPES: {
|
|
34
56
|
/** Diffusion image gen (SDXL / Flux.1 / Pony / Illustrious / SD1.5 / etc.). Use {@link buildTextToImageBody}. */
|
|
@@ -41,8 +63,18 @@ export declare const WORKFLOW_STEP_TYPES: {
|
|
|
41
63
|
readonly imageGen: "Closed-source image gen (Nano Banana, Gemini, GPT-Image, Flux Kontext, Seedream, Grok, fal, …)";
|
|
42
64
|
/** Arbitrary ComfyUI workflow graphs. Pass a `prompt` object (node graph). */
|
|
43
65
|
readonly comfy: "Custom ComfyUI node-graph workflows";
|
|
66
|
+
/**
|
|
67
|
+
* Raw ComfyUI graph + an explicit AIR download manifest (`resources`). The
|
|
68
|
+
* orchestrator forwards the graph opaquely, so anything the graph references
|
|
69
|
+
* must also be declared in `resources` or it fails at load time inside ComfyUI.
|
|
70
|
+
*/
|
|
71
|
+
readonly customComfy: "Raw ComfyUI graph with a declared AIR resource manifest";
|
|
44
72
|
/** Upscale an existing image. Input is a source image URL + scale factor. */
|
|
45
73
|
readonly imageUpscaler: "Image upscaling";
|
|
74
|
+
/** Remove an image background via BiRefNet (runs as a comfy job under the hood). */
|
|
75
|
+
readonly imageBackgroundRemoval: "Image background removal (BiRefNet)";
|
|
76
|
+
/** Vectorize a raster image with a local StarVector / OmniSVG model. */
|
|
77
|
+
readonly imageToSvg: "Raster image → SVG vectorization";
|
|
46
78
|
/** LoRA / DoRA / embedding training. Long-running. */
|
|
47
79
|
readonly imageResourceTraining: "Train a LoRA / DoRA / embedding from a dataset";
|
|
48
80
|
/** Pre-process an image (resize, ControlNet preprocessor, etc.). */
|
|
@@ -61,6 +93,8 @@ export declare const WORKFLOW_STEP_TYPES: {
|
|
|
61
93
|
readonly videoEnhancement: "Per-frame video enhancement";
|
|
62
94
|
/** Extract individual frames from a video. */
|
|
63
95
|
readonly videoFrameExtraction: "Extract frames from a video";
|
|
96
|
+
/** Track a prompted foreground object and return a transparent animated WebP. */
|
|
97
|
+
readonly videoBackgroundRemoval: "Video background removal (prompted object tracking)";
|
|
64
98
|
/** Read video metadata (duration, codec, dimensions). */
|
|
65
99
|
readonly videoMetadata: "Read video file metadata";
|
|
66
100
|
/** Transcode video format / codec. */
|
|
@@ -71,10 +105,27 @@ export declare const WORKFLOW_STEP_TYPES: {
|
|
|
71
105
|
readonly aceStepAudio: "Music generation (ACE Step 1.5)";
|
|
72
106
|
/** Speech-to-text transcription. */
|
|
73
107
|
readonly transcription: "Speech-to-text transcription";
|
|
74
|
-
/** Mix multiple audio tracks. */
|
|
75
|
-
readonly audioMix: "Audio track mixing";
|
|
76
108
|
/** Generate captions from audio. */
|
|
77
109
|
readonly audioCaptioning: "Caption generation from audio";
|
|
110
|
+
/**
|
|
111
|
+
* Compose an ordered list of media `elements` into one output. Its output is
|
|
112
|
+
* discriminated on `type`: `'audio'` for a mixdown, `'video'` for a
|
|
113
|
+
* composition — so this covers audio mixing as well as video assembly.
|
|
114
|
+
*
|
|
115
|
+
* 🔴 This replaced a catalog entry named `audioMix`, which never existed in
|
|
116
|
+
* the orchestrator spec. If you were reaching for `audioMix`, this is it.
|
|
117
|
+
*/
|
|
118
|
+
readonly composeMedia: "Compose media elements into one audio mixdown or video";
|
|
119
|
+
/** Generate a 3D model. Engines: `comfy`, `fal`. */
|
|
120
|
+
readonly polyGen: "3D model generation";
|
|
121
|
+
/** Render a preview of an existing 3D model. */
|
|
122
|
+
readonly model3DPreview: "3D model preview rendering";
|
|
123
|
+
/**
|
|
124
|
+
* Generic training step. Engines: `ai-toolkit`, `comfy`. Distinct from
|
|
125
|
+
* {@link WORKFLOW_STEP_TYPES.imageResourceTraining}, which is the older
|
|
126
|
+
* kohya/musubi/flux-dev-fast LoRA path.
|
|
127
|
+
*/
|
|
128
|
+
readonly training: "Model training (ai-toolkit / comfy engines)";
|
|
78
129
|
/** Hash an image / video / model for dedup or lookup. */
|
|
79
130
|
readonly mediaHash: "Media content hashing";
|
|
80
131
|
/** Hash a model file. */
|
|
@@ -89,6 +140,8 @@ export declare const WORKFLOW_STEP_TYPES: {
|
|
|
89
140
|
readonly ageClassification: "Age range classification";
|
|
90
141
|
/** xGuard NSFW / safety moderation. */
|
|
91
142
|
readonly xGuardModeration: "NSFW / safety moderation";
|
|
143
|
+
/** Shieldstral text/prompt safety moderation (`mode: 'prompt' | 'text'`). */
|
|
144
|
+
readonly shieldstralModeration: "Text / prompt safety moderation (Shieldstral)";
|
|
92
145
|
/** ClamAV scan a model file for malware. */
|
|
93
146
|
readonly modelClamScan: "Antivirus scan a model file";
|
|
94
147
|
/** Pickle-scan a model file for unsafe pickles. */
|
|
@@ -103,6 +156,10 @@ export declare const WORKFLOW_STEP_TYPES: {
|
|
|
103
156
|
readonly echo: "Echo step — round-trip the input for testing";
|
|
104
157
|
/** Package multiple blobs into a zip archive. */
|
|
105
158
|
readonly blobArchive: "Zip multiple blobs into an archive";
|
|
159
|
+
/** Snapshot the installed ComfyUI custom-node packs on a worker. */
|
|
160
|
+
readonly comfyNodepackSnapshot: "Snapshot a worker’s installed ComfyUI node packs (internal)";
|
|
161
|
+
/** Qwen image benchmarking harness. */
|
|
162
|
+
readonly qwenImageBench: "Qwen image benchmarking (internal)";
|
|
106
163
|
};
|
|
107
164
|
export type WorkflowStepType = keyof typeof WORKFLOW_STEP_TYPES;
|
|
108
165
|
/**
|
|
@@ -352,28 +409,116 @@ export declare function estimateWorkflow(client: OrchestratorClient, body: unkno
|
|
|
352
409
|
* const urls = extractImageUrls(finished);
|
|
353
410
|
*/
|
|
354
411
|
export declare function submitWorkflow(client: OrchestratorClient, body: unknown): Promise<WorkflowSnapshot>;
|
|
412
|
+
/** Per-call controls for {@link getWorkflow}. */
|
|
413
|
+
export interface GetWorkflowOptions {
|
|
414
|
+
/**
|
|
415
|
+
* 🔴 THE ORCHESTRATOR'S OWN LONG-POLL HOLD, IN **SECONDS**, NOT MILLISECONDS.
|
|
416
|
+
*
|
|
417
|
+
* Sent as the `?wait=` query parameter, which the orchestrator documents as
|
|
418
|
+
* *"Whether to wait for the workflow to complete before returning or to
|
|
419
|
+
* return immediately. The request may return a 202 if the client waits for
|
|
420
|
+
* the workflow to complete and the workflow does not complete within the
|
|
421
|
+
* requested timeout."*
|
|
422
|
+
*
|
|
423
|
+
* The unit is seconds. That is not obvious from the parameter's name and
|
|
424
|
+
* getting it wrong is silent in both directions — `wait: 30000` asks for
|
|
425
|
+
* ~8 hours and `wait: 0.03` rounds to nothing — so it is spelled out here and
|
|
426
|
+
* in the option's name. Omit (or `0`) for the pre-existing one-shot read.
|
|
427
|
+
*
|
|
428
|
+
* A 202 is a NORMAL outcome, not an error: `callOrchestrator` treats every
|
|
429
|
+
* 2xx as success, so a timed-out hold returns the current (non-terminal)
|
|
430
|
+
* snapshot exactly like a 200 would. Callers re-arm; see {@link pollWorkflow}.
|
|
431
|
+
*/
|
|
432
|
+
waitSeconds?: number;
|
|
433
|
+
/**
|
|
434
|
+
* Abort signal forwarded to `fetch`, so a long hold is genuinely CANCELLED
|
|
435
|
+
* rather than merely abandoned. A `Promise.race` against a timer leaves the
|
|
436
|
+
* underlying request in flight holding a socket, which is precisely the
|
|
437
|
+
* failure mode a long poll makes expensive.
|
|
438
|
+
*/
|
|
439
|
+
signal?: AbortSignal;
|
|
440
|
+
}
|
|
355
441
|
/**
|
|
356
|
-
* Fetch a single workflow's current snapshot by id.
|
|
357
|
-
*
|
|
442
|
+
* Fetch a single workflow's current snapshot by id.
|
|
443
|
+
*
|
|
444
|
+
* One-shot by default. Pass {@link GetWorkflowOptions.waitSeconds} to have the
|
|
445
|
+
* ORCHESTRATOR hold the request open until the workflow reaches a terminal
|
|
446
|
+
* status (a real long poll, server-side) instead of returning immediately; for
|
|
447
|
+
* a full "wait until done, re-arming across timeouts" loop use
|
|
448
|
+
* {@link pollWorkflow}.
|
|
358
449
|
*
|
|
359
450
|
* @example
|
|
360
451
|
* const snap = await getWorkflow(client, workflowId);
|
|
361
452
|
* if (isTerminal(snap)) console.log(extractImageUrls(snap));
|
|
453
|
+
*
|
|
454
|
+
* @example Long poll: one request that returns as soon as the workflow ends
|
|
455
|
+
* const snap = await getWorkflow(client, workflowId, { waitSeconds: 20 });
|
|
456
|
+
* // A non-terminal snapshot here means the hold elapsed (HTTP 202) — ask again.
|
|
457
|
+
* if (!isTerminal(snap)) await getWorkflow(client, workflowId, { waitSeconds: 20 });
|
|
362
458
|
*/
|
|
363
|
-
export declare function getWorkflow(client: OrchestratorClient, workflowId: string): Promise<WorkflowSnapshot>;
|
|
459
|
+
export declare function getWorkflow(client: OrchestratorClient, workflowId: string, opts?: GetWorkflowOptions): Promise<WorkflowSnapshot>;
|
|
460
|
+
/**
|
|
461
|
+
* Default orchestrator-side hold per {@link pollWorkflow} attempt, in seconds.
|
|
462
|
+
*
|
|
463
|
+
* 20s rather than something larger because a held request occupies a socket on
|
|
464
|
+
* BOTH sides for its whole duration, and because the platforms these starters
|
|
465
|
+
* deploy to cap total request time (see PORTING.md's serverless note). It turns
|
|
466
|
+
* the default 30s budget from ~30 requests into ~2 while DETECTING completion
|
|
467
|
+
* sooner, not later — the hold returns the instant the workflow ends.
|
|
468
|
+
*/
|
|
469
|
+
export declare const DEFAULT_POLL_WAIT_SECONDS = 20;
|
|
364
470
|
export interface PollWorkflowOptions {
|
|
365
|
-
/**
|
|
471
|
+
/**
|
|
472
|
+
* Delay BETWEEN attempts, in ms. Default 1000.
|
|
473
|
+
*
|
|
474
|
+
* 🔴 KEPT, AND LOAD-BEARING, EVEN THOUGH LONG POLLING MAKES IT LOOK
|
|
475
|
+
* REDUNDANT. With `waitSeconds > 0` an attempt normally consumes the whole
|
|
476
|
+
* hold, so this is ~3% overhead on a cycle. But if the orchestrator ever
|
|
477
|
+
* stops honouring `wait` — an older deployment, a proxy that strips the query
|
|
478
|
+
* string, a 202 returned instantly — a zero gap turns this loop into a hot
|
|
479
|
+
* loop hammering the API at fetch speed. This delay is the floor that makes
|
|
480
|
+
* that failure slow instead of catastrophic.
|
|
481
|
+
*/
|
|
366
482
|
intervalMs?: number;
|
|
367
483
|
/** Max total time to poll in ms. Default 30000. */
|
|
368
484
|
timeoutMs?: number;
|
|
369
|
-
/** Optional abort signal —
|
|
485
|
+
/** Optional abort signal — cancels the in-flight request and stops the loop. */
|
|
370
486
|
signal?: AbortSignal;
|
|
487
|
+
/**
|
|
488
|
+
* Orchestrator-side hold per attempt, in **seconds**. Default
|
|
489
|
+
* {@link DEFAULT_POLL_WAIT_SECONDS}. Pass `0` to restore the pre-0.31
|
|
490
|
+
* pure-timer behaviour (one immediate read per `intervalMs`).
|
|
491
|
+
*
|
|
492
|
+
* Automatically clamped down to the time left on `timeoutMs`, so a long hold
|
|
493
|
+
* cannot overrun the caller's budget.
|
|
494
|
+
*/
|
|
495
|
+
waitSeconds?: number;
|
|
371
496
|
}
|
|
372
497
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
498
|
+
* Wait for a workflow to reach a terminal status, using the orchestrator's
|
|
499
|
+
* SERVER-SIDE long poll (`?wait=`) and re-arming across each 202 until the
|
|
500
|
+
* workflow ends, the `timeoutMs` budget elapses, or `signal` aborts. Returns
|
|
501
|
+
* the latest snapshot regardless of which condition tripped — callers inspect
|
|
502
|
+
* {@link isTerminal} on the result to decide what to do.
|
|
503
|
+
*
|
|
504
|
+
* 🔴 THIS FUNCTION USED TO BE LABELLED A LONG POLL AND WAS NOT ONE. Until
|
|
505
|
+
* @civitai/app-sdk 0.31.0 it was a client-side `setTimeout` loop re-reading the
|
|
506
|
+
* workflow every `intervalMs` (default 1000) with no `wait` parameter — i.e. a
|
|
507
|
+
* TIMER poll wearing a long poll's docstring, and the same false claim had
|
|
508
|
+
* propagated into this package's README (its API table and the "`pollWorkflow`
|
|
509
|
+
* long-polls to terminal status" line). The label is now true rather than
|
|
510
|
+
* softened, because the orchestrator has supported the parameter all along and
|
|
511
|
+
* four non-blocks civitai call sites were already using it.
|
|
512
|
+
*
|
|
513
|
+
* (PORTING.md's "long-poll" wording is NOT part of that: it describes the
|
|
514
|
+
* BLOCK-IN-THE-HANDLER pattern, which the starters genuinely did regardless of
|
|
515
|
+
* the mechanism underneath, and its advice to cap `timeoutMs` under the
|
|
516
|
+
* platform budget is still correct — more so now that the hold is real.)
|
|
517
|
+
*
|
|
518
|
+
* WHAT A CALLER SEES THAT IS DIFFERENT: fewer requests (~2 instead of ~30 on
|
|
519
|
+
* the default budget) and terminal status detected sooner (the hold returns
|
|
520
|
+
* when the workflow ends, not on the next tick after it ended). The RETURN
|
|
521
|
+
* CONTRACT is unchanged.
|
|
377
522
|
*
|
|
378
523
|
* @example
|
|
379
524
|
* const finished = await pollWorkflow(client, submitted.id, { timeoutMs: 30_000 });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/orchestrator/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,eAAO,MAAM,6BAA6B,sCAAsC,CAAC;AAEjF,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,kDAAkD,CAAC;AAEjF;;GAEG;AACH,eAAO,MAAM,iBAAiB,yDAA0D,CAAC;AACzF,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAIhE
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/orchestrator/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -28,8 +28,30 @@ export const TERMINAL_STATUSES = ['succeeded', 'failed', 'expired', 'canceled'];
|
|
|
28
28
|
* at the matching `build*Body` helper (if one exists) or fall back to
|
|
29
29
|
* {@link callOrchestrator} with a hand-crafted body.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* 🔴 SOURCE OF TRUTH — AND HOW IT IS HELD. The authoritative population is the
|
|
32
|
+
* `WorkflowStepTemplate` **discriminator mapping** in
|
|
33
|
+
* `https://orchestration.civitai.com/openapi/v2-consumers.json` (the submit-side
|
|
34
|
+
* union; `WorkflowStep`'s mapping is identical). That is the *defining* surface —
|
|
35
|
+
* enumerate it, do not sample the spec's type names or a generated client's
|
|
36
|
+
* exports, which include shapes that are not submittable step types.
|
|
37
|
+
*
|
|
38
|
+
* The keys below are pinned EXACTLY against that mapping two ways:
|
|
39
|
+
* - `test/orchestrator.test.ts` compares this catalog's key set to a list
|
|
40
|
+
* transcribed from the spec (hermetic, offline, order-insensitive), and
|
|
41
|
+
* - `scripts/check-orchestrator-catalogs.mjs` re-fetches the LIVE spec and
|
|
42
|
+
* diffs it (network; wired into CI as `pnpm check:catalogs`).
|
|
43
|
+
*
|
|
44
|
+
* The second one is what notices the orchestrator shipping a new step. When it
|
|
45
|
+
* fails, refresh both this catalog and the test's transcribed list in the same
|
|
46
|
+
* PR — the test is a deliberate second copy so a catalog edit cannot mark its
|
|
47
|
+
* own homework.
|
|
48
|
+
*
|
|
49
|
+
* 🔴 A SPOT-CHECK IS NOT A PIN. Until 2026-08 this catalog was guarded only by
|
|
50
|
+
* `expect(keys).toContain('textToImage')`-style assertions over 7 names. It
|
|
51
|
+
* carried a phantom `audioMix` — a `$type` that appears **0 times** in the spec
|
|
52
|
+
* and would have come back a 400 — and was missing 10 real step types, for as
|
|
53
|
+
* long as those assertions existed. An exact-set comparison is the whole
|
|
54
|
+
* difference.
|
|
33
55
|
*/
|
|
34
56
|
export const WORKFLOW_STEP_TYPES = {
|
|
35
57
|
// ----- Image gen ---------------------------------------------------------
|
|
@@ -43,8 +65,18 @@ export const WORKFLOW_STEP_TYPES = {
|
|
|
43
65
|
imageGen: 'Closed-source image gen (Nano Banana, Gemini, GPT-Image, Flux Kontext, Seedream, Grok, fal, …)',
|
|
44
66
|
/** Arbitrary ComfyUI workflow graphs. Pass a `prompt` object (node graph). */
|
|
45
67
|
comfy: 'Custom ComfyUI node-graph workflows',
|
|
68
|
+
/**
|
|
69
|
+
* Raw ComfyUI graph + an explicit AIR download manifest (`resources`). The
|
|
70
|
+
* orchestrator forwards the graph opaquely, so anything the graph references
|
|
71
|
+
* must also be declared in `resources` or it fails at load time inside ComfyUI.
|
|
72
|
+
*/
|
|
73
|
+
customComfy: 'Raw ComfyUI graph with a declared AIR resource manifest',
|
|
46
74
|
/** Upscale an existing image. Input is a source image URL + scale factor. */
|
|
47
75
|
imageUpscaler: 'Image upscaling',
|
|
76
|
+
/** Remove an image background via BiRefNet (runs as a comfy job under the hood). */
|
|
77
|
+
imageBackgroundRemoval: 'Image background removal (BiRefNet)',
|
|
78
|
+
/** Vectorize a raster image with a local StarVector / OmniSVG model. */
|
|
79
|
+
imageToSvg: 'Raster image → SVG vectorization',
|
|
48
80
|
/** LoRA / DoRA / embedding training. Long-running. */
|
|
49
81
|
imageResourceTraining: 'Train a LoRA / DoRA / embedding from a dataset',
|
|
50
82
|
/** Pre-process an image (resize, ControlNet preprocessor, etc.). */
|
|
@@ -64,6 +96,8 @@ export const WORKFLOW_STEP_TYPES = {
|
|
|
64
96
|
videoEnhancement: 'Per-frame video enhancement',
|
|
65
97
|
/** Extract individual frames from a video. */
|
|
66
98
|
videoFrameExtraction: 'Extract frames from a video',
|
|
99
|
+
/** Track a prompted foreground object and return a transparent animated WebP. */
|
|
100
|
+
videoBackgroundRemoval: 'Video background removal (prompted object tracking)',
|
|
67
101
|
/** Read video metadata (duration, codec, dimensions). */
|
|
68
102
|
videoMetadata: 'Read video file metadata',
|
|
69
103
|
/** Transcode video format / codec. */
|
|
@@ -75,10 +109,30 @@ export const WORKFLOW_STEP_TYPES = {
|
|
|
75
109
|
aceStepAudio: 'Music generation (ACE Step 1.5)',
|
|
76
110
|
/** Speech-to-text transcription. */
|
|
77
111
|
transcription: 'Speech-to-text transcription',
|
|
78
|
-
/** Mix multiple audio tracks. */
|
|
79
|
-
audioMix: 'Audio track mixing',
|
|
80
112
|
/** Generate captions from audio. */
|
|
81
113
|
audioCaptioning: 'Caption generation from audio',
|
|
114
|
+
// ----- Media composition -------------------------------------------------
|
|
115
|
+
/**
|
|
116
|
+
* Compose an ordered list of media `elements` into one output. Its output is
|
|
117
|
+
* discriminated on `type`: `'audio'` for a mixdown, `'video'` for a
|
|
118
|
+
* composition — so this covers audio mixing as well as video assembly.
|
|
119
|
+
*
|
|
120
|
+
* 🔴 This replaced a catalog entry named `audioMix`, which never existed in
|
|
121
|
+
* the orchestrator spec. If you were reaching for `audioMix`, this is it.
|
|
122
|
+
*/
|
|
123
|
+
composeMedia: 'Compose media elements into one audio mixdown or video',
|
|
124
|
+
// ----- 3D ----------------------------------------------------------------
|
|
125
|
+
/** Generate a 3D model. Engines: `comfy`, `fal`. */
|
|
126
|
+
polyGen: '3D model generation',
|
|
127
|
+
/** Render a preview of an existing 3D model. */
|
|
128
|
+
model3DPreview: '3D model preview rendering',
|
|
129
|
+
// ----- Training ----------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Generic training step. Engines: `ai-toolkit`, `comfy`. Distinct from
|
|
132
|
+
* {@link WORKFLOW_STEP_TYPES.imageResourceTraining}, which is the older
|
|
133
|
+
* kohya/musubi/flux-dev-fast LoRA path.
|
|
134
|
+
*/
|
|
135
|
+
training: 'Model training (ai-toolkit / comfy engines)',
|
|
82
136
|
// ----- Classification / tagging / moderation ----------------------------
|
|
83
137
|
/** Hash an image / video / model for dedup or lookup. */
|
|
84
138
|
mediaHash: 'Media content hashing',
|
|
@@ -94,6 +148,8 @@ export const WORKFLOW_STEP_TYPES = {
|
|
|
94
148
|
ageClassification: 'Age range classification',
|
|
95
149
|
/** xGuard NSFW / safety moderation. */
|
|
96
150
|
xGuardModeration: 'NSFW / safety moderation',
|
|
151
|
+
/** Shieldstral text/prompt safety moderation (`mode: 'prompt' | 'text'`). */
|
|
152
|
+
shieldstralModeration: 'Text / prompt safety moderation (Shieldstral)',
|
|
97
153
|
/** ClamAV scan a model file for malware. */
|
|
98
154
|
modelClamScan: 'Antivirus scan a model file',
|
|
99
155
|
/** Pickle-scan a model file for unsafe pickles. */
|
|
@@ -110,6 +166,14 @@ export const WORKFLOW_STEP_TYPES = {
|
|
|
110
166
|
echo: 'Echo step — round-trip the input for testing',
|
|
111
167
|
/** Package multiple blobs into a zip archive. */
|
|
112
168
|
blobArchive: 'Zip multiple blobs into an archive',
|
|
169
|
+
// ----- Platform internals ------------------------------------------------
|
|
170
|
+
// Present in the consumer spec, so listed here for completeness — but these
|
|
171
|
+
// exist to serve Civitai's own pipelines. A third-party app has no reason to
|
|
172
|
+
// submit one, and the surrounding features may be gated or disabled.
|
|
173
|
+
/** Snapshot the installed ComfyUI custom-node packs on a worker. */
|
|
174
|
+
comfyNodepackSnapshot: 'Snapshot a worker’s installed ComfyUI node packs (internal)',
|
|
175
|
+
/** Qwen image benchmarking harness. */
|
|
176
|
+
qwenImageBench: 'Qwen image benchmarking (internal)',
|
|
113
177
|
};
|
|
114
178
|
/**
|
|
115
179
|
* Engines that the `imageGen` step accepts. Each one has its own input shape;
|
|
@@ -345,21 +409,82 @@ export function submitWorkflow(client, body) {
|
|
|
345
409
|
});
|
|
346
410
|
}
|
|
347
411
|
/**
|
|
348
|
-
* Fetch a single workflow's current snapshot by id.
|
|
349
|
-
*
|
|
412
|
+
* Fetch a single workflow's current snapshot by id.
|
|
413
|
+
*
|
|
414
|
+
* One-shot by default. Pass {@link GetWorkflowOptions.waitSeconds} to have the
|
|
415
|
+
* ORCHESTRATOR hold the request open until the workflow reaches a terminal
|
|
416
|
+
* status (a real long poll, server-side) instead of returning immediately; for
|
|
417
|
+
* a full "wait until done, re-arming across timeouts" loop use
|
|
418
|
+
* {@link pollWorkflow}.
|
|
350
419
|
*
|
|
351
420
|
* @example
|
|
352
421
|
* const snap = await getWorkflow(client, workflowId);
|
|
353
422
|
* if (isTerminal(snap)) console.log(extractImageUrls(snap));
|
|
423
|
+
*
|
|
424
|
+
* @example Long poll: one request that returns as soon as the workflow ends
|
|
425
|
+
* const snap = await getWorkflow(client, workflowId, { waitSeconds: 20 });
|
|
426
|
+
* // A non-terminal snapshot here means the hold elapsed (HTTP 202) — ask again.
|
|
427
|
+
* if (!isTerminal(snap)) await getWorkflow(client, workflowId, { waitSeconds: 20 });
|
|
354
428
|
*/
|
|
355
|
-
export function getWorkflow(client, workflowId) {
|
|
356
|
-
|
|
429
|
+
export function getWorkflow(client, workflowId, opts = {}) {
|
|
430
|
+
// Built with URLSearchParams rather than string concatenation so a future
|
|
431
|
+
// second parameter cannot reintroduce a `?`-vs-`&` bug, and so a non-finite
|
|
432
|
+
// `waitSeconds` can never be serialised into the URL.
|
|
433
|
+
const wait = typeof opts.waitSeconds === 'number' && Number.isFinite(opts.waitSeconds)
|
|
434
|
+
? Math.max(0, Math.floor(opts.waitSeconds))
|
|
435
|
+
: 0;
|
|
436
|
+
const qs = wait > 0 ? `?${new URLSearchParams({ wait: String(wait) }).toString()}` : '';
|
|
437
|
+
return callOrchestrator(client, `/v2/consumer/workflows/${encodeURIComponent(workflowId)}${qs}`, {
|
|
438
|
+
method: 'GET',
|
|
439
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
440
|
+
});
|
|
357
441
|
}
|
|
442
|
+
// ---------- Polling ---------------------------------------------------------
|
|
443
|
+
/**
|
|
444
|
+
* Default orchestrator-side hold per {@link pollWorkflow} attempt, in seconds.
|
|
445
|
+
*
|
|
446
|
+
* 20s rather than something larger because a held request occupies a socket on
|
|
447
|
+
* BOTH sides for its whole duration, and because the platforms these starters
|
|
448
|
+
* deploy to cap total request time (see PORTING.md's serverless note). It turns
|
|
449
|
+
* the default 30s budget from ~30 requests into ~2 while DETECTING completion
|
|
450
|
+
* sooner, not later — the hold returns the instant the workflow ends.
|
|
451
|
+
*/
|
|
452
|
+
export const DEFAULT_POLL_WAIT_SECONDS = 20;
|
|
358
453
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
454
|
+
* Slack added to a per-attempt abort deadline, in ms.
|
|
455
|
+
*
|
|
456
|
+
* 🔴 THE ABORT MUST SIT ABOVE THE HOLD, NOT AT IT. `waitSeconds` is what we ASK
|
|
457
|
+
* the orchestrator to hold for; the abort is this side's defence against a
|
|
458
|
+
* socket it accepted and abandoned, which `wait` cannot cover because a request
|
|
459
|
+
* that never returns never times out. Set equal to the hold, the abort would
|
|
460
|
+
* race the orchestrator's own timely 202 and cancel healthy requests.
|
|
461
|
+
*/
|
|
462
|
+
const POLL_ABORT_SLACK_MS = 5_000;
|
|
463
|
+
/**
|
|
464
|
+
* Wait for a workflow to reach a terminal status, using the orchestrator's
|
|
465
|
+
* SERVER-SIDE long poll (`?wait=`) and re-arming across each 202 until the
|
|
466
|
+
* workflow ends, the `timeoutMs` budget elapses, or `signal` aborts. Returns
|
|
467
|
+
* the latest snapshot regardless of which condition tripped — callers inspect
|
|
468
|
+
* {@link isTerminal} on the result to decide what to do.
|
|
469
|
+
*
|
|
470
|
+
* 🔴 THIS FUNCTION USED TO BE LABELLED A LONG POLL AND WAS NOT ONE. Until
|
|
471
|
+
* @civitai/app-sdk 0.31.0 it was a client-side `setTimeout` loop re-reading the
|
|
472
|
+
* workflow every `intervalMs` (default 1000) with no `wait` parameter — i.e. a
|
|
473
|
+
* TIMER poll wearing a long poll's docstring, and the same false claim had
|
|
474
|
+
* propagated into this package's README (its API table and the "`pollWorkflow`
|
|
475
|
+
* long-polls to terminal status" line). The label is now true rather than
|
|
476
|
+
* softened, because the orchestrator has supported the parameter all along and
|
|
477
|
+
* four non-blocks civitai call sites were already using it.
|
|
478
|
+
*
|
|
479
|
+
* (PORTING.md's "long-poll" wording is NOT part of that: it describes the
|
|
480
|
+
* BLOCK-IN-THE-HANDLER pattern, which the starters genuinely did regardless of
|
|
481
|
+
* the mechanism underneath, and its advice to cap `timeoutMs` under the
|
|
482
|
+
* platform budget is still correct — more so now that the hold is real.)
|
|
483
|
+
*
|
|
484
|
+
* WHAT A CALLER SEES THAT IS DIFFERENT: fewer requests (~2 instead of ~30 on
|
|
485
|
+
* the default budget) and terminal status detected sooner (the hold returns
|
|
486
|
+
* when the workflow ends, not on the next tick after it ended). The RETURN
|
|
487
|
+
* CONTRACT is unchanged.
|
|
363
488
|
*
|
|
364
489
|
* @example
|
|
365
490
|
* const finished = await pollWorkflow(client, submitted.id, { timeoutMs: 30_000 });
|
|
@@ -368,16 +493,79 @@ export function getWorkflow(client, workflowId) {
|
|
|
368
493
|
export async function pollWorkflow(client, workflowId, opts = {}) {
|
|
369
494
|
const interval = opts.intervalMs ?? 1000;
|
|
370
495
|
const timeout = opts.timeoutMs ?? 30_000;
|
|
496
|
+
const requestedWait = opts.waitSeconds ?? DEFAULT_POLL_WAIT_SECONDS;
|
|
371
497
|
const deadline = Date.now() + timeout;
|
|
372
|
-
|
|
498
|
+
// The hold this attempt may ask for: the caller's `waitSeconds`, floored at 0
|
|
499
|
+
// and clamped to whatever is LEFT of the total budget, so a 20s hold started
|
|
500
|
+
// with 3s remaining asks for 3s rather than overrunning by 17.
|
|
501
|
+
const holdFor = () => {
|
|
502
|
+
if (!Number.isFinite(requestedWait) || requestedWait <= 0)
|
|
503
|
+
return 0;
|
|
504
|
+
const remainingSeconds = Math.floor((deadline - Date.now()) / 1000);
|
|
505
|
+
return Math.max(0, Math.min(Math.floor(requestedWait), remainingSeconds));
|
|
506
|
+
};
|
|
507
|
+
const attempt = (waitSeconds) => {
|
|
508
|
+
if (waitSeconds <= 0) {
|
|
509
|
+
return getWorkflow(client, workflowId, {
|
|
510
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
// Per-attempt deadline as a real AbortController — the fetch is CANCELLED,
|
|
514
|
+
// not just stopped being awaited. Linked to the caller's signal by hand
|
|
515
|
+
// rather than via `AbortSignal.any`, which is too new to require of every
|
|
516
|
+
// browser this client-safe package runs in.
|
|
517
|
+
const ctl = new AbortController();
|
|
518
|
+
const timer = setTimeout(() => ctl.abort(), waitSeconds * 1000 + POLL_ABORT_SLACK_MS);
|
|
519
|
+
const onOuterAbort = () => ctl.abort();
|
|
520
|
+
opts.signal?.addEventListener('abort', onOuterAbort);
|
|
521
|
+
return getWorkflow(client, workflowId, { waitSeconds, signal: ctl.signal }).finally(() => {
|
|
522
|
+
clearTimeout(timer);
|
|
523
|
+
opts.signal?.removeEventListener('abort', onOuterAbort);
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
// The FIRST read is not wrapped: a throw here propagates, exactly as it did
|
|
527
|
+
// before long polling existed. There is no prior snapshot to fall back to, so
|
|
528
|
+
// swallowing it would return `undefined` typed as a snapshot.
|
|
529
|
+
let snapshot = await attempt(holdFor());
|
|
373
530
|
while (!isTerminal(snapshot) && Date.now() + interval <= deadline) {
|
|
374
531
|
if (opts.signal?.aborted)
|
|
375
532
|
break;
|
|
376
533
|
await new Promise((r) => setTimeout(r, interval));
|
|
377
|
-
|
|
534
|
+
if (opts.signal?.aborted)
|
|
535
|
+
break;
|
|
536
|
+
try {
|
|
537
|
+
snapshot = await attempt(holdFor());
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
// 🔴 A LATER ATTEMPT'S ABORT MUST NOT DESTROY A SNAPSHOT WE ALREADY HAVE.
|
|
541
|
+
// Both this function's own per-attempt deadline and the caller's signal
|
|
542
|
+
// surface as an abort throw. Neither is news about the workflow, and the
|
|
543
|
+
// documented contract is "returns the latest snapshot" — so we keep the
|
|
544
|
+
// one we hold and stop. Any OTHER error still propagates, which is what
|
|
545
|
+
// the pre-long-poll loop did for every error.
|
|
546
|
+
if (!isAbortError(err))
|
|
547
|
+
throw err;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
378
550
|
}
|
|
379
551
|
return snapshot;
|
|
380
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Is this thrown value an abort (either our per-attempt deadline or the
|
|
555
|
+
* caller's signal)?
|
|
556
|
+
*
|
|
557
|
+
* Matched on `name`, not `instanceof DOMException`: the abort reason is
|
|
558
|
+
* produced by whichever fetch implementation is in play (undici, a browser, a
|
|
559
|
+
* test double), and those do not share a class. Node's undici throws a
|
|
560
|
+
* `DOMException` named `AbortError`; a polyfilled or mocked fetch may throw a
|
|
561
|
+
* plain `Error` with the same name.
|
|
562
|
+
*/
|
|
563
|
+
function isAbortError(err) {
|
|
564
|
+
return (typeof err === 'object' &&
|
|
565
|
+
err !== null &&
|
|
566
|
+
'name' in err &&
|
|
567
|
+
err.name === 'AbortError');
|
|
568
|
+
}
|
|
381
569
|
// ---------- Snapshot inspection --------------------------------------------
|
|
382
570
|
/**
|
|
383
571
|
* True when a snapshot has reached a terminal status (`succeeded` | `failed` |
|