from __future__ import annotations import json from datetime import date from pathlib import Path from PIL import Image ROOT = Path(r"D:\Work_AI\Dansori") APP = ROOT / "DansoriEQ" / "src" / "DansoriEQ.App" HOST = APP / "Assets" / "Live2DHost" PUPPET = APP / "Assets" / "Characters" / "Puppets" / "LeeSoriV2" PLAN = ROOT / "DansoriEQ" / "docs" / "LIVE2D_CHARACTER_INTEGRATION_PLAN.md" def update_characters() -> None: path = HOST / "characters.json" data = json.loads(path.read_text(encoding="utf-8-sig")) for character in data["characters"]: if character.get("id") == "leesori": character["puppet"] = { "rig": "../Characters/Puppets/LeeSoriV2/rig.json", "imageBase": "../Characters/Puppets/LeeSoriV2/Images/", } break path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def update_css() -> None: path = HOST / "style.css" css = path.read_text(encoding="utf-8") old = """#puppet { display: none; right: 10%; bottom: 1%; width: 80%; height: auto; aspect-ratio: 1086 / 1448; transform-origin: 50% 100%; filter: drop-shadow(0 20px 24px rgba(0, 0, 0, .46)); animation: puppetRootIdle 3.8s ease-in-out infinite; }""" new = """#puppet { display: none; right: 17.5%; bottom: -20%; width: 65%; height: auto; aspect-ratio: 365 / 1058; transform-origin: 50% 100%; filter: drop-shadow(0 20px 24px rgba(0, 0, 0, .46)); animation: puppetRootIdle 3.8s ease-in-out infinite; }""" if old not in css: raise RuntimeError("Expected #puppet CSS block was not found.") path.write_text(css.replace(old, new), encoding="utf-8") def make_css_qa() -> None: src = Image.open(PUPPET / "leesori_v2_source.png").convert("RGBA") width, height = 390, 600 stage = Image.new("RGBA", (width, height), (16, 16, 18, 255)) target_w = int(width * 0.65) target_h = int(target_w * src.height / src.width) resized = src.resize((target_w, target_h), Image.Resampling.LANCZOS) right = int(width * 0.175) bottom = int(height * -0.20) x = width - target_w - right y = height - target_h - bottom stage.alpha_composite(resized, (x, y)) stage.convert("RGB").save(PUPPET / "qa_view_390x600_css_selected.png") def update_plan() -> None: entry = f""" ### {date.today().isoformat()} - New Sheet LeeSoriV2 Conversion User correction: - The previous LeeSoriExtended puppet solved clipping but used the old LeeSori design. - The authoritative new design sheet is: - `Characters_Build_Docs/LeeSori_Profile/03_Assets/Reference/sori_sheet.png` Changed: - Created `LeeSoriV2` from the new sheet's front full-body pose without AI identity drift. - Extracted alpha-clean source and overlap puppet parts: - base, legs, chest, left/right arms, left/right hands, head. - Replaced the app preview image with the new sheet-based LeeSori. - Switched `characters.json` to: - `../Characters/Puppets/LeeSoriV2/rig.json` - `../Characters/Puppets/LeeSoriV2/Images/` - Updated WebView framing for the new tall sheet ratio: - `right: 17.5%` - `bottom: -20%` - `width: 65%` - Framing goal: upper-body biased view for idle/work states while keeping head and both hands visible. Full body remains available in the source/rig for later state-specific framing. QA artifacts: - `src/DansoriEQ.App/Assets/Characters/Puppets/LeeSoriV2/qa_source_black.png` - `src/DansoriEQ.App/Assets/Characters/Puppets/LeeSoriV2/qa_view_390x600_upper_bias.png` - `src/DansoriEQ.App/Assets/Characters/Puppets/LeeSoriV2/qa_view_390x600_css_selected.png` Next: - Build and run in WPF to verify real WebView framing. - If this sheet-based puppet is accepted, add state-specific animation tuning and then generate Haruka/Isabel/Noeul with the same source-first process. """ text = PLAN.read_text(encoding="utf-8") if "New Sheet LeeSoriV2 Conversion" not in text: PLAN.write_text(text.rstrip() + entry + "\n", encoding="utf-8") def main() -> None: update_characters() update_css() make_css_qa() update_plan() if __name__ == "__main__": main()