126 lines
4.1 KiB
Python
126 lines
4.1 KiB
Python
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()
|