@happyvertical/smrt-images 0.55.9 → 0.55.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +224 -2
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -1
- package/dist/photo-cutout-coordinate-guide.d.ts +7 -0
- package/dist/photo-cutout-coordinate-guide.d.ts.map +1 -0
- package/dist/photo-cutout-profile-store.d.ts +76 -0
- package/dist/photo-cutout-profile-store.d.ts.map +1 -0
- package/dist/photo-cutout-setup.d.ts +48 -0
- package/dist/photo-cutout-setup.d.ts.map +1 -0
- package/dist/photo-cutout-setup.js +145 -0
- package/dist/photo-cutout-setup.js.map +1 -0
- package/dist/smrt-knowledge.json +6 -3
- package/dist/svelte/components/PhotoCutoutSetup.svelte +432 -0
- package/dist/svelte/components/PhotoCutoutSetup.svelte.d.ts +24 -0
- package/dist/svelte/components/PhotoCutoutSetup.svelte.d.ts.map +1 -0
- package/dist/svelte/head-isolation.d.ts +13 -0
- package/dist/svelte/head-isolation.d.ts.map +1 -0
- package/dist/svelte/head-isolation.js +83 -0
- package/dist/svelte/i18n.d.ts +18 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +19 -0
- package/dist/svelte/index.d.ts +2 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +2 -1
- package/package.json +11 -9
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-images",
|
|
5
|
-
"packageVersion": "0.55.
|
|
5
|
+
"packageVersion": "0.55.11",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-images:Image": {
|
|
8
8
|
"name": "image",
|
|
@@ -916,6 +916,7 @@
|
|
|
916
916
|
"smrtDependencies": [
|
|
917
917
|
"@happyvertical/smrt-assets",
|
|
918
918
|
"@happyvertical/smrt-core",
|
|
919
|
+
"@happyvertical/smrt-profiles",
|
|
919
920
|
"@happyvertical/smrt-prompts",
|
|
920
921
|
"@happyvertical/smrt-tenancy"
|
|
921
922
|
],
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Produces a transient grid-overlay copy for a vision request. The source
|
|
3
|
+
* image remains the asset used by the browser cutout; this guide is never
|
|
4
|
+
* saved or returned to a client.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createPhotoCutoutCoordinateGuide(sourceDataUrl: string, width: number, height: number): Promise<string>;
|
|
7
|
+
//# sourceMappingURL=photo-cutout-coordinate-guide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"photo-cutout-coordinate-guide.d.ts","sourceRoot":"","sources":["../src/photo-cutout-coordinate-guide.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAsB,gCAAgC,CACpD,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAqCjB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type PhotoCutoutRig } from '@happyvertical/animation';
|
|
2
|
+
import type { Asset, AssetRuntimeLike } from '@happyvertical/smrt-assets';
|
|
3
|
+
/** Stable rig key resolved by the host to the persisted asset bytes. */
|
|
4
|
+
export declare const PHOTO_CUTOUT_PERSISTED_ASSET_REF = "character_cutout";
|
|
5
|
+
export interface PhotoCutoutProfileOwner {
|
|
6
|
+
id?: string;
|
|
7
|
+
tenantId?: string | null;
|
|
8
|
+
getAssets(relationship?: string): Promise<Asset[]>;
|
|
9
|
+
addAsset(asset: Asset, relationship?: string, sortOrder?: number): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface PhotoCutoutProfileStoreAuthorization {
|
|
12
|
+
actorProfileId: string;
|
|
13
|
+
profile: PhotoCutoutProfileOwner;
|
|
14
|
+
tenantId: string | null;
|
|
15
|
+
operation: 'load' | 'save';
|
|
16
|
+
}
|
|
17
|
+
export interface PhotoCutoutProfileStoreOptions {
|
|
18
|
+
runtime: AssetRuntimeLike;
|
|
19
|
+
getProfile(profileId: string): Promise<PhotoCutoutProfileOwner | null>;
|
|
20
|
+
/** The host verifies its authenticated principal and any app-specific grants. */
|
|
21
|
+
authorize(request: PhotoCutoutProfileStoreAuthorization): Promise<boolean> | boolean;
|
|
22
|
+
now?: () => Date;
|
|
23
|
+
}
|
|
24
|
+
export interface SavePhotoCutoutProfileSetupInput {
|
|
25
|
+
actorProfileId: string;
|
|
26
|
+
profileId: string;
|
|
27
|
+
tenantId: string | null;
|
|
28
|
+
png: Buffer;
|
|
29
|
+
rig: PhotoCutoutRig;
|
|
30
|
+
name?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface LoadPhotoCutoutProfileSetupInput {
|
|
33
|
+
actorProfileId: string;
|
|
34
|
+
profileId: string;
|
|
35
|
+
tenantId: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* A photo-cutout asset already linked to this profile. Omitting this keeps
|
|
38
|
+
* the original behavior of loading the most recently saved setup.
|
|
39
|
+
*/
|
|
40
|
+
assetId?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PersistedPhotoCutoutSetup {
|
|
43
|
+
assetId: string;
|
|
44
|
+
rig: PhotoCutoutRig;
|
|
45
|
+
png: Buffer;
|
|
46
|
+
savedAt: string;
|
|
47
|
+
}
|
|
48
|
+
/** Metadata for an authorized saved setup, without reading its PNG bytes. */
|
|
49
|
+
export interface SavedPhotoCutoutProfileSetup {
|
|
50
|
+
assetId: string;
|
|
51
|
+
savedAt: string;
|
|
52
|
+
name?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Persists one profile-owned photographic character setup using the canonical
|
|
56
|
+
* AssetRuntime bytes and ProfileAsset link. Authentication stays with the host;
|
|
57
|
+
* this service refuses cross-profile and cross-tenant calls before it writes.
|
|
58
|
+
*/
|
|
59
|
+
export declare class PhotoCutoutProfileStore {
|
|
60
|
+
private readonly options;
|
|
61
|
+
private readonly now;
|
|
62
|
+
constructor(options: PhotoCutoutProfileStoreOptions);
|
|
63
|
+
save(input: SavePhotoCutoutProfileSetupInput): Promise<PersistedPhotoCutoutSetup>;
|
|
64
|
+
load(input: LoadPhotoCutoutProfileSetupInput): Promise<PersistedPhotoCutoutSetup | null>;
|
|
65
|
+
private loadSelected;
|
|
66
|
+
/**
|
|
67
|
+
* Lists valid, profile-linked saved setups without fetching their image
|
|
68
|
+
* bytes. Authorization deliberately follows `load`: gallery visibility is
|
|
69
|
+
* the same protected read as selecting a setup.
|
|
70
|
+
*/
|
|
71
|
+
list(input: LoadPhotoCutoutProfileSetupInput): Promise<SavedPhotoCutoutProfileSetup[]>;
|
|
72
|
+
private readSaved;
|
|
73
|
+
private readLinkedCandidates;
|
|
74
|
+
private requireAuthorizedProfile;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=photo-cutout-profile-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"photo-cutout-profile-store.d.ts","sourceRoot":"","sources":["../src/photo-cutout-profile-store.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAK1E,wEAAwE;AACxE,eAAO,MAAM,gCAAgC,qBAAqB,CAAC;AAEnE,MAAM,WAAW,uBAAuB;IACtC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACnD,QAAQ,CACN,KAAK,EAAE,KAAK,EACZ,YAAY,CAAC,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAED,MAAM,WAAW,oCAAoC;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,uBAAuB,CAAC;IACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IACvE,iFAAiF;IACjF,SAAS,CACP,OAAO,EAAE,oCAAoC,GAC5C,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,gCAAgC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gCAAgC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAUD;;;;GAIG;AACH,qBAAa,uBAAuB;IAGtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;gBAEJ,OAAO,EAAE,8BAA8B;IAI9D,IAAI,CACR,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,yBAAyB,CAAC;IA4D/B,IAAI,CACR,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;YAc9B,YAAY;IAe1B;;;;OAIG;IACG,IAAI,CACR,KAAK,EAAE,gCAAgC,GACtC,OAAO,CAAC,4BAA4B,EAAE,CAAC;YAW5B,SAAS;IA0BvB,OAAO,CAAC,oBAAoB;YAmBd,wBAAwB;CAqBvC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type PhotoCutoutRig } from '@happyvertical/animation';
|
|
2
|
+
/** A small, server-safe boundary for untrusted vision-model JSON. */
|
|
3
|
+
export interface PhotoCutoutSetupInput {
|
|
4
|
+
assetId: string;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface FaceOutline {
|
|
9
|
+
points: Array<{
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export interface MouthLandmarks {
|
|
15
|
+
mouthLeft: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
};
|
|
19
|
+
mouthRight: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
chin: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Validate a normalized (0..1000) closed face silhouette from the vision pass. */
|
|
29
|
+
export declare function parseFaceOutline(response: string | unknown): FaceOutline;
|
|
30
|
+
/** Build the Canadian horizontal split from real cropped-face landmarks. */
|
|
31
|
+
export declare function assembleCanadianSplitRig(input: {
|
|
32
|
+
outline: FaceOutline;
|
|
33
|
+
landmarks: MouthLandmarks;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
assetId: string;
|
|
37
|
+
}): PhotoCutoutRig;
|
|
38
|
+
export declare function faceOutlinePrompt(): string;
|
|
39
|
+
export declare function parseMouthLandmarks(response: string | unknown): MouthLandmarks;
|
|
40
|
+
export declare function mouthLandmarksPrompt(): string;
|
|
41
|
+
/**
|
|
42
|
+
* Parses a model response and binds every image layer to the authorized source
|
|
43
|
+
* image. The animation validator is the final geometry and bounds authority.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parsePhotoCutoutSetup(response: string | unknown, source: PhotoCutoutSetupInput): PhotoCutoutRig;
|
|
46
|
+
/** Deliberately specific instructions reduce geometry ambiguity from vision models. */
|
|
47
|
+
export declare function photoCutoutSetupPrompt(source: PhotoCutoutSetupInput): string;
|
|
48
|
+
//# sourceMappingURL=photo-cutout-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"photo-cutout-setup.d.ts","sourceRoot":"","sources":["../src/photo-cutout-setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAIpB,MAAM,0BAA0B,CAAC;AAElC,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzC;AACD,MAAM,WAAW,cAAc;IAC7B,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,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAgCxE;AAED,4EAA4E;AAC5E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,cAAc,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,cAAc,CAsDjB;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,GAAG,OAAO,GACzB,cAAc,CAkChB;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AASD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,OAAO,EAC1B,MAAM,EAAE,qBAAqB,GAC5B,cAAc,CA2ChB;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAI5E"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { PhotoCutoutValidationError, clipPhotoCutoutOutlineBelowLine, validatePhotoCutoutOutline, validatePhotoCutoutRig } from "@happyvertical/animation";
|
|
2
|
+
//#region src/photo-cutout-setup.ts
|
|
3
|
+
function parseFaceOutline(response) {
|
|
4
|
+
const points = record(typeof response === "string" ? JSON.parse(response) : response).points;
|
|
5
|
+
if (!Array.isArray(points) || points.length < 5 || points.length > 64) throw new PhotoCutoutValidationError("face outline needs 5-64 points");
|
|
6
|
+
return { points: validatePhotoCutoutOutline(points.map((point) => {
|
|
7
|
+
const p = record(point);
|
|
8
|
+
const x = p.x;
|
|
9
|
+
const y = p.y;
|
|
10
|
+
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x > 1e3 || y > 1e3) throw new PhotoCutoutValidationError("face outline point is outside normalized bounds");
|
|
11
|
+
return {
|
|
12
|
+
x,
|
|
13
|
+
y
|
|
14
|
+
};
|
|
15
|
+
}), {
|
|
16
|
+
width: 1e3,
|
|
17
|
+
height: 1e3,
|
|
18
|
+
minPoints: 5
|
|
19
|
+
}) };
|
|
20
|
+
}
|
|
21
|
+
function assembleCanadianSplitRig(input) {
|
|
22
|
+
const seam = (input.landmarks.mouthLeft.y + input.landmarks.mouthRight.y) / 2;
|
|
23
|
+
const lower = clipPhotoCutoutOutlineBelowLine(input.outline.points, {
|
|
24
|
+
width: 1e3,
|
|
25
|
+
height: 1e3,
|
|
26
|
+
y: seam,
|
|
27
|
+
minPoints: 3
|
|
28
|
+
});
|
|
29
|
+
const scale = (p) => ({
|
|
30
|
+
x: p.x / 1e3 * input.width,
|
|
31
|
+
y: p.y / 1e3 * input.height
|
|
32
|
+
});
|
|
33
|
+
const head = input.outline.points.map(scale);
|
|
34
|
+
const jaw = lower.map(scale);
|
|
35
|
+
const gap = Math.max(2, Math.round((input.landmarks.chin.y - seam) / 1e3 * input.height * .36));
|
|
36
|
+
const canvasHeight = input.height + gap;
|
|
37
|
+
const pivot = scale({
|
|
38
|
+
x: (input.landmarks.mouthLeft.x + input.landmarks.mouthRight.x) / 2,
|
|
39
|
+
y: seam
|
|
40
|
+
});
|
|
41
|
+
const rig = {
|
|
42
|
+
rigKind: "photo-cutout",
|
|
43
|
+
version: 1,
|
|
44
|
+
id: "portrait",
|
|
45
|
+
ariaLabel: "Photographic Canadian character",
|
|
46
|
+
canvas: {
|
|
47
|
+
width: input.width,
|
|
48
|
+
height: canvasHeight
|
|
49
|
+
},
|
|
50
|
+
layers: [{
|
|
51
|
+
id: "head",
|
|
52
|
+
assetId: input.assetId,
|
|
53
|
+
role: "head",
|
|
54
|
+
clip: head,
|
|
55
|
+
imageFrame: {
|
|
56
|
+
x: 0,
|
|
57
|
+
y: 0,
|
|
58
|
+
width: input.width,
|
|
59
|
+
height: input.height
|
|
60
|
+
}
|
|
61
|
+
}, {
|
|
62
|
+
id: "jaw",
|
|
63
|
+
assetId: input.assetId,
|
|
64
|
+
role: "jaw",
|
|
65
|
+
clip: jaw,
|
|
66
|
+
imageFrame: {
|
|
67
|
+
x: 0,
|
|
68
|
+
y: 0,
|
|
69
|
+
width: input.width,
|
|
70
|
+
height: input.height
|
|
71
|
+
}
|
|
72
|
+
}],
|
|
73
|
+
jaw: {
|
|
74
|
+
layerId: "jaw",
|
|
75
|
+
pivot,
|
|
76
|
+
maxOpenDegrees: 0,
|
|
77
|
+
maxOpenOffset: {
|
|
78
|
+
x: 0,
|
|
79
|
+
y: gap
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
validatePhotoCutoutRig(rig);
|
|
84
|
+
return rig;
|
|
85
|
+
}
|
|
86
|
+
function faceOutlinePrompt() {
|
|
87
|
+
return "Return JSON only: {\"points\":[{\"x\":number,\"y\":number},...]}. The first image is the original portrait. The second is the same original pixels with a labeled coordinate grid: labels 0..1000 map directly to this response’s x and y coordinates. Use that grid to place every point. Trace the visible head-and-face silhouette tightly in clockwise order, starting at the highest visible hair point. Include an actual point at the outermost left hair/ear edge, outermost right hair/ear edge, and lowest chin point; trace every visible hair, ear, cheek, and chin boundary between them without jumping through background. Exclude background beside hair and ears. Use 24-48 points where needed for curved hair/ear contours. This is an approximate closed silhouette for transparent background removal, not fine-hair matting and never a rectangular crop. No prose or extra keys.";
|
|
88
|
+
}
|
|
89
|
+
function parseMouthLandmarks(response) {
|
|
90
|
+
const root = record(typeof response === "string" ? JSON.parse(response) : response);
|
|
91
|
+
const point = (name) => {
|
|
92
|
+
const p = record(root[name]);
|
|
93
|
+
const x = p.x;
|
|
94
|
+
const y = p.y;
|
|
95
|
+
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x > 1e3 || y < 0 || y > 1e3) throw new PhotoCutoutValidationError(`${name} is outside normalized bounds`);
|
|
96
|
+
return {
|
|
97
|
+
x,
|
|
98
|
+
y
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const mouthLeft = point("mouthLeft");
|
|
102
|
+
const mouthRight = point("mouthRight");
|
|
103
|
+
const chin = point("chin");
|
|
104
|
+
if (mouthLeft.x >= mouthRight.x || chin.y <= Math.max(mouthLeft.y, mouthRight.y)) throw new PhotoCutoutValidationError("mouth and chin landmarks are anatomically invalid");
|
|
105
|
+
return {
|
|
106
|
+
mouthLeft,
|
|
107
|
+
mouthRight,
|
|
108
|
+
chin
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function mouthLandmarksPrompt() {
|
|
112
|
+
return "Return JSON only: {\"mouthLeft\":{\"x\":number,\"y\":number},\"mouthRight\":{\"x\":number,\"y\":number},\"chin\":{\"x\":number,\"y\":number}}. Analyze this transparent cropped face. Coordinates are normalized 0..1000 in this cropped image. mouthLeft and mouthRight are the actual outer lip corners; chin is the center lowest chin point. Do not return polygons, a rig, or prose.";
|
|
113
|
+
}
|
|
114
|
+
function record(value) {
|
|
115
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new PhotoCutoutValidationError("setup response must be an object");
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function parsePhotoCutoutSetup(response, source) {
|
|
119
|
+
const candidate = typeof response === "string" ? JSON.parse(response) : response;
|
|
120
|
+
const rig = record(candidate).rig ?? candidate;
|
|
121
|
+
const rigRecord = record(rig);
|
|
122
|
+
const canvas = record(rigRecord.canvas);
|
|
123
|
+
if (canvas.width !== source.width || canvas.height !== source.height) throw new PhotoCutoutValidationError("setup canvas does not match source image");
|
|
124
|
+
const layers = rigRecord.layers;
|
|
125
|
+
if (!Array.isArray(layers)) throw new PhotoCutoutValidationError("setup layers are required");
|
|
126
|
+
for (const layer of layers) {
|
|
127
|
+
const layerRecord = record(layer);
|
|
128
|
+
if (layerRecord.kind !== "solid" && layerRecord.assetId !== source.assetId) throw new PhotoCutoutValidationError("setup may reference only the authorized source asset");
|
|
129
|
+
}
|
|
130
|
+
const jaw = layers.find((layer) => record(layer).role === "jaw");
|
|
131
|
+
const mouth = layers.find((layer) => record(layer).role === "mouth-interior" && record(layer).kind === "solid");
|
|
132
|
+
if (!jaw || !mouth) throw new PhotoCutoutValidationError("setup requires jaw and mouth-interior layers");
|
|
133
|
+
record(mouth).clip = record(jaw).clip;
|
|
134
|
+
validatePhotoCutoutRig(rig);
|
|
135
|
+
return rig;
|
|
136
|
+
}
|
|
137
|
+
function photoCutoutSetupPrompt(source) {
|
|
138
|
+
return `Analyze the supplied portrait and return JSON only with this exact shape:
|
|
139
|
+
{"rig":{"rigKind":"photo-cutout","version":1,"id":"portrait","ariaLabel":"Photographic portrait","canvas":{"width":${source.width},"height":${source.height}},"layers":[{"id":"head","assetId":"${source.assetId}","role":"head","clip":[{"x":10,"y":10},{"x":90,"y":10},{"x":90,"y":110},{"x":10,"y":110}]},{"id":"mouth","kind":"solid","role":"mouth-interior","color":{"r":30,"g":10,"b":10},"clip":[{"x":40,"y":70},{"x":60,"y":70},{"x":50,"y":82}]},{"id":"jaw","assetId":"${source.assetId}","role":"jaw","clip":[{"x":30,"y":65},{"x":70,"y":65},{"x":70,"y":105},{"x":30,"y":105}]}],"jaw":{"layerId":"jaw","pivot":{"x":50,"y":65},"maxOpenDegrees":12}}}
|
|
140
|
+
Use the example only as a schema: scale all numeric coordinates to the actual ${source.width}x${source.height} image. Image layers use assetId "${source.assetId}"; the only solid layer is mouth-interior and uses integer RGB. Create a cutout head polygon, jaw polygon, and visible mouth interior from the photographed face. The jaw must be an inset lower-face polygon: leave at least 2% of head width/height between every jaw or pivot point and the head boundary. Place mouth entirely inside jaw. Every polygon must be simple, have at least three points, remain in the canvas, and the jaw/mouth must be inside the head. Do not include URLs, SVG paths, markup, transforms, prose, or keys not shown.`;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { assembleCanadianSplitRig, faceOutlinePrompt, mouthLandmarksPrompt, parseFaceOutline, parseMouthLandmarks, parsePhotoCutoutSetup, photoCutoutSetupPrompt };
|
|
144
|
+
|
|
145
|
+
//# sourceMappingURL=photo-cutout-setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"photo-cutout-setup.js","names":[],"sources":["../src/photo-cutout-setup.ts"],"sourcesContent":["import {\n clipPhotoCutoutOutlineBelowLine,\n type PhotoCutoutRig,\n PhotoCutoutValidationError,\n validatePhotoCutoutOutline,\n validatePhotoCutoutRig,\n} from '@happyvertical/animation';\n\n/** A small, server-safe boundary for untrusted vision-model JSON. */\nexport interface PhotoCutoutSetupInput {\n assetId: string;\n width: number;\n height: number;\n}\n\nexport interface FaceOutline {\n points: Array<{ x: number; y: number }>;\n}\nexport interface MouthLandmarks {\n mouthLeft: { x: number; y: number };\n mouthRight: { x: number; y: number };\n chin: { x: number; y: number };\n}\n\n/** Validate a normalized (0..1000) closed face silhouette from the vision pass. */\nexport function parseFaceOutline(response: string | unknown): FaceOutline {\n const value = typeof response === 'string' ? JSON.parse(response) : response;\n const points = record(value).points;\n if (!Array.isArray(points) || points.length < 5 || points.length > 64) {\n throw new PhotoCutoutValidationError('face outline needs 5-64 points');\n }\n const parsed = points.map((point) => {\n const p = record(point);\n const x = p.x;\n const y = p.y;\n if (\n typeof x !== 'number' ||\n typeof y !== 'number' ||\n !Number.isFinite(x) ||\n !Number.isFinite(y) ||\n x < 0 ||\n y < 0 ||\n x > 1000 ||\n y > 1000\n )\n throw new PhotoCutoutValidationError(\n 'face outline point is outside normalized bounds',\n );\n return { x, y };\n });\n return {\n points: validatePhotoCutoutOutline(parsed, {\n width: 1000,\n height: 1000,\n minPoints: 5,\n }),\n };\n}\n\n/** Build the Canadian horizontal split from real cropped-face landmarks. */\nexport function assembleCanadianSplitRig(input: {\n outline: FaceOutline;\n landmarks: MouthLandmarks;\n width: number;\n height: number;\n assetId: string;\n}): PhotoCutoutRig {\n const seam = (input.landmarks.mouthLeft.y + input.landmarks.mouthRight.y) / 2;\n const lower = clipPhotoCutoutOutlineBelowLine(input.outline.points, {\n width: 1000,\n height: 1000,\n y: seam,\n minPoints: 3,\n });\n const scale = (p: { x: number; y: number }) => ({\n x: (p.x / 1000) * input.width,\n y: (p.y / 1000) * input.height,\n });\n const head = input.outline.points.map(scale);\n const jaw = lower.map(scale);\n const gap = Math.max(\n 2,\n Math.round(((input.landmarks.chin.y - seam) / 1000) * input.height * 0.36),\n );\n const canvasHeight = input.height + gap;\n const pivot = scale({\n x: (input.landmarks.mouthLeft.x + input.landmarks.mouthRight.x) / 2,\n y: seam,\n });\n const rig: PhotoCutoutRig = {\n rigKind: 'photo-cutout',\n version: 1,\n id: 'portrait',\n ariaLabel: 'Photographic Canadian character',\n canvas: { width: input.width, height: canvasHeight },\n layers: [\n {\n id: 'head',\n assetId: input.assetId,\n role: 'head',\n clip: head,\n imageFrame: { x: 0, y: 0, width: input.width, height: input.height },\n },\n {\n id: 'jaw',\n assetId: input.assetId,\n role: 'jaw',\n clip: jaw,\n imageFrame: { x: 0, y: 0, width: input.width, height: input.height },\n },\n ],\n jaw: {\n layerId: 'jaw',\n pivot,\n maxOpenDegrees: 0,\n maxOpenOffset: { x: 0, y: gap },\n },\n };\n validatePhotoCutoutRig(rig);\n return rig;\n}\n\nexport function faceOutlinePrompt(): string {\n return 'Return JSON only: {\"points\":[{\"x\":number,\"y\":number},...]}. The first image is the original portrait. The second is the same original pixels with a labeled coordinate grid: labels 0..1000 map directly to this response’s x and y coordinates. Use that grid to place every point. Trace the visible head-and-face silhouette tightly in clockwise order, starting at the highest visible hair point. Include an actual point at the outermost left hair/ear edge, outermost right hair/ear edge, and lowest chin point; trace every visible hair, ear, cheek, and chin boundary between them without jumping through background. Exclude background beside hair and ears. Use 24-48 points where needed for curved hair/ear contours. This is an approximate closed silhouette for transparent background removal, not fine-hair matting and never a rectangular crop. No prose or extra keys.';\n}\n\nexport function parseMouthLandmarks(\n response: string | unknown,\n): MouthLandmarks {\n const root = record(\n typeof response === 'string' ? JSON.parse(response) : response,\n );\n const point = (name: 'mouthLeft' | 'mouthRight' | 'chin') => {\n const p = record(root[name]);\n const x = p.x;\n const y = p.y;\n if (\n typeof x !== 'number' ||\n typeof y !== 'number' ||\n !Number.isFinite(x) ||\n !Number.isFinite(y) ||\n x < 0 ||\n x > 1000 ||\n y < 0 ||\n y > 1000\n )\n throw new PhotoCutoutValidationError(\n `${name} is outside normalized bounds`,\n );\n return { x, y };\n };\n const mouthLeft = point('mouthLeft');\n const mouthRight = point('mouthRight');\n const chin = point('chin');\n if (\n mouthLeft.x >= mouthRight.x ||\n chin.y <= Math.max(mouthLeft.y, mouthRight.y)\n )\n throw new PhotoCutoutValidationError(\n 'mouth and chin landmarks are anatomically invalid',\n );\n return { mouthLeft, mouthRight, chin };\n}\n\nexport function mouthLandmarksPrompt(): string {\n return 'Return JSON only: {\"mouthLeft\":{\"x\":number,\"y\":number},\"mouthRight\":{\"x\":number,\"y\":number},\"chin\":{\"x\":number,\"y\":number}}. Analyze this transparent cropped face. Coordinates are normalized 0..1000 in this cropped image. mouthLeft and mouthRight are the actual outer lip corners; chin is the center lowest chin point. Do not return polygons, a rig, or prose.';\n}\n\nfunction record(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new PhotoCutoutValidationError('setup response must be an object');\n }\n return value as Record<string, unknown>;\n}\n\n/**\n * Parses a model response and binds every image layer to the authorized source\n * image. The animation validator is the final geometry and bounds authority.\n */\nexport function parsePhotoCutoutSetup(\n response: string | unknown,\n source: PhotoCutoutSetupInput,\n): PhotoCutoutRig {\n const candidate =\n typeof response === 'string' ? JSON.parse(response) : response;\n const root = record(candidate);\n const rig = (root.rig ?? candidate) as unknown;\n const rigRecord = record(rig);\n const canvas = record(rigRecord.canvas);\n if (canvas.width !== source.width || canvas.height !== source.height) {\n throw new PhotoCutoutValidationError(\n 'setup canvas does not match source image',\n );\n }\n const layers = rigRecord.layers;\n if (!Array.isArray(layers)) {\n throw new PhotoCutoutValidationError('setup layers are required');\n }\n for (const layer of layers) {\n const layerRecord = record(layer);\n if (\n layerRecord.kind !== 'solid' &&\n layerRecord.assetId !== source.assetId\n ) {\n throw new PhotoCutoutValidationError(\n 'setup may reference only the authorized source asset',\n );\n }\n }\n // The solid mouth is painted beneath the photographic jaw. Making its clip\n // identical to the jaw guarantees it cannot appear through a neutral jaw;\n // the runtime reveals it only as the jaw rotates open.\n const jaw = layers.find((layer) => record(layer).role === 'jaw');\n const mouth = layers.find(\n (layer) =>\n record(layer).role === 'mouth-interior' && record(layer).kind === 'solid',\n );\n if (!jaw || !mouth) {\n throw new PhotoCutoutValidationError(\n 'setup requires jaw and mouth-interior layers',\n );\n }\n record(mouth).clip = record(jaw).clip;\n validatePhotoCutoutRig(rig);\n return rig;\n}\n\n/** Deliberately specific instructions reduce geometry ambiguity from vision models. */\nexport function photoCutoutSetupPrompt(source: PhotoCutoutSetupInput): string {\n return `Analyze the supplied portrait and return JSON only with this exact shape:\n{\"rig\":{\"rigKind\":\"photo-cutout\",\"version\":1,\"id\":\"portrait\",\"ariaLabel\":\"Photographic portrait\",\"canvas\":{\"width\":${source.width},\"height\":${source.height}},\"layers\":[{\"id\":\"head\",\"assetId\":\"${source.assetId}\",\"role\":\"head\",\"clip\":[{\"x\":10,\"y\":10},{\"x\":90,\"y\":10},{\"x\":90,\"y\":110},{\"x\":10,\"y\":110}]},{\"id\":\"mouth\",\"kind\":\"solid\",\"role\":\"mouth-interior\",\"color\":{\"r\":30,\"g\":10,\"b\":10},\"clip\":[{\"x\":40,\"y\":70},{\"x\":60,\"y\":70},{\"x\":50,\"y\":82}]},{\"id\":\"jaw\",\"assetId\":\"${source.assetId}\",\"role\":\"jaw\",\"clip\":[{\"x\":30,\"y\":65},{\"x\":70,\"y\":65},{\"x\":70,\"y\":105},{\"x\":30,\"y\":105}]}],\"jaw\":{\"layerId\":\"jaw\",\"pivot\":{\"x\":50,\"y\":65},\"maxOpenDegrees\":12}}}\nUse the example only as a schema: scale all numeric coordinates to the actual ${source.width}x${source.height} image. Image layers use assetId \"${source.assetId}\"; the only solid layer is mouth-interior and uses integer RGB. Create a cutout head polygon, jaw polygon, and visible mouth interior from the photographed face. The jaw must be an inset lower-face polygon: leave at least 2% of head width/height between every jaw or pivot point and the head boundary. Place mouth entirely inside jaw. Every polygon must be simple, have at least three points, remain in the canvas, and the jaw/mouth must be inside the head. Do not include URLs, SVG paths, markup, transforms, prose, or keys not shown.`;\n}\n"],"mappings":"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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-images",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.11",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "bd59a52b8e9de6b97339e62c3830231b294c0e7b33f22195dd3a9b4a82af09a7",
|
|
11
|
+
"packageJson": "a3e9432980108b7fcdd48a02d04a560a507928aefba1ec05014925797e6e888b",
|
|
12
12
|
"agents": "82a1905b76f49d9a852bf1e4f591595ae596c5cfa14ec736dc6f8fe32200fd8c"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./workbench"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@happyvertical/animation": "0.5.0",
|
|
24
25
|
"@happyvertical/ai": "catalog:",
|
|
25
26
|
"@happyvertical/images": "catalog:",
|
|
26
27
|
"@happyvertical/smrt-assets": "workspace:*",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"jimp": "catalog:",
|
|
34
35
|
"sharp": "catalog:",
|
|
35
36
|
"@happyvertical/smrt-playground": "workspace:*",
|
|
37
|
+
"@happyvertical/smrt-profiles": "workspace:*",
|
|
36
38
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
37
39
|
"@happyvertical/sql": "catalog:",
|
|
38
40
|
"@sveltejs/kit": "2.69.1",
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"@happyvertical/smrt-assets",
|
|
50
52
|
"@happyvertical/smrt-core",
|
|
51
53
|
"@happyvertical/smrt-playground",
|
|
54
|
+
"@happyvertical/smrt-profiles",
|
|
52
55
|
"@happyvertical/smrt-prompts",
|
|
53
56
|
"@happyvertical/smrt-tenancy",
|
|
54
57
|
"@happyvertical/smrt-types",
|