Add anchor character location CGs
@@ -35,3 +35,9 @@
|
|||||||
python3 scripts/validate_content.py
|
python3 scripts/validate_content.py
|
||||||
python3 scripts/validate_assets.py
|
python3 scripts/validate_assets.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Character Location CG Assets
|
||||||
|
|
||||||
|
- Anchor character-location CG round 01 is stored under `assets/characters/location_cg/<character_id>/`.
|
||||||
|
- The round 01 comparison sheet is `assets/characters/location_cg/anchor_locations_round01_contact_sheet.png`.
|
||||||
|
- The manifest is `data/character_location_assets.json`; `scripts/validate_assets.py` checks character/location references, presence availability, PNG files, and minimum 1280x720 dimensions.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/characters/location_cg/hong_sua/hong_sua_jazz_bar_v1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/characters/location_cg/lee_bom/lee_bom_flower_shop_v1.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/characters/location_cg/marina/marina_global_lounge_v1.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/characters/location_cg/oh_sera/oh_sera_flower_shop_v1.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
174
data/character_location_assets.json
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Accepted anchor-location encounter CGs for the first character-location art pass.",
|
||||||
|
"minimum_dimensions": {
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720
|
||||||
|
},
|
||||||
|
"source_presence_manifest": "data/character_location_presence.json",
|
||||||
|
"contact_sheet": "assets/characters/location_cg/anchor_locations_round01_contact_sheet.png",
|
||||||
|
"generation_rounds": [
|
||||||
|
{
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"description": "One mature glossy anime encounter CG for each character's anchor location.",
|
||||||
|
"status": "accepted",
|
||||||
|
"asset_count": 22
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"character_id": "han_seoyun",
|
||||||
|
"location_id": "cooking_studio",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/han_seoyun/han_seoyun_cooking_studio_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "yu_harin",
|
||||||
|
"location_id": "old_bookstore",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/yu_harin/yu_harin_old_bookstore_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "kang_mirae",
|
||||||
|
"location_id": "office_district",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/kang_mirae/kang_mirae_office_district_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "jung_naeun",
|
||||||
|
"location_id": "downtown_cafe",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/jung_naeun/jung_naeun_downtown_cafe_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "baek_chaewon",
|
||||||
|
"location_id": "rooftop_lounge",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/baek_chaewon/baek_chaewon_rooftop_lounge_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "oh_sera",
|
||||||
|
"location_id": "flower_shop",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/oh_sera/oh_sera_flower_shop_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "moon_jia",
|
||||||
|
"location_id": "amusement_street",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/moon_jia/moon_jia_amusement_street_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "lim_sohee",
|
||||||
|
"location_id": "downtown_cafe",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/lim_sohee/lim_sohee_downtown_cafe_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "cha_yuri",
|
||||||
|
"location_id": "fashion_street",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/cha_yuri/cha_yuri_fashion_street_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "shin_yerim",
|
||||||
|
"location_id": "wine_bar",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/shin_yerim/shin_yerim_wine_bar_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "kim_doa",
|
||||||
|
"location_id": "university_quarter",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/kim_doa/kim_doa_university_quarter_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "park_minseo",
|
||||||
|
"location_id": "fitness_center",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/park_minseo/park_minseo_fitness_center_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "seo_irin",
|
||||||
|
"location_id": "art_studio",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/seo_irin/seo_irin_art_studio_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "yoon_gayoung",
|
||||||
|
"location_id": "apartment_complex",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/yoon_gayoung/yoon_gayoung_apartment_complex_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "choi_ria",
|
||||||
|
"location_id": "boutique_studio",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/choi_ria/choi_ria_boutique_studio_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "hong_sua",
|
||||||
|
"location_id": "jazz_bar",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/hong_sua/hong_sua_jazz_bar_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "lee_bom",
|
||||||
|
"location_id": "flower_shop",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/lee_bom/lee_bom_flower_shop_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "go_eunjae",
|
||||||
|
"location_id": "office_district",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/go_eunjae/go_eunjae_office_district_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "ryu_haena",
|
||||||
|
"location_id": "fashion_street",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/ryu_haena/ryu_haena_fashion_street_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "nam_juhee",
|
||||||
|
"location_id": "police_box",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/nam_juhee/nam_juhee_police_box_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "marina",
|
||||||
|
"location_id": "global_lounge",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/marina/marina_global_lounge_v1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"character_id": "song_arin",
|
||||||
|
"location_id": "apartment_complex",
|
||||||
|
"round_id": "anchor_locations_round01",
|
||||||
|
"status": "accepted",
|
||||||
|
"path": "assets/characters/location_cg/song_arin/song_arin_apartment_complex_v1.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -210,6 +210,88 @@ def main() -> None:
|
|||||||
f"{entry['location_id']} revision candidate",
|
f"{entry['location_id']} revision candidate",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
character_location_asset_path = DATA / "character_location_assets.json"
|
||||||
|
character_location_accepted = 0
|
||||||
|
character_location_revision_count = 0
|
||||||
|
if character_location_asset_path.exists():
|
||||||
|
character_location_assets_doc = load_json(character_location_asset_path)
|
||||||
|
character_location_minimum = character_location_assets_doc[
|
||||||
|
"minimum_dimensions"
|
||||||
|
]
|
||||||
|
character_location_min_width = character_location_minimum["width"]
|
||||||
|
character_location_min_height = character_location_minimum["height"]
|
||||||
|
|
||||||
|
contact_sheet = ROOT / character_location_assets_doc["contact_sheet"]
|
||||||
|
if not contact_sheet.exists():
|
||||||
|
fail(f"character-location contact sheet is missing: {contact_sheet}")
|
||||||
|
png_dimensions(contact_sheet)
|
||||||
|
|
||||||
|
presence_doc = load_json(DATA / "character_location_presence.json")
|
||||||
|
allowed_pairs = {
|
||||||
|
(entry["character_id"], location["location_id"])
|
||||||
|
for entry in presence_doc["characters"]
|
||||||
|
for location in entry["locations"]
|
||||||
|
}
|
||||||
|
|
||||||
|
character_location_asset_entries = character_location_assets_doc["assets"]
|
||||||
|
seen_pairs = set()
|
||||||
|
for entry in character_location_asset_entries:
|
||||||
|
character_id = entry["character_id"]
|
||||||
|
location_id = entry["location_id"]
|
||||||
|
pair = (character_id, location_id)
|
||||||
|
|
||||||
|
if character_id not in character_id_set:
|
||||||
|
fail(
|
||||||
|
"character-location asset references missing character id: "
|
||||||
|
f"{character_id}"
|
||||||
|
)
|
||||||
|
if location_id not in location_ids:
|
||||||
|
fail(
|
||||||
|
"character-location asset references missing location id: "
|
||||||
|
f"{location_id}"
|
||||||
|
)
|
||||||
|
if pair not in allowed_pairs:
|
||||||
|
fail(
|
||||||
|
"character-location asset references unavailable location: "
|
||||||
|
f"{character_id}/{location_id}"
|
||||||
|
)
|
||||||
|
if pair in seen_pairs:
|
||||||
|
fail(
|
||||||
|
"character-location asset manifest contains duplicate pair: "
|
||||||
|
f"{character_id}/{location_id}"
|
||||||
|
)
|
||||||
|
seen_pairs.add(pair)
|
||||||
|
|
||||||
|
status = entry["status"]
|
||||||
|
if status not in {"accepted", "needs_revision", "pending", "rejected"}:
|
||||||
|
fail(
|
||||||
|
f"{character_id}/{location_id} has invalid "
|
||||||
|
f"character-location asset status {status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
path_value = entry.get("path")
|
||||||
|
if status == "accepted":
|
||||||
|
character_location_accepted += 1
|
||||||
|
if not path_value:
|
||||||
|
fail(
|
||||||
|
f"{character_id}/{location_id} accepted asset lacks a path"
|
||||||
|
)
|
||||||
|
validate_asset_image(
|
||||||
|
ROOT / path_value,
|
||||||
|
character_location_min_width,
|
||||||
|
character_location_min_height,
|
||||||
|
f"{character_id}/{location_id}",
|
||||||
|
)
|
||||||
|
elif status == "needs_revision":
|
||||||
|
character_location_revision_count += 1
|
||||||
|
if path_value:
|
||||||
|
validate_asset_image(
|
||||||
|
ROOT / path_value,
|
||||||
|
character_location_min_width,
|
||||||
|
character_location_min_height,
|
||||||
|
f"{character_id}/{location_id} revision candidate",
|
||||||
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"Validated character asset manifest: "
|
"Validated character asset manifest: "
|
||||||
f"{accepted}/{len(asset_entries)} accepted, "
|
f"{accepted}/{len(asset_entries)} accepted, "
|
||||||
@@ -217,7 +299,9 @@ def main() -> None:
|
|||||||
f"{date_cg_accepted} accepted date CG tiers, "
|
f"{date_cg_accepted} accepted date CG tiers, "
|
||||||
f"{date_cg_revision_count} date CG tiers need revision, "
|
f"{date_cg_revision_count} date CG tiers need revision, "
|
||||||
f"{location_accepted} accepted location backgrounds, "
|
f"{location_accepted} accepted location backgrounds, "
|
||||||
f"{location_revision_count} location backgrounds need revision."
|
f"{location_revision_count} location backgrounds need revision, "
|
||||||
|
f"{character_location_accepted} accepted character-location CGs, "
|
||||||
|
f"{character_location_revision_count} character-location CGs need revision."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||