Files
AIGame/Tools/generate_battlefield_components.py

223 lines
8.9 KiB
Python

"""Generate the selected battlefield's ground and reusable prop design sheets.
The selected overview must already be generated by the project
``gpt-image-2.5`` flow. Each request uses the main Candy home illustration as
a style reference and the selected overview as a layout reference.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "Doc/Design/Scene/Battlefield/battlefield_scene_manifest.json"
PROMPTS_DIR = ROOT / "Doc/Design/Scene/Battlefield"
IMAGE_TOOL = ROOT / "Tools/gpt_image_generate.py"
ASSET_ROOT = ROOT / "AIClient/Assets/Bundles"
def load(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8-sig"))
def save(path: Path, data: dict) -> None:
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def generated_overview(manifest: dict, design_id: str) -> bool:
option = next((x for x in manifest.get("designOptions", []) if x.get("id") == design_id), None)
generation = (option or {}).get("generation", {})
return generation.get("model") == "gpt-image-2.5" and generation.get("status") == "generated"
def find_existing_prefab(component_type: str) -> str | None:
"""Return an authored prefab for a reusable component, if one exists.
The search is intentionally limited to the game asset root. Intermediate
GLB/FBX files in Tools/output are not considered reusable Unity components.
"""
if not component_type:
return None
candidates = sorted(ASSET_ROOT.rglob(f"{component_type}.prefab"))
if not candidates:
return None
preferred = [p for p in candidates if "Scenes" in p.parts and component_type in p.parts]
path = (preferred[0] if preferred else candidates[0]).resolve()
return str(path.relative_to(ROOT)).replace("\\", "/")
def ensure_component_entry(manifest: dict, item: dict) -> dict:
component_type = item.get("type")
allowed = manifest.setdefault("allowedComponentTypes", [])
if component_type and component_type not in allowed:
allowed.append(component_type)
component = next((x for x in manifest.setdefault("components", []) if x.get("type") == item.get("type")), None)
if component is None:
component = {
"type": item.get("type"),
"label": item.get("type"),
"targetTriangles": item.get("targetTriangles", 700),
"generationStatus": "pending",
}
manifest["components"].append(component)
for key in ("position", "rotation", "scale", "placements"):
if key in item:
component[key] = item[key]
return component
def main() -> int:
parser = argparse.ArgumentParser(description="Generate ground and prop design sheets for a battlefield design.")
parser.add_argument("--design", default="A", help="方案 ID;对应 Doc/Design/Scene/Battlefield/<ID>/battlefield_component_prompts.json")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument(
"--force-new-components",
action="store_true",
help="即使 Assets/Bundles 中已有同名组件 Prefab,也重新生成该组件设计图",
)
parser.add_argument(
"--use-references",
action="store_true",
help="上传主界面与总览图作为参考;默认关闭,避免外传项目内部图片",
)
parser.add_argument("--quality", default="auto", choices=["auto", "low", "medium", "high"])
args = parser.parse_args()
manifest = load(MANIFEST)
if not generated_overview(manifest, args.design) and not args.dry_run:
raise SystemExit(
f"方案 {args.design} 的总览图尚未由 gpt-image-2.5 生成;"
f"请先运行 python Tools/generate_battlefield_designs.py --design {args.design}"
)
if not generated_overview(manifest, args.design):
print("warning: dry-run uses the current overview path; actual generation requires the AI-generated overview")
prompt_path = PROMPTS_DIR / args.design / "battlefield_component_prompts.json"
if not prompt_path.is_file():
raise SystemExit(f"缺少方案 {args.design} 的组件拆分提示词:{prompt_path}")
prompt_data = load(prompt_path)
style = ROOT / prompt_data["styleReference"]
overview = ROOT / prompt_data["overviewReference"]
output_root = PROMPTS_DIR / args.design
jobs = [prompt_data["ground"], *prompt_data["components"]]
# A manifest describes one selected level at a time. Remove component
# entries from a previous design before adding this design's split list;
# shared types are retained so their authored Prefab can be reused.
job_types = {item.get("type") for item in prompt_data.get("components", [])}
manifest["components"] = [
component for component in manifest.get("components", []) if component.get("type") in job_types
]
reused = []
generated = []
for item in jobs:
component_type = item.get("type")
existing_prefab = (
find_existing_prefab(component_type)
if component_type and item.get("reuseExisting", True) is not False
else None
)
if component_type and existing_prefab and not args.force_new_components:
component = ensure_component_entry(manifest, item)
component.update(
{
"generationStatus": "reused",
"reuseExisting": True,
"existingPrefab": existing_prefab,
"prefab": component.get("prefab") or existing_prefab,
"designImage": component.get("designImage", ""),
}
)
reused.append(component_type)
print(f"[{component_type}] reuse {existing_prefab}; skip new design image")
continue
output = output_root / item["file"]
output.parent.mkdir(parents=True, exist_ok=True)
callback = output.with_name(output.stem + "_gpt_image_status.json")
command = [
sys.executable,
str(IMAGE_TOOL),
"--prompt",
item["prompt"],
"--model",
"gpt-image-2.5",
"--aspect-ratio",
"1:1",
"--quality",
args.quality,
"--output",
str(output),
"--callback-file",
str(callback),
"--result-only",
]
if args.use_references:
command[command.index("--model"):command.index("--model")] = [
"--image",
str(style),
"--image",
str(overview),
]
print(f"[{item.get('type', 'ground')}] {output}")
print(" ".join(json.dumps(part, ensure_ascii=False) for part in command))
if args.dry_run:
continue
try:
subprocess.run(command, cwd=ROOT, check=True)
except subprocess.CalledProcessError as exc:
print(
"component image generation failed; configure GRSAI_API_KEY or "
"GPT_IMAGE_API_KEY and rerun this command",
file=sys.stderr,
)
return exc.returncode or 1
rel_output = str(output.relative_to(ROOT)).replace("\\", "/")
rel_status = str(callback.relative_to(ROOT)).replace("\\", "/")
if "type" not in item:
manifest["ground"].update(
{
"designImage": rel_output,
"designGenerationStatus": "generated",
"designStatusFile": rel_status,
}
)
else:
component = ensure_component_entry(manifest, item)
component.update(
{
"designImage": rel_output,
"generationStatus": "generated",
"statusFile": rel_status,
"reuseExisting": False,
"existingPrefab": "",
"prefab": "",
"modelGenerationStatus": "pending",
}
)
generated.append(component_type)
if not args.dry_run:
manifest["componentDesignGeneration"] = {
"tool": "Tools/gpt_image_generate.py",
"model": "gpt-image-2.5",
"styleReference": prompt_data["styleReference"] if args.use_references else "prompt-described",
"overviewReference": prompt_data["overviewReference"] if args.use_references else "prompt-described",
"referenceUpload": "enabled" if args.use_references else "none",
"status": "generated" if generated else "reused",
"generatedAtUtc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"generatedComponents": generated,
"reusedComponents": reused,
}
save(MANIFEST, manifest)
return 0
if __name__ == "__main__":
raise SystemExit(main())