@civitai/blocks-react 0.76.1 → 0.78.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 +125 -52
- package/dist/hooks/useImageUpload.d.ts +40 -6
- package/dist/hooks/useImageUpload.js +19 -3
- package/dist/hooks/useResourceFile.d.ts +71 -0
- package/dist/hooks/useResourceFile.js +113 -0
- package/dist/hooks/useRunTraining.d.ts +31 -10
- package/dist/hooks/useRunTraining.js +32 -7
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/internal/liveHost.d.ts +7 -8
- package/dist/internal/liveHost.js +43 -28
- package/dist/internal/mockHost.d.ts +53 -44
- package/dist/internal/mockHost.js +219 -84
- package/dist/transport/requestTimeouts.d.ts +32 -7
- package/dist/transport/requestTimeouts.js +36 -12
- package/dist/transport/validate.d.ts +18 -17
- package/dist/transport/validate.js +97 -47
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +2 -2
- package/dist/hooks/usePrepareTrainingDataset.d.ts +0 -99
- package/dist/hooks/usePrepareTrainingDataset.js +0 -159
|
@@ -154,33 +154,81 @@ const trainingStepType = (body) => body.kind === 'training' ? 'training' : passT
|
|
|
154
154
|
*/
|
|
155
155
|
export const MOCK_TRAINING_BOUNDS = {
|
|
156
156
|
datasetMaxItems: 50,
|
|
157
|
-
captionMaxChars:
|
|
157
|
+
captionMaxChars: 8192,
|
|
158
158
|
maxBuzzPerRun: 5000,
|
|
159
159
|
};
|
|
160
|
+
/** A consumer-blob key: the host's `BLOB_KEY_REGEX` (civitai/civitai#6461). */
|
|
161
|
+
const MOCK_BLOB_KEY = /^[A-Za-z0-9]{1,64}(\.[A-Za-z0-9]{1,8})?$/;
|
|
162
|
+
/** The path a blob url names its key under. */
|
|
163
|
+
const MOCK_BLOB_PATH_PREFIX = '/v2/consumer/blobs/';
|
|
164
|
+
/** The host's cap on an item's `air` string (`SOURCE_IMAGE_URL_MAX`). */
|
|
165
|
+
const MOCK_AIR_MAX_CHARS = 2048;
|
|
160
166
|
/**
|
|
161
|
-
* The
|
|
162
|
-
* `
|
|
163
|
-
*
|
|
164
|
-
*
|
|
167
|
+
* The blob key an item's `air` names, or `null` — the host's
|
|
168
|
+
* `trainingBlobKeyFromAir`: the bare key, or an https url whose path is exactly
|
|
169
|
+
* `/v2/consumer/blobs/<key>` (its query dropped).
|
|
170
|
+
*
|
|
171
|
+
* ⚠️ NARROWER THAN THE HOST: the real check also requires the url to be on a
|
|
172
|
+
* Civitai-hosted origin; the mock accepts any https host so a harness can use
|
|
173
|
+
* placeholder urls. Name an item by its `air` (the bare key) and the two agree.
|
|
174
|
+
*/
|
|
175
|
+
const mockBlobKeyFromAir = (air) => {
|
|
176
|
+
if (MOCK_BLOB_KEY.test(air))
|
|
177
|
+
return air;
|
|
178
|
+
let url;
|
|
179
|
+
try {
|
|
180
|
+
url = new URL(air);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (url.protocol !== 'https:' || !url.pathname.startsWith(MOCK_BLOB_PATH_PREFIX))
|
|
186
|
+
return null;
|
|
187
|
+
const key = url.pathname.slice(MOCK_BLOB_PATH_PREFIX.length);
|
|
188
|
+
return MOCK_BLOB_KEY.test(key) ? key : null;
|
|
189
|
+
};
|
|
190
|
+
/** The host's key identity (`trainingBlobKeyIdentity`): keys are case-insensitive. */
|
|
191
|
+
const blobKeyIdentity = (key) => key.toLowerCase();
|
|
192
|
+
/**
|
|
193
|
+
* The server's `kind: 'training'` item schema (`blockTrainingDatasetItemsSchema`,
|
|
194
|
+
* civitai/civitai#6461), mirrored for the ESTIMATE: 1…50 items, each EXACTLY
|
|
195
|
+
* `{ air, caption }` with a caption up to 8,192 characters, every `air` a blob key
|
|
196
|
+
* or blob url, and no blob named twice. Returns the items with each `air`
|
|
197
|
+
* canonicalised to its bare key — what the server quotes and hashes — or the
|
|
198
|
+
* refusal message.
|
|
165
199
|
*/
|
|
166
|
-
function
|
|
200
|
+
function resolveTrainingItems(items) {
|
|
167
201
|
if (!Array.isArray(items))
|
|
168
|
-
return
|
|
169
|
-
if (items.length < 1 || items.length > MOCK_TRAINING_BOUNDS.datasetMaxItems)
|
|
170
|
-
return
|
|
171
|
-
|
|
202
|
+
return { refusal: MOCK_TRAINING_ITEMS_ERROR };
|
|
203
|
+
if (items.length < 1 || items.length > MOCK_TRAINING_BOUNDS.datasetMaxItems) {
|
|
204
|
+
return { refusal: MOCK_TRAINING_ITEMS_ERROR };
|
|
205
|
+
}
|
|
206
|
+
const shaped = items.every((it) => {
|
|
172
207
|
if (typeof it !== 'object' || it === null || Array.isArray(it))
|
|
173
208
|
return false;
|
|
174
209
|
const keys = Object.keys(it);
|
|
175
|
-
if (keys.length !== 2 || !keys.includes('
|
|
210
|
+
if (keys.length !== 2 || !keys.includes('air') || !keys.includes('caption'))
|
|
176
211
|
return false;
|
|
177
|
-
const {
|
|
178
|
-
return (typeof
|
|
179
|
-
|
|
180
|
-
imageId > 0 &&
|
|
212
|
+
const { air, caption } = it;
|
|
213
|
+
return (typeof air === 'string' &&
|
|
214
|
+
air.length <= MOCK_AIR_MAX_CHARS &&
|
|
181
215
|
typeof caption === 'string' &&
|
|
182
216
|
caption.length <= MOCK_TRAINING_BOUNDS.captionMaxChars);
|
|
183
217
|
});
|
|
218
|
+
if (!shaped)
|
|
219
|
+
return { refusal: MOCK_TRAINING_ITEMS_ERROR };
|
|
220
|
+
const out = [];
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
for (const it of items) {
|
|
223
|
+
const key = mockBlobKeyFromAir(it.air);
|
|
224
|
+
if (key === null)
|
|
225
|
+
return { refusal: MOCK_TRAINING_AIR_ERROR };
|
|
226
|
+
if (seen.has(blobKeyIdentity(key)))
|
|
227
|
+
return { refusal: MOCK_TRAINING_DUPLICATE_ERROR };
|
|
228
|
+
seen.add(blobKeyIdentity(key));
|
|
229
|
+
out.push({ air: key, caption: it.caption });
|
|
230
|
+
}
|
|
231
|
+
return { items: out };
|
|
184
232
|
}
|
|
185
233
|
/**
|
|
186
234
|
* The real server's refusal for ANY money call from a token without the spend
|
|
@@ -192,16 +240,19 @@ function isValidTrainingDatasetItems(items) {
|
|
|
192
240
|
export const MOCK_SPEND_SCOPE_ERROR = 'block lacks ai:write:budgeted scope';
|
|
193
241
|
/** The real server's refusal for an unknown, expired or spent quote on `RUN_TRAINING`. */
|
|
194
242
|
export const MOCK_TRAINING_QUOTE_GONE_ERROR = 'training quote not found, expired or already used — estimate again';
|
|
195
|
-
/** The real server's refusal for a training estimate naming a dataset it does not hold. */
|
|
196
|
-
export const MOCK_TRAINING_DATASET_GONE_ERROR = 'training dataset not found or expired';
|
|
197
243
|
/**
|
|
198
|
-
* The
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
244
|
+
* The estimate's refusals for `items` outside the server's schema. The first two
|
|
245
|
+
* are the server's own custom messages (`blockTrainingDatasetItemsSchema`); the
|
|
246
|
+
* third stands for every other zod issue (count, caption length, missing or extra
|
|
247
|
+
* keys), whose real text is zod's formatting and not a contract.
|
|
202
248
|
*/
|
|
203
|
-
export const
|
|
204
|
-
export const
|
|
249
|
+
export const MOCK_TRAINING_AIR_ERROR = 'air must be a consumer blob key or its Civitai-hosted blob url';
|
|
250
|
+
export const MOCK_TRAINING_DUPLICATE_ERROR = 'a training dataset names each image once';
|
|
251
|
+
export const MOCK_TRAINING_ITEMS_ERROR = 'training items must be 1 to 50 { air, caption } entries with captions of at most 8192 characters';
|
|
252
|
+
/** The host code for a quote naming an image the host did not upload for the block. */
|
|
253
|
+
const MOCK_TRAINING_NOT_UPLOADED_HERE = 'training data not uploaded here';
|
|
254
|
+
/** How many images a default `trainingData` upload returns. */
|
|
255
|
+
const DEFAULT_TRAINING_DATA_UPLOAD_COUNT = 3;
|
|
205
256
|
/**
|
|
206
257
|
* The forced `RUN_TRAINING` errors that settle BEFORE the server claims (and so
|
|
207
258
|
* consumes) the quote: the host's own gate and dialog codes. Derived from the
|
|
@@ -496,6 +547,8 @@ const DEFAULT_IMAGE_BYTES_UPLOAD = {
|
|
|
496
547
|
contentRating: 'pg',
|
|
497
548
|
url: 'https://image.civitai.com/mock/original=true/dev-bytes-upload.png',
|
|
498
549
|
};
|
|
550
|
+
/** Where the default `trainingData` upload's placeholder blob urls point. */
|
|
551
|
+
const DEFAULT_TRAINING_DATA_URL_BASE = 'https://blobs.mock.invalid/v2/consumer/blobs/';
|
|
499
552
|
const DEFAULT_GENERATION_SOURCE_UPLOAD = {
|
|
500
553
|
url: 'https://image.civitai.com/mock/original=true/dev-generation-source.jpeg',
|
|
501
554
|
width: 1024,
|
|
@@ -699,6 +752,38 @@ const DEFAULT_WILDCARD_PACK = {
|
|
|
699
752
|
truncatedLists: [],
|
|
700
753
|
maturity: { browsingLevel: SFW_LEVELS, sfwOnly: true },
|
|
701
754
|
};
|
|
755
|
+
/**
|
|
756
|
+
* Default file reported on `GET_RESOURCE_FILE`: one plain `.json` workflow in the
|
|
757
|
+
* ComfyUI UI format (a `nodes` array), so a block renders a graph out of the box.
|
|
758
|
+
*/
|
|
759
|
+
const DEFAULT_RESOURCE_FILE = {
|
|
760
|
+
meta: {
|
|
761
|
+
modelId: 618700,
|
|
762
|
+
modelVersionId: 691700,
|
|
763
|
+
fileId: 591700,
|
|
764
|
+
fileName: 'sample-workflow.json',
|
|
765
|
+
modelName: 'Sample Workflow',
|
|
766
|
+
versionName: 'v1.0',
|
|
767
|
+
creatorUsername: 'creator',
|
|
768
|
+
},
|
|
769
|
+
entries: [
|
|
770
|
+
{
|
|
771
|
+
path: 'sample-workflow.json',
|
|
772
|
+
format: 'ui',
|
|
773
|
+
graph: {
|
|
774
|
+
last_node_id: 2,
|
|
775
|
+
last_link_id: 1,
|
|
776
|
+
nodes: [
|
|
777
|
+
{ id: 1, type: 'CheckpointLoaderSimple', inputs: [], outputs: [{ name: 'MODEL', links: [1] }] },
|
|
778
|
+
{ id: 2, type: 'KSampler', inputs: [{ name: 'model', link: 1 }], outputs: [] },
|
|
779
|
+
],
|
|
780
|
+
links: [[1, 1, 0, 2, 0, 'MODEL']],
|
|
781
|
+
version: 0.4,
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
truncated: false,
|
|
786
|
+
};
|
|
702
787
|
/**
|
|
703
788
|
* Default app generator SUBQUEUE page reported on `QUERY_APP_WORKFLOWS`. A small
|
|
704
789
|
* mixed-status list (a done gen with two images, one still processing) so a block
|
|
@@ -1083,6 +1168,9 @@ export function createMockHost(options = {}) {
|
|
|
1083
1168
|
// Wildcard-pack bridge data + forced discriminated-error knob.
|
|
1084
1169
|
let wildcardPack = options.wildcardPack ?? DEFAULT_WILDCARD_PACK;
|
|
1085
1170
|
let wildcardPackError = options.wildcardPackError;
|
|
1171
|
+
// Resource-file bridge data + forced closed-set error knob.
|
|
1172
|
+
let resourceFile = options.resourceFile ?? DEFAULT_RESOURCE_FILE;
|
|
1173
|
+
let resourceFileError = options.resourceFileError;
|
|
1086
1174
|
// Collection-follow bridge.
|
|
1087
1175
|
//
|
|
1088
1176
|
// 🔴 THERE IS DELIBERATELY NO FOLLOW-STATE MAP HERE, and an earlier version's
|
|
@@ -1108,10 +1196,11 @@ export function createMockHost(options = {}) {
|
|
|
1108
1196
|
// App Blocks → Post bridge: the canned post + a forced-refusal knob.
|
|
1109
1197
|
let createPostResult = options.createPostResult ?? DEFAULT_CREATE_POST_RESULT;
|
|
1110
1198
|
let createPostError = options.createPostError;
|
|
1111
|
-
// App Blocks training
|
|
1112
|
-
// forced-refusal knobs. The
|
|
1113
|
-
|
|
1114
|
-
let
|
|
1199
|
+
// App Blocks training: the canned upload, the "host forgot its uploads" knob,
|
|
1200
|
+
// the quote total, and forced-refusal knobs. The uploaded keys and the quotes
|
|
1201
|
+
// themselves live per install.
|
|
1202
|
+
let cannedTrainingDataUpload = options.cannedTrainingDataUpload;
|
|
1203
|
+
let trainingDataNotUploadedHere = options.trainingDataNotUploadedHere === true;
|
|
1115
1204
|
let trainingQuoteTotal = options.trainingQuoteTotal ?? DEFAULT_TRAINING_QUOTE_TOTAL;
|
|
1116
1205
|
let runTrainingError = options.runTrainingError;
|
|
1117
1206
|
// SAVE_IMAGE: a forced-refusal knob (see `saveImageError`).
|
|
@@ -1368,10 +1457,29 @@ export function createMockHost(options = {}) {
|
|
|
1368
1457
|
// restored after a reload). `canceled` is set by `CANCEL_WORKFLOW`, so later
|
|
1369
1458
|
// polls keep answering `canceled` instead of resuming the countdown.
|
|
1370
1459
|
const workflows = new Map();
|
|
1371
|
-
// Training:
|
|
1372
|
-
//
|
|
1373
|
-
|
|
1460
|
+
// Training: the blob keys this install's uploads handed out (by identity —
|
|
1461
|
+
// the host's per-block-instance memory), and the quotes its estimates stored
|
|
1462
|
+
// (id → what a run is checked against).
|
|
1463
|
+
const trainingUploads = new Set();
|
|
1464
|
+
let trainingUploadSerial = 0;
|
|
1374
1465
|
const trainingQuotes = new Map();
|
|
1466
|
+
/**
|
|
1467
|
+
* What a training quote is bound to: the body without `quoteId`, its items
|
|
1468
|
+
* canonicalised (the server quotes the bare key). `null` when the body is not
|
|
1469
|
+
* plain JSON or its items do not resolve — then no run can match it.
|
|
1470
|
+
*/
|
|
1471
|
+
const trainingFingerprint = (raw) => {
|
|
1472
|
+
const resolved = resolveTrainingItems(raw.items);
|
|
1473
|
+
if ('refusal' in resolved)
|
|
1474
|
+
return null;
|
|
1475
|
+
const { quoteId: _quoteId, ...rest } = raw;
|
|
1476
|
+
try {
|
|
1477
|
+
return canonicalStepJson({ ...rest, items: resolved.items }, 'body');
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1375
1483
|
// Confirmed steps: the quotes this install's estimates stored (id → what a
|
|
1376
1484
|
// run is checked against). Same serial as the training handles, so every
|
|
1377
1485
|
// handle this install mints is distinct.
|
|
@@ -1667,9 +1775,10 @@ export function createMockHost(options = {}) {
|
|
|
1667
1775
|
}
|
|
1668
1776
|
// TRAINING ESTIMATE — KIND-FAITHFUL, the one exception to the
|
|
1669
1777
|
// kind-agnostic rule above, because the real arm is not a priced
|
|
1670
|
-
// pass-through: it
|
|
1671
|
-
// price above the per-run ceiling, and STORES a quote
|
|
1672
|
-
// checked against. (
|
|
1778
|
+
// pass-through: it checks the dataset items against the server's
|
|
1779
|
+
// schema, refuses a price above the per-run ceiling, and STORES a quote
|
|
1780
|
+
// the run is later checked against. (Uploads are NOT checked here —
|
|
1781
|
+
// the server's estimate does not; the host checks them at the run.) (The spend scope is checked above, for every kind.)
|
|
1673
1782
|
// Each refusal is the server's message in the host's `failureSnapshot`
|
|
1674
1783
|
// shape.
|
|
1675
1784
|
if (body.kind === 'training') {
|
|
@@ -1678,9 +1787,10 @@ export function createMockHost(options = {}) {
|
|
|
1678
1787
|
status: 'failed',
|
|
1679
1788
|
error,
|
|
1680
1789
|
});
|
|
1681
|
-
const
|
|
1682
|
-
if (
|
|
1683
|
-
return refuse(
|
|
1790
|
+
const resolved = resolveTrainingItems(body.items);
|
|
1791
|
+
if ('refusal' in resolved)
|
|
1792
|
+
return refuse(resolved.refusal);
|
|
1793
|
+
const imageCount = resolved.items.length;
|
|
1684
1794
|
const total = trainingQuoteTotal;
|
|
1685
1795
|
if (total > MOCK_TRAINING_BOUNDS.maxBuzzPerRun) {
|
|
1686
1796
|
return refuse(`this training run costs ${total} Buzz, above the per-run limit of ${MOCK_TRAINING_BOUNDS.maxBuzzPerRun} for apps`);
|
|
@@ -1688,7 +1798,8 @@ export function createMockHost(options = {}) {
|
|
|
1688
1798
|
const quoteId = trainingHandle('tq_');
|
|
1689
1799
|
const expiresAtMs = Date.now() + TRAINING_QUOTE_TTL_MS;
|
|
1690
1800
|
trainingQuotes.set(quoteId, {
|
|
1691
|
-
|
|
1801
|
+
fingerprint: trainingFingerprint(body),
|
|
1802
|
+
itemKeys: resolved.items.map((it) => it.air),
|
|
1692
1803
|
total,
|
|
1693
1804
|
expiresAtMs,
|
|
1694
1805
|
spent: false,
|
|
@@ -2540,6 +2651,25 @@ export function createMockHost(options = {}) {
|
|
|
2540
2651
|
});
|
|
2541
2652
|
return;
|
|
2542
2653
|
}
|
|
2654
|
+
case 'GET_RESOURCE_FILE': {
|
|
2655
|
+
// Token-INDEPENDENT read, like GET_WILDCARD_PACK. A forced error
|
|
2656
|
+
// replies with the closed-set code (never free text), as the real
|
|
2657
|
+
// host does. The canned file is returned for any version / file id.
|
|
2658
|
+
if (!isRoutableRequestId(requestId))
|
|
2659
|
+
return;
|
|
2660
|
+
if (resourceFileError !== undefined) {
|
|
2661
|
+
dispatchToBlock({
|
|
2662
|
+
type: 'RESOURCE_FILE_RESULT',
|
|
2663
|
+
payload: { requestId, error: resourceFileError },
|
|
2664
|
+
});
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
dispatchToBlock({
|
|
2668
|
+
type: 'RESOURCE_FILE_RESULT',
|
|
2669
|
+
payload: { requestId, file: resourceFile },
|
|
2670
|
+
});
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2543
2673
|
case 'QUERY_APP_WORKFLOWS': {
|
|
2544
2674
|
// App generator SUBQUEUE read. Drop a request with no requestId
|
|
2545
2675
|
// (unroutable). A forced error replies with the FREE-TEXT error
|
|
@@ -2712,46 +2842,6 @@ export function createMockHost(options = {}) {
|
|
|
2712
2842
|
});
|
|
2713
2843
|
return;
|
|
2714
2844
|
}
|
|
2715
|
-
case 'PREPARE_TRAINING_DATASET': {
|
|
2716
|
-
// The host's gate order (`prepareTrainingDatasetGate.ts`): no
|
|
2717
|
-
// requestId → drop; signed-out → `sign in to train`; items outside
|
|
2718
|
-
// the server's schema → `invalid training dataset`, before any call.
|
|
2719
|
-
// Then the server: the spend scope, then the dataset.
|
|
2720
|
-
if (!isRoutableRequestId(requestId))
|
|
2721
|
-
return;
|
|
2722
|
-
const reply = (payload) => dispatchToBlock({ type: 'TRAINING_DATASET_RESULT', payload: { requestId, ...payload } });
|
|
2723
|
-
if (viewer === null)
|
|
2724
|
-
return reply({ error: 'sign in to train' });
|
|
2725
|
-
const items = typed.payload?.items;
|
|
2726
|
-
if (!isValidTrainingDatasetItems(items)) {
|
|
2727
|
-
return reply({ error: 'invalid training dataset' });
|
|
2728
|
-
}
|
|
2729
|
-
if (!consentGranted)
|
|
2730
|
-
return reply({ error: MOCK_SPEND_SCOPE_ERROR });
|
|
2731
|
-
if (trainingDatasetError !== undefined)
|
|
2732
|
-
return reply({ error: trainingDatasetError });
|
|
2733
|
-
const named = new Set(items.map((it) => it.imageId));
|
|
2734
|
-
const rejected = trainingDatasetRejected
|
|
2735
|
-
.filter((r) => named.has(r.imageId))
|
|
2736
|
-
.map(({ imageId, reason }) => ({ imageId, reason }));
|
|
2737
|
-
const rejectedIds = new Set(rejected.map((r) => r.imageId));
|
|
2738
|
-
const count = [...named].filter((id) => !rejectedIds.has(id)).length;
|
|
2739
|
-
if (count === 0) {
|
|
2740
|
-
// The server THROWS here, eligibility before import: an `import-*`
|
|
2741
|
-
// rejection means that image PASSED eligibility, so the server
|
|
2742
|
-
// reached the import and refuses there.
|
|
2743
|
-
const anyImported = rejected.some((r) => r.reason.startsWith('import-'));
|
|
2744
|
-
return reply({
|
|
2745
|
-
error: anyImported
|
|
2746
|
-
? MOCK_TRAINING_NONE_IMPORTED_ERROR
|
|
2747
|
-
: MOCK_TRAINING_NONE_ELIGIBLE_ERROR,
|
|
2748
|
-
});
|
|
2749
|
-
}
|
|
2750
|
-
const datasetId = trainingHandle('tds_');
|
|
2751
|
-
trainingDatasets.set(datasetId, count);
|
|
2752
|
-
const result = { datasetId, count, rejected };
|
|
2753
|
-
return reply({ result });
|
|
2754
|
-
}
|
|
2755
2845
|
case 'RUN_TRAINING': {
|
|
2756
2846
|
// The host's gate (`runTrainingGate.ts`): no requestId → drop;
|
|
2757
2847
|
// signed-out → `sign in to train`; not a `kind:'training'` body with
|
|
@@ -2775,6 +2865,13 @@ export function createMockHost(options = {}) {
|
|
|
2775
2865
|
if (!quote || quote.spent || Date.now() >= quote.expiresAtMs) {
|
|
2776
2866
|
return reply({ error: MOCK_TRAINING_QUOTE_GONE_ERROR });
|
|
2777
2867
|
}
|
|
2868
|
+
// The host reads the quote back and refuses, with no dialog, one
|
|
2869
|
+
// naming any blob it did not upload for this block — before the
|
|
2870
|
+
// server claims it, so the quote survives.
|
|
2871
|
+
if (trainingDataNotUploadedHere ||
|
|
2872
|
+
quote.itemKeys.some((key) => !trainingUploads.has(blobKeyIdentity(key)))) {
|
|
2873
|
+
return reply({ error: MOCK_TRAINING_NOT_UPLOADED_HERE });
|
|
2874
|
+
}
|
|
2778
2875
|
// Which outcomes consume the quote, modelled on the real flow: the
|
|
2779
2876
|
// host's gate and consent dialog come first (quote kept); the
|
|
2780
2877
|
// server's submit then claims the quote with a GETDEL
|
|
@@ -2788,7 +2885,8 @@ export function createMockHost(options = {}) {
|
|
|
2788
2885
|
return reply({ error: runTrainingError });
|
|
2789
2886
|
}
|
|
2790
2887
|
quote.spent = true;
|
|
2791
|
-
|
|
2888
|
+
const runFingerprint = trainingFingerprint(b);
|
|
2889
|
+
if (runFingerprint === null || runFingerprint !== quote.fingerprint) {
|
|
2792
2890
|
return reply({
|
|
2793
2891
|
error: 'the training body differs from the one that was quoted — estimate again',
|
|
2794
2892
|
});
|
|
@@ -3120,6 +3218,36 @@ export function createMockHost(options = {}) {
|
|
|
3120
3218
|
postableImageIds.add(uploadImageBytesResult.imageId);
|
|
3121
3219
|
return reply({ selected: { ...uploadImageBytesResult } });
|
|
3122
3220
|
}
|
|
3221
|
+
// TRAINING DATA (civitai/civitai#6461): several private images in ONE
|
|
3222
|
+
// reply, `{ images: [{ url, air }] }`. Every key handed out is
|
|
3223
|
+
// remembered for RUN_TRAINING's not-uploaded-here check.
|
|
3224
|
+
if (typed.payload?.purpose === 'trainingData') {
|
|
3225
|
+
let images;
|
|
3226
|
+
if (cannedTrainingDataUpload === undefined) {
|
|
3227
|
+
images = Array.from({ length: DEFAULT_TRAINING_DATA_UPLOAD_COUNT }, () => {
|
|
3228
|
+
trainingUploadSerial += 1;
|
|
3229
|
+
const air = `mockblob${trainingUploadSerial.toString(16).padStart(8, '0')}.png`;
|
|
3230
|
+
return { url: `${DEFAULT_TRAINING_DATA_URL_BASE}${air}`, air };
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
else {
|
|
3234
|
+
images = cannedTrainingDataUpload;
|
|
3235
|
+
}
|
|
3236
|
+
if (!images || images.length === 0) {
|
|
3237
|
+
dispatchToBlock({ type: 'IMAGE_UPLOAD_RESULT', payload: { requestId } });
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
for (const img of images) {
|
|
3241
|
+
const key = mockBlobKeyFromAir(img.air);
|
|
3242
|
+
if (key !== null)
|
|
3243
|
+
trainingUploads.add(blobKeyIdentity(key));
|
|
3244
|
+
}
|
|
3245
|
+
dispatchToBlock({
|
|
3246
|
+
type: 'IMAGE_UPLOAD_RESULT',
|
|
3247
|
+
payload: { requestId, selected: { images: images.map(({ url, air }) => ({ url, air })) } },
|
|
3248
|
+
});
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3123
3251
|
const isGenerationSource = typed.payload?.purpose === 'generationSource';
|
|
3124
3252
|
// NON-BLOCKING display path (asyncScan:true): early-resolve with a
|
|
3125
3253
|
// PENDING handle, then stream the canned scan verdict on a later tick.
|
|
@@ -3847,6 +3975,11 @@ export function createMockHost(options = {}) {
|
|
|
3847
3975
|
wildcardPack = patch.wildcardPack;
|
|
3848
3976
|
if (patch.wildcardPackError !== undefined)
|
|
3849
3977
|
wildcardPackError = patch.wildcardPackError;
|
|
3978
|
+
if (patch.resourceFile !== undefined)
|
|
3979
|
+
resourceFile = patch.resourceFile;
|
|
3980
|
+
// Key PRESENCE, not value: `{ resourceFileError: undefined }` clears it.
|
|
3981
|
+
if ('resourceFileError' in patch)
|
|
3982
|
+
resourceFileError = patch.resourceFileError;
|
|
3850
3983
|
if (patch.collectionFollowError !== undefined)
|
|
3851
3984
|
collectionFollowError = patch.collectionFollowError;
|
|
3852
3985
|
if (patch.reactionStates !== undefined || patch.reactionError !== undefined) {
|
|
@@ -3856,13 +3989,15 @@ export function createMockHost(options = {}) {
|
|
|
3856
3989
|
createPostResult = patch.createPostResult;
|
|
3857
3990
|
if (patch.createPostError !== undefined)
|
|
3858
3991
|
createPostError = patch.createPostError;
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3992
|
+
// `null` is meaningful (a cancel) and `undefined` restores the default, so
|
|
3993
|
+
// check for the KEY's presence.
|
|
3994
|
+
if ('cannedTrainingDataUpload' in patch)
|
|
3995
|
+
cannedTrainingDataUpload = patch.cannedTrainingDataUpload;
|
|
3996
|
+
// The training refusal knobs clear on an explicit `undefined` (key PRESENT in
|
|
3863
3997
|
// the patch), so a harness can turn a refusal off without re-installing.
|
|
3864
|
-
if ('
|
|
3865
|
-
|
|
3998
|
+
if ('trainingDataNotUploadedHere' in patch) {
|
|
3999
|
+
trainingDataNotUploadedHere = patch.trainingDataNotUploadedHere === true;
|
|
4000
|
+
}
|
|
3866
4001
|
if (patch.trainingQuoteTotal !== undefined)
|
|
3867
4002
|
trainingQuoteTotal = patch.trainingQuoteTotal;
|
|
3868
4003
|
if ('runTrainingError' in patch)
|
|
@@ -28,6 +28,23 @@ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
|
28
28
|
* no deep hook paths, so the rename touches no reachable API.
|
|
29
29
|
*/
|
|
30
30
|
export declare const HUMAN_INTERACTION_TIMEOUT_MS: number;
|
|
31
|
+
/**
|
|
32
|
+
* The bound for `OPEN_IMAGE_UPLOAD { purpose: 'trainingData' }` (civitai/civitai#6461),
|
|
33
|
+
* which is longer than {@link HUMAN_INTERACTION_TIMEOUT_MS} on purpose.
|
|
34
|
+
*
|
|
35
|
+
* The host replies ONCE, when its modal closes — after the viewer picked up to 50
|
|
36
|
+
* images AND the host uploaded every original. That second half is network work
|
|
37
|
+
* the viewer does not control, so ten minutes from `open()` can expire while the
|
|
38
|
+
* host is still uploading. The block would then reject, drop the late reply, and
|
|
39
|
+
* never learn the keys the host now remembers for `RUN_TRAINING`. The host always
|
|
40
|
+
* answers on close (images, or a bare cancel), so the bound only has to end a wait
|
|
41
|
+
* on a host that will never answer; an hour does that without cutting off a slow
|
|
42
|
+
* upload.
|
|
43
|
+
*
|
|
44
|
+
* Still in the `'human'` bucket of {@link REQUEST_TIMEOUT_CLASS}: it is at least
|
|
45
|
+
* the human bound, which is what that bucket guarantees.
|
|
46
|
+
*/
|
|
47
|
+
export declare const TRAINING_DATA_UPLOAD_TIMEOUT_MS: number;
|
|
31
48
|
/**
|
|
32
49
|
* The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
|
|
33
50
|
* host forwards it to the orchestrator and replies only once that answers. No
|
|
@@ -36,17 +53,25 @@ export declare const HUMAN_INTERACTION_TIMEOUT_MS: number;
|
|
|
36
53
|
*
|
|
37
54
|
* Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
|
|
38
55
|
* preflight plus the real submit plus a prompt audit), and
|
|
39
|
-
* `
|
|
40
|
-
* server moderates the captions and imports up to 50 images into the
|
|
41
|
-
* orchestrator, the import alone under a 60s budget
|
|
42
|
-
* (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
|
|
43
|
-
* `block-training-dataset.service.ts`) — so the 30s default would reject a
|
|
44
|
-
* healthy prepare mid-import.
|
|
56
|
+
* `ESTIMATE_WORKFLOW_BATCH` in `hooks/useBatchEstimate.ts`.
|
|
45
57
|
*
|
|
46
|
-
* Was a module-local constant in `useBuzzWorkflow.ts`; moved here so
|
|
58
|
+
* Was a module-local constant in `useBuzzWorkflow.ts`; moved here so every
|
|
47
59
|
* caller shares the number instead of re-typing it.
|
|
48
60
|
*/
|
|
49
61
|
export declare const WORKFLOW_REQUEST_TIMEOUT_MS = 120000;
|
|
62
|
+
/**
|
|
63
|
+
* The bound for `GET_RESOURCE_FILE` (`hooks/useResourceFile.ts`), a `'protocol'`
|
|
64
|
+
* request whose HOST work is slow: a session-authed resolve, a fetch of the
|
|
65
|
+
* signed URL (up to 32 MB), then an unzip and parse of up to 16 MB in the
|
|
66
|
+
* viewer's tab. The host bounds all three with ONE 110 s deadline
|
|
67
|
+
* (civitai/civitai#6818 `RESOURCE_FILE_DEADLINE_MS`), after which it replies
|
|
68
|
+
* `parse-failed`. At {@link DEFAULT_REQUEST_TIMEOUT_MS} a large file on a slow
|
|
69
|
+
* connection would time out on the block while the host was still working, and
|
|
70
|
+
* the host's valid reply would be dropped as unroutable. 120 s leaves the host's
|
|
71
|
+
* 110 s deadline room to answer first. Same number as
|
|
72
|
+
* {@link WORKFLOW_REQUEST_TIMEOUT_MS}.
|
|
73
|
+
*/
|
|
74
|
+
export declare const RESOURCE_FILE_REQUEST_TIMEOUT_MS = 120000;
|
|
50
75
|
/** Which bound a block→parent message is answered under. */
|
|
51
76
|
export type RequestTimeoutClass =
|
|
52
77
|
/** Reply waits on a PERSON — must pass {@link HUMAN_INTERACTION_TIMEOUT_MS}. */
|
|
@@ -27,6 +27,23 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
27
27
|
* no deep hook paths, so the rename touches no reachable API.
|
|
28
28
|
*/
|
|
29
29
|
export const HUMAN_INTERACTION_TIMEOUT_MS = 10 * 60_000;
|
|
30
|
+
/**
|
|
31
|
+
* The bound for `OPEN_IMAGE_UPLOAD { purpose: 'trainingData' }` (civitai/civitai#6461),
|
|
32
|
+
* which is longer than {@link HUMAN_INTERACTION_TIMEOUT_MS} on purpose.
|
|
33
|
+
*
|
|
34
|
+
* The host replies ONCE, when its modal closes — after the viewer picked up to 50
|
|
35
|
+
* images AND the host uploaded every original. That second half is network work
|
|
36
|
+
* the viewer does not control, so ten minutes from `open()` can expire while the
|
|
37
|
+
* host is still uploading. The block would then reject, drop the late reply, and
|
|
38
|
+
* never learn the keys the host now remembers for `RUN_TRAINING`. The host always
|
|
39
|
+
* answers on close (images, or a bare cancel), so the bound only has to end a wait
|
|
40
|
+
* on a host that will never answer; an hour does that without cutting off a slow
|
|
41
|
+
* upload.
|
|
42
|
+
*
|
|
43
|
+
* Still in the `'human'` bucket of {@link REQUEST_TIMEOUT_CLASS}: it is at least
|
|
44
|
+
* the human bound, which is what that bucket guarantees.
|
|
45
|
+
*/
|
|
46
|
+
export const TRAINING_DATA_UPLOAD_TIMEOUT_MS = 60 * 60_000;
|
|
30
47
|
/**
|
|
31
48
|
* The bound for a `'protocol'` request whose SERVER WORK is itself slow — the
|
|
32
49
|
* host forwards it to the orchestrator and replies only once that answers. No
|
|
@@ -35,17 +52,25 @@ export const HUMAN_INTERACTION_TIMEOUT_MS = 10 * 60_000;
|
|
|
35
52
|
*
|
|
36
53
|
* Callers: the workflow trio in `hooks/useBuzzWorkflow.ts` (a submit is a whatif
|
|
37
54
|
* preflight plus the real submit plus a prompt audit), and
|
|
38
|
-
* `
|
|
39
|
-
* server moderates the captions and imports up to 50 images into the
|
|
40
|
-
* orchestrator, the import alone under a 60s budget
|
|
41
|
-
* (`BLOCK_TRAINING_IMPORT_BUDGET_MS` in civitai/civitai's
|
|
42
|
-
* `block-training-dataset.service.ts`) — so the 30s default would reject a
|
|
43
|
-
* healthy prepare mid-import.
|
|
55
|
+
* `ESTIMATE_WORKFLOW_BATCH` in `hooks/useBatchEstimate.ts`.
|
|
44
56
|
*
|
|
45
|
-
* Was a module-local constant in `useBuzzWorkflow.ts`; moved here so
|
|
57
|
+
* Was a module-local constant in `useBuzzWorkflow.ts`; moved here so every
|
|
46
58
|
* caller shares the number instead of re-typing it.
|
|
47
59
|
*/
|
|
48
60
|
export const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
|
|
61
|
+
/**
|
|
62
|
+
* The bound for `GET_RESOURCE_FILE` (`hooks/useResourceFile.ts`), a `'protocol'`
|
|
63
|
+
* request whose HOST work is slow: a session-authed resolve, a fetch of the
|
|
64
|
+
* signed URL (up to 32 MB), then an unzip and parse of up to 16 MB in the
|
|
65
|
+
* viewer's tab. The host bounds all three with ONE 110 s deadline
|
|
66
|
+
* (civitai/civitai#6818 `RESOURCE_FILE_DEADLINE_MS`), after which it replies
|
|
67
|
+
* `parse-failed`. At {@link DEFAULT_REQUEST_TIMEOUT_MS} a large file on a slow
|
|
68
|
+
* connection would time out on the block while the host was still working, and
|
|
69
|
+
* the host's valid reply would be dropped as unroutable. 120 s leaves the host's
|
|
70
|
+
* 110 s deadline room to answer first. Same number as
|
|
71
|
+
* {@link WORKFLOW_REQUEST_TIMEOUT_MS}.
|
|
72
|
+
*/
|
|
73
|
+
export const RESOURCE_FILE_REQUEST_TIMEOUT_MS = 120_000;
|
|
49
74
|
/**
|
|
50
75
|
* 🔴 THE TOTALITY GATE — every block→parent message type, bucketed.
|
|
51
76
|
*
|
|
@@ -130,11 +155,6 @@ const REQUEST_TIMEOUT_CLASS = {
|
|
|
130
155
|
APP_STORAGE_SET: 'protocol',
|
|
131
156
|
CANCEL_APP_WORKFLOW: 'protocol',
|
|
132
157
|
CANCEL_WORKFLOW: 'protocol',
|
|
133
|
-
// No dialog — preparing a dataset charges nothing and the host replies when
|
|
134
|
-
// the server does. Server-bound, so `'protocol'`; but sent under
|
|
135
|
-
// `WORKFLOW_REQUEST_TIMEOUT_MS`, not the 30s default, because the server's
|
|
136
|
-
// image import alone may take 60s. Same reasoning as the workflow trio below.
|
|
137
|
-
PREPARE_TRAINING_DATASET: 'protocol',
|
|
138
158
|
// The workflow trio is server-bound, not person-bound. It carries its OWN
|
|
139
159
|
// longer bound (`WORKFLOW_REQUEST_TIMEOUT_MS` in `hooks/useBuzzWorkflow.ts`)
|
|
140
160
|
// for orchestrator latency — a different reason for a different number.
|
|
@@ -151,6 +171,10 @@ const REQUEST_TIMEOUT_CLASS = {
|
|
|
151
171
|
GET_IMAGES_BY_IDS: 'protocol',
|
|
152
172
|
GET_VIEWER: 'protocol',
|
|
153
173
|
GET_WILDCARD_PACK: 'protocol',
|
|
174
|
+
// No person in the loop, so `'protocol'` — but sent under its own longer
|
|
175
|
+
// `RESOURCE_FILE_REQUEST_TIMEOUT_MS`: the host's resolve + fetch + unzip run
|
|
176
|
+
// under a 110 s deadline, which the 30 s default would cut short.
|
|
177
|
+
GET_RESOURCE_FILE: 'protocol',
|
|
154
178
|
QUERY_APP_WORKFLOWS: 'protocol',
|
|
155
179
|
REQUEST_TOKEN: 'protocol',
|
|
156
180
|
// The host fetches the blob and triggers the browser's own download; it replies
|
|
@@ -245,7 +245,8 @@ export declare function isValidViewerResult(p: unknown): p is {
|
|
|
245
245
|
* value, `url` non-empty).
|
|
246
246
|
* • `'generationSource'` → the source `{ url, width, height }` (positive finite
|
|
247
247
|
* dimensions, non-empty url; no imageId/nsfwLevel).
|
|
248
|
-
*
|
|
248
|
+
* • `'trainingData'` → `{ images: [{ url, air }] }` (non-empty strings).
|
|
249
|
+
* The guard accepts ANY of the shapes (the result carries no purpose discriminator).
|
|
249
250
|
* Kept in lockstep with the host's `IMAGE_UPLOAD_RESULT` (civitai/civitai
|
|
250
251
|
* `PageBlockHost.tsx` / `BlockImageUploadModal.tsx` /
|
|
251
252
|
* `BlockGenerationSourceUploadModal.tsx`).
|
|
@@ -265,6 +266,11 @@ export declare function isValidImageUploadResult(p: unknown): p is {
|
|
|
265
266
|
status: 'pending';
|
|
266
267
|
imageId: number;
|
|
267
268
|
url: string;
|
|
269
|
+
} | {
|
|
270
|
+
images: Array<{
|
|
271
|
+
url: string;
|
|
272
|
+
air: string;
|
|
273
|
+
}>;
|
|
268
274
|
};
|
|
269
275
|
error?: string;
|
|
270
276
|
};
|
|
@@ -341,6 +347,17 @@ export declare function isValidDailyCompensationResult(p: unknown): boolean;
|
|
|
341
347
|
* reply, mirroring the host's enum contract.
|
|
342
348
|
*/
|
|
343
349
|
export declare function isValidWildcardPackResult(p: unknown): boolean;
|
|
350
|
+
/**
|
|
351
|
+
* Reply to a block-initiated `GET_RESOURCE_FILE`. Same contract shape as
|
|
352
|
+
* {@link isValidWildcardPackResult}: EITHER a `file` OR an `error` that is one of
|
|
353
|
+
* {@link RESOURCE_FILE_ERROR_CODES}; a free-text or unknown `error`, or a reply
|
|
354
|
+
* with neither, is malformed and dropped. The `file` is checked field by field —
|
|
355
|
+
* `meta` ids positive integers, names strings, `creatorUsername` a string or
|
|
356
|
+
* `null`, `truncated` a boolean, and EVERY entry an object with a string `path`,
|
|
357
|
+
* a known `format` and a `graph` key. `graph` itself is the host's parsed JSON
|
|
358
|
+
* and may be any JSON value.
|
|
359
|
+
*/
|
|
360
|
+
export declare function isValidResourceFileResult(p: unknown): boolean;
|
|
344
361
|
/**
|
|
345
362
|
* Reply to a block-initiated `QUERY_APP_WORKFLOWS`. A well-formed reply carries
|
|
346
363
|
* EITHER a `result` ({ workflows: AppWorkflow[], cursor: string | null }) OR a
|
|
@@ -442,22 +459,6 @@ export declare function isValidEntityReactionsResult(p: unknown): boolean;
|
|
|
442
459
|
* so a drop at least times out visibly.
|
|
443
460
|
*/
|
|
444
461
|
export declare function isValidCreatePostResult(p: unknown): boolean;
|
|
445
|
-
/**
|
|
446
|
-
* Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
|
|
447
|
-
* `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
|
|
448
|
-
* imageId: number, reason: string }] }`) OR an `error`; one with neither is
|
|
449
|
-
* malformed and dropped.
|
|
450
|
-
*
|
|
451
|
-
* 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
|
|
452
|
-
* {@link isValidCreatePostResult}: the host's closed codes share the field with
|
|
453
|
-
* any server message it forwards, and a membership check would drop those
|
|
454
|
-
* replies before correlation, leaving the request to its timer.
|
|
455
|
-
*
|
|
456
|
-
* `rejected[].reason` is checked as a STRING, not against today's six members —
|
|
457
|
-
* the host adding a reason must not make every prepare reply that carries it a
|
|
458
|
-
* dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
|
|
459
|
-
*/
|
|
460
|
-
export declare function isValidTrainingDatasetResult(p: unknown): boolean;
|
|
461
462
|
/**
|
|
462
463
|
* Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
|
|
463
464
|
* valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
|