223 lines
8.9 KiB
Python
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())
|