cursedbelt-core 1.0.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/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// Pure, dependency-free promptable image segmentation — the engine behind the
|
|
2
|
+
// <MaskPainter>'s "auto-select" (Stage I, task 52). Given a source image's RGBA pixels
|
|
3
|
+
// and a point/box prompt, it produces a BINARY alpha plane (0/255) marking the selected
|
|
4
|
+
// object, which the painter lands as an EDITABLE mask region (never a final answer — the
|
|
5
|
+
// owner refines or discards it). It is the object-select counterpart to {@link floodFillBGS}
|
|
6
|
+
// (which clears an edge-connected BACKGROUND): both grow regions by color similarity, but
|
|
7
|
+
// this one selects a FOREGROUND object from a user prompt and returns a mask, not an alpha.
|
|
8
|
+
//
|
|
9
|
+
// No DOM, no Bun, no npm dep — just typed pixel arrays — so it runs identically in the
|
|
10
|
+
// browser, on a server (sharp's raw RGBA), and in unit tests, and is fully testable with no
|
|
11
|
+
// canvas. The {@link ImageSegmenter} interface is the seam: the default region grower needs
|
|
12
|
+
// no model download (instant, offline), and a heavier engine (SAM2-tiny / MobileSAM via
|
|
13
|
+
// onnxruntime-web) can implement the SAME interface and drop in unchanged.
|
|
14
|
+
|
|
15
|
+
/** A source image as raw RGBA pixels at its working dimensions. */
|
|
16
|
+
export interface SegmentSource {
|
|
17
|
+
/** RGBA pixels, length `width * height * 4`. */
|
|
18
|
+
readonly data: Uint8ClampedArray;
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What to select. `point` = magic-wand from a clicked pixel; `box` = the dominant object
|
|
25
|
+
* inside a dragged rectangle. Coordinates are in the SOURCE pixel space passed in `source`
|
|
26
|
+
* (the painter downscales both the image and the prompt together for large images).
|
|
27
|
+
*/
|
|
28
|
+
export type SegmentPrompt =
|
|
29
|
+
| { readonly type: 'point'; readonly x: number; readonly y: number }
|
|
30
|
+
| {
|
|
31
|
+
readonly type: 'box';
|
|
32
|
+
readonly x0: number;
|
|
33
|
+
readonly y0: number;
|
|
34
|
+
readonly x1: number;
|
|
35
|
+
readonly y1: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface SegmentOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Color-distance tolerance (RGB euclidean, 0–441) for region growth: a neighbor joins
|
|
41
|
+
* the region when it is within this distance of the seed color. Higher = looser (selects
|
|
42
|
+
* more, leaks across gradients sooner). Default {@link DEFAULT_TOLERANCE}. The painter maps
|
|
43
|
+
* a 0–100 "sensitivity" slider onto this via {@link sensitivityToTolerance}.
|
|
44
|
+
*/
|
|
45
|
+
readonly tolerance?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Morphological close radius (px) applied after growth to fill speckle + small holes and
|
|
48
|
+
* smooth the edge. 0 disables it. Default {@link DEFAULT_CLOSE_RADIUS}.
|
|
49
|
+
*/
|
|
50
|
+
readonly closeRadius?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A promptable image segmenter: `(source, prompt) → binary alpha plane (0/255)` at the
|
|
55
|
+
* source's dimensions, white where selected. `segment` may be sync (the pure region grower)
|
|
56
|
+
* or async (a model-backed engine); the painter always awaits it. `dispose` releases any
|
|
57
|
+
* held resources (e.g. an ONNX session) and is optional.
|
|
58
|
+
*/
|
|
59
|
+
export interface ImageSegmenter {
|
|
60
|
+
/** Stable identifier for diagnostics / telemetry (e.g. `'region-grow'`, `'mobile-sam'`). */
|
|
61
|
+
readonly id: string;
|
|
62
|
+
segment(
|
|
63
|
+
source: SegmentSource,
|
|
64
|
+
prompt: SegmentPrompt,
|
|
65
|
+
options?: SegmentOptions,
|
|
66
|
+
): Uint8ClampedArray | Promise<Uint8ClampedArray>;
|
|
67
|
+
dispose?(): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const DEFAULT_TOLERANCE = 28;
|
|
71
|
+
export const DEFAULT_CLOSE_RADIUS = 2;
|
|
72
|
+
|
|
73
|
+
/** Map a 0–100 "sensitivity" slider to a growth tolerance. 0 → tight (exact color), 100 →
|
|
74
|
+
* loose. A gentle quadratic keeps the low end fine-grained where most clicks land. */
|
|
75
|
+
export function sensitivityToTolerance(sensitivity: number): number {
|
|
76
|
+
const s = Math.max(0, Math.min(100, sensitivity)) / 100;
|
|
77
|
+
return Math.round(6 + s * s * 150); // ~6 (exact) … 156 (very loose)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Region growth (the pure CV core) ───────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/** Flood a single connected region from `seed`, including 4-connected neighbors within
|
|
83
|
+
* `tolSq` of the SEED color and inside the inclusive `bounds`. Returns a 0/255 mask. */
|
|
84
|
+
function floodFrom(
|
|
85
|
+
data: Uint8ClampedArray,
|
|
86
|
+
width: number,
|
|
87
|
+
seed: readonly [number, number],
|
|
88
|
+
tolSq: number,
|
|
89
|
+
bounds: readonly [number, number, number, number],
|
|
90
|
+
out: Uint8ClampedArray,
|
|
91
|
+
visited: Uint8Array,
|
|
92
|
+
queue: Int32Array,
|
|
93
|
+
): void {
|
|
94
|
+
const [bx0, by0, bx1, by1] = bounds;
|
|
95
|
+
const [sx, sy] = seed;
|
|
96
|
+
if (sx < bx0 || sy < by0 || sx > bx1 || sy > by1) return;
|
|
97
|
+
const seedIdx = sy * width + sx;
|
|
98
|
+
if (visited[seedIdx]) return;
|
|
99
|
+
const sr = data[seedIdx * 4];
|
|
100
|
+
const sg = data[seedIdx * 4 + 1];
|
|
101
|
+
const sb = data[seedIdx * 4 + 2];
|
|
102
|
+
|
|
103
|
+
let head = 0;
|
|
104
|
+
let tail = 0;
|
|
105
|
+
visited[seedIdx] = 1;
|
|
106
|
+
queue[tail++] = seedIdx;
|
|
107
|
+
while (head < tail) {
|
|
108
|
+
const idx = queue[head++];
|
|
109
|
+
out[idx] = 255;
|
|
110
|
+
const x = idx % width;
|
|
111
|
+
const y = (idx - x) / width;
|
|
112
|
+
for (let k = 0; k < 4; k++) {
|
|
113
|
+
let n: number;
|
|
114
|
+
if (k === 0) {
|
|
115
|
+
if (x <= bx0) continue;
|
|
116
|
+
n = idx - 1;
|
|
117
|
+
} else if (k === 1) {
|
|
118
|
+
if (x >= bx1) continue;
|
|
119
|
+
n = idx + 1;
|
|
120
|
+
} else if (k === 2) {
|
|
121
|
+
if (y <= by0) continue;
|
|
122
|
+
n = idx - width;
|
|
123
|
+
} else {
|
|
124
|
+
if (y >= by1) continue;
|
|
125
|
+
n = idx + width;
|
|
126
|
+
}
|
|
127
|
+
if (visited[n]) continue;
|
|
128
|
+
const dr = data[n * 4] - sr;
|
|
129
|
+
const dg = data[n * 4 + 1] - sg;
|
|
130
|
+
const db = data[n * 4 + 2] - sb;
|
|
131
|
+
if (dr * dr + dg * dg + db * db <= tolSq) {
|
|
132
|
+
visited[n] = 1;
|
|
133
|
+
queue[tail++] = n;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Grow + union a region from one or more seeds (each its own color-relative flood), then
|
|
140
|
+
* morphologically close it. The shared building block for both prompt types. */
|
|
141
|
+
function growRegion(
|
|
142
|
+
source: SegmentSource,
|
|
143
|
+
seeds: ReadonlyArray<readonly [number, number]>,
|
|
144
|
+
bounds: readonly [number, number, number, number],
|
|
145
|
+
tolerance: number,
|
|
146
|
+
closeRadius: number,
|
|
147
|
+
): Uint8ClampedArray {
|
|
148
|
+
const { data, width, height } = source;
|
|
149
|
+
const px = width * height;
|
|
150
|
+
const mask = new Uint8ClampedArray(px);
|
|
151
|
+
const queue = new Int32Array(px);
|
|
152
|
+
const tolSq = tolerance * tolerance;
|
|
153
|
+
// Independent floods (fresh `visited` each) unioned into `mask`, so a later seed isn't
|
|
154
|
+
// starved by an earlier seed's rejected pixels — important for multi-tone box selects.
|
|
155
|
+
for (const seed of seeds) {
|
|
156
|
+
floodFrom(data, width, seed, tolSq, bounds, mask, new Uint8Array(px), queue);
|
|
157
|
+
}
|
|
158
|
+
return closeRadius > 0 ? closeMask(mask, width, height, closeRadius) : mask;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Morphology ──────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/** 4-connected binary dilate (grow by 1px). */
|
|
164
|
+
export function dilateMask(
|
|
165
|
+
mask: Uint8ClampedArray,
|
|
166
|
+
width: number,
|
|
167
|
+
height: number,
|
|
168
|
+
): Uint8ClampedArray {
|
|
169
|
+
const out = new Uint8ClampedArray(mask.length);
|
|
170
|
+
for (let y = 0; y < height; y++) {
|
|
171
|
+
for (let x = 0; x < width; x++) {
|
|
172
|
+
const i = y * width + x;
|
|
173
|
+
if (
|
|
174
|
+
mask[i] ||
|
|
175
|
+
(x > 0 && mask[i - 1]) ||
|
|
176
|
+
(x < width - 1 && mask[i + 1]) ||
|
|
177
|
+
(y > 0 && mask[i - width]) ||
|
|
178
|
+
(y < height - 1 && mask[i + width])
|
|
179
|
+
) {
|
|
180
|
+
out[i] = 255;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 4-connected binary erode (shrink by 1px). */
|
|
188
|
+
export function erodeMask(
|
|
189
|
+
mask: Uint8ClampedArray,
|
|
190
|
+
width: number,
|
|
191
|
+
height: number,
|
|
192
|
+
): Uint8ClampedArray {
|
|
193
|
+
const out = new Uint8ClampedArray(mask.length);
|
|
194
|
+
for (let y = 0; y < height; y++) {
|
|
195
|
+
for (let x = 0; x < width; x++) {
|
|
196
|
+
const i = y * width + x;
|
|
197
|
+
if (
|
|
198
|
+
mask[i] &&
|
|
199
|
+
(x === 0 || mask[i - 1]) &&
|
|
200
|
+
(x === width - 1 || mask[i + 1]) &&
|
|
201
|
+
(y === 0 || mask[i - width]) &&
|
|
202
|
+
(y === height - 1 || mask[i + width])
|
|
203
|
+
) {
|
|
204
|
+
out[i] = 255;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Morphological close (dilate×r then erode×r): fills holes/gaps ≤ ~2·radius and smooths
|
|
212
|
+
* the edge without changing the overall area. */
|
|
213
|
+
export function closeMask(
|
|
214
|
+
mask: Uint8ClampedArray,
|
|
215
|
+
width: number,
|
|
216
|
+
height: number,
|
|
217
|
+
radius: number,
|
|
218
|
+
): Uint8ClampedArray {
|
|
219
|
+
let m = mask;
|
|
220
|
+
for (let i = 0; i < radius; i++) m = dilateMask(m, width, height);
|
|
221
|
+
for (let i = 0; i < radius; i++) m = erodeMask(m, width, height);
|
|
222
|
+
return m;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Resolution helpers (the painter caps work to keep auto-select ~instant) ──────
|
|
226
|
+
|
|
227
|
+
/** Working size that fits within `maxPixels` while preserving aspect (never upsizes). */
|
|
228
|
+
export function fitWorkingSize(
|
|
229
|
+
width: number,
|
|
230
|
+
height: number,
|
|
231
|
+
maxPixels: number,
|
|
232
|
+
): { width: number; height: number; scale: number } {
|
|
233
|
+
if (width <= 0 || height <= 0) return { width: 0, height: 0, scale: 1 };
|
|
234
|
+
const total = width * height;
|
|
235
|
+
if (total <= maxPixels) return { width, height, scale: 1 };
|
|
236
|
+
const scale = Math.sqrt(maxPixels / total);
|
|
237
|
+
// Floor (not round) the dims so the working area is GUARANTEED ≤ maxPixels
|
|
238
|
+
// (floor(w·s)·floor(h·s) ≤ w·s·h·s = maxPixels), bounding auto-select latency.
|
|
239
|
+
return {
|
|
240
|
+
width: Math.max(1, Math.floor(width * scale)),
|
|
241
|
+
height: Math.max(1, Math.floor(height * scale)),
|
|
242
|
+
scale,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Area-average downscale of an RGBA buffer (stable colors → cleaner growth than nearest). */
|
|
247
|
+
export function downscaleRgba(
|
|
248
|
+
data: Uint8ClampedArray,
|
|
249
|
+
sw: number,
|
|
250
|
+
sh: number,
|
|
251
|
+
dw: number,
|
|
252
|
+
dh: number,
|
|
253
|
+
): Uint8ClampedArray {
|
|
254
|
+
if (dw === sw && dh === sh) return data;
|
|
255
|
+
const out = new Uint8ClampedArray(dw * dh * 4);
|
|
256
|
+
for (let y = 0; y < dh; y++) {
|
|
257
|
+
const sy0 = Math.floor((y * sh) / dh);
|
|
258
|
+
const sy1 = Math.max(sy0 + 1, Math.floor(((y + 1) * sh) / dh));
|
|
259
|
+
for (let x = 0; x < dw; x++) {
|
|
260
|
+
const sx0 = Math.floor((x * sw) / dw);
|
|
261
|
+
const sx1 = Math.max(sx0 + 1, Math.floor(((x + 1) * sw) / dw));
|
|
262
|
+
let r = 0;
|
|
263
|
+
let g = 0;
|
|
264
|
+
let b = 0;
|
|
265
|
+
let a = 0;
|
|
266
|
+
let count = 0;
|
|
267
|
+
for (let yy = sy0; yy < sy1 && yy < sh; yy++) {
|
|
268
|
+
for (let xx = sx0; xx < sx1 && xx < sw; xx++) {
|
|
269
|
+
const s = (yy * sw + xx) << 2;
|
|
270
|
+
r += data[s];
|
|
271
|
+
g += data[s + 1];
|
|
272
|
+
b += data[s + 2];
|
|
273
|
+
a += data[s + 3];
|
|
274
|
+
count++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const o = (y * dw + x) << 2;
|
|
278
|
+
out[o] = count ? r / count : 0;
|
|
279
|
+
out[o + 1] = count ? g / count : 0;
|
|
280
|
+
out[o + 2] = count ? b / count : 0;
|
|
281
|
+
out[o + 3] = count ? a / count : 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Nearest-neighbor upscale of a 0/255 mask back to the source's natural dimensions. */
|
|
288
|
+
export function upscaleMask(
|
|
289
|
+
mask: Uint8ClampedArray,
|
|
290
|
+
sw: number,
|
|
291
|
+
sh: number,
|
|
292
|
+
dw: number,
|
|
293
|
+
dh: number,
|
|
294
|
+
): Uint8ClampedArray {
|
|
295
|
+
if (sw === dw && sh === dh) return mask;
|
|
296
|
+
const out = new Uint8ClampedArray(dw * dh);
|
|
297
|
+
for (let y = 0; y < dh; y++) {
|
|
298
|
+
const sy = Math.min(sh - 1, Math.floor((y * sh) / dh));
|
|
299
|
+
for (let x = 0; x < dw; x++) {
|
|
300
|
+
const sx = Math.min(sw - 1, Math.floor((x * sw) / dw));
|
|
301
|
+
out[y * dw + x] = mask[sy * sw + sx];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── The default engine ────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
const clampInt = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, Math.round(v)));
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The default, pure region-growing segmenter — no model, no download, instant + offline.
|
|
313
|
+
* `point` grows a magic-wand selection from the clicked pixel; `box` selects the dominant
|
|
314
|
+
* object inside the rectangle by growing from a spread of interior seeds, clipped to the
|
|
315
|
+
* box so it can't leak into the surrounding scene. Always returns a binary alpha plane at
|
|
316
|
+
* the source dimensions; the painter lands it as an editable region for accept/refine.
|
|
317
|
+
*/
|
|
318
|
+
export function createRegionSegmenter(defaults: SegmentOptions = {}): ImageSegmenter {
|
|
319
|
+
return {
|
|
320
|
+
id: 'region-grow',
|
|
321
|
+
segment(source, prompt, options) {
|
|
322
|
+
const { width, height } = source;
|
|
323
|
+
if (width <= 0 || height <= 0) return new Uint8ClampedArray(0);
|
|
324
|
+
const tolerance = options?.tolerance ?? defaults.tolerance ?? DEFAULT_TOLERANCE;
|
|
325
|
+
const closeRadius = options?.closeRadius ?? defaults.closeRadius ?? DEFAULT_CLOSE_RADIUS;
|
|
326
|
+
|
|
327
|
+
if (prompt.type === 'point') {
|
|
328
|
+
const seed: [number, number] = [
|
|
329
|
+
clampInt(prompt.x, 0, width - 1),
|
|
330
|
+
clampInt(prompt.y, 0, height - 1),
|
|
331
|
+
];
|
|
332
|
+
return growRegion(source, [seed], [0, 0, width - 1, height - 1], tolerance, closeRadius);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// box — normalize + clamp, then seed a 3×3 grid of interior points and grow within bounds.
|
|
336
|
+
const bx0 = clampInt(Math.min(prompt.x0, prompt.x1), 0, width - 1);
|
|
337
|
+
const by0 = clampInt(Math.min(prompt.y0, prompt.y1), 0, height - 1);
|
|
338
|
+
const bx1 = clampInt(Math.max(prompt.x0, prompt.x1), 0, width - 1);
|
|
339
|
+
const by1 = clampInt(Math.max(prompt.y0, prompt.y1), 0, height - 1);
|
|
340
|
+
const seeds: Array<[number, number]> = [];
|
|
341
|
+
for (let r = 1; r <= 3; r++) {
|
|
342
|
+
for (let c = 1; c <= 3; c++) {
|
|
343
|
+
seeds.push([
|
|
344
|
+
clampInt(bx0 + ((bx1 - bx0) * c) / 4, bx0, bx1),
|
|
345
|
+
clampInt(by0 + ((by1 - by0) * r) / 4, by0, by1),
|
|
346
|
+
]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return growRegion(source, seeds, [bx0, by0, bx1, by1], tolerance, closeRadius);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { formatBytes } from './size';
|
|
3
|
+
|
|
4
|
+
describe('formatBytes', () => {
|
|
5
|
+
test('formats across unit boundaries, dropping the decimal at ≥ 100 / in bytes', () => {
|
|
6
|
+
expect(formatBytes(0)).toBe('0 B');
|
|
7
|
+
expect(formatBytes(512)).toBe('512 B');
|
|
8
|
+
expect(formatBytes(1024)).toBe('1.0 KB');
|
|
9
|
+
expect(formatBytes(1536)).toBe('1.5 KB');
|
|
10
|
+
expect(formatBytes(200 * 1024)).toBe('200 KB');
|
|
11
|
+
expect(formatBytes(1.4 * 1024 * 1024)).toBe('1.4 MB');
|
|
12
|
+
expect(formatBytes(2.1 * 1024 * 1024 * 1024)).toBe('2.1 GB');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('non-finite / falsy inputs are 0 B, not NaN', () => {
|
|
16
|
+
expect(formatBytes(Number.NaN)).toBe('0 B');
|
|
17
|
+
expect(formatBytes(Number.POSITIVE_INFINITY)).toBe('0 B');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { clampTrimRange, formatTimecode, trimDurationSeconds } from './trim';
|
|
3
|
+
|
|
4
|
+
describe('clampTrimRange', () => {
|
|
5
|
+
test('passes a valid range through', () => {
|
|
6
|
+
expect(clampTrimRange(2, 8, { duration: 10 })).toEqual({ startSeconds: 2, endSeconds: 8 });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('clamps the out-point to the duration', () => {
|
|
10
|
+
expect(clampTrimRange(2, 99, { duration: 10 })).toEqual({ startSeconds: 2, endSeconds: 10 });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('un-inverts a backwards drag', () => {
|
|
14
|
+
expect(clampTrimRange(8, 2, { duration: 10 })).toEqual({ startSeconds: 2, endSeconds: 8 });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('enforces a minimum selection length', () => {
|
|
18
|
+
const r = clampTrimRange(5, 5, { duration: 10, minDurationSeconds: 1 });
|
|
19
|
+
expect(r.endSeconds - r.startSeconds).toBeCloseTo(1, 5);
|
|
20
|
+
expect(r.startSeconds).toBe(5);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('caps the selection at maxDurationSeconds, keeping the in-point', () => {
|
|
24
|
+
const r = clampTrimRange(2, 30, { duration: 60, maxDurationSeconds: 10 });
|
|
25
|
+
expect(r).toEqual({ startSeconds: 2, endSeconds: 12 });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('when start near the end with a max cap, pulls start back to fit', () => {
|
|
29
|
+
const r = clampTrimRange(58, 60, {
|
|
30
|
+
duration: 60,
|
|
31
|
+
maxDurationSeconds: 10,
|
|
32
|
+
minDurationSeconds: 1,
|
|
33
|
+
});
|
|
34
|
+
expect(r.endSeconds).toBeLessThanOrEqual(60);
|
|
35
|
+
expect(r.endSeconds - r.startSeconds).toBeGreaterThanOrEqual(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('handles missing duration (unbounded)', () => {
|
|
39
|
+
expect(clampTrimRange(1, 4)).toEqual({ startSeconds: 1, endSeconds: 4 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('coerces non-finite bounds', () => {
|
|
43
|
+
const r = clampTrimRange(Number.NaN, Number.NaN, { duration: 10 });
|
|
44
|
+
expect(r.startSeconds).toBeGreaterThanOrEqual(0);
|
|
45
|
+
expect(r.endSeconds).toBeGreaterThan(r.startSeconds);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('trimDurationSeconds', () => {
|
|
50
|
+
test('is the non-negative difference', () => {
|
|
51
|
+
expect(trimDurationSeconds(2, 8)).toBe(6);
|
|
52
|
+
expect(trimDurationSeconds(8, 2)).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('formatTimecode', () => {
|
|
57
|
+
test('m:ss under an hour', () => {
|
|
58
|
+
expect(formatTimecode(0)).toBe('0:00');
|
|
59
|
+
expect(formatTimecode(5)).toBe('0:05');
|
|
60
|
+
expect(formatTimecode(75)).toBe('1:15');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('h:mm:ss past an hour', () => {
|
|
64
|
+
expect(formatTimecode(3661)).toBe('1:01:01');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('tenths suffix', () => {
|
|
68
|
+
expect(formatTimecode(5.4, { tenths: true })).toBe('0:05.4');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('negative/NaN render as 0:00', () => {
|
|
72
|
+
expect(formatTimecode(-3)).toBe('0:00');
|
|
73
|
+
expect(formatTimecode(Number.NaN)).toBe('0:00');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pure trim-range math + timecode formatting for the `<VideoTrimmer>` slider. Kept
|
|
2
|
+
// in media-core (no DOM) so the in/out clamping — the part that must be CORRECT — is
|
|
3
|
+
// unit-tested without rendering a video element.
|
|
4
|
+
|
|
5
|
+
export interface ClampTrimOptions {
|
|
6
|
+
/** Total media duration; the out-point can never exceed it. */
|
|
7
|
+
duration?: number;
|
|
8
|
+
/** Optional cap on the selected length (end - start). */
|
|
9
|
+
maxDurationSeconds?: number;
|
|
10
|
+
/** Smallest allowed selection; defaults to 0.1s so start !== end. */
|
|
11
|
+
minDurationSeconds?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TrimRange {
|
|
15
|
+
startSeconds: number;
|
|
16
|
+
endSeconds: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Clamp a proposed [start, end] into a valid, ordered range:
|
|
21
|
+
* `0 <= start < end <= duration`, honoring an optional min/max selection length.
|
|
22
|
+
* Always returns `end > start` (by at least `minDurationSeconds`) so a degenerate or
|
|
23
|
+
* inverted drag can never produce a zero/negative trim.
|
|
24
|
+
*/
|
|
25
|
+
export function clampTrimRange(start: number, end: number, opts: ClampTrimOptions = {}): TrimRange {
|
|
26
|
+
const duration =
|
|
27
|
+
opts.duration != null && Number.isFinite(opts.duration) ? opts.duration : Infinity;
|
|
28
|
+
const minLen = Math.max(0.0001, opts.minDurationSeconds ?? 0.1);
|
|
29
|
+
const maxLen =
|
|
30
|
+
opts.maxDurationSeconds != null ? Math.max(minLen, opts.maxDurationSeconds) : Infinity;
|
|
31
|
+
|
|
32
|
+
let s = Number.isFinite(start) ? start : 0;
|
|
33
|
+
let e = Number.isFinite(end) ? end : duration;
|
|
34
|
+
|
|
35
|
+
if (e < s) [s, e] = [e, s]; // un-invert a backwards drag
|
|
36
|
+
|
|
37
|
+
s = Math.max(0, Math.min(s, Number.isFinite(duration) ? duration : s));
|
|
38
|
+
e = Math.min(e, duration);
|
|
39
|
+
|
|
40
|
+
// Enforce min length, nudging whichever bound has room.
|
|
41
|
+
if (e - s < minLen) {
|
|
42
|
+
if (s + minLen <= duration) e = s + minLen;
|
|
43
|
+
else s = Math.max(0, e - minLen);
|
|
44
|
+
}
|
|
45
|
+
// Enforce max length by pulling the end in (keep the in-point the user set).
|
|
46
|
+
if (e - s > maxLen) e = s + maxLen;
|
|
47
|
+
if (e > duration) {
|
|
48
|
+
e = duration;
|
|
49
|
+
s = Math.max(0, e - maxLen);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { startSeconds: s, endSeconds: e };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function trimDurationSeconds(start: number, end: number): number {
|
|
56
|
+
return Math.max(0, end - start);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Implemented in cwip/core/format; aliased here for backward compat.
|
|
60
|
+
export { formatClock as formatTimecode } from 'cwip/format';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// cursedbelt media-core — the pure, dependency-free contract shared by the Bun
|
|
2
|
+
// server adapter (`src/server/media-bun`), the React components
|
|
3
|
+
// (`cursedbelt/react/media`), and any app/test. ZERO npm deps: types + pure
|
|
4
|
+
// functions only, so the exact same module loads identically in Bun, the browser,
|
|
5
|
+
// and a unit test.
|
|
6
|
+
//
|
|
7
|
+
// Storage, auth, and signing are deliberately ABSENT here — a `MediaItem` carries
|
|
8
|
+
// only a plain URL (the app resolves/signs it). Binary downloads stay plain GET
|
|
9
|
+
// (00-README invariant #3): never route a `MediaItem.url` through the ctgr codec,
|
|
10
|
+
// which base64-mangles binary bodies.
|
|
11
|
+
|
|
12
|
+
/** Output container/codecs for {@link renderImage}. */
|
|
13
|
+
export type ImageOutputFormat = 'jpeg' | 'png' | 'webp' | 'avif';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A single, declarative image transform. The list is applied in order by the
|
|
17
|
+
* server adapter's `renderImage` (sharp) — the SAME spec can be built client-side
|
|
18
|
+
* (from the editor's design state) or server-side and produces an identical result.
|
|
19
|
+
*/
|
|
20
|
+
export type ImageOperation =
|
|
21
|
+
| { type: 'resize'; width?: number; height?: number; fit?: 'cover' | 'contain' | 'fill' }
|
|
22
|
+
| { type: 'crop'; left: number; top: number; width: number; height: number }
|
|
23
|
+
| { type: 'rotate'; angle: number }
|
|
24
|
+
| { type: 'flip'; direction: 'horizontal' | 'vertical' }
|
|
25
|
+
| { type: 'format-convert' }
|
|
26
|
+
| { type: 'compress'; quality: number }
|
|
27
|
+
/**
|
|
28
|
+
* Remove a uniform, edge-connected background. The pure BFS flood-fill lives in
|
|
29
|
+
* {@link floodFillBGS} (media-core); the server runs it on sharp's raw RGBA buffer,
|
|
30
|
+
* a client could call a rembg API instead. `tolerance` is the per-pixel color
|
|
31
|
+
* distance (0–441, euclidean over RGB) treated as "same as the seed".
|
|
32
|
+
*/
|
|
33
|
+
| { type: 'remove-bg'; tolerance?: number };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A complete image pipeline: a source (a file path on the server, or raw bytes) +
|
|
37
|
+
* an ordered op list + an output target. `input` is `Uint8Array` rather than Node's
|
|
38
|
+
* `Buffer` so the type stays browser-safe; a server `Buffer` is a `Uint8Array` and
|
|
39
|
+
* assigns cleanly.
|
|
40
|
+
*/
|
|
41
|
+
export type ImagePipelineSpec = {
|
|
42
|
+
input: Uint8Array | string;
|
|
43
|
+
operations: ImageOperation[];
|
|
44
|
+
output: { format: ImageOutputFormat; quality?: number };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Output container for {@link trimVideo}. */
|
|
48
|
+
export type VideoFormat = 'mp4' | 'webm';
|
|
49
|
+
/** Encode-quality preset → a CRF + bitrate ladder (see `videoCodecPlan`). */
|
|
50
|
+
export type VideoQuality = 'high' | 'medium' | 'low';
|
|
51
|
+
|
|
52
|
+
/** Args for a server-side trim (re-encode between two timestamps). */
|
|
53
|
+
export type VideoTrimSpec = {
|
|
54
|
+
inputPath: string;
|
|
55
|
+
outputPath: string;
|
|
56
|
+
startSeconds: number;
|
|
57
|
+
endSeconds: number;
|
|
58
|
+
format?: VideoFormat;
|
|
59
|
+
quality?: VideoQuality;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Progress emitted while a server video op runs (parsed from ffmpeg stderr). */
|
|
63
|
+
export type VideoProgressEvent = {
|
|
64
|
+
phase: 'probe' | 'encode' | 'done' | 'error';
|
|
65
|
+
progressPercent?: number;
|
|
66
|
+
error?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The three media kinds the catalogue + lightbox understand. */
|
|
70
|
+
export type MediaKind = 'image' | 'video' | 'audio';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A URL-only catalogue item — the unit the `<MediaCatalogueViewer>` renders. It
|
|
74
|
+
* holds NO storage identity (no FsNode, no binary id): the app maps its own records
|
|
75
|
+
* to this and pre-resolves `url`/`thumbnailUrl` (signing private files as needed).
|
|
76
|
+
*/
|
|
77
|
+
export type MediaItem = {
|
|
78
|
+
id: string;
|
|
79
|
+
type: MediaKind;
|
|
80
|
+
/** The final URL the browser fetches. Signing is the app's job. Plain GET only. */
|
|
81
|
+
url: string;
|
|
82
|
+
thumbnailUrl?: string;
|
|
83
|
+
/** A browser-safe full-size URL to display instead of `url` when the original can't render inline
|
|
84
|
+
* (e.g. a JPEG derivative of a HEIC). The full view should prefer this over `url` when present. */
|
|
85
|
+
displayUrl?: string;
|
|
86
|
+
name: string;
|
|
87
|
+
sizeBytes?: number;
|
|
88
|
+
mimeType?: string;
|
|
89
|
+
width?: number;
|
|
90
|
+
height?: number;
|
|
91
|
+
durationSeconds?: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A gallery photo — a {@link MediaItem} specialized to images, plus the catalogue affordances a
|
|
96
|
+
* photo grid renders (tags for the filter strip, createdAt for ordering, favorite for the ♥).
|
|
97
|
+
* Like every media-core unit it carries only resolved URLs — no storage identity.
|
|
98
|
+
*/
|
|
99
|
+
export type GalleryPhoto = MediaItem & {
|
|
100
|
+
type: 'image';
|
|
101
|
+
tags?: string[];
|
|
102
|
+
createdAt?: number;
|
|
103
|
+
favorite?: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A single item in a MIXED media gallery (photos + videos in one grid) — a
|
|
108
|
+
* discriminated union so a consumer can render the right tile/stage per item.
|
|
109
|
+
* `mediaKind` is the explicit tag: `GalleryPhoto` carries `type:'image'` while
|
|
110
|
+
* `PlayableVideo` has no `type`, so a dedicated discriminator keeps the two
|
|
111
|
+
* unambiguous (and future audio can join without a structural clash).
|
|
112
|
+
*/
|
|
113
|
+
export type GalleryMedia =
|
|
114
|
+
| (PlayableVideo & { mediaKind: 'video' })
|
|
115
|
+
| (GalleryPhoto & { mediaKind: 'photo' });
|
|
116
|
+
|
|
117
|
+
/** Which kinds a mixed media source/gallery lists — `all` = photos + videos. */
|
|
118
|
+
export type MediaKindFilter = 'all' | 'image' | 'video';
|
|
119
|
+
|
|
120
|
+
/** Background-processing lifecycle of a video's web-safe derivatives. */
|
|
121
|
+
export type ProcessingStatus = 'pending' | 'processing' | 'ready' | 'failed';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A surface-agnostic description of a playable video with every URL already resolved —
|
|
125
|
+
* the unit the `<VideoPlayer>` primitive consumes (distinct from {@link MediaItem}, which
|
|
126
|
+
* is the lightbox catalogue unit). It carries NO storage identity: a source pre-resolves
|
|
127
|
+
* the streaming URLs (signing/tokenising private ones), so the player couples to nothing.
|
|
128
|
+
* Richer than `MediaItem`: an HLS master + scrub-preview track for a real player HUD.
|
|
129
|
+
*/
|
|
130
|
+
export type PlayableVideo = {
|
|
131
|
+
id: string;
|
|
132
|
+
name: string;
|
|
133
|
+
/** Range-streamed progressive source (works for plaintext + decrypted-private bytes). */
|
|
134
|
+
streamUrl: string;
|
|
135
|
+
/** HLS adaptive master — when present, the player prefers it over `streamUrl`. */
|
|
136
|
+
hlsUrl?: string;
|
|
137
|
+
posterUrl?: string;
|
|
138
|
+
/** WebVTT scrub-preview track + its sprite sheet (hover/drag thumbnails). */
|
|
139
|
+
vttUrl?: string;
|
|
140
|
+
spriteUrl?: string;
|
|
141
|
+
durationSeconds?: number;
|
|
142
|
+
/** Free-form tags for a gallery filter strip. */
|
|
143
|
+
tags?: string[];
|
|
144
|
+
/** Undefined ⇒ plays raw; else drives the player's processing overlay. */
|
|
145
|
+
processingStatus?: ProcessingStatus;
|
|
146
|
+
processingProgress?: number;
|
|
147
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Upload size limits — the ONE source of truth shared by the upload client
|
|
2
|
+
// (`cursedbelt/react/media` fileUploadClient) and the server file routes
|
|
3
|
+
// (`cursedbelt-server/storage`). Pure constants, no deps, so both the browser
|
|
4
|
+
// and Bun sides import the same numbers and can never drift.
|
|
5
|
+
//
|
|
6
|
+
// The law: NO single HTTP request may carry more than MAX_SINGLE_UPLOAD_BYTES of body.
|
|
7
|
+
// Anything larger MUST use the chunked, direct-to-backend path (each chunk is a
|
|
8
|
+
// separate request of at most DEFAULT_UPLOAD_CHUNK_SIZE). This keeps every upload
|
|
9
|
+
// request comfortably under edge proxy body caps (e.g. Cloudflare's ~100 MB limit).
|
|
10
|
+
|
|
11
|
+
/** Hard cap on the body of any SINGLE upload request (streamed PUT). 20 MiB. Bodies
|
|
12
|
+
* above this are rejected 413 server-side and must go through chunked upload. */
|
|
13
|
+
export const MAX_SINGLE_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
/** Default per-chunk size for the chunked-upload path. Must stay < MAX_SINGLE_UPLOAD_BYTES
|
|
16
|
+
* (each chunk is its own request). Also the client's default chunk THRESHOLD: files at or
|
|
17
|
+
* below this upload as a single streamed PUT, larger files chunk. 8 MiB. */
|
|
18
|
+
export const DEFAULT_UPLOAD_CHUNK_SIZE = 8 * 1024 * 1024;
|