diff --git a/README.md b/README.md index 98a393c2e..3c48d18f2 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,20 @@ const gif = { }; ``` +### Build OTB talking-bear runtime assets + +Install Python dependencies: + +```sh +pip install -r requirements.txt +``` + +Then generate runtime frames, spritesheets, and manifest: + +```sh +npm run otb_sprite_runtime +``` + ### Printing rarity data (Experimental feature) To see the percentages of each attribute across your collection, run: diff --git a/layers/Accessories/Logo#1.png b/layers/Accessories/Logo#1.png new file mode 100644 index 000000000..a921a8acd Binary files /dev/null and b/layers/Accessories/Logo#1.png differ diff --git a/official-logo-2000.png b/official-logo-2000.png new file mode 100644 index 000000000..a921a8acd Binary files /dev/null and b/official-logo-2000.png differ diff --git a/package.json b/package.json index 141abf44e..3e241beb1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "pixelate": "node utils/pixelate.js", "update_info": "node utils/update_info.js", "preview_gif": "node utils/preview_gif.js", - "generate_metadata": "node utils/generate_metadata.js" + "generate_metadata": "node utils/generate_metadata.js", + "otb_sprite_runtime": "node utils/run_python3_script.js utils/build_otb_sprite_runtime.py" }, "author": "Daniel Eugene Botha (HashLips)", "license": "MIT", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..c7e63dd8a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Pillow>=10.0.0 diff --git a/sprites/otb_talking_bear/base/otb_hybrid_base.png b/sprites/otb_talking_bear/base/otb_hybrid_base.png new file mode 100644 index 000000000..d2a85e14d Binary files /dev/null and b/sprites/otb_talking_bear/base/otb_hybrid_base.png differ diff --git a/sprites/otb_talking_bear/brows/B0_neutral.png b/sprites/otb_talking_bear/brows/B0_neutral.png new file mode 100644 index 000000000..08d8c1aee Binary files /dev/null and b/sprites/otb_talking_bear/brows/B0_neutral.png differ diff --git a/sprites/otb_talking_bear/brows/B1_up.png b/sprites/otb_talking_bear/brows/B1_up.png new file mode 100644 index 000000000..d90581ef3 Binary files /dev/null and b/sprites/otb_talking_bear/brows/B1_up.png differ diff --git a/sprites/otb_talking_bear/brows/B2_down.png b/sprites/otb_talking_bear/brows/B2_down.png new file mode 100644 index 000000000..e923421d9 Binary files /dev/null and b/sprites/otb_talking_bear/brows/B2_down.png differ diff --git a/sprites/otb_talking_bear/brows/B3_angry.png b/sprites/otb_talking_bear/brows/B3_angry.png new file mode 100644 index 000000000..a15e391c1 Binary files /dev/null and b/sprites/otb_talking_bear/brows/B3_angry.png differ diff --git a/sprites/otb_talking_bear/eyes/E0_neutral.png b/sprites/otb_talking_bear/eyes/E0_neutral.png new file mode 100644 index 000000000..971f762cb Binary files /dev/null and b/sprites/otb_talking_bear/eyes/E0_neutral.png differ diff --git a/sprites/otb_talking_bear/eyes/E1_blink.png b/sprites/otb_talking_bear/eyes/E1_blink.png new file mode 100644 index 000000000..0ca690928 Binary files /dev/null and b/sprites/otb_talking_bear/eyes/E1_blink.png differ diff --git a/sprites/otb_talking_bear/eyes/E2_squint.png b/sprites/otb_talking_bear/eyes/E2_squint.png new file mode 100644 index 000000000..294e98183 Binary files /dev/null and b/sprites/otb_talking_bear/eyes/E2_squint.png differ diff --git a/sprites/otb_talking_bear/eyes/E3_wide.png b/sprites/otb_talking_bear/eyes/E3_wide.png new file mode 100644 index 000000000..0694f201d Binary files /dev/null and b/sprites/otb_talking_bear/eyes/E3_wide.png differ diff --git a/sprites/otb_talking_bear/eyes/E4_lookLeft.png b/sprites/otb_talking_bear/eyes/E4_lookLeft.png new file mode 100644 index 000000000..2bd3c91c0 Binary files /dev/null and b/sprites/otb_talking_bear/eyes/E4_lookLeft.png differ diff --git a/sprites/otb_talking_bear/eyes/E5_lookRight.png b/sprites/otb_talking_bear/eyes/E5_lookRight.png new file mode 100644 index 000000000..8c7873013 Binary files /dev/null and b/sprites/otb_talking_bear/eyes/E5_lookRight.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H00.png b/sprites/otb_talking_bear/hair_fx/H00.png new file mode 100644 index 000000000..0861626af Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H00.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H01.png b/sprites/otb_talking_bear/hair_fx/H01.png new file mode 100644 index 000000000..07e1a2105 Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H01.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H02.png b/sprites/otb_talking_bear/hair_fx/H02.png new file mode 100644 index 000000000..831edb162 Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H02.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H03.png b/sprites/otb_talking_bear/hair_fx/H03.png new file mode 100644 index 000000000..d4f5ffe74 Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H03.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H04.png b/sprites/otb_talking_bear/hair_fx/H04.png new file mode 100644 index 000000000..60758d9f1 Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H04.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H05.png b/sprites/otb_talking_bear/hair_fx/H05.png new file mode 100644 index 000000000..f3f49ed32 Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H05.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H06.png b/sprites/otb_talking_bear/hair_fx/H06.png new file mode 100644 index 000000000..b690cf2cb Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H06.png differ diff --git a/sprites/otb_talking_bear/hair_fx/H07.png b/sprites/otb_talking_bear/hair_fx/H07.png new file mode 100644 index 000000000..74cc4235e Binary files /dev/null and b/sprites/otb_talking_bear/hair_fx/H07.png differ diff --git a/sprites/otb_talking_bear/mouth/M0_closed.png b/sprites/otb_talking_bear/mouth/M0_closed.png new file mode 100644 index 000000000..fd2b6c6c3 Binary files /dev/null and b/sprites/otb_talking_bear/mouth/M0_closed.png differ diff --git a/sprites/otb_talking_bear/mouth/M1_tight.png b/sprites/otb_talking_bear/mouth/M1_tight.png new file mode 100644 index 000000000..a50573c75 Binary files /dev/null and b/sprites/otb_talking_bear/mouth/M1_tight.png differ diff --git a/sprites/otb_talking_bear/mouth/M2_halfOpen.png b/sprites/otb_talking_bear/mouth/M2_halfOpen.png new file mode 100644 index 000000000..b857c1ae1 Binary files /dev/null and b/sprites/otb_talking_bear/mouth/M2_halfOpen.png differ diff --git a/sprites/otb_talking_bear/mouth/M3_open.png b/sprites/otb_talking_bear/mouth/M3_open.png new file mode 100644 index 000000000..921eb2635 Binary files /dev/null and b/sprites/otb_talking_bear/mouth/M3_open.png differ diff --git a/sprites/otb_talking_bear/mouth/M4_wide.png b/sprites/otb_talking_bear/mouth/M4_wide.png new file mode 100644 index 000000000..8086c7880 Binary files /dev/null and b/sprites/otb_talking_bear/mouth/M4_wide.png differ diff --git a/sprites/otb_talking_bear/mouth/M5_roundO.png b/sprites/otb_talking_bear/mouth/M5_roundO.png new file mode 100644 index 000000000..8aec2cdd3 Binary files /dev/null and b/sprites/otb_talking_bear/mouth/M5_roundO.png differ diff --git a/sprites/otb_talking_bear/nose/N0_neutral.png b/sprites/otb_talking_bear/nose/N0_neutral.png new file mode 100644 index 000000000..3f28e319f Binary files /dev/null and b/sprites/otb_talking_bear/nose/N0_neutral.png differ diff --git a/sprites/otb_talking_bear/nose/N1_flare.png b/sprites/otb_talking_bear/nose/N1_flare.png new file mode 100644 index 000000000..8b555522c Binary files /dev/null and b/sprites/otb_talking_bear/nose/N1_flare.png differ diff --git a/sprites/otb_talking_bear/nose/N2_wrinkle.png b/sprites/otb_talking_bear/nose/N2_wrinkle.png new file mode 100644 index 000000000..4ef41916c Binary files /dev/null and b/sprites/otb_talking_bear/nose/N2_wrinkle.png differ diff --git a/sprites/otb_talking_bear/preview/preview_hype.png b/sprites/otb_talking_bear/preview/preview_hype.png new file mode 100644 index 000000000..37988c74a Binary files /dev/null and b/sprites/otb_talking_bear/preview/preview_hype.png differ diff --git a/sprites/otb_talking_bear/preview/preview_idle.png b/sprites/otb_talking_bear/preview/preview_idle.png new file mode 100644 index 000000000..86bdafb89 Binary files /dev/null and b/sprites/otb_talking_bear/preview/preview_idle.png differ diff --git a/sprites/otb_talking_bear/preview/preview_talk_1.png b/sprites/otb_talking_bear/preview/preview_talk_1.png new file mode 100644 index 000000000..dfc17415d Binary files /dev/null and b/sprites/otb_talking_bear/preview/preview_talk_1.png differ diff --git a/sprites/otb_talking_bear/preview/preview_talk_2.png b/sprites/otb_talking_bear/preview/preview_talk_2.png new file mode 100644 index 000000000..02fa5ec81 Binary files /dev/null and b/sprites/otb_talking_bear/preview/preview_talk_2.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_00.png b/sprites/otb_talking_bear/runtime/frames/idle_00.png new file mode 100644 index 000000000..86bdafb89 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_00.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_01.png b/sprites/otb_talking_bear/runtime/frames/idle_01.png new file mode 100644 index 000000000..fb2f4a245 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_01.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_02.png b/sprites/otb_talking_bear/runtime/frames/idle_02.png new file mode 100644 index 000000000..07bb593bf Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_02.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_03.png b/sprites/otb_talking_bear/runtime/frames/idle_03.png new file mode 100644 index 000000000..0a09b99f2 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_03.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_04.png b/sprites/otb_talking_bear/runtime/frames/idle_04.png new file mode 100644 index 000000000..77e00ccb7 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_04.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_05.png b/sprites/otb_talking_bear/runtime/frames/idle_05.png new file mode 100644 index 000000000..d79f6049d Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_05.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_06.png b/sprites/otb_talking_bear/runtime/frames/idle_06.png new file mode 100644 index 000000000..4bf756c55 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_06.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/idle_07.png b/sprites/otb_talking_bear/runtime/frames/idle_07.png new file mode 100644 index 000000000..60aa7346c Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/idle_07.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/talk_00.png b/sprites/otb_talking_bear/runtime/frames/talk_00.png new file mode 100644 index 000000000..86bdafb89 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/talk_00.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/talk_01.png b/sprites/otb_talking_bear/runtime/frames/talk_01.png new file mode 100644 index 000000000..ce1c85611 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/talk_01.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/talk_02.png b/sprites/otb_talking_bear/runtime/frames/talk_02.png new file mode 100644 index 000000000..b8dc4408d Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/talk_02.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/talk_03.png b/sprites/otb_talking_bear/runtime/frames/talk_03.png new file mode 100644 index 000000000..7daeaf0c3 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/talk_03.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/talk_04.png b/sprites/otb_talking_bear/runtime/frames/talk_04.png new file mode 100644 index 000000000..9300d66cb Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/talk_04.png differ diff --git a/sprites/otb_talking_bear/runtime/frames/talk_05.png b/sprites/otb_talking_bear/runtime/frames/talk_05.png new file mode 100644 index 000000000..97ae76825 Binary files /dev/null and b/sprites/otb_talking_bear/runtime/frames/talk_05.png differ diff --git a/sprites/otb_talking_bear/runtime/idle_spritesheet.png b/sprites/otb_talking_bear/runtime/idle_spritesheet.png new file mode 100644 index 000000000..842ab9e9b Binary files /dev/null and b/sprites/otb_talking_bear/runtime/idle_spritesheet.png differ diff --git a/sprites/otb_talking_bear/runtime/manifest.json b/sprites/otb_talking_bear/runtime/manifest.json new file mode 100644 index 000000000..956c3fd26 --- /dev/null +++ b/sprites/otb_talking_bear/runtime/manifest.json @@ -0,0 +1,184 @@ +{ + "name": "otb_talking_bear_runtime", + "size": { + "width": 2000, + "height": 2000 + }, + "layers": { + "base": "base/otb_hybrid_base.png", + "mouth": [ + "M0_closed.png", + "M1_tight.png", + "M2_halfOpen.png", + "M3_open.png", + "M4_wide.png", + "M5_roundO.png" + ], + "eyes": [ + "E0_neutral.png", + "E1_blink.png", + "E2_squint.png", + "E3_wide.png", + "E4_lookLeft.png", + "E5_lookRight.png" + ], + "brows": [ + "B0_neutral.png", + "B1_up.png", + "B2_down.png", + "B3_angry.png" + ], + "nose": [ + "N0_neutral.png", + "N1_flare.png", + "N2_wrinkle.png" + ], + "hair_fx": [ + "H00.png", + "H01.png", + "H02.png", + "H03.png", + "H04.png", + "H05.png", + "H06.png", + "H07.png" + ] + }, + "visemeMap": { + "silence": "M0_closed", + "low": "M1_tight", + "mid": "M2_halfOpen", + "open": "M3_open", + "loud": "M4_wide", + "round": "M5_roundO" + }, + "states": { + "talkCycle": { + "fps": 24, + "frameCount": 6, + "frames": [ + { + "index": 0, + "viseme": "M0_closed", + "hair": "H00", + "file": "runtime/frames/talk_00.png" + }, + { + "index": 1, + "viseme": "M1_tight", + "hair": "H01", + "file": "runtime/frames/talk_01.png" + }, + { + "index": 2, + "viseme": "M2_halfOpen", + "hair": "H02", + "file": "runtime/frames/talk_02.png" + }, + { + "index": 3, + "viseme": "M3_open", + "hair": "H03", + "file": "runtime/frames/talk_03.png" + }, + { + "index": 4, + "viseme": "M4_wide", + "hair": "H04", + "file": "runtime/frames/talk_04.png" + }, + { + "index": 5, + "viseme": "M5_roundO", + "hair": "H05", + "file": "runtime/frames/talk_05.png" + } + ], + "spritesheet": "runtime/talk_spritesheet.png" + }, + "idleLoop": { + "fps": 12, + "frameCount": 8, + "frames": [ + { + "index": 0, + "eye": "E0_neutral", + "brow": "B0_neutral", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H00", + "file": "runtime/frames/idle_00.png" + }, + { + "index": 1, + "eye": "E0_neutral", + "brow": "B0_neutral", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H01", + "file": "runtime/frames/idle_01.png" + }, + { + "index": 2, + "eye": "E0_neutral", + "brow": "B0_neutral", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H02", + "file": "runtime/frames/idle_02.png" + }, + { + "index": 3, + "eye": "E1_blink", + "brow": "B1_up", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H03", + "file": "runtime/frames/idle_03.png" + }, + { + "index": 4, + "eye": "E2_squint", + "brow": "B0_neutral", + "nose": "N2_wrinkle", + "mouth": "M1_tight", + "hair": "H04", + "file": "runtime/frames/idle_04.png" + }, + { + "index": 5, + "eye": "E0_neutral", + "brow": "B0_neutral", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H05", + "file": "runtime/frames/idle_05.png" + }, + { + "index": 6, + "eye": "E0_neutral", + "brow": "B0_neutral", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H06", + "file": "runtime/frames/idle_06.png" + }, + { + "index": 7, + "eye": "E0_neutral", + "brow": "B0_neutral", + "nose": "N0_neutral", + "mouth": "M0_closed", + "hair": "H07", + "file": "runtime/frames/idle_07.png" + } + ], + "spritesheet": "runtime/idle_spritesheet.png" + } + }, + "runtimeHints": { + "faceSolveFps": 60, + "visemeUpdateHz": 24, + "audioSmoothingMs": 80 + } +} \ No newline at end of file diff --git a/sprites/otb_talking_bear/runtime/talk_spritesheet.png b/sprites/otb_talking_bear/runtime/talk_spritesheet.png new file mode 100644 index 000000000..e0b9ddeef Binary files /dev/null and b/sprites/otb_talking_bear/runtime/talk_spritesheet.png differ diff --git a/src/config.js b/src/config.js index c46d867a0..4da89c126 100644 --- a/src/config.js +++ b/src/config.js @@ -5,9 +5,9 @@ const { NETWORK } = require(`${basePath}/constants/network.js`); const network = NETWORK.eth; // General metadata for Ethereum -const namePrefix = "Your Collection"; -const description = "Remember to replace this description"; -const baseUri = "ipfs://NewUriToReplace"; +const namePrefix = "My Collection"; +const description = "10,000 unique..."; +const baseUri = "ipfs://YOUR_CID/images/"; const solanaMetadata = { symbol: "YC", @@ -24,26 +24,26 @@ const solanaMetadata = { // If you have selected Solana then the collection starts from 0 automatically const layerConfigurations = [ { - growEditionSizeTo: 5, + growEditionSizeTo: 10000, layersOrder: [ { name: "Background" }, - { name: "Eyeball" }, - { name: "Eye color" }, - { name: "Iris" }, - { name: "Shine" }, - { name: "Bottom lid" }, - { name: "Top lid" }, + { name: "Body" }, + { name: "Clothing" }, + { name: "Eyes" }, + { name: "Mouth" }, + { name: "Headwear" }, + { name: "Accessories" }, ], }, ]; -const shuffleLayerConfigurations = false; +const shuffleLayerConfigurations = true; const debugLogs = false; const format = { - width: 512, - height: 512, + width: 2000, + height: 2000, smoothing: false, }; @@ -72,7 +72,7 @@ const pixelFormat = { }; const background = { - generate: true, + generate: false, brightness: "80%", static: false, default: "#000000", diff --git a/utils/build_otb_sprite_runtime.py b/utils/build_otb_sprite_runtime.py new file mode 100644 index 000000000..3077e29d8 --- /dev/null +++ b/utils/build_otb_sprite_runtime.py @@ -0,0 +1,200 @@ +from pathlib import Path +import json +from PIL import Image + +BASE_PATH = Path(__file__).resolve().parents[1] +SPRITE_ROOT = BASE_PATH / "sprites" / "otb_talking_bear" +RUNTIME_DIR = SPRITE_ROOT / "runtime" +FRAME_DIR = RUNTIME_DIR / "frames" +SIZE = 2000 + + +def sorted_pngs(dir_path: Path) -> list[str]: + if not dir_path.exists(): + return [] + return sorted([p.name for p in dir_path.glob("*.png")]) + + +def require_file(file_path: Path) -> None: + if not file_path.exists(): + raise FileNotFoundError(f"Missing required layer file: {file_path}") + + +def composite_frame(output_path: Path, layers: list[Path]) -> None: + base = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) + for layer_path in layers: + require_file(layer_path) + with Image.open(layer_path) as opened_layer: + layer = opened_layer.convert("RGBA") + if layer.size != (SIZE, SIZE): + raise ValueError(f"Layer size mismatch for {layer_path}: got {layer.size}, expected {(SIZE, SIZE)}") + base.alpha_composite(layer) + base.save(output_path, "PNG") + + +def build_spritesheet(frame_paths: list[Path], output_path: Path, columns: int) -> None: + rows = (len(frame_paths) + columns - 1) // columns + sheet = Image.new("RGBA", (SIZE * columns, SIZE * rows), (0, 0, 0, 0)) + for i, frame_path in enumerate(frame_paths): + col = i % columns + row = i // columns + with Image.open(frame_path) as opened_frame: + frame = opened_frame.convert("RGBA") + sheet.alpha_composite(frame, (col * SIZE, row * SIZE)) + sheet.save(output_path, "PNG") + + +def rel(path: Path) -> str: + return path.relative_to(SPRITE_ROOT).as_posix() + + +def main() -> None: + base_layer = SPRITE_ROOT / "base" / "otb_hybrid_base.png" + mouth_dir = SPRITE_ROOT / "mouth" + eye_dir = SPRITE_ROOT / "eyes" + brow_dir = SPRITE_ROOT / "brows" + nose_dir = SPRITE_ROOT / "nose" + hair_dir = SPRITE_ROOT / "hair_fx" + + require_file(base_layer) + mouths = sorted_pngs(mouth_dir) + eyes = sorted_pngs(eye_dir) + brows = sorted_pngs(brow_dir) + noses = sorted_pngs(nose_dir) + hairs = sorted_pngs(hair_dir) + + if len(mouths) < 6: + raise ValueError(f"Expected at least 6 mouth frames, found {len(mouths)}") + if not eyes or not brows or not noses or not hairs: + raise ValueError("Missing one or more facial overlay sets (eyes, brows, nose, hair_fx)") + if len(hairs) < 8: + raise ValueError(f"Expected at least 8 hair_fx frames for idle/talk cycles, found {len(hairs)}") + + RUNTIME_DIR.mkdir(parents=True, exist_ok=True) + FRAME_DIR.mkdir(parents=True, exist_ok=True) + + talk_frames = [] + idle_frames = [] + + eye_neutral = eye_dir / "E0_neutral.png" + brow_neutral = brow_dir / "B0_neutral.png" + nose_neutral = nose_dir / "N0_neutral.png" + + for i in range(6): + output = FRAME_DIR / f"talk_{i:02d}.png" + mouth = mouth_dir / mouths[i] + hair = hair_dir / hairs[i % len(hairs)] + composite_frame(output, [base_layer, hair, brow_neutral, eye_neutral, nose_neutral, mouth]) + talk_frames.append( + { + "index": i, + "viseme": mouth.stem, + "hair": hair.stem, + "path": output, + } + ) + + idle_pattern = [ + ("E0_neutral.png", "B0_neutral.png", "N0_neutral.png", "M0_closed.png", hairs[0]), + ("E0_neutral.png", "B0_neutral.png", "N0_neutral.png", "M0_closed.png", hairs[1]), + ("E0_neutral.png", "B0_neutral.png", "N0_neutral.png", "M0_closed.png", hairs[2]), + ("E1_blink.png", "B1_up.png", "N0_neutral.png", "M0_closed.png", hairs[3]), + ("E2_squint.png", "B0_neutral.png", "N2_wrinkle.png", "M1_tight.png", hairs[4]), + ("E0_neutral.png", "B0_neutral.png", "N0_neutral.png", "M0_closed.png", hairs[5]), + ("E0_neutral.png", "B0_neutral.png", "N0_neutral.png", "M0_closed.png", hairs[6 % len(hairs)]), + ("E0_neutral.png", "B0_neutral.png", "N0_neutral.png", "M0_closed.png", hairs[7 % len(hairs)]), + ] + + for i, (eye_name, brow_name, nose_name, mouth_name, hair_name) in enumerate(idle_pattern): + output = FRAME_DIR / f"idle_{i:02d}.png" + eye = eye_dir / eye_name + brow = brow_dir / brow_name + nose = nose_dir / nose_name + mouth = mouth_dir / mouth_name + hair = hair_dir / hair_name + composite_frame(output, [base_layer, hair, brow, eye, nose, mouth]) + idle_frames.append( + { + "index": i, + "eye": eye.stem, + "brow": brow.stem, + "nose": nose.stem, + "mouth": mouth.stem, + "hair": hair.stem, + "path": output, + } + ) + + talk_sheet_path = RUNTIME_DIR / "talk_spritesheet.png" + idle_sheet_path = RUNTIME_DIR / "idle_spritesheet.png" + build_spritesheet([f["path"] for f in talk_frames], talk_sheet_path, 6) + build_spritesheet([f["path"] for f in idle_frames], idle_sheet_path, 8) + + manifest = { + "name": "otb_talking_bear_runtime", + "size": {"width": SIZE, "height": SIZE}, + "layers": { + "base": "base/otb_hybrid_base.png", + "mouth": mouths, + "eyes": eyes, + "brows": brows, + "nose": noses, + "hair_fx": hairs, + }, + "visemeMap": { + "silence": "M0_closed", + "low": "M1_tight", + "mid": "M2_halfOpen", + "open": "M3_open", + "loud": "M4_wide", + "round": "M5_roundO", + }, + "states": { + "talkCycle": { + "fps": 24, + "frameCount": len(talk_frames), + "frames": [ + { + "index": f["index"], + "viseme": f["viseme"], + "hair": f["hair"], + "file": rel(f["path"]), + } + for f in talk_frames + ], + "spritesheet": rel(talk_sheet_path), + }, + "idleLoop": { + "fps": 12, + "frameCount": len(idle_frames), + "frames": [ + { + "index": f["index"], + "eye": f["eye"], + "brow": f["brow"], + "nose": f["nose"], + "mouth": f["mouth"], + "hair": f["hair"], + "file": rel(f["path"]), + } + for f in idle_frames + ], + "spritesheet": rel(idle_sheet_path), + }, + }, + "runtimeHints": { + "faceSolveFps": 60, + "visemeUpdateHz": 24, + "audioSmoothingMs": 80, + }, + } + + manifest_path = RUNTIME_DIR / "manifest.json" + manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") + + print(f"Runtime pack generated at: {RUNTIME_DIR}") + print(f"Talk frames: {len(talk_frames)}, Idle frames: {len(idle_frames)}") + + +if __name__ == "__main__": + main() diff --git a/utils/run_python3_script.js b/utils/run_python3_script.js new file mode 100644 index 000000000..41a9c1603 --- /dev/null +++ b/utils/run_python3_script.js @@ -0,0 +1,40 @@ +const { spawnSync } = require("child_process"); +const path = require("path"); + +const scriptArg = process.argv[2]; +if (!scriptArg) { + console.error("Usage: node utils/run_python3_script.js "); + process.exit(1); +} + +const scriptPath = path.resolve(process.cwd(), scriptArg); +const candidates = [ + { cmd: "python3", args: [scriptPath] }, + { cmd: "py", args: ["-3", scriptPath] }, + { cmd: "python", args: [scriptPath] }, +]; + +for (const candidate of candidates) { + const probe = spawnSync(candidate.cmd, [...candidate.args.slice(0, -1), "-c", "import PIL"], { + stdio: "ignore", + shell: false, + }); + if (probe.error && probe.error.code === "ENOENT") { + continue; + } + if (probe.status !== 0) { + continue; + } + + const result = spawnSync(candidate.cmd, candidate.args, { + stdio: "inherit", + shell: false, + }); + + if (typeof result.status === "number") { + process.exit(result.status); + } +} + +console.error("No Python 3 + Pillow interpreter found. Install dependencies with pip install -r requirements.txt."); +process.exit(1);