@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.
@@ -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
- export {};
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
@@ -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
- * Source of truth is `https://orchestration.civitai.com/openapi/v2-consumers.json`.
31
- * If a step type is missing here, the catalog is stale — open a PR.
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. One-shot; for "wait until
357
- * done" use {@link pollWorkflow}.
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
- /** Polling interval in ms. Default 1000. */
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 — checked between ticks. */
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
- * Server-side long-poll helper. Re-fetches the workflow every `intervalMs`
374
- * until it reaches a terminal status, the timeout elapses, or the signal
375
- * aborts. Returns the latest snapshot regardless of which condition tripped —
376
- * callers inspect {@link isTerminal} on the result to decide what to do.
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":"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"}
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
- * Source of truth is `https://orchestration.civitai.com/openapi/v2-consumers.json`.
32
- * If a step type is missing here, the catalog is stale — open a PR.
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. One-shot; for "wait until
349
- * done" use {@link pollWorkflow}.
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
- return callOrchestrator(client, `/v2/consumer/workflows/${encodeURIComponent(workflowId)}`, { method: 'GET' });
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
- * Server-side long-poll helper. Re-fetches the workflow every `intervalMs`
360
- * until it reaches a terminal status, the timeout elapses, or the signal
361
- * aborts. Returns the latest snapshot regardless of which condition tripped —
362
- * callers inspect {@link isTerminal} on the result to decide what to do.
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
- let snapshot = await getWorkflow(client, workflowId);
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
- snapshot = await getWorkflow(client, workflowId);
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` |