음성 재생과 운영 배포 정리

This commit is contained in:
Yun Chan 2026-06-28 12:18:20 +09:00
parent 8ed185ce6c
commit ac7db95542
1020 changed files with 46863 additions and 2175 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View file

@ -0,0 +1,881 @@
{
"source": "D:\\workspace\\vignette\\docs\\avatar-art\\seoyeon\\live2d-psd-v2\\source-latudi-female-v2.psd",
"sourceCanvas": [
1080,
1920
],
"targetCanvas": [
900,
1125
],
"cropBox": [
0,
240,
1080,
1590
],
"neutralLayerOrder": [
"hair-back-base",
"hair-back-left",
"hair-back-right",
"body",
"outfit",
"ear-left",
"ear-right",
"face-base",
"blush-left",
"blush-right",
"eye-white-left",
"eye-white-right",
"iris-left",
"iris-right",
"pupil-left",
"pupil-right",
"highlight-left",
"highlight-right",
"lash-left",
"lash-right",
"eyelid-upper-left",
"eyelid-upper-right",
"eyelid-lower-left",
"eyelid-lower-right",
"brow-left",
"brow-right",
"nose",
"hair-side-left-1",
"hair-side-right-1",
"hair-side-left-2",
"hair-side-right-2",
"hair-bangs",
"mouth-neutral"
],
"sadLayerOrder": [
"hair-back-base",
"hair-back-left",
"hair-back-right",
"body",
"outfit",
"ear-left",
"ear-right",
"face-base",
"blush-left",
"blush-right",
"eye-white-left",
"eye-white-right",
"iris-left",
"iris-right",
"pupil-left",
"pupil-right",
"highlight-left",
"highlight-right",
"lash-left",
"lash-right",
"eyelid-upper-left",
"eyelid-upper-right",
"eyelid-lower-left",
"eyelid-lower-right",
"brow-sad-left",
"brow-sad-right",
"tear-left",
"tear-right",
"nose",
"hair-side-left-1",
"hair-side-right-1",
"hair-side-left-2",
"hair-side-right-2",
"hair-bangs",
"mouth-sad"
],
"parts": [
{
"id": "hair-back-left",
"source": "back hair left group",
"file": "hair-back-left.png",
"alphaBox": [
0,
0,
463,
1125
],
"originPercent": {
"x": 25.72,
"y": 8.0
}
},
{
"id": "hair-back-right",
"source": "back hair right group",
"file": "hair-back-right.png",
"alphaBox": [
437,
0,
900,
1125
],
"originPercent": {
"x": 74.28,
"y": 8.0
}
},
{
"id": "body",
"source": "body group",
"file": "body.png",
"alphaBox": [
339,
439,
605,
759
],
"originPercent": {
"x": 52.44,
"y": 53.24
}
},
{
"id": "neck",
"source": "neck group",
"file": "neck.png",
"alphaBox": [
339,
439,
605,
759
],
"originPercent": {
"x": 52.44,
"y": 53.24
}
},
{
"id": "clavicle",
"source": "clavicle group",
"file": "clavicle.png",
"alphaBox": [
368,
632,
560,
664
],
"originPercent": {
"x": 51.56,
"y": 57.6
}
},
{
"id": "outfit",
"source": "outfit group",
"file": "outfit.png",
"alphaBox": [
130,
521,
806,
1015
],
"originPercent": {
"x": 52.0,
"y": 68.27
}
},
{
"id": "outfit-inner",
"source": "inner outfit group",
"file": "outfit-inner.png",
"alphaBox": [
232,
521,
705,
1009
],
"originPercent": {
"x": 52.06,
"y": 68.0
}
},
{
"id": "outfit-outer",
"source": "outer outfit group",
"file": "outfit-outer.png",
"alphaBox": [
130,
541,
806,
1015
],
"originPercent": {
"x": 52.0,
"y": 69.16
}
},
{
"id": "ribbon",
"source": "ribbon group",
"file": "ribbon.png",
"alphaBox": [
368,
636,
570,
832
],
"originPercent": {
"x": 52.11,
"y": 65.24
}
},
{
"id": "face",
"source": "face group",
"file": "face.png",
"alphaBox": [
267,
32,
647,
533
],
"originPercent": {
"x": 50.78,
"y": 25.11
}
},
{
"id": "face-base",
"source": "face base group",
"file": "face-base.png",
"alphaBox": [
275,
32,
630,
533
],
"originPercent": {
"x": 50.28,
"y": 25.11
}
},
{
"id": "ear-left",
"source": "left ear group",
"file": "ear-left.png",
"alphaBox": [
267,
357,
344,
442
],
"originPercent": {
"x": 33.94,
"y": 35.51
}
},
{
"id": "ear-right",
"source": "right ear group",
"file": "ear-right.png",
"alphaBox": [
567,
359,
647,
442
],
"originPercent": {
"x": 67.44,
"y": 35.6
}
},
{
"id": "blush-left",
"source": "left blush group",
"file": "blush-left.png",
"alphaBox": [
297,
337,
445,
444
],
"originPercent": {
"x": 41.22,
"y": 34.71
}
},
{
"id": "blush-right",
"source": "right blush group",
"file": "blush-right.png",
"alphaBox": [
462,
332,
611,
441
],
"originPercent": {
"x": 59.61,
"y": 34.36
}
},
{
"id": "nose",
"source": "nose group",
"file": "nose.png",
"alphaBox": [
444,
410,
456,
426
],
"originPercent": {
"x": 50.0,
"y": 37.16
}
},
{
"id": "eye-left",
"source": "left eye group",
"file": "eye-left.png",
"alphaBox": [
293,
314,
419,
389
],
"originPercent": {
"x": 39.56,
"y": 31.24
}
},
{
"id": "eye-right",
"source": "right eye group",
"file": "eye-right.png",
"alphaBox": [
488,
307,
616,
380
],
"originPercent": {
"x": 61.33,
"y": 30.53
}
},
{
"id": "eye-white-left",
"source": "left eye white group",
"file": "eye-white-left.png",
"alphaBox": [
324,
339,
410,
382
],
"originPercent": {
"x": 40.78,
"y": 32.04
}
},
{
"id": "eye-white-right",
"source": "right eye white group",
"file": "eye-white-right.png",
"alphaBox": [
500,
330,
585,
373
],
"originPercent": {
"x": 60.28,
"y": 31.24
}
},
{
"id": "iris-left",
"source": "left iris group",
"file": "iris-left.png",
"alphaBox": [
346,
331,
401,
382
],
"originPercent": {
"x": 41.5,
"y": 31.69
}
},
{
"id": "iris-right",
"source": "right iris group",
"file": "iris-right.png",
"alphaBox": [
507,
324,
563,
374
],
"originPercent": {
"x": 59.44,
"y": 31.02
}
},
{
"id": "pupil-left",
"source": "left pupil group",
"file": "pupil-left.png",
"alphaBox": [
371,
348,
386,
367
],
"originPercent": {
"x": 42.06,
"y": 31.78
}
},
{
"id": "pupil-right",
"source": "right pupil group",
"file": "pupil-right.png",
"alphaBox": [
523,
341,
538,
360
],
"originPercent": {
"x": 58.94,
"y": 31.16
}
},
{
"id": "highlight-left",
"source": "left eye highlight group",
"file": "highlight-left.png",
"alphaBox": [
364,
347,
386,
376
],
"originPercent": {
"x": 41.67,
"y": 32.13
}
},
{
"id": "highlight-right",
"source": "right eye highlight group",
"file": "highlight-right.png",
"alphaBox": [
526,
339,
549,
369
],
"originPercent": {
"x": 59.72,
"y": 31.47
}
},
{
"id": "lash-left",
"source": "left lash group",
"file": "lash-left.png",
"alphaBox": [
321,
314,
419,
354
],
"originPercent": {
"x": 41.11,
"y": 29.69
}
},
{
"id": "lash-right",
"source": "right lash group",
"file": "lash-right.png",
"alphaBox": [
488,
307,
586,
349
],
"originPercent": {
"x": 59.67,
"y": 29.16
}
},
{
"id": "eyelid-upper-left",
"source": "left upper eyelid group",
"file": "eyelid-upper-left.png",
"alphaBox": [
293,
327,
415,
378
],
"originPercent": {
"x": 39.33,
"y": 31.33
}
},
{
"id": "eyelid-upper-right",
"source": "right upper eyelid group",
"file": "eyelid-upper-right.png",
"alphaBox": [
495,
317,
616,
368
],
"originPercent": {
"x": 61.72,
"y": 30.44
}
},
{
"id": "eyelid-lower-left",
"source": "left lower eyelid group",
"file": "eyelid-lower-left.png",
"alphaBox": [
320,
368,
412,
389
],
"originPercent": {
"x": 40.67,
"y": 33.64
}
},
{
"id": "eyelid-lower-right",
"source": "right lower eyelid group",
"file": "eyelid-lower-right.png",
"alphaBox": [
498,
357,
590,
380
],
"originPercent": {
"x": 60.44,
"y": 32.76
}
},
{
"id": "hair-front",
"source": "front hair group",
"file": "hair-front.png",
"alphaBox": [
212,
20,
692,
829
],
"originPercent": {
"x": 50.22,
"y": 37.73
}
},
{
"id": "hair-bangs",
"source": "bangs group",
"file": "hair-bangs.png",
"alphaBox": [
290,
52,
622,
386
],
"originPercent": {
"x": 50.67,
"y": 7.0
}
},
{
"id": "hair-side-left-1",
"source": "left side hair 1 group",
"file": "hair-side-left-1.png",
"alphaBox": [
212,
21,
457,
441
],
"originPercent": {
"x": 37.17,
"y": 6.35
}
},
{
"id": "hair-side-left-2",
"source": "left side hair 2 group",
"file": "hair-side-left-2.png",
"alphaBox": [
219,
299,
351,
813
],
"originPercent": {
"x": 31.67,
"y": 32.06
}
},
{
"id": "hair-side-right-1",
"source": "right side hair 1 group",
"file": "hair-side-right-1.png",
"alphaBox": [
447,
20,
692,
440
],
"originPercent": {
"x": 63.28,
"y": 6.26
}
},
{
"id": "hair-side-right-2",
"source": "right side hair 2 group",
"file": "hair-side-right-2.png",
"alphaBox": [
552,
315,
684,
829
],
"originPercent": {
"x": 68.67,
"y": 33.48
}
},
{
"id": "brow-left",
"source": "hidden neutral left brow",
"file": "brow-left.png",
"alphaBox": [
306,
291,
434,
314
],
"originPercent": {
"x": 41.11,
"y": 26.89
}
},
{
"id": "brow-right",
"source": "hidden neutral right brow",
"file": "brow-right.png",
"alphaBox": [
471,
285,
600,
302
],
"originPercent": {
"x": 59.5,
"y": 26.09
}
},
{
"id": "brow-sad-left",
"source": "v2 crying left brow",
"file": "brow-sad-left.png",
"alphaBox": [
290,
281,
430,
311
],
"originPercent": {
"x": 40.0,
"y": 26.31
}
},
{
"id": "brow-sad-right",
"source": "v2 crying right brow",
"file": "brow-sad-right.png",
"alphaBox": [
479,
283,
619,
313
],
"originPercent": {
"x": 61.0,
"y": 26.49
}
},
{
"id": "tear-left",
"source": "v2 left tear layer",
"file": "tear-left.png",
"alphaBox": [
320,
369,
416,
390
],
"originPercent": {
"x": 40.89,
"y": 33.73
}
},
{
"id": "tear-right",
"source": "v2 right tear layer",
"file": "tear-right.png",
"alphaBox": [
496,
362,
591,
381
],
"originPercent": {
"x": 60.39,
"y": 33.02
}
},
{
"id": "hair-back-base",
"source": "top extra back hair layers",
"file": "hair-back-base.png",
"alphaBox": null,
"originPercent": {
"x": 50.0,
"y": 50.0
}
},
{
"id": "mouth-neutral",
"source": "hidden smile mouth group",
"file": "mouth-neutral.png",
"alphaBox": [
426,
454,
493,
474
],
"originPercent": {
"x": 51.06,
"y": 41.24
}
},
{
"id": "mouth-sad",
"source": "v2 crying mouth group",
"file": "mouth-sad.png",
"alphaBox": [
424,
456,
490,
474
],
"originPercent": {
"x": 50.78,
"y": 41.33
}
},
{
"id": "mouth-open",
"source": "hidden open mouth groups",
"file": "mouth-open.png",
"alphaBox": [
400,
424,
519,
539
],
"originPercent": {
"x": 51.06,
"y": 42.8
}
},
{
"id": "mouth-warm",
"source": "derived from neutral mouth bbox",
"file": "mouth-warm.png",
"alphaBox": [
422,
453,
497,
473
],
"originPercent": {
"x": 51.06,
"y": 41.16
}
},
{
"id": "mouth-tired",
"source": "derived from sad mouth bbox",
"file": "mouth-tired.png",
"alphaBox": [
428,
458,
487,
473
],
"originPercent": {
"x": 50.83,
"y": 41.38
}
},
{
"id": "mouth-anxious",
"source": "derived from sad mouth bbox",
"file": "mouth-anxious.png",
"alphaBox": [
430,
458,
484,
473
],
"originPercent": {
"x": 50.78,
"y": 41.38
}
},
{
"id": "mouth-startled",
"source": "derived from mouth-open bbox",
"file": "mouth-startled.png",
"alphaBox": [
419,
439,
500,
522
],
"originPercent": {
"x": 51.06,
"y": 42.71
}
},
{
"id": "mouth-open-small",
"source": "derived from mouth-open bbox",
"file": "mouth-open-small.png",
"alphaBox": [
408,
445,
510,
516
],
"originPercent": {
"x": 51.0,
"y": 42.71
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

View file

@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""Export app-ready Live2D-style parts from the v2 PSD.
This reuses the PSB detailed-parts helpers but keeps the v2 PSD as a distinct
art set so previous assets remain auditable.
"""
from __future__ import annotations
import importlib.util
import json
import shutil
from pathlib import Path
from typing import Sequence
import numpy as np
from PIL import Image, ImageDraw
from psd_tools import PSDImage
from scipy import ndimage as ndi
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parents[3]
BASE_SCRIPT = HERE.parent / "live2d-psb" / "export_detailed_parts.py"
PSD = HERE / "source-latudi-female-v2.psd"
OUT = HERE / "detailed-parts"
PARTS = OUT / "parts"
APP_PARTS = REPO_ROOT / "apps" / "web" / "public" / "avatar" / "seoyeon-live2d-psd-v2" / "parts"
def load_base_module():
spec = importlib.util.spec_from_file_location("seoyeon_psb_export", BASE_SCRIPT)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load {BASE_SCRIPT}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.HERE = HERE
module.REPO_ROOT = REPO_ROOT
module.PSB = PSD
module.OUT = OUT
module.PARTS = PARTS
module.APP_PARTS = APP_PARTS
return module
B = load_base_module()
N = dict(B.N)
N.update(
{
"cry_brow_left": "좌 울상 눈썹 ",
"cry_brow_right": "우 울상 눈썹",
"closed_eye_left": "좌 감은눈",
"closed_eye_right": "우 감은눈",
"tears": "눈물",
"tear_left": "좌 눈물",
"tear_right": "우 눈물",
"cry_mouth": "우는 입",
}
)
NEUTRAL_LAYER_ORDER = [
"hair-back-base",
"hair-back-left",
"hair-back-right",
"body",
"outfit",
"ear-left",
"ear-right",
"face-base",
"blush-left",
"blush-right",
"eye-white-left",
"eye-white-right",
"iris-left",
"iris-right",
"pupil-left",
"pupil-right",
"highlight-left",
"highlight-right",
"lash-left",
"lash-right",
"eyelid-upper-left",
"eyelid-upper-right",
"eyelid-lower-left",
"eyelid-lower-right",
"brow-left",
"brow-right",
"nose",
"hair-side-left-1",
"hair-side-right-1",
"hair-side-left-2",
"hair-side-right-2",
"hair-bangs",
"mouth-neutral",
]
SAD_LAYER_ORDER = [
*NEUTRAL_LAYER_ORDER[:24],
"brow-sad-left",
"brow-sad-right",
"tear-left",
"tear-right",
"nose",
"hair-side-left-1",
"hair-side-right-1",
"hair-side-left-2",
"hair-side-right-2",
"hair-bangs",
"mouth-sad",
]
def visible(psd: PSDImage, root: Sequence[str], *, hidden: Sequence[Sequence[str]] = ()) -> Image.Image:
return B.composite_with_visibility(psd, root, visible_paths=[root], hidden_paths=hidden)
def enhance_tears(image: Image.Image) -> Image.Image:
arr = np.array(image.convert("RGBA")).astype(np.float32)
alpha = arr[:, :, 3]
spread = ndi.maximum_filter(alpha, size=3)
alpha = np.maximum(alpha, spread * 0.72)
alpha = np.clip(alpha * 1.32, 0, 255)
mask = alpha > 4
cool = np.array([226, 241, 255], dtype=np.float32)
arr[mask, :3] = arr[mask, :3] * 0.34 + cool * 0.66
arr[:, :, 3] = alpha
arr[arr[:, :, 3] < 1, :3] = 0
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8), "RGBA")
def save_enhanced_part(entries: list[dict[str, object]], part_id: str, source: str, image: Image.Image) -> Image.Image:
normalized = B.normalize(image) if image.size == B.SOURCE_CANVAS else image.convert("RGBA")
normalized = enhance_tears(normalized)
path = PARTS / f"{part_id}.png"
normalized.save(path, optimize=True)
box = B.alpha_bbox(normalized)
entries.append(
{
"id": part_id,
"source": source,
"file": path.name,
"alphaBox": list(box) if box else None,
"originPercent": B.origin_for(part_id, box),
}
)
return normalized
def make_preview(name: str, order: Sequence[str]) -> None:
preview = Image.new("RGBA", B.TARGET_CANVAS, (30, 39, 36, 255))
transparent = B.blank(B.TARGET_CANVAS)
for part_id in order:
layer = Image.open(PARTS / f"{part_id}.png").convert("RGBA")
preview.alpha_composite(layer)
transparent.alpha_composite(layer)
preview.save(OUT / f"preview-{name}.png", optimize=True)
transparent.save(OUT / f"preview-{name}-transparent.png", optimize=True)
if name == "neutral":
preview.save(OUT / "preview-detailed-parts.png", optimize=True)
transparent.save(OUT / "preview-detailed-parts-transparent.png", optimize=True)
def sync_to_app() -> None:
APP_PARTS.mkdir(parents=True, exist_ok=True)
for source in PARTS.glob("*.png"):
shutil.copy2(source, APP_PARTS / source.name)
def main() -> int:
if not PSD.exists():
raise FileNotFoundError(PSD)
OUT.mkdir(parents=True, exist_ok=True)
PARTS.mkdir(parents=True, exist_ok=True)
psd = PSDImage.open(PSD)
entries: list[dict[str, object]] = []
simple_parts: list[tuple[str, str, Sequence[str]]] = [
("hair-back-left", "back hair left group", [N["back_hair"], N["back_hair_left"]]),
("hair-back-right", "back hair right group", [N["back_hair"], N["back_hair_right"]]),
("body", "body group", [N["body"]]),
("neck", "neck group", [N["body"], N["neck"]]),
("clavicle", "clavicle group", [N["body"], N["clavicle"]]),
("outfit", "outfit group", [N["outfit"]]),
("outfit-inner", "inner outfit group", [N["outfit"], N["inner_outfit"]]),
("outfit-outer", "outer outfit group", [N["outfit"], N["outer_outfit"]]),
("ribbon", "ribbon group", [N["outfit"], N["ribbon"]]),
("face", "face group", [N["face"]]),
("face-base", "face base group", [N["face"], N["face_base"]]),
("ear-left", "left ear group", [N["face"], N["ear_left"]]),
("ear-right", "right ear group", [N["face"], N["ear_right"]]),
("blush-left", "left blush group", [N["face"], N["blush_left"]]),
("blush-right", "right blush group", [N["face"], N["blush_right"]]),
("nose", "nose group", [N["face"], N["nose"]]),
("eye-left", "left eye group", [N["eye_left"]]),
("eye-right", "right eye group", [N["eye_right"]]),
("eye-white-left", "left eye white group", [N["eye_left"], N["white"]]),
("eye-white-right", "right eye white group", [N["eye_right"], N["white_copy"]]),
("iris-left", "left iris group", [N["eye_left"], N["iris"]]),
("iris-right", "right iris group", [N["eye_right"], N["iris_copy"]]),
("pupil-left", "left pupil group", [N["eye_left"], N["pupil"]]),
("pupil-right", "right pupil group", [N["eye_right"], N["pupil_copy"]]),
("highlight-left", "left eye highlight group", [N["eye_left"], N["highlight"]]),
("highlight-right", "right eye highlight group", [N["eye_right"], N["highlight_copy"]]),
("lash-left", "left lash group", [N["eye_left"], N["lash"]]),
("lash-right", "right lash group", [N["eye_right"], N["lash_copy"]]),
("eyelid-upper-left", "left upper eyelid group", [N["eye_left"], N["upper_lid"]]),
("eyelid-upper-right", "right upper eyelid group", [N["eye_right"], N["upper_lid_copy"]]),
("eyelid-lower-left", "left lower eyelid group", [N["eye_left"], N["lower_lid"]]),
("eyelid-lower-right", "right lower eyelid group", [N["eye_right"], N["lower_lid_copy"]]),
("hair-front", "front hair group", [N["front_hair"]]),
("hair-bangs", "bangs group", [N["front_hair"], N["bangs"]]),
("hair-side-left-1", "left side hair 1 group", [N["front_hair"], N["side_left_1"]]),
("hair-side-left-2", "left side hair 2 group", [N["front_hair"], N["side_left_2"]]),
("hair-side-right-1", "right side hair 1 group", [N["front_hair"], N["side_right_1"]]),
("hair-side-right-2", "right side hair 2 group", [N["front_hair"], N["side_right_2"]]),
]
for part_id, source, path in simple_parts:
B.save_part(entries, part_id, source, B.composite_path(psd, path))
B.save_part(entries, "brow-left", "hidden neutral left brow", visible(psd, [N["eye_left"], N["brow"]]))
B.save_part(entries, "brow-right", "hidden neutral right brow", visible(psd, [N["eye_right"], N["brow_copy"]]))
B.save_part(entries, "brow-sad-left", "v2 crying left brow", visible(psd, [N["cry_brow_left"]]))
B.save_part(entries, "brow-sad-right", "v2 crying right brow", visible(psd, [N["cry_brow_right"]]))
save_enhanced_part(entries, "tear-left", "v2 left tear layer", visible(psd, [N["tears"], N["tear_left"]]))
save_enhanced_part(entries, "tear-right", "v2 right tear layer", visible(psd, [N["tears"], N["tear_right"]]))
hair_back_base = B.composite_many(
[
B.composite_path(psd, [N["back_extra_left"]]),
B.composite_path(psd, [N["back_extra_right"]]),
]
)
B.save_part(entries, "hair-back-base", "top extra back hair layers", hair_back_base)
neutral_mouth = B.save_part(
entries,
"mouth-neutral",
"hidden smile mouth group",
visible(psd, [N["mouth"], N["smile"]], hidden=[[N["mouth"], N["cry_mouth"]]]),
)
sad_mouth = B.save_part(
entries,
"mouth-sad",
"v2 crying mouth group",
visible(psd, [N["mouth"], N["cry_mouth"]], hidden=[[N["mouth"], N["smile"]]]),
)
mouth_open_groups = [
[N["mouth"], N["mouth_inner"]],
[N["mouth"], N["upper_teeth"]],
[N["mouth"], N["lower_side_teeth"]],
[N["mouth"], N["tongue"]],
[N["mouth"], N["lower_teeth"]],
[N["mouth"], N["upper_lip"]],
[N["mouth"], N["lower_lip"]],
]
mouth_open = B.save_part(
entries,
"mouth-open",
"hidden open mouth groups",
B.composite_with_visibility(
psd,
[N["mouth"]],
visible_paths=mouth_open_groups,
hidden_paths=[[N["mouth"], N["smile"]], [N["mouth"], N["cry_mouth"]]],
),
)
B.save_part(entries, "mouth-warm", "derived from neutral mouth bbox", B.paste_scaled(neutral_mouth, 1.12, 1.02, offset_y=-1))
B.save_part(entries, "mouth-tired", "derived from sad mouth bbox", B.paste_scaled(sad_mouth, 0.9, 0.82, offset_y=0))
B.save_part(entries, "mouth-anxious", "derived from sad mouth bbox", B.paste_scaled(sad_mouth, 0.82, 0.86, offset_y=0))
B.save_part(entries, "mouth-startled", "derived from mouth-open bbox", B.paste_scaled(mouth_open, 0.68, 0.72, offset_y=-1))
B.save_part(entries, "mouth-open-small", "derived from mouth-open bbox", B.paste_scaled(mouth_open, 0.86, 0.62, offset_y=-1))
make_preview("neutral", NEUTRAL_LAYER_ORDER)
make_preview("sad", SAD_LAYER_ORDER)
B.make_contact(entries)
B.make_imagegen_harness(entries)
(OUT / "detailed-parts-manifest.json").write_text(
json.dumps(
{
"source": str(PSD),
"sourceCanvas": B.SOURCE_CANVAS,
"targetCanvas": B.TARGET_CANVAS,
"cropBox": B.CROP_BOX,
"neutralLayerOrder": NEUTRAL_LAYER_ORDER,
"sadLayerOrder": SAD_LAYER_ORDER,
"parts": entries,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
sync_to_app()
print(f"exported {len(entries)} detailed parts")
print(f"parts {PARTS}")
print(f"app parts {APP_PARTS}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 KiB