@civitai/blocks-react 0.76.1 → 0.77.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 +63 -52
- package/dist/hooks/useImageUpload.d.ts +40 -6
- package/dist/hooks/useImageUpload.js +19 -3
- package/dist/hooks/useRunTraining.d.ts +31 -10
- package/dist/hooks/useRunTraining.js +32 -7
- package/dist/index.d.ts +2 -4
- package/dist/index.js +0 -1
- package/dist/internal/liveHost.d.ts +5 -9
- package/dist/internal/liveHost.js +21 -29
- package/dist/internal/mockHost.d.ts +37 -44
- package/dist/internal/mockHost.js +160 -84
- package/dist/transport/requestTimeouts.d.ts +19 -7
- package/dist/transport/requestTimeouts.js +19 -12
- package/dist/transport/validate.d.ts +7 -17
- package/dist/transport/validate.js +21 -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
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
* fetch chokepoints ..... 2
|
|
48
48
|
* REST endpoints ........ GET /api/v1/blocks/me
|
|
49
49
|
* tRPC procedures ....... 29 = blocks.* (14) + apps.shared.* (10) + apps.storage.* (5)
|
|
50
|
-
* switch case labels ....
|
|
50
|
+
* switch case labels .... 52, of which 14 REFUSE (enumerated under SCOPE below)
|
|
51
51
|
* --- END DERIVED ---
|
|
52
52
|
*
|
|
53
53
|
* PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
|
|
@@ -113,12 +113,9 @@
|
|
|
113
113
|
* `entity-unavailable`.
|
|
114
114
|
* • CREATE_POST_FROM_APP — needs the server-resolved preview plus host-chrome
|
|
115
115
|
* confirm before a PUBLIC post is written. Replies with a refusal.
|
|
116
|
-
* •
|
|
117
|
-
*
|
|
118
|
-
* refusal.
|
|
119
|
-
* • RUN_TRAINING — the same dev-token refusal, plus the host-chrome consent
|
|
120
|
-
* dialog a run is confirmed in, which this harness does not have. Replies
|
|
121
|
-
* with a refusal.
|
|
116
|
+
* • RUN_TRAINING — the server refuses every `kind: 'training'` request from a
|
|
117
|
+
* dev token, and a run is confirmed in a host-chrome consent dialog this
|
|
118
|
+
* harness does not have. Replies with a refusal.
|
|
122
119
|
* • RUN_CONFIRMED_STEP — the server refuses a confirmed step from a dev token
|
|
123
120
|
* and the estimate never returns a `stepQuote` for one, and the price is
|
|
124
121
|
* confirmed in host chrome this harness does not have. Replies with a
|
|
@@ -174,8 +171,7 @@ const DEFAULT_BACKEND_BASE_URL = 'https://civitai.com';
|
|
|
174
171
|
/** The budgeted-spend scope the workflow procedures require. */
|
|
175
172
|
const BUDGETED_SCOPE = 'ai:write:budgeted';
|
|
176
173
|
/**
|
|
177
|
-
* The reply
|
|
178
|
-
* string so the two cannot drift.
|
|
174
|
+
* The reply `RUN_TRAINING` gets in dev:live — see its handler.
|
|
179
175
|
*/
|
|
180
176
|
export const TRAINING_UNSUPPORTED_ERROR = 'training is not supported in dev:live — use dev:mock';
|
|
181
177
|
/** The once-per-session console line for a refused training bridge. */
|
|
@@ -1139,35 +1135,25 @@ export function createLiveHost(options) {
|
|
|
1139
1135
|
});
|
|
1140
1136
|
return;
|
|
1141
1137
|
}
|
|
1142
|
-
case '
|
|
1138
|
+
case 'RUN_TRAINING': {
|
|
1143
1139
|
// UNSUPPORTED in live, and here the SERVER decides it, not this
|
|
1144
1140
|
// harness: every `kind: 'training'` procedure refuses a dev token
|
|
1145
1141
|
// ("training is not available from a development or review session"
|
|
1146
1142
|
// — `assertTrainingRequestAllowed` in civitai/civitai), because there
|
|
1147
1143
|
// is no host page whose chrome can show the viewer a price. A
|
|
1148
1144
|
// training ESTIMATE is still forwarded (ESTIMATE_WORKFLOW above) and
|
|
1149
|
-
// comes back carrying that same server refusal.
|
|
1145
|
+
// comes back carrying that same server refusal. A run is also
|
|
1146
|
+
// CHARGED and confirmed in host chrome this harness does not have, so
|
|
1147
|
+
// faking either half would let dev prove out a flow production does
|
|
1148
|
+
// not have. The upload half (`OPEN_IMAGE_UPLOAD { purpose:
|
|
1149
|
+
// 'trainingData' }`) is answered as a dismissal (`null`), like every
|
|
1150
|
+
// other upload mode — see OPEN_IMAGE_UPLOAD below.
|
|
1150
1151
|
//
|
|
1151
1152
|
// FREE TEXT on the message's own reply channel; no host code is
|
|
1152
1153
|
// honest about "this harness cannot do it". Use dev:mock — its
|
|
1153
|
-
// `
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
return;
|
|
1157
|
-
logOnce('training-dataset', trainingUnsupportedLog('PREPARE_TRAINING_DATASET'));
|
|
1158
|
-
dispatchToBlock({
|
|
1159
|
-
type: 'TRAINING_DATASET_RESULT',
|
|
1160
|
-
payload: { requestId, error: TRAINING_UNSUPPORTED_ERROR },
|
|
1161
|
-
});
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
case 'RUN_TRAINING': {
|
|
1165
|
-
// UNSUPPORTED in live for the same server-side reason as
|
|
1166
|
-
// PREPARE_TRAINING_DATASET directly above — and doubly so: a run is
|
|
1167
|
-
// CHARGED and confirmed in host chrome this harness does not have, so
|
|
1168
|
-
// faking either half would let dev prove out a flow production does
|
|
1169
|
-
// not have. Use dev:mock — its `runTrainingError` knob covers
|
|
1170
|
-
// `declined`, `submission-unconfirmed` and the rest.
|
|
1154
|
+
// `cannedTrainingDataUpload`, `trainingDataNotUploadedHere` and
|
|
1155
|
+
// `runTrainingError` knobs cover the upload, the not-uploaded-here
|
|
1156
|
+
// refusal, `declined`, `submission-unconfirmed` and the rest.
|
|
1171
1157
|
if (!isRoutableRequestId(requestId))
|
|
1172
1158
|
return;
|
|
1173
1159
|
logOnce('training-run', trainingUnsupportedLog('RUN_TRAINING'));
|
|
@@ -1391,6 +1377,12 @@ export function createLiveHost(options) {
|
|
|
1391
1377
|
'Test the bytes upload against the real site, or use ' +
|
|
1392
1378
|
'createMockHost({ uploadImageBytesResult }) in dev:mock.');
|
|
1393
1379
|
}
|
|
1380
|
+
else if (typed.payload?.purpose === 'trainingData') {
|
|
1381
|
+
logOnce('open-image-upload-training-data', "OPEN_IMAGE_UPLOAD { purpose: 'trainingData' } received but live dev mode " +
|
|
1382
|
+
'cannot serve the training-data upload modal — resolving the upload as cancelled (null). ' +
|
|
1383
|
+
'Test the training flow against the real site, or use ' +
|
|
1384
|
+
'createMockHost({ cannedTrainingDataUpload }) in dev:mock.');
|
|
1385
|
+
}
|
|
1394
1386
|
else {
|
|
1395
1387
|
logOnce('open-image-upload', 'OPEN_IMAGE_UPLOAD received but live dev mode cannot serve the native upload ' +
|
|
1396
1388
|
'modal — resolving the upload as dismissed (null). Test the image-upload flow ' +
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* called). Only the postMessage bridge round-trips are exercised. Never import
|
|
38
38
|
* this from production code.
|
|
39
39
|
*/
|
|
40
|
-
import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockModel3dOutput, type BlockPublishedModel, type BlockRunConfirmedStepHostError, type BlockGatedImage, type BlockPickedImageInfo, type BlockCollectionFollowErrorCode, type BlockEntityReactionErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type
|
|
40
|
+
import { type BlockContext, type BlockResourceInfo, type BlockResourcePickerType, type BlockUploadedImageInfo, type BlockGenerationSourceImageInfo, type BuzzAccountType, type BlockBuzzTransaction, type BlockBuzzAccount, type BlockDailyCompensationResource, type BlockViewer, type BlockWildcardPack, type BlockWildcardPackErrorCode, type AppWorkflow, type BlockModel3dOutput, type BlockPublishedModel, type BlockRunConfirmedStepHostError, type BlockGatedImage, type BlockPickedImageInfo, type BlockCollectionFollowErrorCode, type BlockEntityReactionErrorCode, type BlockCreatePostHostError, type BlockCreatePostResult, type BlockRunTrainingHostError, type BlockTrainingDataImage, type ColorDomain, type SharedStorageValue, type Theme, type ViewerInfo, type WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
41
41
|
import { type MockMediaUploads } from './mediaUploadBytes.js';
|
|
42
42
|
import { type MockWorkflowQuoteFailure } from './mockWorkflowGraph.js';
|
|
43
43
|
import { type MockReactionSeed } from './mockHostReactions.js';
|
|
@@ -412,7 +412,7 @@ export interface MockGenerationScenario {
|
|
|
412
412
|
*/
|
|
413
413
|
export declare const MOCK_TRAINING_BOUNDS: {
|
|
414
414
|
readonly datasetMaxItems: 50;
|
|
415
|
-
readonly captionMaxChars:
|
|
415
|
+
readonly captionMaxChars: 8192;
|
|
416
416
|
readonly maxBuzzPerRun: 5000;
|
|
417
417
|
};
|
|
418
418
|
/**
|
|
@@ -425,16 +425,15 @@ export declare const MOCK_TRAINING_BOUNDS: {
|
|
|
425
425
|
export declare const MOCK_SPEND_SCOPE_ERROR = "block lacks ai:write:budgeted scope";
|
|
426
426
|
/** The real server's refusal for an unknown, expired or spent quote on `RUN_TRAINING`. */
|
|
427
427
|
export declare const MOCK_TRAINING_QUOTE_GONE_ERROR = "training quote not found, expired or already used \u2014 estimate again";
|
|
428
|
-
/** The real server's refusal for a training estimate naming a dataset it does not hold. */
|
|
429
|
-
export declare const MOCK_TRAINING_DATASET_GONE_ERROR = "training dataset not found or expired";
|
|
430
428
|
/**
|
|
431
|
-
* The
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
429
|
+
* The estimate's refusals for `items` outside the server's schema. The first two
|
|
430
|
+
* are the server's own custom messages (`blockTrainingDatasetItemsSchema`); the
|
|
431
|
+
* third stands for every other zod issue (count, caption length, missing or extra
|
|
432
|
+
* keys), whose real text is zod's formatting and not a contract.
|
|
435
433
|
*/
|
|
436
|
-
export declare const
|
|
437
|
-
export declare const
|
|
434
|
+
export declare const MOCK_TRAINING_AIR_ERROR = "air must be a consumer blob key or its Civitai-hosted blob url";
|
|
435
|
+
export declare const MOCK_TRAINING_DUPLICATE_ERROR = "a training dataset names each image once";
|
|
436
|
+
export declare const MOCK_TRAINING_ITEMS_ERROR = "training items must be 1 to 50 { air, caption } entries with captions of at most 8192 characters";
|
|
438
437
|
/**
|
|
439
438
|
* The per-job ceiling of a confirmed step — a COPY of `@civitai/app-sdk/blocks`'s
|
|
440
439
|
* `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB`, not value-imported for the reason
|
|
@@ -1014,41 +1013,34 @@ export interface MockHostOptions {
|
|
|
1014
1013
|
*/
|
|
1015
1014
|
createPostError?: BlockCreatePostHostError | string;
|
|
1016
1015
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
* `
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1016
|
+
* The images an `OPEN_IMAGE_UPLOAD { purpose: 'trainingData' }` returns — what
|
|
1017
|
+
* `useImageUpload({ purpose: 'trainingData' }).open()` resolves with. Absent →
|
|
1018
|
+
* {@link DEFAULT_TRAINING_DATA_UPLOAD_COUNT} images with FRESH keys on every
|
|
1019
|
+
* upload, so two uploads never collide. `null` (or `[]`) → the viewer cancelled.
|
|
1021
1020
|
* Live-tunable via {@link MockHost.setScenario}.
|
|
1022
1021
|
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
* `
|
|
1028
|
-
*
|
|
1029
|
-
* KIND-FAITHFUL, like the rest of the training path: the
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
* `ai:write:budgeted` on the token
|
|
1033
|
-
* a signed-in viewer. What it
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
*
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
reason: BlockTrainingRejectionReason;
|
|
1044
|
-
}>;
|
|
1045
|
-
/**
|
|
1046
|
-
* Force `PREPARE_TRAINING_DATASET` to reply with an `error` — a
|
|
1047
|
-
* {@link BlockPrepareTrainingDatasetHostError} for a host refusal, or any other
|
|
1048
|
-
* string for the free-text server-message variant. Absent → it prepares.
|
|
1049
|
-
* Live-tunable via {@link MockHost.setScenario}.
|
|
1022
|
+
* The mock REMEMBERS every key it hands out, per install, as the real host
|
|
1023
|
+
* does per block instance: `RUN_TRAINING` refuses a quote naming any other key
|
|
1024
|
+
* with `training data not uploaded here`, before any other check, and keeps the
|
|
1025
|
+
* quote. Keys compare case-insensitively; an item may name its blob by the
|
|
1026
|
+
* `url` too.
|
|
1027
|
+
*
|
|
1028
|
+
* KIND-FAITHFUL, like the rest of the training path: the estimate refuses items
|
|
1029
|
+
* outside the server's schema, a quote is run at most once and is checked
|
|
1030
|
+
* against the whole body it was quoted for (minus `quoteId`), and every
|
|
1031
|
+
* training call needs `ai:write:budgeted` on the token
|
|
1032
|
+
* ({@link MockHostOptions.consentGranted}) and a signed-in viewer. What it
|
|
1033
|
+
* cannot model: blob storage, the Civitai-hosted-url rule on `air`, the flag,
|
|
1034
|
+
* and the page-only / dev-token refusals.
|
|
1035
|
+
*/
|
|
1036
|
+
cannedTrainingDataUpload?: BlockTrainingDataImage[] | null;
|
|
1037
|
+
/**
|
|
1038
|
+
* Make `RUN_TRAINING` refuse EVERY quote with `training data not uploaded
|
|
1039
|
+
* here`, as the real host does after a page reload forgot the uploads. Settles
|
|
1040
|
+
* before the server claims the quote, so the quote survives. Live-tunable via
|
|
1041
|
+
* {@link MockHost.setScenario}; `undefined` clears it.
|
|
1050
1042
|
*/
|
|
1051
|
-
|
|
1043
|
+
trainingDataNotUploadedHere?: boolean;
|
|
1052
1044
|
/**
|
|
1053
1045
|
* The Buzz total a `kind: 'training'` estimate quotes. Default `500`. Above
|
|
1054
1046
|
* `BLOCK_TRAINING_MAX_BUZZ_PER_RUN` (5,000) the estimate fails exactly as the
|
|
@@ -1064,7 +1056,8 @@ export interface MockHostOptions {
|
|
|
1064
1056
|
*
|
|
1065
1057
|
* Whether the QUOTE survives follows the real flow: the host's own gate and
|
|
1066
1058
|
* dialog codes (`declined`, `review-mode`, `block is not ready`, `sign in to
|
|
1067
|
-
* train`, `invalid training request`, `no block token`
|
|
1059
|
+
* train`, `invalid training request`, `no block token`, `training data not
|
|
1060
|
+
* uploaded here`) settle before the
|
|
1068
1061
|
* server claims the quote, so it stays runnable; `submission-unconfirmed` and
|
|
1069
1062
|
* any server message settle after the claim, so it is used up — re-running it
|
|
1070
1063
|
* gets the quote-gone error. Live-tunable via {@link MockHost.setScenario};
|
|
@@ -1431,7 +1424,7 @@ export interface MockHostOptions {
|
|
|
1431
1424
|
* {@link MockHost.setTheme}, which pushes a real `THEME_CHANGE` — the mock of
|
|
1432
1425
|
* the viewer toggling dark mode with the block already mounted.)
|
|
1433
1426
|
*/
|
|
1434
|
-
export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedMultiPicks' | 'cannedImagePicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'reactionStates' | 'reactionError' | 'createPostResult' | 'createPostError' | '
|
|
1427
|
+
export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedMultiPicks' | 'cannedImagePicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'reactionStates' | 'reactionError' | 'createPostResult' | 'createPostError' | 'cannedTrainingDataUpload' | 'trainingDataNotUploadedHere' | 'trainingQuoteTotal' | 'runTrainingError' | 'runTrainingCapRefusal' | 'runConfirmedStepError' | 'runConfirmedStepCapRefusal' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes' | 'saveImageError' | 'uploadImageBytesResult' | 'uploadImageBytesError' | 'cannedMediaUploads' | 'mediaUploadError'>;
|
|
1435
1428
|
/** Runtime Buzz-balance handle exposed on {@link MockHost.buzz}. */
|
|
1436
1429
|
export interface MockBuzzHandle {
|
|
1437
1430
|
/** Current simulated balance, or `undefined` when balance isn't simulated. */
|
|
@@ -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.
|
|
165
174
|
*/
|
|
166
|
-
|
|
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.
|
|
199
|
+
*/
|
|
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,
|
|
@@ -1108,10 +1161,11 @@ export function createMockHost(options = {}) {
|
|
|
1108
1161
|
// App Blocks → Post bridge: the canned post + a forced-refusal knob.
|
|
1109
1162
|
let createPostResult = options.createPostResult ?? DEFAULT_CREATE_POST_RESULT;
|
|
1110
1163
|
let createPostError = options.createPostError;
|
|
1111
|
-
// App Blocks training
|
|
1112
|
-
// forced-refusal knobs. The
|
|
1113
|
-
|
|
1114
|
-
let
|
|
1164
|
+
// App Blocks training: the canned upload, the "host forgot its uploads" knob,
|
|
1165
|
+
// the quote total, and forced-refusal knobs. The uploaded keys and the quotes
|
|
1166
|
+
// themselves live per install.
|
|
1167
|
+
let cannedTrainingDataUpload = options.cannedTrainingDataUpload;
|
|
1168
|
+
let trainingDataNotUploadedHere = options.trainingDataNotUploadedHere === true;
|
|
1115
1169
|
let trainingQuoteTotal = options.trainingQuoteTotal ?? DEFAULT_TRAINING_QUOTE_TOTAL;
|
|
1116
1170
|
let runTrainingError = options.runTrainingError;
|
|
1117
1171
|
// SAVE_IMAGE: a forced-refusal knob (see `saveImageError`).
|
|
@@ -1368,10 +1422,29 @@ export function createMockHost(options = {}) {
|
|
|
1368
1422
|
// restored after a reload). `canceled` is set by `CANCEL_WORKFLOW`, so later
|
|
1369
1423
|
// polls keep answering `canceled` instead of resuming the countdown.
|
|
1370
1424
|
const workflows = new Map();
|
|
1371
|
-
// Training:
|
|
1372
|
-
//
|
|
1373
|
-
|
|
1425
|
+
// Training: the blob keys this install's uploads handed out (by identity —
|
|
1426
|
+
// the host's per-block-instance memory), and the quotes its estimates stored
|
|
1427
|
+
// (id → what a run is checked against).
|
|
1428
|
+
const trainingUploads = new Set();
|
|
1429
|
+
let trainingUploadSerial = 0;
|
|
1374
1430
|
const trainingQuotes = new Map();
|
|
1431
|
+
/**
|
|
1432
|
+
* What a training quote is bound to: the body without `quoteId`, its items
|
|
1433
|
+
* canonicalised (the server quotes the bare key). `null` when the body is not
|
|
1434
|
+
* plain JSON or its items do not resolve — then no run can match it.
|
|
1435
|
+
*/
|
|
1436
|
+
const trainingFingerprint = (raw) => {
|
|
1437
|
+
const resolved = resolveTrainingItems(raw.items);
|
|
1438
|
+
if ('refusal' in resolved)
|
|
1439
|
+
return null;
|
|
1440
|
+
const { quoteId: _quoteId, ...rest } = raw;
|
|
1441
|
+
try {
|
|
1442
|
+
return canonicalStepJson({ ...rest, items: resolved.items }, 'body');
|
|
1443
|
+
}
|
|
1444
|
+
catch {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1375
1448
|
// Confirmed steps: the quotes this install's estimates stored (id → what a
|
|
1376
1449
|
// run is checked against). Same serial as the training handles, so every
|
|
1377
1450
|
// handle this install mints is distinct.
|
|
@@ -1667,9 +1740,10 @@ export function createMockHost(options = {}) {
|
|
|
1667
1740
|
}
|
|
1668
1741
|
// TRAINING ESTIMATE — KIND-FAITHFUL, the one exception to the
|
|
1669
1742
|
// 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. (
|
|
1743
|
+
// pass-through: it checks the dataset items against the server's
|
|
1744
|
+
// schema, refuses a price above the per-run ceiling, and STORES a quote
|
|
1745
|
+
// the run is later checked against. (Uploads are NOT checked here —
|
|
1746
|
+
// the server's estimate does not; the host checks them at the run.) (The spend scope is checked above, for every kind.)
|
|
1673
1747
|
// Each refusal is the server's message in the host's `failureSnapshot`
|
|
1674
1748
|
// shape.
|
|
1675
1749
|
if (body.kind === 'training') {
|
|
@@ -1678,9 +1752,10 @@ export function createMockHost(options = {}) {
|
|
|
1678
1752
|
status: 'failed',
|
|
1679
1753
|
error,
|
|
1680
1754
|
});
|
|
1681
|
-
const
|
|
1682
|
-
if (
|
|
1683
|
-
return refuse(
|
|
1755
|
+
const resolved = resolveTrainingItems(body.items);
|
|
1756
|
+
if ('refusal' in resolved)
|
|
1757
|
+
return refuse(resolved.refusal);
|
|
1758
|
+
const imageCount = resolved.items.length;
|
|
1684
1759
|
const total = trainingQuoteTotal;
|
|
1685
1760
|
if (total > MOCK_TRAINING_BOUNDS.maxBuzzPerRun) {
|
|
1686
1761
|
return refuse(`this training run costs ${total} Buzz, above the per-run limit of ${MOCK_TRAINING_BOUNDS.maxBuzzPerRun} for apps`);
|
|
@@ -1688,7 +1763,8 @@ export function createMockHost(options = {}) {
|
|
|
1688
1763
|
const quoteId = trainingHandle('tq_');
|
|
1689
1764
|
const expiresAtMs = Date.now() + TRAINING_QUOTE_TTL_MS;
|
|
1690
1765
|
trainingQuotes.set(quoteId, {
|
|
1691
|
-
|
|
1766
|
+
fingerprint: trainingFingerprint(body),
|
|
1767
|
+
itemKeys: resolved.items.map((it) => it.air),
|
|
1692
1768
|
total,
|
|
1693
1769
|
expiresAtMs,
|
|
1694
1770
|
spent: false,
|
|
@@ -2712,46 +2788,6 @@ export function createMockHost(options = {}) {
|
|
|
2712
2788
|
});
|
|
2713
2789
|
return;
|
|
2714
2790
|
}
|
|
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
2791
|
case 'RUN_TRAINING': {
|
|
2756
2792
|
// The host's gate (`runTrainingGate.ts`): no requestId → drop;
|
|
2757
2793
|
// signed-out → `sign in to train`; not a `kind:'training'` body with
|
|
@@ -2775,6 +2811,13 @@ export function createMockHost(options = {}) {
|
|
|
2775
2811
|
if (!quote || quote.spent || Date.now() >= quote.expiresAtMs) {
|
|
2776
2812
|
return reply({ error: MOCK_TRAINING_QUOTE_GONE_ERROR });
|
|
2777
2813
|
}
|
|
2814
|
+
// The host reads the quote back and refuses, with no dialog, one
|
|
2815
|
+
// naming any blob it did not upload for this block — before the
|
|
2816
|
+
// server claims it, so the quote survives.
|
|
2817
|
+
if (trainingDataNotUploadedHere ||
|
|
2818
|
+
quote.itemKeys.some((key) => !trainingUploads.has(blobKeyIdentity(key)))) {
|
|
2819
|
+
return reply({ error: MOCK_TRAINING_NOT_UPLOADED_HERE });
|
|
2820
|
+
}
|
|
2778
2821
|
// Which outcomes consume the quote, modelled on the real flow: the
|
|
2779
2822
|
// host's gate and consent dialog come first (quote kept); the
|
|
2780
2823
|
// server's submit then claims the quote with a GETDEL
|
|
@@ -2788,7 +2831,8 @@ export function createMockHost(options = {}) {
|
|
|
2788
2831
|
return reply({ error: runTrainingError });
|
|
2789
2832
|
}
|
|
2790
2833
|
quote.spent = true;
|
|
2791
|
-
|
|
2834
|
+
const runFingerprint = trainingFingerprint(b);
|
|
2835
|
+
if (runFingerprint === null || runFingerprint !== quote.fingerprint) {
|
|
2792
2836
|
return reply({
|
|
2793
2837
|
error: 'the training body differs from the one that was quoted — estimate again',
|
|
2794
2838
|
});
|
|
@@ -3120,6 +3164,36 @@ export function createMockHost(options = {}) {
|
|
|
3120
3164
|
postableImageIds.add(uploadImageBytesResult.imageId);
|
|
3121
3165
|
return reply({ selected: { ...uploadImageBytesResult } });
|
|
3122
3166
|
}
|
|
3167
|
+
// TRAINING DATA (civitai/civitai#6461): several private images in ONE
|
|
3168
|
+
// reply, `{ images: [{ url, air }] }`. Every key handed out is
|
|
3169
|
+
// remembered for RUN_TRAINING's not-uploaded-here check.
|
|
3170
|
+
if (typed.payload?.purpose === 'trainingData') {
|
|
3171
|
+
let images;
|
|
3172
|
+
if (cannedTrainingDataUpload === undefined) {
|
|
3173
|
+
images = Array.from({ length: DEFAULT_TRAINING_DATA_UPLOAD_COUNT }, () => {
|
|
3174
|
+
trainingUploadSerial += 1;
|
|
3175
|
+
const air = `mockblob${trainingUploadSerial.toString(16).padStart(8, '0')}.png`;
|
|
3176
|
+
return { url: `${DEFAULT_TRAINING_DATA_URL_BASE}${air}`, air };
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
else {
|
|
3180
|
+
images = cannedTrainingDataUpload;
|
|
3181
|
+
}
|
|
3182
|
+
if (!images || images.length === 0) {
|
|
3183
|
+
dispatchToBlock({ type: 'IMAGE_UPLOAD_RESULT', payload: { requestId } });
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
for (const img of images) {
|
|
3187
|
+
const key = mockBlobKeyFromAir(img.air);
|
|
3188
|
+
if (key !== null)
|
|
3189
|
+
trainingUploads.add(blobKeyIdentity(key));
|
|
3190
|
+
}
|
|
3191
|
+
dispatchToBlock({
|
|
3192
|
+
type: 'IMAGE_UPLOAD_RESULT',
|
|
3193
|
+
payload: { requestId, selected: { images: images.map(({ url, air }) => ({ url, air })) } },
|
|
3194
|
+
});
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3123
3197
|
const isGenerationSource = typed.payload?.purpose === 'generationSource';
|
|
3124
3198
|
// NON-BLOCKING display path (asyncScan:true): early-resolve with a
|
|
3125
3199
|
// PENDING handle, then stream the canned scan verdict on a later tick.
|
|
@@ -3856,13 +3930,15 @@ export function createMockHost(options = {}) {
|
|
|
3856
3930
|
createPostResult = patch.createPostResult;
|
|
3857
3931
|
if (patch.createPostError !== undefined)
|
|
3858
3932
|
createPostError = patch.createPostError;
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3933
|
+
// `null` is meaningful (a cancel) and `undefined` restores the default, so
|
|
3934
|
+
// check for the KEY's presence.
|
|
3935
|
+
if ('cannedTrainingDataUpload' in patch)
|
|
3936
|
+
cannedTrainingDataUpload = patch.cannedTrainingDataUpload;
|
|
3937
|
+
// The training refusal knobs clear on an explicit `undefined` (key PRESENT in
|
|
3863
3938
|
// the patch), so a harness can turn a refusal off without re-installing.
|
|
3864
|
-
if ('
|
|
3865
|
-
|
|
3939
|
+
if ('trainingDataNotUploadedHere' in patch) {
|
|
3940
|
+
trainingDataNotUploadedHere = patch.trainingDataNotUploadedHere === true;
|
|
3941
|
+
}
|
|
3866
3942
|
if (patch.trainingQuoteTotal !== undefined)
|
|
3867
3943
|
trainingQuoteTotal = patch.trainingQuoteTotal;
|
|
3868
3944
|
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,14 +53,9 @@ 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;
|