@happyvertical/images 0.102.7 → 0.103.1

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/AGENT.md CHANGED
@@ -27,6 +27,6 @@ pnpm --filter @happyvertical/images clean
27
27
  ## Ecosystem Relationships
28
28
  - Provides: Image processing utilities with adapter pattern for scaling from static to enterprise
29
29
  - Implements: none
30
- - Requires: @resvg/resvg-js, jimp, satori, sharp
30
+ - Requires: @mediapipe/tasks-vision, @resvg/resvg-js, jimp, satori, sharp
31
31
  - Stability: stable (Primary package surface is described as implemented and production-oriented.)
32
32
  <!-- END AGENT:GENERATED -->
@@ -0,0 +1,23 @@
1
+ # Local semantic segmentation
2
+
3
+ `@happyvertical/images/segmentation` is an optional browser entry point. It does not import Node image adapters. `segmentImage(ImageData, { assetBaseUrl, signal, onProgress })` runs the pinned MediaPipe 1.1.0 CPU runtime locally; no photo is uploaded. `segmentationAlpha(result, { classes, lower, upper, largestComponent })` converts semantic probabilities into a soft alpha mask. It does not alter original RGB channels. Select classes as consumer policy; the SDK does not define a head or portrait workflow.
4
+
5
+ `detectFaceLandmarks(HTMLImageElement, options)` runs the pinned FaceLandmarker model locally and returns one face's outer lip corners and chin in 0–1000 image coordinates. It rejects zero or multiple faces and never substitutes an estimated facial line.
6
+
7
+ Before serving this optional feature from source, run `pnpm --filter @happyvertical/images prepare:segmentation`. The package runs the same verified preparation step automatically before it is packed, so installed packages include their offline assets. It downloads the version-1 selfie multiclass model (SHA256 `c6748b1253a99067ef71f7e26ca71096cd449baefa8f101900ea23016507e0e0`) and version-1 FaceLandmarker task model (SHA256 `64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff`), then copies the installed runtime WASM files into `segmentation-assets/`. Serve these files from a same-origin path with immutable cache headers; the Node-only `segmentationAssetPath(name)` export validates the fixed asset allowlist. Prepared assets work without external network access. Browser model loading independently verifies the digest and uses its HTTP cache. The runtime and models are Apache-2.0 licensed: [runtime license](https://github.com/google-ai-edge/mediapipe/blob/master/LICENSE). Distributors must preserve the Apache license and attribution for bundled third-party assets.
8
+
9
+ Classes: background 0, hair 1, body skin 2, face skin 3, clothes 4, accessories 5. The model has a 256×256 internal resolution; output confidence masks are aligned to input dimensions. It is not a pixel-perfect hair matting model. The caller should expose review/retry rather than silently accept uncertain contours. Input is capped at 16 megapixels to bound mask memory. Cancellation is checked during loading and before/after synchronous inference; inference itself cannot be interrupted once started.
10
+
11
+ ## Test design and validation
12
+
13
+ | Behavior/invariant | Trigger and cases | Context and external edge | Test level and evidence |
14
+ | --- | --- | --- | --- |
15
+ | Semantic alpha preserves selected foreground only | Combine selected classes; reject duplicate/missing classes, invalid dimensions, probabilities, thresholds, and empty masks | Browser-local pure mask processing; no transaction or dialect applies | Unit: `pnpm --filter @happyvertical/images test` |
16
+ | Alpha selection remains visually coherent | Retain the largest 8-connected region while preserving soft confidence alpha | Consumer chooses classes; the SDK deliberately has no portrait policy | Unit: `pnpm --filter @happyvertical/images test` |
17
+ | Original RGB bytes survive transparent PNG export | Encode fully transparent, partially transparent, and opaque pixels; reject malformed buffers | Browser `CompressionStream`; no network or persistence applies | Unit: `pnpm --filter @happyvertical/images test` |
18
+ | Model is local, pinned, and verified before use | Abort before fetch; reject a digest mismatch; retry a failed load in the consumer | Same-origin model and WASM assets; upstream or HTTP failure surfaces as an error | Unit for abort/integrity: `pnpm --filter @happyvertical/images test`; browser acceptance for load/retry |
19
+ | Face mesh maps only a complete single face | Map lip/chin indices into normalized coordinates; reject incomplete, zero, or multiple faces | Browser-local detector; no provider, transaction, or dialect applies | Unit mapping and lifecycle tests verify ambiguity rejection and task cleanup: `pnpm --filter @happyvertical/images test` |
20
+ | Face runtime fails closed and releases resources | Reject a bad model digest before runtime initialization; abort after initialization; make detector throw | The detector task closes on every post-creation exit and never estimates landmarks | Unit: `pnpm --filter @happyvertical/images test` |
21
+ | Private photo remains local and reviewable | Run the consumer’s real-photo flow, inspect head boundary and RGB output, then cancel/retry without stale UI writes | User-owned private photo; it is excluded from source control, issue evidence, and release artifacts | Browser acceptance performed privately; no photo or derived output retained in this repository |
22
+
23
+ Run `pnpm --filter @happyvertical/images test` and `pnpm --filter @happyvertical/images build`. The browser acceptance row requires a real photo, original RGB preservation, explicit load failure/retry, and cancellation without stale UI writes; do not check private photos into the repository.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
3
2
  import { dirname, join } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
4
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
5
5
  //#region src/cli/claude-context.ts
6
6
  /**
7
7
  * CLI script to install agent context for @happyvertical/images
@@ -0,0 +1,2 @@
1
+ export declare function segmentationAssetPath(name: string): string;
2
+ //# sourceMappingURL=segmentation-assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmentation-assets.d.ts","sourceRoot":"","sources":["../src/segmentation-assets.ts"],"names":[],"mappings":"AAgBA,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO1D"}
@@ -0,0 +1,24 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ //#region src/segmentation-assets.ts
4
+ /**
5
+ * Returns the absolute path for one allowlisted optional local segmentation asset.
6
+ *
7
+ * @throws {Error} When `name` is not a known model or MediaPipe runtime asset.
8
+ */
9
+ var assets = /* @__PURE__ */ new Set([
10
+ "face_landmarker.task",
11
+ "selfie_multiclass_256x256.tflite",
12
+ "vision_wasm_internal.js",
13
+ "vision_wasm_internal.wasm",
14
+ "vision_wasm_nosimd_internal.js",
15
+ "vision_wasm_nosimd_internal.wasm"
16
+ ]);
17
+ function segmentationAssetPath(name) {
18
+ if (!assets.has(name)) throw new Error("Unknown segmentation asset.");
19
+ return resolve(dirname(fileURLToPath(import.meta.url)), "../segmentation-assets", name);
20
+ }
21
+ //#endregion
22
+ export { segmentationAssetPath };
23
+
24
+ //# sourceMappingURL=segmentation-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmentation-assets.js","names":[],"sources":["../src/segmentation-assets.ts"],"sourcesContent":["/**\n * Returns the absolute path for one allowlisted optional local segmentation asset.\n *\n * @throws {Error} When `name` is not a known model or MediaPipe runtime asset.\n */\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst assets = new Set([\n 'face_landmarker.task',\n 'selfie_multiclass_256x256.tflite',\n 'vision_wasm_internal.js',\n 'vision_wasm_internal.wasm',\n 'vision_wasm_nosimd_internal.js',\n 'vision_wasm_nosimd_internal.wasm',\n]);\nexport function segmentationAssetPath(name: string): string {\n if (!assets.has(name)) throw new Error('Unknown segmentation asset.');\n return resolve(\n dirname(fileURLToPath(import.meta.url)),\n '../segmentation-assets',\n name,\n );\n}\n"],"mappings":";;;;;;;;AAQA,IAAM,yBAAS,IAAI,IAAI;CACrB;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,SAAgB,sBAAsB,MAAsB;CAC1D,IAAI,CAAC,OAAO,IAAI,IAAI,GAAG,MAAM,IAAI,MAAM,6BAA6B;CACpE,OAAO,QACL,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GACtC,0BACA,IACF;AACF"}
@@ -0,0 +1,94 @@
1
+ /** Optional browser-only semantic segmentation. Source pixels never leave the device. */
2
+ export declare const SELFIE_MULTICLASS_MODEL: {
3
+ readonly file: "selfie_multiclass_256x256.tflite";
4
+ readonly url: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/1/selfie_multiclass_256x256.tflite";
5
+ readonly sha256: "c6748b1253a99067ef71f7e26ca71096cd449baefa8f101900ea23016507e0e0";
6
+ readonly license: "Apache-2.0";
7
+ };
8
+ /** Pinned MediaPipe face mesh used to locate lips and chin on-device. */
9
+ export declare const FACE_LANDMARKER_MODEL: {
10
+ readonly file: "face_landmarker.task";
11
+ readonly url: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
12
+ readonly sha256: "64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff";
13
+ readonly license: "Apache-2.0";
14
+ };
15
+ /** Pinned model is 16,371,837 bytes; leave bounded room for an upstream repack. */
16
+ export declare const MAX_SEGMENTATION_MODEL_BYTES = 17500000;
17
+ export declare const MAX_FACE_LANDMARKER_MODEL_BYTES = 4000000;
18
+ /** Reads a response incrementally and cancels before an oversized body is retained. */
19
+ export declare function readBoundedModel(response: Response, limit?: number): Promise<ArrayBuffer>;
20
+ /** Semantic per-class confidence masks returned by local inference. */
21
+ export interface SegmentationMask {
22
+ /** Width shared by every confidence mask, in pixels. */
23
+ width: number;
24
+ /** Height shared by every confidence mask, in pixels. */
25
+ height: number;
26
+ /** Class-indexed, row-major probabilities in the inclusive range 0–1. */
27
+ confidenceMasks: Float32Array[];
28
+ }
29
+ /** Selects classes and confidence thresholds for alpha-mask generation. */
30
+ export interface AlphaMaskOptions {
31
+ /** Distinct class indexes to combine. */
32
+ classes: number[];
33
+ /** Probability at which alpha begins rising; defaults to 0.15. */
34
+ lower?: number;
35
+ /** Probability at which alpha reaches full opacity; defaults to 0.85. */
36
+ upper?: number;
37
+ /** Retain only the largest 8-connected foreground area; defaults to true. */
38
+ largestComponent?: boolean;
39
+ }
40
+ /**
41
+ * Converts selected semantic class probabilities to a soft, connected alpha mask.
42
+ *
43
+ * @throws {Error} When dimensions, classes, probabilities, or thresholds are invalid,
44
+ * or when no selected foreground remains.
45
+ */
46
+ export declare function segmentationAlpha(mask: SegmentationMask, options: AlphaMaskOptions): Uint8ClampedArray;
47
+ /** Configuration for one browser-local segmentation operation. */
48
+ export interface SegmentImageOptions {
49
+ /** Same-origin directory containing the pinned model and MediaPipe WASM assets. */
50
+ assetBaseUrl: string;
51
+ /** Cancels loading or prevents inference from starting when already aborted. */
52
+ signal?: AbortSignal;
53
+ /** Receives the current asynchronous stage before inference begins. */
54
+ onProgress?: (stage: 'loading' | 'segmenting') => void;
55
+ }
56
+ /**
57
+ * Runs CPU-only local inference; the caller owns the decoded original image and result masks.
58
+ *
59
+ * @throws {Error} When the source exceeds the pixel limit, assets cannot be loaded or
60
+ * verified, inference returns no confidence masks, or the supplied signal is aborted.
61
+ */
62
+ export declare function segmentImage(image: ImageData, options: SegmentImageOptions): Promise<SegmentationMask>;
63
+ export interface FaceLandmarks {
64
+ mouthLeft: {
65
+ x: number;
66
+ y: number;
67
+ };
68
+ mouthRight: {
69
+ x: number;
70
+ y: number;
71
+ };
72
+ chin: {
73
+ x: number;
74
+ y: number;
75
+ };
76
+ }
77
+ /** Maps MediaPipe's face-mesh lip-corner and chin indices into UI coordinates. */
78
+ export declare function faceLandmarksFromMesh(landmarks: ReadonlyArray<{
79
+ x: number;
80
+ y: number;
81
+ }>): FaceLandmarks;
82
+ /**
83
+ * Detects exactly one face locally and returns the visible lip corners and
84
+ * chin in the caller image's 0..1000 coordinate system. Source pixels remain
85
+ * in the browser and the MediaPipe task is always closed after inference.
86
+ */
87
+ export declare function detectFaceLandmarks(image: HTMLImageElement, options: SegmentImageOptions): Promise<FaceLandmarks>;
88
+ /**
89
+ * Encodes RGBA bytes without canvas premultiplication changing transparent or soft-edge RGB.
90
+ *
91
+ * @throws {Error} When dimensions or the RGBA buffer length are invalid.
92
+ */
93
+ export declare function encodeRgbaPng(width: number, height: number, rgba: Uint8ClampedArray): Promise<Blob>;
94
+ //# sourceMappingURL=segmentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmentation.d.ts","sourceRoot":"","sources":["../src/segmentation.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,eAAO,MAAM,uBAAuB;;;;;CAK1B,CAAC;AAEX,yEAAyE;AACzE,eAAO,MAAM,qBAAqB;;;;;CAKxB,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,4BAA4B,WAAa,CAAC;AACvD,eAAO,MAAM,+BAA+B,UAAY,CAAC;AAEzD,uFAAuF;AACvF,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,QAAQ,EAClB,KAAK,SAA+B,GACnC,OAAO,CAAC,WAAW,CAAC,CAsCtB;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,eAAe,EAAE,YAAY,EAAE,CAAC;CACjC;AACD,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,gBAAgB,GACxB,iBAAiB,CAwFnB;AAED,kEAAkE;AAClE,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,YAAY,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,YAAY,KAAK,IAAI,CAAC;CACxD;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAyD3B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,UAAU,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChC;AAED,kFAAkF;AAClF,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,aAAa,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACjD,aAAa,CAqBf;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,gBAAgB,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CA+CxB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,CAmDf"}
@@ -0,0 +1,271 @@
1
+ //#region src/segmentation.ts
2
+ /** Optional browser-only semantic segmentation. Source pixels never leave the device. */
3
+ var SELFIE_MULTICLASS_MODEL = {
4
+ file: "selfie_multiclass_256x256.tflite",
5
+ url: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/1/selfie_multiclass_256x256.tflite",
6
+ sha256: "c6748b1253a99067ef71f7e26ca71096cd449baefa8f101900ea23016507e0e0",
7
+ license: "Apache-2.0"
8
+ };
9
+ /** Pinned MediaPipe face mesh used to locate lips and chin on-device. */
10
+ var FACE_LANDMARKER_MODEL = {
11
+ file: "face_landmarker.task",
12
+ url: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",
13
+ sha256: "64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff",
14
+ license: "Apache-2.0"
15
+ };
16
+ /** Pinned model is 16,371,837 bytes; leave bounded room for an upstream repack. */
17
+ var MAX_SEGMENTATION_MODEL_BYTES = 175e5;
18
+ var MAX_FACE_LANDMARKER_MODEL_BYTES = 4e6;
19
+ /** Reads a response incrementally and cancels before an oversized body is retained. */
20
+ async function readBoundedModel(response, limit = MAX_SEGMENTATION_MODEL_BYTES) {
21
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("Segmentation model download limit is invalid.");
22
+ const declared = Number(response.headers.get("content-length"));
23
+ if (Number.isFinite(declared) && declared > limit) {
24
+ await response.body?.cancel();
25
+ throw new Error("Segmentation model exceeds the download limit. Please retry.");
26
+ }
27
+ if (!response.body) throw new Error("Segmentation model response has no body. Please retry.");
28
+ const reader = response.body.getReader(), chunks = [];
29
+ let total = 0;
30
+ try {
31
+ while (true) {
32
+ const next = await reader.read();
33
+ if (next.done) break;
34
+ total += next.value.byteLength;
35
+ if (total > limit) {
36
+ await reader.cancel();
37
+ throw new Error("Segmentation model exceeds the download limit. Please retry.");
38
+ }
39
+ chunks.push(next.value);
40
+ }
41
+ } finally {
42
+ reader.releaseLock();
43
+ }
44
+ const bytes = new Uint8Array(total);
45
+ let offset = 0;
46
+ for (const chunk of chunks) {
47
+ bytes.set(chunk, offset);
48
+ offset += chunk.byteLength;
49
+ }
50
+ return bytes.buffer;
51
+ }
52
+ /**
53
+ * Converts selected semantic class probabilities to a soft, connected alpha mask.
54
+ *
55
+ * @throws {Error} When dimensions, classes, probabilities, or thresholds are invalid,
56
+ * or when no selected foreground remains.
57
+ */
58
+ function segmentationAlpha(mask, options) {
59
+ const { width, height, confidenceMasks } = mask;
60
+ const { classes, lower = .15, upper = .85, largestComponent = true } = options;
61
+ const size = width * height;
62
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1 || size > 16777216) throw new Error("Segmentation dimensions must be positive and at most 16 megapixels.");
63
+ if (!(lower >= 0 && upper <= 1 && lower < upper) || !classes.length || new Set(classes).size !== classes.length) throw new Error("Invalid segmentation classes or confidence thresholds.");
64
+ const selected = classes.map((index) => {
65
+ if (!Number.isInteger(index) || index < 0 || !confidenceMasks[index] || confidenceMasks[index].length !== size) throw new Error("Segmentation class is missing or has an invalid mask size.");
66
+ return confidenceMasks[index];
67
+ });
68
+ const alpha = new Uint8ClampedArray(size);
69
+ for (let i = 0; i < size; i++) {
70
+ let probability = 0;
71
+ for (const values of selected) {
72
+ const value = values[i];
73
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new Error("Invalid segmentation probability.");
74
+ probability += value;
75
+ }
76
+ const t = Math.max(0, Math.min(1, (probability - lower) / (upper - lower)));
77
+ alpha[i] = Math.round(255 * t * t * (3 - 2 * t));
78
+ }
79
+ if (largestComponent) {
80
+ const labels = new Int32Array(size);
81
+ const queue = new Int32Array(size);
82
+ let label = 0, bestLabel = 0, bestSize = 0;
83
+ for (let seed = 0; seed < size; seed++) {
84
+ if (!alpha[seed] || labels[seed]) continue;
85
+ label++;
86
+ let read = 0, write = 1;
87
+ queue[0] = seed;
88
+ labels[seed] = label;
89
+ while (read < write) {
90
+ const at = queue[read++], x = at % width, y = Math.floor(at / width);
91
+ for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
92
+ const nx = x + dx, ny = y + dy;
93
+ if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
94
+ const next = ny * width + nx;
95
+ if (alpha[next] && !labels[next]) {
96
+ labels[next] = label;
97
+ queue[write++] = next;
98
+ }
99
+ }
100
+ }
101
+ if (write > bestSize) {
102
+ bestSize = write;
103
+ bestLabel = label;
104
+ }
105
+ }
106
+ for (let i = 0; i < size; i++) if (labels[i] !== bestLabel) alpha[i] = 0;
107
+ }
108
+ if (!alpha.some((value) => value > 0)) throw new Error("No selected foreground found. Try a clearer photo.");
109
+ return alpha;
110
+ }
111
+ /**
112
+ * Runs CPU-only local inference; the caller owns the decoded original image and result masks.
113
+ *
114
+ * @throws {Error} When the source exceeds the pixel limit, assets cannot be loaded or
115
+ * verified, inference returns no confidence masks, or the supplied signal is aborted.
116
+ */
117
+ async function segmentImage(image, options) {
118
+ const { signal, onProgress } = options;
119
+ signal?.throwIfAborted();
120
+ if (image.width * image.height > 16777216) throw new Error("Photo exceeds the 16 megapixel segmentation limit.");
121
+ onProgress?.("loading");
122
+ const base = options.assetBaseUrl.replace(/\/$/, "");
123
+ const response = await fetch(`${base}/${SELFIE_MULTICLASS_MODEL.file}`, {
124
+ signal,
125
+ cache: "force-cache"
126
+ });
127
+ if (!response.ok) throw new Error(`Could not load segmentation model (${response.status}).`);
128
+ const bytes = await readBoundedModel(response);
129
+ if (Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (value) => value.toString(16).padStart(2, "0")).join("") !== SELFIE_MULTICLASS_MODEL.sha256) throw new Error("Segmentation model integrity check failed.");
130
+ signal?.throwIfAborted();
131
+ const { FilesetResolver, ImageSegmenter } = await import("@mediapipe/tasks-vision");
132
+ const files = await FilesetResolver.forVisionTasks(base);
133
+ signal?.throwIfAborted();
134
+ const segmenter = await ImageSegmenter.createFromOptions(files, {
135
+ baseOptions: {
136
+ modelAssetBuffer: new Uint8Array(bytes),
137
+ delegate: "CPU"
138
+ },
139
+ runningMode: "IMAGE",
140
+ outputConfidenceMasks: true,
141
+ outputCategoryMask: false
142
+ });
143
+ try {
144
+ signal?.throwIfAborted();
145
+ onProgress?.("segmenting");
146
+ await new Promise((resolve) => setTimeout(resolve, 0));
147
+ signal?.throwIfAborted();
148
+ const result = segmenter.segment(image);
149
+ try {
150
+ signal?.throwIfAborted();
151
+ if (!result.confidenceMasks?.length) throw new Error("Segmentation returned no confidence masks.");
152
+ const first = result.confidenceMasks[0];
153
+ return {
154
+ width: first.width,
155
+ height: first.height,
156
+ confidenceMasks: result.confidenceMasks.map((mask) => new Float32Array(mask.getAsFloat32Array()))
157
+ };
158
+ } finally {
159
+ result.close();
160
+ }
161
+ } finally {
162
+ segmenter.close();
163
+ }
164
+ }
165
+ /** Maps MediaPipe's face-mesh lip-corner and chin indices into UI coordinates. */
166
+ function faceLandmarksFromMesh(landmarks) {
167
+ const mouth = [landmarks[61], landmarks[291]];
168
+ const chin = landmarks[152];
169
+ if (!mouth[0] || !mouth[1] || !chin) throw new Error("Face landmarks are incomplete. Try a clearer photo.");
170
+ const point = ({ x, y }) => {
171
+ if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x > 1 || y < 0 || y > 1) throw new Error("Face landmarks fall outside the image. Try a clearer photo.");
172
+ return {
173
+ x: x * 1e3,
174
+ y: y * 1e3
175
+ };
176
+ };
177
+ const [first, second] = mouth.map(point).sort((a, b) => a.x - b.x);
178
+ return {
179
+ mouthLeft: first,
180
+ mouthRight: second,
181
+ chin: point(chin)
182
+ };
183
+ }
184
+ /**
185
+ * Detects exactly one face locally and returns the visible lip corners and
186
+ * chin in the caller image's 0..1000 coordinate system. Source pixels remain
187
+ * in the browser and the MediaPipe task is always closed after inference.
188
+ */
189
+ async function detectFaceLandmarks(image, options) {
190
+ const { signal, onProgress } = options;
191
+ signal?.throwIfAborted();
192
+ onProgress?.("loading");
193
+ const base = options.assetBaseUrl.replace(/\/$/, "");
194
+ const response = await fetch(`${base}/${FACE_LANDMARKER_MODEL.file}`, {
195
+ signal,
196
+ cache: "force-cache"
197
+ });
198
+ if (!response.ok) throw new Error(`Could not load face landmark model (${response.status}).`);
199
+ const bytes = await readBoundedModel(response, MAX_FACE_LANDMARKER_MODEL_BYTES);
200
+ if (Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (value) => value.toString(16).padStart(2, "0")).join("") !== FACE_LANDMARKER_MODEL.sha256) throw new Error("Face landmark model integrity check failed.");
201
+ signal?.throwIfAborted();
202
+ const { FaceLandmarker, FilesetResolver } = await import("@mediapipe/tasks-vision");
203
+ const files = await FilesetResolver.forVisionTasks(base);
204
+ signal?.throwIfAborted();
205
+ const landmarker = await FaceLandmarker.createFromOptions(files, {
206
+ baseOptions: {
207
+ modelAssetBuffer: new Uint8Array(bytes),
208
+ delegate: "CPU"
209
+ },
210
+ runningMode: "IMAGE",
211
+ numFaces: 2
212
+ });
213
+ try {
214
+ onProgress?.("segmenting");
215
+ await new Promise((resolve) => setTimeout(resolve, 0));
216
+ signal?.throwIfAborted();
217
+ const faces = landmarker.detect(image).faceLandmarks;
218
+ if (faces.length !== 1) throw new Error(faces.length ? "Use a photo with one clear face." : "No clear face found. Try a closer, front-facing photo.");
219
+ return faceLandmarksFromMesh(faces[0]);
220
+ } finally {
221
+ landmarker.close();
222
+ }
223
+ }
224
+ /**
225
+ * Encodes RGBA bytes without canvas premultiplication changing transparent or soft-edge RGB.
226
+ *
227
+ * @throws {Error} When dimensions or the RGBA buffer length are invalid.
228
+ */
229
+ async function encodeRgbaPng(width, height, rgba) {
230
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1 || width * height > 16777216 || rgba.length !== width * height * 4) throw new Error("Invalid RGBA image dimensions.");
231
+ const chunk = (type, data) => {
232
+ const output = new Uint8Array(data.length + 12), view = new DataView(output.buffer);
233
+ view.setUint32(0, data.length);
234
+ output.set(new TextEncoder().encode(type), 4);
235
+ output.set(data, 8);
236
+ let crc = 4294967295;
237
+ for (let i = 4; i < data.length + 8; i++) {
238
+ crc ^= output[i];
239
+ for (let bit = 0; bit < 8; bit++) crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
240
+ }
241
+ view.setUint32(data.length + 8, (crc ^ 4294967295) >>> 0);
242
+ return output;
243
+ };
244
+ const header = /* @__PURE__ */ new Uint8Array(13), view = new DataView(header.buffer);
245
+ view.setUint32(0, width);
246
+ view.setUint32(4, height);
247
+ header[8] = 8;
248
+ header[9] = 6;
249
+ const rows = new Uint8Array((width * 4 + 1) * height);
250
+ for (let y = 0; y < height; y++) rows.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
251
+ const compressed = new Uint8Array(await new Response(new Blob([rows]).stream().pipeThrough(new CompressionStream("deflate"))).arrayBuffer());
252
+ return new Blob([
253
+ new Uint8Array([
254
+ 137,
255
+ 80,
256
+ 78,
257
+ 71,
258
+ 13,
259
+ 10,
260
+ 26,
261
+ 10
262
+ ]),
263
+ chunk("IHDR", header),
264
+ chunk("IDAT", compressed),
265
+ chunk("IEND", /* @__PURE__ */ new Uint8Array())
266
+ ], { type: "image/png" });
267
+ }
268
+ //#endregion
269
+ export { FACE_LANDMARKER_MODEL, MAX_FACE_LANDMARKER_MODEL_BYTES, MAX_SEGMENTATION_MODEL_BYTES, SELFIE_MULTICLASS_MODEL, detectFaceLandmarks, encodeRgbaPng, faceLandmarksFromMesh, readBoundedModel, segmentImage, segmentationAlpha };
270
+
271
+ //# sourceMappingURL=segmentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmentation.js","names":[],"sources":["../src/segmentation.ts"],"sourcesContent":["/** Optional browser-only semantic segmentation. Source pixels never leave the device. */\nexport const SELFIE_MULTICLASS_MODEL = {\n file: 'selfie_multiclass_256x256.tflite',\n url: 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/1/selfie_multiclass_256x256.tflite',\n sha256: 'c6748b1253a99067ef71f7e26ca71096cd449baefa8f101900ea23016507e0e0',\n license: 'Apache-2.0',\n} as const;\n\n/** Pinned MediaPipe face mesh used to locate lips and chin on-device. */\nexport const FACE_LANDMARKER_MODEL = {\n file: 'face_landmarker.task',\n url: 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',\n sha256: '64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff',\n license: 'Apache-2.0',\n} as const;\n\n/** Pinned model is 16,371,837 bytes; leave bounded room for an upstream repack. */\nexport const MAX_SEGMENTATION_MODEL_BYTES = 17_500_000;\nexport const MAX_FACE_LANDMARKER_MODEL_BYTES = 4_000_000;\n\n/** Reads a response incrementally and cancels before an oversized body is retained. */\nexport async function readBoundedModel(\n response: Response,\n limit = MAX_SEGMENTATION_MODEL_BYTES,\n): Promise<ArrayBuffer> {\n if (!Number.isSafeInteger(limit) || limit < 1)\n throw new Error('Segmentation model download limit is invalid.');\n const declared = Number(response.headers.get('content-length'));\n if (Number.isFinite(declared) && declared > limit) {\n await response.body?.cancel();\n throw new Error(\n 'Segmentation model exceeds the download limit. Please retry.',\n );\n }\n if (!response.body)\n throw new Error('Segmentation model response has no body. Please retry.');\n const reader = response.body.getReader(),\n chunks: Uint8Array[] = [];\n let total = 0;\n try {\n while (true) {\n const next = await reader.read();\n if (next.done) break;\n total += next.value.byteLength;\n if (total > limit) {\n await reader.cancel();\n throw new Error(\n 'Segmentation model exceeds the download limit. Please retry.',\n );\n }\n chunks.push(next.value);\n }\n } finally {\n reader.releaseLock();\n }\n const bytes = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return bytes.buffer;\n}\n\n/** Semantic per-class confidence masks returned by local inference. */\nexport interface SegmentationMask {\n /** Width shared by every confidence mask, in pixels. */\n width: number;\n /** Height shared by every confidence mask, in pixels. */\n height: number;\n /** Class-indexed, row-major probabilities in the inclusive range 0–1. */\n confidenceMasks: Float32Array[];\n}\n/** Selects classes and confidence thresholds for alpha-mask generation. */\nexport interface AlphaMaskOptions {\n /** Distinct class indexes to combine. */\n classes: number[];\n /** Probability at which alpha begins rising; defaults to 0.15. */\n lower?: number;\n /** Probability at which alpha reaches full opacity; defaults to 0.85. */\n upper?: number;\n /** Retain only the largest 8-connected foreground area; defaults to true. */\n largestComponent?: boolean;\n}\n\n/**\n * Converts selected semantic class probabilities to a soft, connected alpha mask.\n *\n * @throws {Error} When dimensions, classes, probabilities, or thresholds are invalid,\n * or when no selected foreground remains.\n */\nexport function segmentationAlpha(\n mask: SegmentationMask,\n options: AlphaMaskOptions,\n): Uint8ClampedArray {\n const { width, height, confidenceMasks } = mask;\n const {\n classes,\n lower = 0.15,\n upper = 0.85,\n largestComponent = true,\n } = options;\n const size = width * height;\n if (\n !Number.isSafeInteger(width) ||\n !Number.isSafeInteger(height) ||\n width < 1 ||\n height < 1 ||\n size > 16_777_216\n )\n throw new Error(\n 'Segmentation dimensions must be positive and at most 16 megapixels.',\n );\n if (\n !(lower >= 0 && upper <= 1 && lower < upper) ||\n !classes.length ||\n new Set(classes).size !== classes.length\n )\n throw new Error('Invalid segmentation classes or confidence thresholds.');\n const selected = classes.map((index) => {\n if (\n !Number.isInteger(index) ||\n index < 0 ||\n !confidenceMasks[index] ||\n confidenceMasks[index].length !== size\n )\n throw new Error(\n 'Segmentation class is missing or has an invalid mask size.',\n );\n return confidenceMasks[index];\n });\n const alpha = new Uint8ClampedArray(size);\n for (let i = 0; i < size; i++) {\n let probability = 0;\n for (const values of selected) {\n const value = values[i];\n if (!Number.isFinite(value) || value < 0 || value > 1)\n throw new Error('Invalid segmentation probability.');\n probability += value;\n }\n const t = Math.max(0, Math.min(1, (probability - lower) / (upper - lower)));\n alpha[i] = Math.round(255 * t * t * (3 - 2 * t));\n }\n if (largestComponent) {\n const labels = new Int32Array(size);\n const queue = new Int32Array(size);\n let label = 0,\n bestLabel = 0,\n bestSize = 0;\n for (let seed = 0; seed < size; seed++) {\n if (!alpha[seed] || labels[seed]) continue;\n label++;\n let read = 0,\n write = 1;\n queue[0] = seed;\n labels[seed] = label;\n while (read < write) {\n const at = queue[read++],\n x = at % width,\n y = Math.floor(at / width);\n for (let dy = -1; dy <= 1; dy++)\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx,\n ny = y + dy;\n if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;\n const next = ny * width + nx;\n if (alpha[next] && !labels[next]) {\n labels[next] = label;\n queue[write++] = next;\n }\n }\n }\n if (write > bestSize) {\n bestSize = write;\n bestLabel = label;\n }\n }\n for (let i = 0; i < size; i++) if (labels[i] !== bestLabel) alpha[i] = 0;\n }\n if (!alpha.some((value) => value > 0))\n throw new Error('No selected foreground found. Try a clearer photo.');\n return alpha;\n}\n\n/** Configuration for one browser-local segmentation operation. */\nexport interface SegmentImageOptions {\n /** Same-origin directory containing the pinned model and MediaPipe WASM assets. */\n assetBaseUrl: string;\n /** Cancels loading or prevents inference from starting when already aborted. */\n signal?: AbortSignal;\n /** Receives the current asynchronous stage before inference begins. */\n onProgress?: (stage: 'loading' | 'segmenting') => void;\n}\n\n/**\n * Runs CPU-only local inference; the caller owns the decoded original image and result masks.\n *\n * @throws {Error} When the source exceeds the pixel limit, assets cannot be loaded or\n * verified, inference returns no confidence masks, or the supplied signal is aborted.\n */\nexport async function segmentImage(\n image: ImageData,\n options: SegmentImageOptions,\n): Promise<SegmentationMask> {\n const { signal, onProgress } = options;\n signal?.throwIfAborted();\n if (image.width * image.height > 16_777_216)\n throw new Error('Photo exceeds the 16 megapixel segmentation limit.');\n onProgress?.('loading');\n const base = options.assetBaseUrl.replace(/\\/$/, '');\n const response = await fetch(`${base}/${SELFIE_MULTICLASS_MODEL.file}`, {\n signal,\n cache: 'force-cache',\n });\n if (!response.ok)\n throw new Error(`Could not load segmentation model (${response.status}).`);\n const bytes = await readBoundedModel(response);\n const digest = Array.from(\n new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)),\n (value) => value.toString(16).padStart(2, '0'),\n ).join('');\n if (digest !== SELFIE_MULTICLASS_MODEL.sha256)\n throw new Error('Segmentation model integrity check failed.');\n signal?.throwIfAborted();\n const { FilesetResolver, ImageSegmenter } = await import(\n '@mediapipe/tasks-vision'\n );\n const files = await FilesetResolver.forVisionTasks(base);\n signal?.throwIfAborted();\n const segmenter = await ImageSegmenter.createFromOptions(files, {\n baseOptions: { modelAssetBuffer: new Uint8Array(bytes), delegate: 'CPU' },\n runningMode: 'IMAGE',\n outputConfidenceMasks: true,\n outputCategoryMask: false,\n });\n try {\n signal?.throwIfAborted();\n onProgress?.('segmenting');\n // Let the browser paint progress and deliver a pending cancellation before CPU inference.\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n signal?.throwIfAborted();\n const result = segmenter.segment(image);\n try {\n signal?.throwIfAborted();\n if (!result.confidenceMasks?.length)\n throw new Error('Segmentation returned no confidence masks.');\n const first = result.confidenceMasks[0];\n return {\n width: first.width,\n height: first.height,\n confidenceMasks: result.confidenceMasks.map(\n (mask) => new Float32Array(mask.getAsFloat32Array()),\n ),\n };\n } finally {\n result.close();\n }\n } finally {\n segmenter.close();\n }\n}\n\nexport interface FaceLandmarks {\n mouthLeft: { x: number; y: number };\n mouthRight: { x: number; y: number };\n chin: { x: number; y: number };\n}\n\n/** Maps MediaPipe's face-mesh lip-corner and chin indices into UI coordinates. */\nexport function faceLandmarksFromMesh(\n landmarks: ReadonlyArray<{ x: number; y: number }>,\n): FaceLandmarks {\n const mouth = [landmarks[61], landmarks[291]];\n const chin = landmarks[152];\n if (!mouth[0] || !mouth[1] || !chin)\n throw new Error('Face landmarks are incomplete. Try a clearer photo.');\n const point = ({ x, y }: { x: number; y: number }) => {\n if (\n !Number.isFinite(x) ||\n !Number.isFinite(y) ||\n x < 0 ||\n x > 1 ||\n y < 0 ||\n y > 1\n )\n throw new Error(\n 'Face landmarks fall outside the image. Try a clearer photo.',\n );\n return { x: x * 1000, y: y * 1000 };\n };\n const [first, second] = mouth.map(point).sort((a, b) => a.x - b.x);\n return { mouthLeft: first, mouthRight: second, chin: point(chin) };\n}\n\n/**\n * Detects exactly one face locally and returns the visible lip corners and\n * chin in the caller image's 0..1000 coordinate system. Source pixels remain\n * in the browser and the MediaPipe task is always closed after inference.\n */\nexport async function detectFaceLandmarks(\n image: HTMLImageElement,\n options: SegmentImageOptions,\n): Promise<FaceLandmarks> {\n const { signal, onProgress } = options;\n signal?.throwIfAborted();\n onProgress?.('loading');\n const base = options.assetBaseUrl.replace(/\\/$/, '');\n const response = await fetch(`${base}/${FACE_LANDMARKER_MODEL.file}`, {\n signal,\n cache: 'force-cache',\n });\n if (!response.ok)\n throw new Error(`Could not load face landmark model (${response.status}).`);\n const bytes = await readBoundedModel(\n response,\n MAX_FACE_LANDMARKER_MODEL_BYTES,\n );\n const digest = Array.from(\n new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)),\n (value) => value.toString(16).padStart(2, '0'),\n ).join('');\n if (digest !== FACE_LANDMARKER_MODEL.sha256)\n throw new Error('Face landmark model integrity check failed.');\n signal?.throwIfAborted();\n const { FaceLandmarker, FilesetResolver } = await import(\n '@mediapipe/tasks-vision'\n );\n const files = await FilesetResolver.forVisionTasks(base);\n signal?.throwIfAborted();\n const landmarker = await FaceLandmarker.createFromOptions(files, {\n baseOptions: { modelAssetBuffer: new Uint8Array(bytes), delegate: 'CPU' },\n runningMode: 'IMAGE',\n numFaces: 2,\n });\n try {\n onProgress?.('segmenting');\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n signal?.throwIfAborted();\n const faces = landmarker.detect(image).faceLandmarks;\n if (faces.length !== 1)\n throw new Error(\n faces.length\n ? 'Use a photo with one clear face.'\n : 'No clear face found. Try a closer, front-facing photo.',\n );\n return faceLandmarksFromMesh(faces[0]);\n } finally {\n landmarker.close();\n }\n}\n\n/**\n * Encodes RGBA bytes without canvas premultiplication changing transparent or soft-edge RGB.\n *\n * @throws {Error} When dimensions or the RGBA buffer length are invalid.\n */\nexport async function encodeRgbaPng(\n width: number,\n height: number,\n rgba: Uint8ClampedArray,\n): Promise<Blob> {\n if (\n !Number.isSafeInteger(width) ||\n !Number.isSafeInteger(height) ||\n width < 1 ||\n height < 1 ||\n width * height > 16_777_216 ||\n rgba.length !== width * height * 4\n )\n throw new Error('Invalid RGBA image dimensions.');\n const chunk = (type: string, data: Uint8Array) => {\n const output = new Uint8Array(data.length + 12),\n view = new DataView(output.buffer);\n view.setUint32(0, data.length);\n output.set(new TextEncoder().encode(type), 4);\n output.set(data, 8);\n let crc = 0xffffffff;\n for (let i = 4; i < data.length + 8; i++) {\n crc ^= output[i];\n for (let bit = 0; bit < 8; bit++)\n crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);\n }\n view.setUint32(data.length + 8, (crc ^ 0xffffffff) >>> 0);\n return output;\n };\n const header = new Uint8Array(13),\n view = new DataView(header.buffer);\n view.setUint32(0, width);\n view.setUint32(4, height);\n header[8] = 8;\n header[9] = 6;\n const rows = new Uint8Array((width * 4 + 1) * height);\n for (let y = 0; y < height; y++)\n rows.set(\n rgba.subarray(y * width * 4, (y + 1) * width * 4),\n y * (width * 4 + 1) + 1,\n );\n const compressed = new Uint8Array(\n await new Response(\n new Blob([rows]).stream().pipeThrough(new CompressionStream('deflate')),\n ).arrayBuffer(),\n );\n return new Blob(\n [\n new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),\n chunk('IHDR', header),\n chunk('IDAT', compressed),\n chunk('IEND', new Uint8Array()),\n ],\n { type: 'image/png' },\n );\n}\n"],"mappings":";;AACA,IAAa,0BAA0B;CACrC,MAAM;CACN,KAAK;CACL,QAAQ;CACR,SAAS;AACX;;AAGA,IAAa,wBAAwB;CACnC,MAAM;CACN,KAAK;CACL,QAAQ;CACR,SAAS;AACX;;AAGA,IAAa,+BAA+B;AAC5C,IAAa,kCAAkC;;AAG/C,eAAsB,iBACpB,UACA,QAAQ,8BACc;CACtB,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,GAC1C,MAAM,IAAI,MAAM,+CAA+C;CACjE,MAAM,WAAW,OAAO,SAAS,QAAQ,IAAI,gBAAgB,CAAC;CAC9D,IAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,OAAO;EACjD,MAAM,SAAS,MAAM,OAAO;EAC5B,MAAM,IAAI,MACR,8DACF;CACF;CACA,IAAI,CAAC,SAAS,MACZ,MAAM,IAAI,MAAM,wDAAwD;CAC1E,MAAM,SAAS,SAAS,KAAK,UAAU,GACrC,SAAuB,CAAC;CAC1B,IAAI,QAAQ;CACZ,IAAI;EACF,OAAO,MAAM;GACX,MAAM,OAAO,MAAM,OAAO,KAAK;GAC/B,IAAI,KAAK,MAAM;GACf,SAAS,KAAK,MAAM;GACpB,IAAI,QAAQ,OAAO;IACjB,MAAM,OAAO,OAAO;IACpB,MAAM,IAAI,MACR,8DACF;GACF;GACA,OAAO,KAAK,KAAK,KAAK;EACxB;CACF,UAAU;EACR,OAAO,YAAY;CACrB;CACA,MAAM,QAAQ,IAAI,WAAW,KAAK;CAClC,IAAI,SAAS;CACb,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,IAAI,OAAO,MAAM;EACvB,UAAU,MAAM;CAClB;CACA,OAAO,MAAM;AACf;;;;;;;AA6BA,SAAgB,kBACd,MACA,SACmB;CACnB,MAAM,EAAE,OAAO,QAAQ,oBAAoB;CAC3C,MAAM,EACJ,SACA,QAAQ,KACR,QAAQ,KACR,mBAAmB,SACjB;CACJ,MAAM,OAAO,QAAQ;CACrB,IACE,CAAC,OAAO,cAAc,KAAK,KAC3B,CAAC,OAAO,cAAc,MAAM,KAC5B,QAAQ,KACR,SAAS,KACT,OAAO,UAEP,MAAM,IAAI,MACR,qEACF;CACF,IACE,EAAE,SAAS,KAAK,SAAS,KAAK,QAAQ,UACtC,CAAC,QAAQ,UACT,IAAI,IAAI,OAAO,CAAC,CAAC,SAAS,QAAQ,QAElC,MAAM,IAAI,MAAM,wDAAwD;CAC1E,MAAM,WAAW,QAAQ,KAAK,UAAU;EACtC,IACE,CAAC,OAAO,UAAU,KAAK,KACvB,QAAQ,KACR,CAAC,gBAAgB,UACjB,gBAAgB,MAAM,CAAC,WAAW,MAElC,MAAM,IAAI,MACR,4DACF;EACF,OAAO,gBAAgB;CACzB,CAAC;CACD,MAAM,QAAQ,IAAI,kBAAkB,IAAI;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,KAAK;EAC7B,IAAI,cAAc;EAClB,KAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,QAAQ,OAAO;GACrB,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,KAAK,QAAQ,GAClD,MAAM,IAAI,MAAM,mCAAmC;GACrD,eAAe;EACjB;EACA,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,cAAc,UAAU,QAAQ,MAAM,CAAC;EAC1E,MAAM,KAAK,KAAK,MAAM,MAAM,IAAI,KAAK,IAAI,IAAI,EAAE;CACjD;CACA,IAAI,kBAAkB;EACpB,MAAM,SAAS,IAAI,WAAW,IAAI;EAClC,MAAM,QAAQ,IAAI,WAAW,IAAI;EACjC,IAAI,QAAQ,GACV,YAAY,GACZ,WAAW;EACb,KAAK,IAAI,OAAO,GAAG,OAAO,MAAM,QAAQ;GACtC,IAAI,CAAC,MAAM,SAAS,OAAO,OAAO;GAClC;GACA,IAAI,OAAO,GACT,QAAQ;GACV,MAAM,KAAK;GACX,OAAO,QAAQ;GACf,OAAO,OAAO,OAAO;IACnB,MAAM,KAAK,MAAM,SACf,IAAI,KAAK,OACT,IAAI,KAAK,MAAM,KAAK,KAAK;IAC3B,KAAK,IAAI,KAAK,IAAI,MAAM,GAAG,MACzB,KAAK,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM;KAC/B,MAAM,KAAK,IAAI,IACb,KAAK,IAAI;KACX,IAAI,KAAK,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,QAAQ;KACrD,MAAM,OAAO,KAAK,QAAQ;KAC1B,IAAI,MAAM,SAAS,CAAC,OAAO,OAAO;MAChC,OAAO,QAAQ;MACf,MAAM,WAAW;KACnB;IACF;GACJ;GACA,IAAI,QAAQ,UAAU;IACpB,WAAW;IACX,YAAY;GACd;EACF;EACA,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,KAAK,IAAI,OAAO,OAAO,WAAW,MAAM,KAAK;CACzE;CACA,IAAI,CAAC,MAAM,MAAM,UAAU,QAAQ,CAAC,GAClC,MAAM,IAAI,MAAM,oDAAoD;CACtE,OAAO;AACT;;;;;;;AAkBA,eAAsB,aACpB,OACA,SAC2B;CAC3B,MAAM,EAAE,QAAQ,eAAe;CAC/B,QAAQ,eAAe;CACvB,IAAI,MAAM,QAAQ,MAAM,SAAS,UAC/B,MAAM,IAAI,MAAM,oDAAoD;CACtE,aAAa,SAAS;CACtB,MAAM,OAAO,QAAQ,aAAa,QAAQ,OAAO,EAAE;CACnD,MAAM,WAAW,MAAM,MAAM,GAAG,KAAK,GAAG,wBAAwB,QAAQ;EACtE;EACA,OAAO;CACT,CAAC;CACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,sCAAsC,SAAS,OAAO,GAAG;CAC3E,MAAM,QAAQ,MAAM,iBAAiB,QAAQ;CAK7C,IAJe,MAAM,KACnB,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,CAAC,IAC1D,UAAU,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAC/C,CAAC,CAAC,KAAK,EACH,MAAW,wBAAwB,QACrC,MAAM,IAAI,MAAM,4CAA4C;CAC9D,QAAQ,eAAe;CACvB,MAAM,EAAE,iBAAiB,mBAAmB,MAAM,OAChD;CAEF,MAAM,QAAQ,MAAM,gBAAgB,eAAe,IAAI;CACvD,QAAQ,eAAe;CACvB,MAAM,YAAY,MAAM,eAAe,kBAAkB,OAAO;EAC9D,aAAa;GAAE,kBAAkB,IAAI,WAAW,KAAK;GAAG,UAAU;EAAM;EACxE,aAAa;EACb,uBAAuB;EACvB,oBAAoB;CACtB,CAAC;CACD,IAAI;EACF,QAAQ,eAAe;EACvB,aAAa,YAAY;EAEzB,MAAM,IAAI,SAAe,YAAY,WAAW,SAAS,CAAC,CAAC;EAC3D,QAAQ,eAAe;EACvB,MAAM,SAAS,UAAU,QAAQ,KAAK;EACtC,IAAI;GACF,QAAQ,eAAe;GACvB,IAAI,CAAC,OAAO,iBAAiB,QAC3B,MAAM,IAAI,MAAM,4CAA4C;GAC9D,MAAM,QAAQ,OAAO,gBAAgB;GACrC,OAAO;IACL,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,iBAAiB,OAAO,gBAAgB,KACrC,SAAS,IAAI,aAAa,KAAK,kBAAkB,CAAC,CACrD;GACF;EACF,UAAU;GACR,OAAO,MAAM;EACf;CACF,UAAU;EACR,UAAU,MAAM;CAClB;AACF;;AASA,SAAgB,sBACd,WACe;CACf,MAAM,QAAQ,CAAC,UAAU,KAAK,UAAU,IAAI;CAC5C,MAAM,OAAO,UAAU;CACvB,IAAI,CAAC,MAAM,MAAM,CAAC,MAAM,MAAM,CAAC,MAC7B,MAAM,IAAI,MAAM,qDAAqD;CACvE,MAAM,SAAS,EAAE,GAAG,QAAkC;EACpD,IACE,CAAC,OAAO,SAAS,CAAC,KAClB,CAAC,OAAO,SAAS,CAAC,KAClB,IAAI,KACJ,IAAI,KACJ,IAAI,KACJ,IAAI,GAEJ,MAAM,IAAI,MACR,6DACF;EACF,OAAO;GAAE,GAAG,IAAI;GAAM,GAAG,IAAI;EAAK;CACpC;CACA,MAAM,CAAC,OAAO,UAAU,MAAM,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CACjE,OAAO;EAAE,WAAW;EAAO,YAAY;EAAQ,MAAM,MAAM,IAAI;CAAE;AACnE;;;;;;AAOA,eAAsB,oBACpB,OACA,SACwB;CACxB,MAAM,EAAE,QAAQ,eAAe;CAC/B,QAAQ,eAAe;CACvB,aAAa,SAAS;CACtB,MAAM,OAAO,QAAQ,aAAa,QAAQ,OAAO,EAAE;CACnD,MAAM,WAAW,MAAM,MAAM,GAAG,KAAK,GAAG,sBAAsB,QAAQ;EACpE;EACA,OAAO;CACT,CAAC;CACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,uCAAuC,SAAS,OAAO,GAAG;CAC5E,MAAM,QAAQ,MAAM,iBAClB,UACA,+BACF;CAKA,IAJe,MAAM,KACnB,IAAI,WAAW,MAAM,OAAO,OAAO,OAAO,WAAW,KAAK,CAAC,IAC1D,UAAU,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAC/C,CAAC,CAAC,KAAK,EACH,MAAW,sBAAsB,QACnC,MAAM,IAAI,MAAM,6CAA6C;CAC/D,QAAQ,eAAe;CACvB,MAAM,EAAE,gBAAgB,oBAAoB,MAAM,OAChD;CAEF,MAAM,QAAQ,MAAM,gBAAgB,eAAe,IAAI;CACvD,QAAQ,eAAe;CACvB,MAAM,aAAa,MAAM,eAAe,kBAAkB,OAAO;EAC/D,aAAa;GAAE,kBAAkB,IAAI,WAAW,KAAK;GAAG,UAAU;EAAM;EACxE,aAAa;EACb,UAAU;CACZ,CAAC;CACD,IAAI;EACF,aAAa,YAAY;EACzB,MAAM,IAAI,SAAe,YAAY,WAAW,SAAS,CAAC,CAAC;EAC3D,QAAQ,eAAe;EACvB,MAAM,QAAQ,WAAW,OAAO,KAAK,CAAC,CAAC;EACvC,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,MACR,MAAM,SACF,qCACA,wDACN;EACF,OAAO,sBAAsB,MAAM,EAAE;CACvC,UAAU;EACR,WAAW,MAAM;CACnB;AACF;;;;;;AAOA,eAAsB,cACpB,OACA,QACA,MACe;CACf,IACE,CAAC,OAAO,cAAc,KAAK,KAC3B,CAAC,OAAO,cAAc,MAAM,KAC5B,QAAQ,KACR,SAAS,KACT,QAAQ,SAAS,YACjB,KAAK,WAAW,QAAQ,SAAS,GAEjC,MAAM,IAAI,MAAM,gCAAgC;CAClD,MAAM,SAAS,MAAc,SAAqB;EAChD,MAAM,SAAS,IAAI,WAAW,KAAK,SAAS,EAAE,GAC5C,OAAO,IAAI,SAAS,OAAO,MAAM;EACnC,KAAK,UAAU,GAAG,KAAK,MAAM;EAC7B,OAAO,IAAI,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC;EAC5C,OAAO,IAAI,MAAM,CAAC;EAClB,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;GACxC,OAAO,OAAO;GACd,KAAK,IAAI,MAAM,GAAG,MAAM,GAAG,OACzB,MAAO,QAAQ,KAAM,MAAM,IAAI,aAAa;EAChD;EACA,KAAK,UAAU,KAAK,SAAS,IAAI,MAAM,gBAAgB,CAAC;EACxD,OAAO;CACT;CACA,MAAM,yBAAS,IAAI,WAAW,EAAE,GAC9B,OAAO,IAAI,SAAS,OAAO,MAAM;CACnC,KAAK,UAAU,GAAG,KAAK;CACvB,KAAK,UAAU,GAAG,MAAM;CACxB,OAAO,KAAK;CACZ,OAAO,KAAK;CACZ,MAAM,OAAO,IAAI,YAAY,QAAQ,IAAI,KAAK,MAAM;CACpD,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAC1B,KAAK,IACH,KAAK,SAAS,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,CAAC,GAChD,KAAK,QAAQ,IAAI,KAAK,CACxB;CACF,MAAM,aAAa,IAAI,WACrB,MAAM,IAAI,SACR,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,IAAI,kBAAkB,SAAS,CAAC,CACxE,CAAC,CAAC,YAAY,CAChB;CACA,OAAO,IAAI,KACT;EACE,IAAI,WAAW;GAAC;GAAK;GAAI;GAAI;GAAI;GAAI;GAAI;GAAI;EAAE,CAAC;EAChD,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,UAAU;EACxB,MAAM,wBAAQ,IAAI,WAAW,CAAC;CAChC,GACA,EAAE,MAAM,YAAY,CACtB;AACF"}