@civitai/blocks-react 0.75.0 → 0.76.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 +129 -0
- package/dist/hooks/useMediaUpload.d.ts +97 -0
- package/dist/hooks/useMediaUpload.js +108 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal/liveHost.d.ts +9 -1
- package/dist/internal/liveHost.js +27 -1
- package/dist/internal/mediaUploadBytes.d.ts +169 -0
- package/dist/internal/mediaUploadBytes.js +268 -0
- package/dist/internal/mockHost.d.ts +33 -1
- package/dist/internal/mockHost.js +69 -2
- package/dist/internal/mockHostStepInput.d.ts +62 -0
- package/dist/internal/mockHostStepInput.js +120 -0
- package/dist/internal/mockWorkflowGraph.d.ts +21 -1
- package/dist/internal/mockWorkflowGraph.js +22 -6
- package/dist/internal/stepInputLimits.d.ts +104 -0
- package/dist/internal/stepInputLimits.js +179 -0
- package/dist/testing.d.ts +1 -0
- package/dist/transport/requestTimeouts.js +1 -0
- package/dist/transport/validate.d.ts +11 -0
- package/dist/transport/validate.js +56 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -675,6 +675,14 @@ showImages(done.imageUrls ?? []);
|
|
|
675
675
|
KB (262144 bytes) for every step's `input` plus `arguments` together. An
|
|
676
676
|
unknown key on the body or on a step is rejected. The app's velocity limit
|
|
677
677
|
counts one unit per step, and a `repeat` step one per iteration instead.
|
|
678
|
+
- **No inline media** in a step `input` or in `arguments` — here or in a
|
|
679
|
+
pass-through `kind: 'step'` body's `input`: no `data:image|video|audio` URI
|
|
680
|
+
and no raw base64 string of 4096+ characters, at any depth. Upload the image,
|
|
681
|
+
video or audio with [`useMediaUpload()`](#usemediaupload) (an image also works
|
|
682
|
+
with `useImageUpload({ purpose: 'generationSource' })` or `useImagePicker()`)
|
|
683
|
+
and pass the returned `url`. A body holding more than
|
|
684
|
+
262144 values in those fields is refused outright. `createMockHost` refuses
|
|
685
|
+
both as the host does, naming the path.
|
|
678
686
|
- `suppressOutput: true` marks an intermediate step. Its images are left out of
|
|
679
687
|
`snapshot.imageUrls` and out of its own `steps[i].imageUrls`. It still runs
|
|
680
688
|
and is still charged.
|
|
@@ -2475,6 +2483,126 @@ an invalid `sources` or `multiple` with an error, and answers
|
|
|
2475
2483
|
list the viewer's images, so there `open` rejects with a message saying to use
|
|
2476
2484
|
the mock host.
|
|
2477
2485
|
|
|
2486
|
+
### `useMediaUpload()`
|
|
2487
|
+
|
|
2488
|
+
**The way to pass any media (image, video or audio) into a workflow step.** The
|
|
2489
|
+
host refuses data URIs and inline base64 in step inputs, so upload the file
|
|
2490
|
+
with this hook and pass the `url` it returns into the step input. Returns
|
|
2491
|
+
`{ open, upload }`.
|
|
2492
|
+
|
|
2493
|
+
- **`open({ kind })`** opens the host's own file picker for that kind. The
|
|
2494
|
+
viewer picks a file, the host uploads it, and `open` resolves with the
|
|
2495
|
+
upload, or `null` if nothing was uploaded.
|
|
2496
|
+
- **`upload({ kind, bytes, mimeType? })`** uploads media your app already
|
|
2497
|
+
holds in the tab, with no picker: a canvas export, a crop, a `MediaRecorder`
|
|
2498
|
+
recording. It resolves with the upload, and never resolves without one.
|
|
2499
|
+
|
|
2500
|
+
```tsx
|
|
2501
|
+
import { useMediaUpload } from '@civitai/blocks-react';
|
|
2502
|
+
|
|
2503
|
+
function UseRecording({ recording }: { recording: Blob }) {
|
|
2504
|
+
const { upload } = useMediaUpload();
|
|
2505
|
+
|
|
2506
|
+
async function onClick() {
|
|
2507
|
+
const audio = await upload({
|
|
2508
|
+
kind: 'audio',
|
|
2509
|
+
bytes: await recording.arrayBuffer(),
|
|
2510
|
+
mimeType: recording.type, // optional; checked against the content
|
|
2511
|
+
});
|
|
2512
|
+
// audio.url goes into the step's audio input
|
|
2513
|
+
startStep({ audioUrl: audio.url });
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
return <button onClick={onClick}>Use this recording</button>;
|
|
2517
|
+
}
|
|
2518
|
+
```
|
|
2519
|
+
|
|
2520
|
+
```tsx
|
|
2521
|
+
const { open } = useMediaUpload();
|
|
2522
|
+
|
|
2523
|
+
const video = await open({ kind: 'video' }); // BlockMediaUploadInfo | null
|
|
2524
|
+
if (video) {
|
|
2525
|
+
// video.url, video.width, video.height, video.durationMs (when known)
|
|
2526
|
+
}
|
|
2527
|
+
```
|
|
2528
|
+
|
|
2529
|
+
What each kind accepts, decided by the file's **content** (its first bytes),
|
|
2530
|
+
never by its name or declared type:
|
|
2531
|
+
|
|
2532
|
+
| `kind` | Accepted | What you get back |
|
|
2533
|
+
|---|---|---|
|
|
2534
|
+
| `'image'` | PNG, JPEG, WebP | Resized so both sides are 64 to 2048 pixels and re-encoded as JPEG: `mimeType` is always `'image/jpeg'`, and `sizeBytes`, `width` and `height` describe that JPEG. |
|
|
2535
|
+
| `'video'` | MP4, WebM | Uploaded as sent. `width` and `height` are always set (a file with no video track is refused). `durationMs` when the browser reports a duration. |
|
|
2536
|
+
| `'audio'` | MP3, WAV, WebM | Uploaded as sent. No `width` or `height`. `durationMs` when the browser reports a duration: a `MediaRecorder` WebM often reports none, so do not rely on it. |
|
|
2537
|
+
|
|
2538
|
+
- **At most 24 MB per image, and 64 MB per video or audio file.** The image
|
|
2539
|
+
cap is lower because the host decodes an image in full before it resizes it.
|
|
2540
|
+
`upload` refuses a larger buffer before sending it, with the host's own
|
|
2541
|
+
message: `file exceeds the 24 MB image upload limit` or
|
|
2542
|
+
`file exceeds the 64 MB upload limit`.
|
|
2543
|
+
- **`upload` is rate-limited:** at most 8 uploads and 128 MB per 60 seconds
|
|
2544
|
+
per page. Past that the host replies `busy`; wait and retry. A refused upload
|
|
2545
|
+
does not count. `open` is not limited this way, because a person picks each
|
|
2546
|
+
file.
|
|
2547
|
+
- **`mimeType` is optional.** When you pass it (for example `blob.type`) it
|
|
2548
|
+
must match the content, or the upload is refused with
|
|
2549
|
+
`mimeType does not match the file's content`. Case and parameters such as
|
|
2550
|
+
`;codecs=opus` are ignored, and `audio/x-wav` counts as `audio/wav`.
|
|
2551
|
+
- Pass an `ArrayBuffer` (`await blob.arrayBuffer()`). A `Blob`, a typed array
|
|
2552
|
+
or an empty buffer is refused before sending with
|
|
2553
|
+
`invalid media-upload request`. The buffer is copied, not transferred.
|
|
2554
|
+
- A file whose content is not one of its kind's types is refused with
|
|
2555
|
+
`images must be PNG, JPEG or WebP`, `videos must be MP4 or WebM` or
|
|
2556
|
+
`audio must be MP3, WAV or WebM`. The host also refuses a file it cannot
|
|
2557
|
+
read (`could not read this video file`, `could not read this audio file`), a
|
|
2558
|
+
video with no video track (`this file has no video track`), and an image too
|
|
2559
|
+
small to resize to 64 pixels. A failed upload rejects with
|
|
2560
|
+
`media upload failed — please try again`, and a file the upload service
|
|
2561
|
+
refuses with `this video was refused by the upload service` or
|
|
2562
|
+
`this audio was refused by the upload service`. A bytes upload made while
|
|
2563
|
+
the host holds no block token rejects with `no block token`.
|
|
2564
|
+
|
|
2565
|
+
What `open` and `upload` resolve and reject with:
|
|
2566
|
+
|
|
2567
|
+
- **`open` resolves `null` when nothing was uploaded.** Usually the viewer
|
|
2568
|
+
closed the picker. The host also answers that way at once, without showing a
|
|
2569
|
+
picker, while your app is viewed in review mode or is not ready yet. A
|
|
2570
|
+
problem with the picked file is shown in the picker, where the viewer can
|
|
2571
|
+
pick another one.
|
|
2572
|
+
- **`upload` never resolves `null`.** When the host declines without
|
|
2573
|
+
uploading (review mode, not ready), `upload` rejects with
|
|
2574
|
+
`the host did not upload the media (the app is in review mode or not ready)`.
|
|
2575
|
+
- **A refusal rejects** with the host's message, in both modes. On a surface
|
|
2576
|
+
other than a page app the host answers at once with
|
|
2577
|
+
`unsupported on this host`.
|
|
2578
|
+
- **Both wait up to 10 minutes** for the host (a person picking a file, or a
|
|
2579
|
+
large upload), then reject with a `RequestTimeoutError`.
|
|
2580
|
+
|
|
2581
|
+
For images only, `useImageUpload().open({ purpose: 'generationSource' })` still
|
|
2582
|
+
works and returns `{ url, width, height }`. Use `useMediaUpload` for video,
|
|
2583
|
+
audio, or bytes your app made in the tab.
|
|
2584
|
+
|
|
2585
|
+
🔴 **Needs a host that answers `OPEN_MEDIA_UPLOAD`** (civitai/civitai#6149).
|
|
2586
|
+
On a civitai.com that predates it, the request gets no answer and both calls reject with a
|
|
2587
|
+
`RequestTimeoutError` after 10 minutes.
|
|
2588
|
+
|
|
2589
|
+
With the mock host (`createMockHost` or `Harness` from
|
|
2590
|
+
`@civitai/blocks-react/testing`) there is no picker: `open` resolves at once
|
|
2591
|
+
with a built-in upload for the kind. Set `cannedMediaUploads: { video: {...} }`
|
|
2592
|
+
to choose what a kind returns, or `{ audio: null }` for a viewer who closes
|
|
2593
|
+
that picker. `upload` runs the host's checks in the host's order (kind,
|
|
2594
|
+
purpose, an invalid buffer, the per-kind cap, the rate window, the content type,
|
|
2595
|
+
a mismatching `mimeType`) and answers with that kind's upload, carrying the
|
|
2596
|
+
sniffed `mimeType` and the buffer's size for video and audio. The mock cannot
|
|
2597
|
+
decode media, so it never produces the host's decode-time refusals (an
|
|
2598
|
+
unreadable file, no video track, an image too small, an upload failure): set
|
|
2599
|
+
`mediaUploadError: 'this file has no video track'` to force one.
|
|
2600
|
+
`mediaUploadError: 'no block token'` is checked where the host checks it: a
|
|
2601
|
+
bytes upload is refused before the cap and the window, and is not counted. A model-slot
|
|
2602
|
+
`context` answers `unsupported on this host`. `dev:live` cannot upload
|
|
2603
|
+
generation inputs, so there both calls reject with a message saying to use the
|
|
2604
|
+
mock host.
|
|
2605
|
+
|
|
2478
2606
|
### `useSaveImage()`
|
|
2479
2607
|
|
|
2480
2608
|
Ask the host to download a file to the viewer's device. **A block cannot
|
|
@@ -3424,6 +3552,7 @@ MockHost
|
|
|
3424
3552
|
MockHostFailMode
|
|
3425
3553
|
MockHostOptions
|
|
3426
3554
|
MockHostScenarioPatch
|
|
3555
|
+
MockMediaUploads
|
|
3427
3556
|
MockReactionSeed
|
|
3428
3557
|
MockSharedScenario
|
|
3429
3558
|
MockSharedSeed
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { BlockMediaKind, BlockMediaUploadInfo } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/**
|
|
3
|
+
* What `upload()` rejects with when the host answers with neither a file nor
|
|
4
|
+
* an error. For a bytes upload that means the host DECLINED it without trying
|
|
5
|
+
* (the app is viewed in review mode, or is not ready yet). Nothing was
|
|
6
|
+
* uploaded, so it must never read as a success.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MEDIA_UPLOAD_DECLINED_ERROR = "the host did not upload the media (the app is in review mode or not ready)";
|
|
9
|
+
/** Options for {@link UseMediaUpload.open}. */
|
|
10
|
+
export interface MediaUploadOpenOptions {
|
|
11
|
+
/** What the viewer picks: `'image'`, `'video'` or `'audio'`. */
|
|
12
|
+
kind: BlockMediaKind;
|
|
13
|
+
}
|
|
14
|
+
/** Options for {@link UseMediaUpload.upload}. */
|
|
15
|
+
export interface MediaUploadBytesOptions {
|
|
16
|
+
/** What the bytes are: `'image'`, `'video'` or `'audio'`. */
|
|
17
|
+
kind: BlockMediaKind;
|
|
18
|
+
/**
|
|
19
|
+
* The file, as an `ArrayBuffer` (`await blob.arrayBuffer()`). At most 24 MB
|
|
20
|
+
* for an image and 64 MB for video or audio.
|
|
21
|
+
* Copied across `postMessage`, never transferred. A `Blob`, a typed array or
|
|
22
|
+
* an empty buffer is refused before sending.
|
|
23
|
+
*/
|
|
24
|
+
bytes: ArrayBuffer;
|
|
25
|
+
/**
|
|
26
|
+
* Optional: the type you believe you are sending (`blob.type`). The host
|
|
27
|
+
* decides the type from the content; when this is given and disagrees, the
|
|
28
|
+
* upload is refused with `mimeType does not match the file's content`.
|
|
29
|
+
* Parameters (`;codecs=…`) and case are ignored.
|
|
30
|
+
*/
|
|
31
|
+
mimeType?: string;
|
|
32
|
+
}
|
|
33
|
+
/** What {@link useMediaUpload} returns. */
|
|
34
|
+
export interface UseMediaUpload {
|
|
35
|
+
/**
|
|
36
|
+
* PICKER mode: the host opens its own file picker for `kind`, uploads what
|
|
37
|
+
* the viewer picks, and resolves with it. Resolves `null` when nothing was
|
|
38
|
+
* uploaded: the viewer closed the picker, or the host declined at once
|
|
39
|
+
* (review mode, not ready). Rejects with the host's message on a refusal.
|
|
40
|
+
*/
|
|
41
|
+
open: (options: MediaUploadOpenOptions) => Promise<BlockMediaUploadInfo | null>;
|
|
42
|
+
/**
|
|
43
|
+
* BYTES mode: upload media the block holds (a canvas export, a crop, a
|
|
44
|
+
* `MediaRecorder` recording) with no picker. Resolves with the uploaded
|
|
45
|
+
* file. Rejects with the host's message on every refusal or failure, and
|
|
46
|
+
* with {@link MEDIA_UPLOAD_DECLINED_ERROR} when the host declined without
|
|
47
|
+
* uploading. It never resolves without a file.
|
|
48
|
+
*/
|
|
49
|
+
upload: (options: MediaUploadBytesOptions) => Promise<BlockMediaUploadInfo>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Upload an image, video or audio file as a GENERATION INPUT, and get back a
|
|
53
|
+
* `url` to pass into a workflow step input.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 THIS IS THE WAY TO PUT MEDIA INTO A STEP. The host refuses data URIs and
|
|
56
|
+
* inline base64 in step inputs, so upload the file here and pass the returned
|
|
57
|
+
* `url`.
|
|
58
|
+
*
|
|
59
|
+
* - `open({ kind })` lets the viewer pick a file in the host's own picker.
|
|
60
|
+
* - `upload({ kind, bytes, mimeType? })` uploads bytes the block made in the
|
|
61
|
+
* tab, with no picker.
|
|
62
|
+
*
|
|
63
|
+
* The type is decided by the file's content: images PNG, JPEG or WebP (the
|
|
64
|
+
* host resizes them to 64–2048 px per side and re-encodes them as JPEG);
|
|
65
|
+
* videos MP4 or WebM; audio MP3, WAV or WebM. At most 24 MB per image (the
|
|
66
|
+
* host decodes it in full before resizing) and 64 MB per video or audio; bytes
|
|
67
|
+
* mode also allows at most 8 uploads and 128 MB per 60 s per page, and
|
|
68
|
+
* rejects `busy` past that (retry later).
|
|
69
|
+
*
|
|
70
|
+
* Refused BEFORE sending, with the host's own strings: an unknown `kind`, an
|
|
71
|
+
* `upload` whose `bytes` is not a non-empty `ArrayBuffer` (or whose `mimeType`
|
|
72
|
+
* is not a string), and a buffer over the kind's cap.
|
|
73
|
+
*
|
|
74
|
+
* Page apps only: a model-slot host rejects with `unsupported on this host`.
|
|
75
|
+
* A host that predates this message does not answer it, so the call rejects
|
|
76
|
+
* with `RequestTimeoutError` after the human-interaction timeout
|
|
77
|
+
* ({@link HUMAN_INTERACTION_TIMEOUT_MS}, 10 minutes).
|
|
78
|
+
*
|
|
79
|
+
* For images, `useImageUpload().open({ purpose: 'generationSource' })` remains
|
|
80
|
+
* valid; this hook is the one that also takes video, audio and in-tab bytes.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // A recording made in the tab, passed into a step.
|
|
84
|
+
* const { upload } = useMediaUpload();
|
|
85
|
+
* const audio = await upload({ kind: 'audio', bytes: await recording.arrayBuffer(), mimeType: recording.type });
|
|
86
|
+
* // audio.url goes into the step's audio input
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // The viewer picks a video.
|
|
90
|
+
* const { open } = useMediaUpload();
|
|
91
|
+
* const video = await open({ kind: 'video' });
|
|
92
|
+
* if (video) {
|
|
93
|
+
* // video.url, video.width, video.height, video.durationMs
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
96
|
+
export declare function useMediaUpload(): UseMediaUpload;
|
|
97
|
+
//# sourceMappingURL=useMediaUpload.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { MEDIA_UPLOAD_INVALID_ERROR, MEDIA_UPLOAD_KIND_ERROR, isMediaKind, mediaUploadCap, } from '../internal/mediaUploadBytes.js';
|
|
3
|
+
import { throwOnReplyError } from '../internal/replyError.js';
|
|
4
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
5
|
+
import { getTransport } from '../transport/singleton.js';
|
|
6
|
+
import { sendTypedRequest } from '../transport/transport.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
8
|
+
/**
|
|
9
|
+
* What `upload()` rejects with when the host answers with neither a file nor
|
|
10
|
+
* an error. For a bytes upload that means the host DECLINED it without trying
|
|
11
|
+
* (the app is viewed in review mode, or is not ready yet). Nothing was
|
|
12
|
+
* uploaded, so it must never read as a success.
|
|
13
|
+
*/
|
|
14
|
+
export const MEDIA_UPLOAD_DECLINED_ERROR = 'the host did not upload the media (the app is in review mode or not ready)';
|
|
15
|
+
/**
|
|
16
|
+
* Upload an image, video or audio file as a GENERATION INPUT, and get back a
|
|
17
|
+
* `url` to pass into a workflow step input.
|
|
18
|
+
*
|
|
19
|
+
* 🔴 THIS IS THE WAY TO PUT MEDIA INTO A STEP. The host refuses data URIs and
|
|
20
|
+
* inline base64 in step inputs, so upload the file here and pass the returned
|
|
21
|
+
* `url`.
|
|
22
|
+
*
|
|
23
|
+
* - `open({ kind })` lets the viewer pick a file in the host's own picker.
|
|
24
|
+
* - `upload({ kind, bytes, mimeType? })` uploads bytes the block made in the
|
|
25
|
+
* tab, with no picker.
|
|
26
|
+
*
|
|
27
|
+
* The type is decided by the file's content: images PNG, JPEG or WebP (the
|
|
28
|
+
* host resizes them to 64–2048 px per side and re-encodes them as JPEG);
|
|
29
|
+
* videos MP4 or WebM; audio MP3, WAV or WebM. At most 24 MB per image (the
|
|
30
|
+
* host decodes it in full before resizing) and 64 MB per video or audio; bytes
|
|
31
|
+
* mode also allows at most 8 uploads and 128 MB per 60 s per page, and
|
|
32
|
+
* rejects `busy` past that (retry later).
|
|
33
|
+
*
|
|
34
|
+
* Refused BEFORE sending, with the host's own strings: an unknown `kind`, an
|
|
35
|
+
* `upload` whose `bytes` is not a non-empty `ArrayBuffer` (or whose `mimeType`
|
|
36
|
+
* is not a string), and a buffer over the kind's cap.
|
|
37
|
+
*
|
|
38
|
+
* Page apps only: a model-slot host rejects with `unsupported on this host`.
|
|
39
|
+
* A host that predates this message does not answer it, so the call rejects
|
|
40
|
+
* with `RequestTimeoutError` after the human-interaction timeout
|
|
41
|
+
* ({@link HUMAN_INTERACTION_TIMEOUT_MS}, 10 minutes).
|
|
42
|
+
*
|
|
43
|
+
* For images, `useImageUpload().open({ purpose: 'generationSource' })` remains
|
|
44
|
+
* valid; this hook is the one that also takes video, audio and in-tab bytes.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // A recording made in the tab, passed into a step.
|
|
48
|
+
* const { upload } = useMediaUpload();
|
|
49
|
+
* const audio = await upload({ kind: 'audio', bytes: await recording.arrayBuffer(), mimeType: recording.type });
|
|
50
|
+
* // audio.url goes into the step's audio input
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // The viewer picks a video.
|
|
54
|
+
* const { open } = useMediaUpload();
|
|
55
|
+
* const video = await open({ kind: 'video' });
|
|
56
|
+
* if (video) {
|
|
57
|
+
* // video.url, video.width, video.height, video.durationMs
|
|
58
|
+
* }
|
|
59
|
+
*/
|
|
60
|
+
export function useMediaUpload() {
|
|
61
|
+
useTransportConnection();
|
|
62
|
+
const open = useCallback(async ({ kind }) => {
|
|
63
|
+
if (!isMediaKind(kind))
|
|
64
|
+
throw new Error(MEDIA_UPLOAD_KIND_ERROR);
|
|
65
|
+
const reply = await sendTypedRequest(getTransport(), { type: 'OPEN_MEDIA_UPLOAD', payload: { kind, purpose: 'generationSource' } }, 'MEDIA_UPLOAD_RESULT',
|
|
66
|
+
// A person picks the file, and the reply waits for the upload.
|
|
67
|
+
{ timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS });
|
|
68
|
+
// 🔴 BEFORE reading `selected`: a refusal carries none, and would
|
|
69
|
+
// otherwise resolve `null` exactly like a dismiss.
|
|
70
|
+
throwOnReplyError(reply, 'media upload failed');
|
|
71
|
+
return reply.selected ?? null;
|
|
72
|
+
}, []);
|
|
73
|
+
const upload = useCallback(async ({ kind, bytes, mimeType }) => {
|
|
74
|
+
// The host's own refusals, in the host's order, raised here so a doomed
|
|
75
|
+
// buffer is never copied across postMessage.
|
|
76
|
+
if (!isMediaKind(kind))
|
|
77
|
+
throw new Error(MEDIA_UPLOAD_KIND_ERROR);
|
|
78
|
+
if (!(bytes instanceof ArrayBuffer) || bytes.byteLength === 0) {
|
|
79
|
+
throw new Error(MEDIA_UPLOAD_INVALID_ERROR);
|
|
80
|
+
}
|
|
81
|
+
if (mimeType !== undefined && typeof mimeType !== 'string') {
|
|
82
|
+
throw new Error(MEDIA_UPLOAD_INVALID_ERROR);
|
|
83
|
+
}
|
|
84
|
+
const cap = mediaUploadCap(kind);
|
|
85
|
+
if (bytes.byteLength > cap.maxBytes)
|
|
86
|
+
throw new Error(cap.error);
|
|
87
|
+
const reply = await sendTypedRequest(getTransport(), {
|
|
88
|
+
type: 'OPEN_MEDIA_UPLOAD',
|
|
89
|
+
payload: {
|
|
90
|
+
kind,
|
|
91
|
+
purpose: 'generationSource',
|
|
92
|
+
bytes,
|
|
93
|
+
...(mimeType !== undefined ? { mimeType } : {}),
|
|
94
|
+
},
|
|
95
|
+
}, 'MEDIA_UPLOAD_RESULT',
|
|
96
|
+
// `'human'` class: a 64 MB upload plus the host's retries can outlast
|
|
97
|
+
// the default protocol timeout.
|
|
98
|
+
{ timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS });
|
|
99
|
+
throwOnReplyError(reply, 'media upload failed');
|
|
100
|
+
// A bare reply is the host declining (review mode / not ready). Nothing
|
|
101
|
+
// was uploaded, so this is a failure, not a success.
|
|
102
|
+
if (!reply.selected)
|
|
103
|
+
throw new Error(MEDIA_UPLOAD_DECLINED_ERROR);
|
|
104
|
+
return reply.selected;
|
|
105
|
+
}, []);
|
|
106
|
+
return { open, upload };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=useMediaUpload.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export { useImageUpload } from './hooks/useImageUpload.js';
|
|
|
88
88
|
export type { UseImageUpload, UseImageUploadAsyncScan, UseImageUploadGenerationSource, UseImageUploadOptions, } from './hooks/useImageUpload.js';
|
|
89
89
|
export { useUploadImageBytes } from './hooks/useUploadImageBytes.js';
|
|
90
90
|
export type { UploadImageBytesOptions, UseUploadImageBytes, } from './hooks/useUploadImageBytes.js';
|
|
91
|
+
export { useMediaUpload } from './hooks/useMediaUpload.js';
|
|
92
|
+
export type { UseMediaUpload, MediaUploadOpenOptions, MediaUploadBytesOptions, } from './hooks/useMediaUpload.js';
|
|
91
93
|
export { useImagePicker, IMAGE_PICKER_MULTIPLE_MAX } from './hooks/useImagePicker.js';
|
|
92
94
|
export type { UseImagePicker, ImagePickerOpenOptions, ImagePickerMultiple, } from './hooks/useImagePicker.js';
|
|
93
95
|
export { useGenerationResources } from './hooks/useGenerationResources.js';
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,7 @@ export { useCheckpointPicker } from './hooks/useCheckpointPicker.js';
|
|
|
69
69
|
export { useResourcePicker, RESOURCE_PICKER_MULTIPLE_MAX } from './hooks/useResourcePicker.js';
|
|
70
70
|
export { useImageUpload } from './hooks/useImageUpload.js';
|
|
71
71
|
export { useUploadImageBytes } from './hooks/useUploadImageBytes.js';
|
|
72
|
+
export { useMediaUpload } from './hooks/useMediaUpload.js';
|
|
72
73
|
export { useImagePicker, IMAGE_PICKER_MULTIPLE_MAX } from './hooks/useImagePicker.js';
|
|
73
74
|
export { useGenerationResources } from './hooks/useGenerationResources.js';
|
|
74
75
|
export { GENERATION_RESOURCES_API_BASE, MAX_GENERATION_RESOURCE_IDS, buildGenerationResourcesUrl, responseToResources, } from './api/generationResources.js';
|
|
@@ -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 .... 53, of which 15 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
|
|
@@ -135,6 +135,9 @@
|
|
|
135
135
|
* • OPEN_IMAGE_PICKER — needs the host's picker over the viewer's own posted
|
|
136
136
|
* images and generator outputs, read off their signed-in session; no
|
|
137
137
|
* block-token endpoint lists either. Replies with a refusal.
|
|
138
|
+
* • OPEN_MEDIA_UPLOAD — the generation-input upload runs in the host with the
|
|
139
|
+
* viewer's signed-in session, which the local harness does not have.
|
|
140
|
+
* Replies with a refusal.
|
|
138
141
|
* • SAVE_IMAGE — the download bridge is the production host's unsandboxed top
|
|
139
142
|
* frame plus its CDN origin allowlist / gated per-viewer read. Replies with
|
|
140
143
|
* a refusal (#386).
|
|
@@ -153,6 +156,11 @@ import { type PickerOverlayHandle } from './pickerOverlay.js';
|
|
|
153
156
|
* rejects with it, so it says what to do instead.
|
|
154
157
|
*/
|
|
155
158
|
export declare const IMAGE_PICKER_UNSUPPORTED_ERROR = "the image picker is not supported in dev:live \u2014 use dev:mock";
|
|
159
|
+
/**
|
|
160
|
+
* The `error` the live host answers `OPEN_MEDIA_UPLOAD` with. `useMediaUpload()`
|
|
161
|
+
* rejects with it, so it says what to do instead.
|
|
162
|
+
*/
|
|
163
|
+
export declare const MEDIA_UPLOAD_UNSUPPORTED_ERROR = "media upload is not supported in dev:live \u2014 use dev:mock";
|
|
156
164
|
/**
|
|
157
165
|
* Options for {@link createLiveHost}. Mirrors the {@link MockHost}-relevant
|
|
158
166
|
* subset of `MockHostOptions`, plus the live-only `blockToken` / `backendBaseUrl`
|
|
@@ -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 .... 53, of which 15 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
|
|
@@ -135,6 +135,9 @@
|
|
|
135
135
|
* • OPEN_IMAGE_PICKER — needs the host's picker over the viewer's own posted
|
|
136
136
|
* images and generator outputs, read off their signed-in session; no
|
|
137
137
|
* block-token endpoint lists either. Replies with a refusal.
|
|
138
|
+
* • OPEN_MEDIA_UPLOAD — the generation-input upload runs in the host with the
|
|
139
|
+
* viewer's signed-in session, which the local harness does not have.
|
|
140
|
+
* Replies with a refusal.
|
|
138
141
|
* • SAVE_IMAGE — the download bridge is the production host's unsandboxed top
|
|
139
142
|
* frame plus its CDN origin allowlist / gated per-viewer read. Replies with
|
|
140
143
|
* a refusal (#386).
|
|
@@ -161,6 +164,11 @@ const REACTIONS_UNSUPPORTED_LOG = 'SET_ENTITY_REACTION / GET_ENTITY_REACTIONS ar
|
|
|
161
164
|
* rejects with it, so it says what to do instead.
|
|
162
165
|
*/
|
|
163
166
|
export const IMAGE_PICKER_UNSUPPORTED_ERROR = 'the image picker is not supported in dev:live — use dev:mock';
|
|
167
|
+
/**
|
|
168
|
+
* The `error` the live host answers `OPEN_MEDIA_UPLOAD` with. `useMediaUpload()`
|
|
169
|
+
* rejects with it, so it says what to do instead.
|
|
170
|
+
*/
|
|
171
|
+
export const MEDIA_UPLOAD_UNSUPPORTED_ERROR = 'media upload is not supported in dev:live — use dev:mock';
|
|
164
172
|
/** Default civitai backend the live host forwards to. */
|
|
165
173
|
const DEFAULT_BACKEND_BASE_URL = 'https://civitai.com';
|
|
166
174
|
/** The budgeted-spend scope the workflow procedures require. */
|
|
@@ -1414,6 +1422,24 @@ export function createLiveHost(options) {
|
|
|
1414
1422
|
});
|
|
1415
1423
|
return;
|
|
1416
1424
|
}
|
|
1425
|
+
case 'OPEN_MEDIA_UPLOAD': {
|
|
1426
|
+
// REFUSED. The host uploads the file with the viewer's signed-in
|
|
1427
|
+
// session; this harness has none, and nothing a dev block token can
|
|
1428
|
+
// call takes the upload. Rather than let `useMediaUpload()` hang to
|
|
1429
|
+
// its 10-minute timeout, or hand back an invented URL a step would
|
|
1430
|
+
// then fail on, answer with an error (the hook rejects).
|
|
1431
|
+
logOnce('open-media-upload', 'OPEN_MEDIA_UPLOAD received but live dev mode cannot upload generation inputs ' +
|
|
1432
|
+
'— useMediaUpload() REJECTS. Test it against the real site, ' +
|
|
1433
|
+
'or use createMockHost({ cannedMediaUploads }) in dev:mock.');
|
|
1434
|
+
// A refusal nobody can route is not worth sending.
|
|
1435
|
+
if (!isRoutableRequestId(requestId))
|
|
1436
|
+
return;
|
|
1437
|
+
dispatchToBlock({
|
|
1438
|
+
type: 'MEDIA_UPLOAD_RESULT',
|
|
1439
|
+
payload: { requestId, error: MEDIA_UPLOAD_UNSUPPORTED_ERROR },
|
|
1440
|
+
});
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1417
1443
|
case 'SET_USER_CHECKPOINT': {
|
|
1418
1444
|
// 🔴 A PAGE REFUSES, ALWAYS, WITHOUT A BACKEND CALL — and the default
|
|
1419
1445
|
// context is a page. That is the production page host exactly: the
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `OPEN_MEDIA_UPLOAD`: the strings and limits both sides of the bridge agree
|
|
3
|
+
* on, the content sniffers, and the mock host's copy of the host's admission
|
|
4
|
+
* rules for BYTES mode.
|
|
5
|
+
*
|
|
6
|
+
* The host owns the real rules. This copy exists so `createMockHost` (and so
|
|
7
|
+
* `dev:harness` and every block test) accepts and refuses the same buffers
|
|
8
|
+
* production does, and so `useMediaUpload` can refuse locally what the host
|
|
9
|
+
* would refuse anyway. Keep it exactly as narrow as the contract: a mock that
|
|
10
|
+
* accepts a GIF as an image teaches a block that GIFs upload.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 The host spells its OWN copy of every constant and string here, and the
|
|
13
|
+
* two MUST match. Nothing in this repository can read the host's values, so a
|
|
14
|
+
* drift is invisible here until a block hits a refusal the SDK did not
|
|
15
|
+
* predict. Kept local to this package (not exported from `@civitai/app-sdk`).
|
|
16
|
+
*
|
|
17
|
+
* WHAT THIS CANNOT MODEL: the host decodes the media before it uploads.
|
|
18
|
+
* Its decode-time refusals (`could not read this video file`,
|
|
19
|
+
* `could not read this audio file`, `this file has no video track`, an image
|
|
20
|
+
* too small to resize to at least 64 px, and upload failures) need a real
|
|
21
|
+
* decoder and network, so the mock never produces them.
|
|
22
|
+
*/
|
|
23
|
+
import type { BlockMediaKind, BlockMediaUploadInfo } from '@civitai/app-sdk/blocks';
|
|
24
|
+
/** The cap on one VIDEO or AUDIO upload: 64 MiB of raw bytes (the consumer-blob upload cap). */
|
|
25
|
+
export declare const MEDIA_UPLOAD_MAX_BYTES: number;
|
|
26
|
+
/**
|
|
27
|
+
* The cap on one IMAGE upload: 24 MiB. Lower than {@link MEDIA_UPLOAD_MAX_BYTES}
|
|
28
|
+
* because the host decodes an image in full before it resizes it.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MEDIA_UPLOAD_IMAGE_MAX_BYTES: number;
|
|
31
|
+
/** The bytes-mode rolling window: per this many milliseconds, per mounted page host… */
|
|
32
|
+
export declare const MEDIA_UPLOAD_WINDOW_MS = 60000;
|
|
33
|
+
/** …at most this many admitted bytes uploads… */
|
|
34
|
+
export declare const MEDIA_UPLOAD_MAX_PER_WINDOW = 8;
|
|
35
|
+
/** …and this many admitted bytes. */
|
|
36
|
+
export declare const MEDIA_UPLOAD_MAX_BYTES_PER_WINDOW: number;
|
|
37
|
+
/** `bytes` is not a non-empty `ArrayBuffer`, or `mimeType` is present but not a string. */
|
|
38
|
+
export declare const MEDIA_UPLOAD_INVALID_ERROR = "invalid media-upload request";
|
|
39
|
+
export declare const MEDIA_UPLOAD_KIND_ERROR = "kind must be 'image', 'video' or 'audio'";
|
|
40
|
+
export declare const MEDIA_UPLOAD_PURPOSE_ERROR = "purpose must be 'generationSource'";
|
|
41
|
+
/** Over {@link MEDIA_UPLOAD_MAX_BYTES} (video, audio). */
|
|
42
|
+
export declare const MEDIA_UPLOAD_TOO_LARGE_ERROR = "file exceeds the 64 MB upload limit";
|
|
43
|
+
/** Over {@link MEDIA_UPLOAD_IMAGE_MAX_BYTES} (image). */
|
|
44
|
+
export declare const MEDIA_UPLOAD_IMAGE_TOO_LARGE_ERROR = "file exceeds the 24 MB image upload limit";
|
|
45
|
+
/** Bytes mode only: the host holds no block token. Checked before the cap and the window. */
|
|
46
|
+
export declare const MEDIA_UPLOAD_NO_TOKEN_ERROR = "no block token";
|
|
47
|
+
/** The sniffed content is not one of the requested kind's types. */
|
|
48
|
+
export declare const MEDIA_UPLOAD_TYPE_ERROR: Readonly<Record<BlockMediaKind, string>>;
|
|
49
|
+
export declare const MEDIA_UPLOAD_MIME_MISMATCH_ERROR = "mimeType does not match the file's content";
|
|
50
|
+
/** The bytes-mode window is full. Retryable. */
|
|
51
|
+
export declare const MEDIA_UPLOAD_BUSY_ERROR = "busy";
|
|
52
|
+
/** The kinds the bridge knows. */
|
|
53
|
+
export declare const MEDIA_UPLOAD_KINDS: readonly BlockMediaKind[];
|
|
54
|
+
export declare function isMediaKind(v: unknown): v is BlockMediaKind;
|
|
55
|
+
/** The per-file cap for a kind, and the refusal past it. */
|
|
56
|
+
export declare function mediaUploadCap(kind: BlockMediaKind): {
|
|
57
|
+
maxBytes: number;
|
|
58
|
+
error: string;
|
|
59
|
+
};
|
|
60
|
+
/** One admitted bytes upload in the window: when, and how many bytes. */
|
|
61
|
+
export type MediaUploadWindowEntry = {
|
|
62
|
+
at: number;
|
|
63
|
+
size: number;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The content type of `bytes` WITHIN the requested kind, by magic bytes, or
|
|
67
|
+
* `null` when the content is none of that kind's types (a PNG sent as `audio`
|
|
68
|
+
* is `null`: a type refusal). WebM is `video/webm` under `video` and
|
|
69
|
+
* `audio/webm` under `audio`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sniffMediaBytes(kind: BlockMediaKind, u8: Uint8Array): string | null;
|
|
72
|
+
/** A declared MIME type as the host compares it: lowercased, `;params` cut, `audio/x-wav` read as `audio/wav`. */
|
|
73
|
+
export declare function normaliseMediaMimeType(mimeType: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* The host's decision for one `OPEN_MEDIA_UPLOAD`, in the host's order, from
|
|
76
|
+
* step 4 of the contract on (the caller has already dropped an unroutable
|
|
77
|
+
* `requestId`; review mode and readiness do not exist in the mock):
|
|
78
|
+
* 1. `kind` not known → kind error; `purpose` not `'generationSource'` →
|
|
79
|
+
* purpose error;
|
|
80
|
+
* 2. no `bytes` (absent or `null`) → PICKER mode (not windowed: a human
|
|
81
|
+
* picks);
|
|
82
|
+
* 3. `bytes` not a non-empty `ArrayBuffer`, or a non-string `mimeType` →
|
|
83
|
+
* invalid;
|
|
84
|
+
* 4. no block token (`opts.noToken`) → `no block token`, before the cap
|
|
85
|
+
* and the window;
|
|
86
|
+
* 5. over the kind's cap ({@link mediaUploadCap}: 24 MiB for an image,
|
|
87
|
+
* 64 MiB for video and audio) → too large, before the window, so an
|
|
88
|
+
* over-cap file is never `busy` (a retry could never succeed);
|
|
89
|
+
* 6. the window, NOT recording, so a full window refuses `busy` before any
|
|
90
|
+
* sniffing;
|
|
91
|
+
* 7. the sniffed content in the kind's set → else the kind's type error;
|
|
92
|
+
* 8. a present `mimeType` (normalised) equal to the sniffed type → else
|
|
93
|
+
* mismatch;
|
|
94
|
+
* 9. record the upload in the window. Only an upload that will run counts.
|
|
95
|
+
* Returns the pruned window to store back.
|
|
96
|
+
*/
|
|
97
|
+
export declare function admitMediaUpload(raw: {
|
|
98
|
+
kind?: unknown;
|
|
99
|
+
purpose?: unknown;
|
|
100
|
+
bytes?: unknown;
|
|
101
|
+
mimeType?: unknown;
|
|
102
|
+
}, recent: readonly MediaUploadWindowEntry[], now: number, opts?: {
|
|
103
|
+
noToken?: boolean;
|
|
104
|
+
}): {
|
|
105
|
+
result: {
|
|
106
|
+
ok: true;
|
|
107
|
+
mode: 'picker';
|
|
108
|
+
kind: BlockMediaKind;
|
|
109
|
+
} | {
|
|
110
|
+
ok: true;
|
|
111
|
+
mode: 'bytes';
|
|
112
|
+
kind: BlockMediaKind;
|
|
113
|
+
bytes: ArrayBuffer;
|
|
114
|
+
mimeType: string;
|
|
115
|
+
} | {
|
|
116
|
+
ok: false;
|
|
117
|
+
error: string;
|
|
118
|
+
};
|
|
119
|
+
recent: MediaUploadWindowEntry[];
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* The mock host's canned uploads, one per kind. A kind left out answers
|
|
123
|
+
* {@link DEFAULT_MOCK_MEDIA_UPLOADS}; `null` for a kind = the viewer closes that
|
|
124
|
+
* picker (bytes mode, which has no picker, then answers the default).
|
|
125
|
+
*/
|
|
126
|
+
export type MockMediaUploads = Partial<Record<BlockMediaKind, BlockMediaUploadInfo | null>>;
|
|
127
|
+
/**
|
|
128
|
+
* What the mock host answers when no canned upload is configured for a kind.
|
|
129
|
+
* Every field differs between kinds, so a block that mixes two up shows it.
|
|
130
|
+
* The URLs are deliberately unresolvable: the mock uploads nothing.
|
|
131
|
+
*/
|
|
132
|
+
export declare const DEFAULT_MOCK_MEDIA_UPLOADS: Readonly<Record<BlockMediaKind, BlockMediaUploadInfo>>;
|
|
133
|
+
/**
|
|
134
|
+
* The mock host's whole answer to one routable `OPEN_MEDIA_UPLOAD`, on a page
|
|
135
|
+
* surface: {@link admitMediaUpload}, then
|
|
136
|
+
* - picker mode: the canned upload for the kind, or a bare reply (dismissed)
|
|
137
|
+
* when that kind is canned `null`;
|
|
138
|
+
* - bytes mode: the canned upload for the kind (the default when it is
|
|
139
|
+
* `null`: there is no viewer to dismiss), carrying what the host would
|
|
140
|
+
* report for THESE bytes: the sniffed `mimeType` and the buffer's size for
|
|
141
|
+
* video and audio; `image/jpeg` and the canned size for an image, which the
|
|
142
|
+
* host re-encodes;
|
|
143
|
+
* - `forcedError`, when set, replaces a success: the stand-in for the
|
|
144
|
+
* decode and upload failures the mock cannot produce. A bytes upload it
|
|
145
|
+
* fails has still been admitted, so it still counts against the window, as
|
|
146
|
+
* on the host. ONE value is placed where the host checks it instead:
|
|
147
|
+
* `no block token` refuses a bytes upload right after the buffer check,
|
|
148
|
+
* before the cap and the window, and is not counted.
|
|
149
|
+
*/
|
|
150
|
+
export declare function mockMediaUploadReply(args: {
|
|
151
|
+
payload: {
|
|
152
|
+
kind?: unknown;
|
|
153
|
+
purpose?: unknown;
|
|
154
|
+
bytes?: unknown;
|
|
155
|
+
mimeType?: unknown;
|
|
156
|
+
};
|
|
157
|
+
canned: MockMediaUploads;
|
|
158
|
+
forcedError: string | undefined;
|
|
159
|
+
recent: readonly MediaUploadWindowEntry[];
|
|
160
|
+
now: number;
|
|
161
|
+
}): {
|
|
162
|
+
reply: {
|
|
163
|
+
selected: BlockMediaUploadInfo;
|
|
164
|
+
} | {
|
|
165
|
+
error: string;
|
|
166
|
+
} | Record<string, never>;
|
|
167
|
+
recent: MediaUploadWindowEntry[];
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=mediaUploadBytes.d.ts.map
|