@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
|
@@ -770,6 +770,18 @@ function isPendingUpload(s) {
|
|
|
770
770
|
return false;
|
|
771
771
|
return true;
|
|
772
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* The `purpose:'trainingData'` `IMAGE_UPLOAD_RESULT.selected` shape
|
|
775
|
+
* (civitai/civitai#6461): `{ images: [{ url, air }] }`, each `url` and `air` a
|
|
776
|
+
* non-empty string. Mirrors the host's `TrainingDataUpload`. `air` is opaque —
|
|
777
|
+
* its key format is the host's to check, at the estimate — so only its type is
|
|
778
|
+
* pinned here.
|
|
779
|
+
*/
|
|
780
|
+
function isTrainingDataUpload(s) {
|
|
781
|
+
if (!Array.isArray(s.images))
|
|
782
|
+
return false;
|
|
783
|
+
return s.images.every((img) => isObject(img) && isNonEmptyString(img.url) && isNonEmptyString(img.air));
|
|
784
|
+
}
|
|
773
785
|
/**
|
|
774
786
|
* Reply to a block-initiated `OPEN_IMAGE_UPLOAD`. `selected` is ABSENT when the
|
|
775
787
|
* user dismissed the upload modal (the block's hook resolves to `null`). When
|
|
@@ -779,7 +791,8 @@ function isPendingUpload(s) {
|
|
|
779
791
|
* value, `url` non-empty).
|
|
780
792
|
* • `'generationSource'` → the source `{ url, width, height }` (positive finite
|
|
781
793
|
* dimensions, non-empty url; no imageId/nsfwLevel).
|
|
782
|
-
*
|
|
794
|
+
* • `'trainingData'` → `{ images: [{ url, air }] }` (non-empty strings).
|
|
795
|
+
* The guard accepts ANY of the shapes (the result carries no purpose discriminator).
|
|
783
796
|
* Kept in lockstep with the host's `IMAGE_UPLOAD_RESULT` (civitai/civitai
|
|
784
797
|
* `PageBlockHost.tsx` / `BlockImageUploadModal.tsx` /
|
|
785
798
|
* `BlockGenerationSourceUploadModal.tsx`).
|
|
@@ -797,13 +810,18 @@ export function isValidImageUploadResult(p) {
|
|
|
797
810
|
if (p.error !== undefined && typeof p.error !== 'string')
|
|
798
811
|
return false;
|
|
799
812
|
// `selected` absent → cancelled upload (valid). When present, it must match
|
|
800
|
-
// one of the purpose-keyed shapes (moderated / generationSource / pending
|
|
813
|
+
// one of the purpose-keyed shapes (moderated / generationSource / pending /
|
|
814
|
+
// trainingData).
|
|
801
815
|
if (p.selected !== undefined) {
|
|
802
816
|
const s = p.selected;
|
|
803
817
|
if (!isObject(s))
|
|
804
818
|
return false;
|
|
805
|
-
if (!isModeratedUpload(s) &&
|
|
819
|
+
if (!isModeratedUpload(s) &&
|
|
820
|
+
!isGenerationSourceUpload(s) &&
|
|
821
|
+
!isPendingUpload(s) &&
|
|
822
|
+
!isTrainingDataUpload(s)) {
|
|
806
823
|
return false;
|
|
824
|
+
}
|
|
807
825
|
}
|
|
808
826
|
return true;
|
|
809
827
|
}
|
|
@@ -1022,6 +1040,80 @@ export function isValidWildcardPackResult(p) {
|
|
|
1022
1040
|
return false;
|
|
1023
1041
|
return true;
|
|
1024
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* The CLOSED `RESOURCE_FILE_RESULT` error set, mirroring
|
|
1045
|
+
* `BlockResourceFileErrorCode` (civitai/civitai-app-starters#664).
|
|
1046
|
+
*/
|
|
1047
|
+
const RESOURCE_FILE_ERROR_CODES = new Set([
|
|
1048
|
+
'not-found',
|
|
1049
|
+
'forbidden',
|
|
1050
|
+
'unauthorized',
|
|
1051
|
+
'too-large',
|
|
1052
|
+
'parse-failed',
|
|
1053
|
+
'busy',
|
|
1054
|
+
'rate-limited',
|
|
1055
|
+
]);
|
|
1056
|
+
const RESOURCE_FILE_ENTRY_FORMATS = new Set(['ui', 'api', 'unknown']);
|
|
1057
|
+
/**
|
|
1058
|
+
* Reply to a block-initiated `GET_RESOURCE_FILE`. Same contract shape as
|
|
1059
|
+
* {@link isValidWildcardPackResult}: EITHER a `file` OR an `error` that is one of
|
|
1060
|
+
* {@link RESOURCE_FILE_ERROR_CODES}; a free-text or unknown `error`, or a reply
|
|
1061
|
+
* with neither, is malformed and dropped. The `file` is checked field by field —
|
|
1062
|
+
* `meta` ids positive integers, names strings, `creatorUsername` a string or
|
|
1063
|
+
* `null`, `truncated` a boolean, and EVERY entry an object with a string `path`,
|
|
1064
|
+
* a known `format` and a `graph` key. `graph` itself is the host's parsed JSON
|
|
1065
|
+
* and may be any JSON value.
|
|
1066
|
+
*/
|
|
1067
|
+
export function isValidResourceFileResult(p) {
|
|
1068
|
+
if (!isObject(p))
|
|
1069
|
+
return false;
|
|
1070
|
+
if (!isWireRequestIdShape(p.requestId))
|
|
1071
|
+
return false;
|
|
1072
|
+
if (p.error !== undefined) {
|
|
1073
|
+
if (typeof p.error !== 'string' || !RESOURCE_FILE_ERROR_CODES.has(p.error))
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
if (p.file !== undefined) {
|
|
1077
|
+
const file = p.file;
|
|
1078
|
+
if (!isObject(file))
|
|
1079
|
+
return false;
|
|
1080
|
+
const meta = file.meta;
|
|
1081
|
+
if (!isObject(meta))
|
|
1082
|
+
return false;
|
|
1083
|
+
if (!isPositiveInteger(meta.modelId))
|
|
1084
|
+
return false;
|
|
1085
|
+
if (!isPositiveInteger(meta.modelVersionId))
|
|
1086
|
+
return false;
|
|
1087
|
+
if (!isPositiveInteger(meta.fileId))
|
|
1088
|
+
return false;
|
|
1089
|
+
if (typeof meta.fileName !== 'string')
|
|
1090
|
+
return false;
|
|
1091
|
+
if (typeof meta.modelName !== 'string')
|
|
1092
|
+
return false;
|
|
1093
|
+
if (typeof meta.versionName !== 'string')
|
|
1094
|
+
return false;
|
|
1095
|
+
if (meta.creatorUsername !== null && typeof meta.creatorUsername !== 'string')
|
|
1096
|
+
return false;
|
|
1097
|
+
if (typeof file.truncated !== 'boolean')
|
|
1098
|
+
return false;
|
|
1099
|
+
if (!Array.isArray(file.entries))
|
|
1100
|
+
return false;
|
|
1101
|
+
for (const entry of file.entries) {
|
|
1102
|
+
if (!isObject(entry))
|
|
1103
|
+
return false;
|
|
1104
|
+
if (typeof entry.path !== 'string')
|
|
1105
|
+
return false;
|
|
1106
|
+
if (typeof entry.format !== 'string' || !RESOURCE_FILE_ENTRY_FORMATS.has(entry.format)) {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
if (!('graph' in entry))
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (p.file === undefined && p.error === undefined)
|
|
1114
|
+
return false;
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1025
1117
|
// ============================================================
|
|
1026
1118
|
// App generator SUBQUEUE reply validators
|
|
1027
1119
|
// ============================================================
|
|
@@ -1533,48 +1625,6 @@ export function isValidCreatePostResult(p) {
|
|
|
1533
1625
|
return false;
|
|
1534
1626
|
return true;
|
|
1535
1627
|
}
|
|
1536
|
-
/**
|
|
1537
|
-
* Reply to `PREPARE_TRAINING_DATASET`. A well-formed reply carries EITHER a
|
|
1538
|
-
* `result` (`{ datasetId: non-empty string, count: integer ≥ 0, rejected: [{
|
|
1539
|
-
* imageId: number, reason: string }] }`) OR an `error`; one with neither is
|
|
1540
|
-
* malformed and dropped.
|
|
1541
|
-
*
|
|
1542
|
-
* 🔴 `error` IS SHAPE-CHECKED ONLY — the same decision as
|
|
1543
|
-
* {@link isValidCreatePostResult}: the host's closed codes share the field with
|
|
1544
|
-
* any server message it forwards, and a membership check would drop those
|
|
1545
|
-
* replies before correlation, leaving the request to its timer.
|
|
1546
|
-
*
|
|
1547
|
-
* `rejected[].reason` is checked as a STRING, not against today's six members —
|
|
1548
|
-
* the host adding a reason must not make every prepare reply that carries it a
|
|
1549
|
-
* dropped one (the forward-compatibility rule in {@link isValidWorkflowSnapshot}).
|
|
1550
|
-
*/
|
|
1551
|
-
export function isValidTrainingDatasetResult(p) {
|
|
1552
|
-
if (!isObject(p))
|
|
1553
|
-
return false;
|
|
1554
|
-
if (!isWireRequestIdShape(p.requestId))
|
|
1555
|
-
return false;
|
|
1556
|
-
if (p.error !== undefined && typeof p.error !== 'string')
|
|
1557
|
-
return false;
|
|
1558
|
-
if (p.result !== undefined) {
|
|
1559
|
-
const r = p.result;
|
|
1560
|
-
if (!isObject(r))
|
|
1561
|
-
return false;
|
|
1562
|
-
if (!isNonEmptyString(r.datasetId))
|
|
1563
|
-
return false;
|
|
1564
|
-
if (!Number.isInteger(r.count) || r.count < 0)
|
|
1565
|
-
return false;
|
|
1566
|
-
if (!Array.isArray(r.rejected))
|
|
1567
|
-
return false;
|
|
1568
|
-
for (const x of r.rejected) {
|
|
1569
|
-
if (!isObject(x) || !isFiniteNumber(x.imageId) || typeof x.reason !== 'string') {
|
|
1570
|
-
return false;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
if (p.result === undefined && p.error === undefined)
|
|
1575
|
-
return false;
|
|
1576
|
-
return true;
|
|
1577
|
-
}
|
|
1578
1628
|
/**
|
|
1579
1629
|
* Reply to `RUN_TRAINING`. A well-formed reply carries EITHER a `snapshot` (a
|
|
1580
1630
|
* valid {@link isValidWorkflowSnapshot} — the block polls its `workflowId`) OR an
|
|
@@ -2326,6 +2376,8 @@ export function payloadValidatorFor(type) {
|
|
|
2326
2376
|
return isValidDailyCompensationResult;
|
|
2327
2377
|
case 'WILDCARD_PACK_RESULT':
|
|
2328
2378
|
return isValidWildcardPackResult;
|
|
2379
|
+
case 'RESOURCE_FILE_RESULT':
|
|
2380
|
+
return isValidResourceFileResult;
|
|
2329
2381
|
case 'APP_WORKFLOWS_RESULT':
|
|
2330
2382
|
return isValidAppWorkflowsResult;
|
|
2331
2383
|
case 'CANCEL_APP_WORKFLOW_RESULT':
|
|
@@ -2349,8 +2401,6 @@ export function payloadValidatorFor(type) {
|
|
|
2349
2401
|
// Same caveat as CREATE_POST_RESULT: presence is compiler-enforced, the
|
|
2350
2402
|
// identity is pinned by test (`validate.test.ts`). TRAINING_RESULT settles a
|
|
2351
2403
|
// charged run.
|
|
2352
|
-
case 'TRAINING_DATASET_RESULT':
|
|
2353
|
-
return isValidTrainingDatasetResult;
|
|
2354
2404
|
case 'TRAINING_RESULT':
|
|
2355
2405
|
return isValidTrainingResult;
|
|
2356
2406
|
// Settles a charged job, like TRAINING_RESULT directly above.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
|
|
2
2
|
/** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
|
|
3
|
-
export const VERSION = "0.
|
|
3
|
+
export const VERSION = "0.78.0";
|
|
4
4
|
//# sourceMappingURL=version.generated.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitai/blocks-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"vite": "^8.0.14",
|
|
60
60
|
"vitest": "^4.1.11",
|
|
61
|
-
"@civitai/app-sdk": "^0.
|
|
61
|
+
"@civitai/app-sdk": "^0.79.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { BlockPrepareTrainingDatasetHostError, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason } from '@civitai/app-sdk/blocks';
|
|
2
|
-
import type { ConsentRetryOptions } from './consentRetryOptions.js';
|
|
3
|
-
export type { BlockPrepareTrainingDatasetHostError, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, };
|
|
4
|
-
/**
|
|
5
|
-
* The host's CLOSED refusal codes for `PREPARE_TRAINING_DATASET`, as a runtime
|
|
6
|
-
* list. Mirrors civitai/civitai's `PREPARE_TRAINING_DATASET_HOST_ERRORS`.
|
|
7
|
-
*
|
|
8
|
-
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: it also carries free-text server
|
|
9
|
-
* messages, so "is this a code?" is a membership question at runtime — see
|
|
10
|
-
* {@link isPrepareTrainingDatasetErrorCode}.
|
|
11
|
-
*/
|
|
12
|
-
export declare const PREPARE_TRAINING_DATASET_ERROR_CODES: readonly ["review-mode", "block is not ready", "sign in to train", "invalid training dataset", "no block token"];
|
|
13
|
-
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
14
|
-
export declare function isPrepareTrainingDatasetErrorCode(error: string): error is BlockPrepareTrainingDatasetHostError;
|
|
15
|
-
/**
|
|
16
|
-
* A dataset-preparation failure.
|
|
17
|
-
*
|
|
18
|
-
* `.code` is the host's refusal code when the HOST refused, `undefined` for a
|
|
19
|
-
* server message or a transport timeout. Check `.timedOut` before rendering
|
|
20
|
-
* `.message` — a timeout's message is an SDK-internal string.
|
|
21
|
-
*
|
|
22
|
-
* Preparing charges nothing, so no failure here costs the viewer Buzz.
|
|
23
|
-
*/
|
|
24
|
-
export declare class PrepareTrainingDatasetError extends Error {
|
|
25
|
-
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
26
|
-
readonly code?: BlockPrepareTrainingDatasetHostError;
|
|
27
|
-
/**
|
|
28
|
-
* No reply arrived within the bound. The server may still have prepared a
|
|
29
|
-
* dataset; nothing was charged either way, and preparing again simply makes a
|
|
30
|
-
* new handle.
|
|
31
|
-
*/
|
|
32
|
-
readonly timedOut: boolean;
|
|
33
|
-
/**
|
|
34
|
-
* There is no session — the viewer is signed out (`sign in to train`), or their
|
|
35
|
-
* session ended (a bare `UNAUTHORIZED`). Route into `useRequestSignIn()`.
|
|
36
|
-
*/
|
|
37
|
-
readonly signInRequired: boolean;
|
|
38
|
-
constructor(error: string, opts?: {
|
|
39
|
-
timedOut?: boolean;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** What {@link usePrepareTrainingDataset} returns. */
|
|
43
|
-
export interface UsePrepareTrainingDataset {
|
|
44
|
-
/**
|
|
45
|
-
* Ask the host to prepare a training dataset from the viewer's OWN images.
|
|
46
|
-
* Resolves with the opaque `datasetId`, the admitted `count` and the
|
|
47
|
-
* `rejected` images. REJECTS with a {@link PrepareTrainingDatasetError}.
|
|
48
|
-
*
|
|
49
|
-
* A resolved result admitted AT LEAST ONE image (`count >= 1`) — when nothing
|
|
50
|
-
* is admitted the server refuses instead, and this rejects with its message
|
|
51
|
-
* (`none of the requested images can be used for training` / `… could be
|
|
52
|
-
* prepared for training`, no `.code`). Read `rejected` for what was left out:
|
|
53
|
-
* `pending-scan` and `import-unavailable` are retryable; the rest are not.
|
|
54
|
-
* The per-image reasons are not available when everything was rejected.
|
|
55
|
-
*/
|
|
56
|
-
prepareDataset: (items: BlockTrainingDatasetItem[], options?: ConsentRetryOptions) => Promise<BlockTrainingDatasetResult>;
|
|
57
|
-
/** `true` while a request is in flight. */
|
|
58
|
-
pending: boolean;
|
|
59
|
-
/** The last request's failure, or `null`. Cleared at the start of the next call. */
|
|
60
|
-
error: PrepareTrainingDatasetError | null;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Step 1 of the App Blocks `kind: 'training'` flow: turn the viewer's own images
|
|
64
|
-
* and your captions into a dataset handle for `WorkflowBodyTraining.datasetId`,
|
|
65
|
-
* through the host-mediated `PREPARE_TRAINING_DATASET` → `TRAINING_DATASET_RESULT`
|
|
66
|
-
* bridge.
|
|
67
|
-
*
|
|
68
|
-
* 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
|
|
69
|
-
* (ships off), with `ai:write:budgeted` declared in the manifest and granted by a
|
|
70
|
-
* signed-in viewer. Refused from `dev:live` and from review sessions — build the
|
|
71
|
-
* flow against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
|
|
72
|
-
*
|
|
73
|
-
* Only the viewer's OWN scanned, unflagged images within the page's maturity
|
|
74
|
-
* ceiling are admitted; captions are moderated. Bounds: 1–50 items, captions up
|
|
75
|
-
* to 1,000 characters (`BLOCK_TRAINING_DATASET_MAX_ITEMS`,
|
|
76
|
-
* `BLOCK_TRAINING_CAPTION_MAX_CHARS`); the host refuses anything else with
|
|
77
|
-
* `invalid training dataset` before calling the server.
|
|
78
|
-
*
|
|
79
|
-
* No dialog and no charge. When the token lacks `ai:write:budgeted`, the call
|
|
80
|
-
* opens the host's consent prompt and retries once on a grant (opt out with
|
|
81
|
-
* `{ autoRequestConsent: false }`).
|
|
82
|
-
*
|
|
83
|
-
* Sent under the 120s server-work bound, not the 30s protocol default: the server
|
|
84
|
-
* imports up to 50 images into the orchestrator within its own 60s budget.
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* const { prepareDataset } = usePrepareTrainingDataset();
|
|
88
|
-
* try {
|
|
89
|
-
* const dataset = await prepareDataset(
|
|
90
|
-
* picked.map((img) => ({ imageId: img.id, caption: img.caption })),
|
|
91
|
-
* );
|
|
92
|
-
* if (dataset.rejected.length > 0) showNotice(`${dataset.rejected.length} image(s) left out`);
|
|
93
|
-
* } catch (e) {
|
|
94
|
-
* // Includes "none of the requested images can be used for training".
|
|
95
|
-
* if (e instanceof PrepareTrainingDatasetError) showError(e.message);
|
|
96
|
-
* }
|
|
97
|
-
*/
|
|
98
|
-
export declare function usePrepareTrainingDataset(): UsePrepareTrainingDataset;
|
|
99
|
-
//# sourceMappingURL=usePrepareTrainingDataset.d.ts.map
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { BLOCK_SCOPES } from '@civitai/app-sdk/blocks';
|
|
3
|
-
import { withConsentRetry } from '../internal/withConsentRetry.js';
|
|
4
|
-
import { WORKFLOW_REQUEST_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
5
|
-
import { getTransport } from '../transport/singleton.js';
|
|
6
|
-
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
7
|
-
import { useTransportConnection } from './useTransportConnection.js';
|
|
8
|
-
/**
|
|
9
|
-
* The consent-gated scope the training flow requires — `ai:write:budgeted`. The
|
|
10
|
-
* server refuses a dataset request from a token without it (it is the spend
|
|
11
|
-
* scope, and preparing is gated exactly like the run it prepares for). From
|
|
12
|
-
* {@link BLOCK_SCOPES}, never a literal: a typo would make the automatic consent
|
|
13
|
-
* prompt silently do nothing.
|
|
14
|
-
*/
|
|
15
|
-
const TRAINING_SCOPES = [BLOCK_SCOPES.AI_WRITE_BUDGETED];
|
|
16
|
-
/**
|
|
17
|
-
* The host's CLOSED refusal codes for `PREPARE_TRAINING_DATASET`, as a runtime
|
|
18
|
-
* list. Mirrors civitai/civitai's `PREPARE_TRAINING_DATASET_HOST_ERRORS`.
|
|
19
|
-
*
|
|
20
|
-
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: it also carries free-text server
|
|
21
|
-
* messages, so "is this a code?" is a membership question at runtime — see
|
|
22
|
-
* {@link isPrepareTrainingDatasetErrorCode}.
|
|
23
|
-
*/
|
|
24
|
-
export const PREPARE_TRAINING_DATASET_ERROR_CODES = [
|
|
25
|
-
'review-mode',
|
|
26
|
-
'block is not ready',
|
|
27
|
-
'sign in to train',
|
|
28
|
-
'invalid training dataset',
|
|
29
|
-
'no block token',
|
|
30
|
-
];
|
|
31
|
-
const CODE_SET = new Set(PREPARE_TRAINING_DATASET_ERROR_CODES);
|
|
32
|
-
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
33
|
-
export function isPrepareTrainingDatasetErrorCode(error) {
|
|
34
|
-
return CODE_SET.has(error);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* A dataset-preparation failure.
|
|
38
|
-
*
|
|
39
|
-
* `.code` is the host's refusal code when the HOST refused, `undefined` for a
|
|
40
|
-
* server message or a transport timeout. Check `.timedOut` before rendering
|
|
41
|
-
* `.message` — a timeout's message is an SDK-internal string.
|
|
42
|
-
*
|
|
43
|
-
* Preparing charges nothing, so no failure here costs the viewer Buzz.
|
|
44
|
-
*/
|
|
45
|
-
export class PrepareTrainingDatasetError extends Error {
|
|
46
|
-
/** The closed host refusal code, or `undefined` for a server/transport error. */
|
|
47
|
-
code;
|
|
48
|
-
/**
|
|
49
|
-
* No reply arrived within the bound. The server may still have prepared a
|
|
50
|
-
* dataset; nothing was charged either way, and preparing again simply makes a
|
|
51
|
-
* new handle.
|
|
52
|
-
*/
|
|
53
|
-
timedOut;
|
|
54
|
-
/**
|
|
55
|
-
* There is no session — the viewer is signed out (`sign in to train`), or their
|
|
56
|
-
* session ended (a bare `UNAUTHORIZED`). Route into `useRequestSignIn()`.
|
|
57
|
-
*/
|
|
58
|
-
signInRequired;
|
|
59
|
-
constructor(error, opts) {
|
|
60
|
-
super(error);
|
|
61
|
-
this.name = 'PrepareTrainingDatasetError';
|
|
62
|
-
this.timedOut = opts?.timedOut === true;
|
|
63
|
-
if (isPrepareTrainingDatasetErrorCode(error))
|
|
64
|
-
this.code = error;
|
|
65
|
-
this.signInRequired = error === 'sign in to train' || error === 'UNAUTHORIZED';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Step 1 of the App Blocks `kind: 'training'` flow: turn the viewer's own images
|
|
70
|
-
* and your captions into a dataset handle for `WorkflowBodyTraining.datasetId`,
|
|
71
|
-
* through the host-mediated `PREPARE_TRAINING_DATASET` → `TRAINING_DATASET_RESULT`
|
|
72
|
-
* bridge.
|
|
73
|
-
*
|
|
74
|
-
* 🔴 AVAILABILITY: page apps only, behind the host flag `app-blocks-training-kind`
|
|
75
|
-
* (ships off), with `ai:write:budgeted` declared in the manifest and granted by a
|
|
76
|
-
* signed-in viewer. Refused from `dev:live` and from review sessions — build the
|
|
77
|
-
* flow against the mock host. See `WorkflowBodyTraining` in `@civitai/app-sdk`.
|
|
78
|
-
*
|
|
79
|
-
* Only the viewer's OWN scanned, unflagged images within the page's maturity
|
|
80
|
-
* ceiling are admitted; captions are moderated. Bounds: 1–50 items, captions up
|
|
81
|
-
* to 1,000 characters (`BLOCK_TRAINING_DATASET_MAX_ITEMS`,
|
|
82
|
-
* `BLOCK_TRAINING_CAPTION_MAX_CHARS`); the host refuses anything else with
|
|
83
|
-
* `invalid training dataset` before calling the server.
|
|
84
|
-
*
|
|
85
|
-
* No dialog and no charge. When the token lacks `ai:write:budgeted`, the call
|
|
86
|
-
* opens the host's consent prompt and retries once on a grant (opt out with
|
|
87
|
-
* `{ autoRequestConsent: false }`).
|
|
88
|
-
*
|
|
89
|
-
* Sent under the 120s server-work bound, not the 30s protocol default: the server
|
|
90
|
-
* imports up to 50 images into the orchestrator within its own 60s budget.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* const { prepareDataset } = usePrepareTrainingDataset();
|
|
94
|
-
* try {
|
|
95
|
-
* const dataset = await prepareDataset(
|
|
96
|
-
* picked.map((img) => ({ imageId: img.id, caption: img.caption })),
|
|
97
|
-
* );
|
|
98
|
-
* if (dataset.rejected.length > 0) showNotice(`${dataset.rejected.length} image(s) left out`);
|
|
99
|
-
* } catch (e) {
|
|
100
|
-
* // Includes "none of the requested images can be used for training".
|
|
101
|
-
* if (e instanceof PrepareTrainingDatasetError) showError(e.message);
|
|
102
|
-
* }
|
|
103
|
-
*/
|
|
104
|
-
export function usePrepareTrainingDataset() {
|
|
105
|
-
useTransportConnection();
|
|
106
|
-
const [pending, setPending] = useState(false);
|
|
107
|
-
const [error, setError] = useState(null);
|
|
108
|
-
// The consent wait can outlive the component; see `useCreatePostFromApp`.
|
|
109
|
-
const mountedRef = useRef(true);
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
mountedRef.current = true;
|
|
112
|
-
return () => {
|
|
113
|
-
mountedRef.current = false;
|
|
114
|
-
};
|
|
115
|
-
}, []);
|
|
116
|
-
/** ONE round-trip + its result contract. Re-invoked verbatim on a consent retry. */
|
|
117
|
-
const prepareOnce = useCallback(async (items) => {
|
|
118
|
-
const reply = await sendTypedRequest(getTransport(), { type: 'PREPARE_TRAINING_DATASET', payload: { items } }, 'TRAINING_DATASET_RESULT', { timeoutMs: WORKFLOW_REQUEST_TIMEOUT_MS }).catch((err) => {
|
|
119
|
-
// Stamped INSIDE the closure `withConsentRetry` re-invokes, so its rule 4
|
|
120
|
-
// sees it: a 120s wait is not repeated behind a consent prompt.
|
|
121
|
-
if (err instanceof RequestTimeoutError) {
|
|
122
|
-
throw new PrepareTrainingDatasetError(err.message, { timedOut: true });
|
|
123
|
-
}
|
|
124
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
125
|
-
});
|
|
126
|
-
if (reply.error || !reply.result) {
|
|
127
|
-
// `||`, not `??`: the validator shape-checks `error`, so `''` is a valid
|
|
128
|
-
// reply that must not become an Error with an empty message. The
|
|
129
|
-
// fallback is deliberately NOT a host code — `invalid training dataset`
|
|
130
|
-
// would claim "nothing reached the server", which an empty error does
|
|
131
|
-
// not tell us.
|
|
132
|
-
throw new PrepareTrainingDatasetError(reply.error || 'training dataset could not be prepared');
|
|
133
|
-
}
|
|
134
|
-
return reply.result;
|
|
135
|
-
}, []);
|
|
136
|
-
const prepareDataset = useCallback(async (items, options) => {
|
|
137
|
-
if (mountedRef.current) {
|
|
138
|
-
setPending(true);
|
|
139
|
-
setError(null);
|
|
140
|
-
}
|
|
141
|
-
try {
|
|
142
|
-
return await withConsentRetry(getTransport(), TRAINING_SCOPES, () => prepareOnce(items), options, () => mountedRef.current);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const wrapped = err instanceof PrepareTrainingDatasetError
|
|
146
|
-
? err
|
|
147
|
-
: new PrepareTrainingDatasetError(err instanceof Error ? err.message : String(err));
|
|
148
|
-
if (mountedRef.current)
|
|
149
|
-
setError(wrapped);
|
|
150
|
-
throw wrapped;
|
|
151
|
-
}
|
|
152
|
-
finally {
|
|
153
|
-
if (mountedRef.current)
|
|
154
|
-
setPending(false);
|
|
155
|
-
}
|
|
156
|
-
}, [prepareOnce]);
|
|
157
|
-
return { prepareDataset, pending, error };
|
|
158
|
-
}
|
|
159
|
-
//# sourceMappingURL=usePrepareTrainingDataset.js.map
|