One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:
Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the
shared imports (they need the engine)
Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
(they need the Blender Flatpak)
Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
visual_targets.json
Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers,
reference fetching
Tools/input/ OS-level input injection
root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/
Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.
51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.
Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
131 lines
6.4 KiB
Python
131 lines
6.4 KiB
Python
"""End-to-end asset check: Blender render -> Unreal build -> captures -> judge -> README.
|
|
|
|
python3 Tools/review/e2e_asset_check.py # everything
|
|
python3 Tools/review/e2e_asset_check.py --skip-blender # reuse the existing Blender renders
|
|
python3 Tools/review/e2e_asset_check.py --skip-judge # no API calls, just the images
|
|
|
|
The chain, in order, each step failing loudly with its log under /tmp/gigline/:
|
|
|
|
1. **Blender renders** - `Art/blender/*/` assets rendered by `Tools/blender/blender_screenshots.py`, so the
|
|
source the imports came from has an image of its own.
|
|
2. **Empty maps** - `Tools/unreal/ue_recreate_maps.py` rebuilds both maps, so a run never inherits an actor
|
|
from an experiment. The builders also clear the level they load; this is the belt-and-braces step.
|
|
3. **Unreal build** - `Tools/unreal/ue_build_demo_map.py` (game map) and `Tools/unreal/ue_build_review_map.py`
|
|
(staging map for individual assets).
|
|
4. **Captures** - `Tools/review/render_screenshots.sh`, which renders both maps into `Screenshots/`.
|
|
5. **Judge** - every shot in `Tools/review/visual_targets.json` judged by **two models** through
|
|
`Tools/review/judge_screenshot.py --shots`, which reconciles agreement and reports disagreement.
|
|
6. **Review file** - `Tools/review/build_comparison_md.py` rewrites the Blender-vs-UE block in
|
|
`Screenshots/README.md`, then prettier formats it.
|
|
|
|
The report lands in `/tmp/gigline/e2e-report.txt`; the exit code is non-zero if any step failed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
ENGINE = os.environ.get("ENGINE", "/run/media/system/DATA/Engines/UE_5.8.4")
|
|
UPROJECT = os.path.join(ROOT, "Game", "GigLine", "GigLine.uproject")
|
|
BLENDER = os.environ.get(
|
|
"BLENDER_FILES", "/var/lib/flatpak/app/org.blender.Blender/x86_64/stable/active/files")
|
|
UNREAL = os.path.join(ENGINE, "Engine", "Binaries", "Linux", "UnrealEditor-Cmd")
|
|
REPORT = "/tmp/gigline/e2e-report.txt"
|
|
|
|
|
|
def run(step, cmd, env=None, timeout=None, result_file=None, expect=None):
|
|
"""Run one step and record it. A step fails on a non-zero exit or a missing marker."""
|
|
started = time.time()
|
|
print(f"== {step}")
|
|
try:
|
|
proc = subprocess.run(cmd, cwd=ROOT, env={**os.environ, **(env or {})},
|
|
capture_output=True, text=True, timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
return step, False, f"timed out after {timeout}s"
|
|
elapsed = time.time() - started
|
|
if proc.returncode != 0:
|
|
tail = (proc.stderr or proc.stdout or "").strip().splitlines()[-3:]
|
|
return step, False, f"exit {proc.returncode} ({elapsed:.0f}s): {' / '.join(tail)}"
|
|
if result_file and expect:
|
|
if not os.path.exists(result_file):
|
|
return step, False, f"no result file {result_file}"
|
|
text = open(result_file, errors="replace").read()
|
|
if expect not in text:
|
|
return step, False, f"{os.path.basename(result_file)} lacks {expect!r}"
|
|
return step, True, f"{elapsed:.0f}s"
|
|
|
|
|
|
def unreal_script(script, timeout=1800):
|
|
return [UNREAL, UPROJECT, "-run=pythonscript", f"-script={os.path.join(ROOT, script)}",
|
|
"-nullrhi", "-unattended", "-nopause", "-stdout"]
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--skip-blender", action="store_true", help="reuse the Blender renders")
|
|
parser.add_argument("--skip-judge", action="store_true", help="no API calls")
|
|
parser.add_argument("--skip-recreate", action="store_true",
|
|
help="keep the existing maps (the builders still clear their levels)")
|
|
args = parser.parse_args()
|
|
|
|
steps = []
|
|
if not args.skip_blender:
|
|
steps.append(run("1/6 blender renders",
|
|
[os.path.join(BLENDER, "blender", "blender"), "-b", "--factory-startup",
|
|
"--python", os.path.join(ROOT, "Tools", "blender", "blender_screenshots.py"),
|
|
"--", "--mode", "both", "--out", os.path.join(ROOT, "Screenshots", "blender")],
|
|
env={"LD_LIBRARY_PATH": os.path.join(BLENDER, "blender", "lib")},
|
|
timeout=1800))
|
|
else:
|
|
steps.append(("1/6 blender renders", True, "skipped, reusing Screenshots/blender"))
|
|
|
|
if not args.skip_recreate:
|
|
steps.append(run("2/6 empty maps", unreal_script("Tools/unreal/ue_recreate_maps.py"),
|
|
timeout=900, result_file="/tmp/gigline/recreate-maps.txt",
|
|
expect="VERDICT: PASS"))
|
|
else:
|
|
steps.append(("2/6 empty maps", True, "skipped"))
|
|
|
|
steps.append(run("3/6 unreal build: demo map", unreal_script("Tools/unreal/ue_build_demo_map.py"),
|
|
timeout=1800, result_file="/tmp/gigline/demo-map.txt", expect="save_map"))
|
|
steps.append(run("3/6 unreal build: review map", unreal_script("Tools/unreal/ue_build_review_map.py"),
|
|
timeout=1800, result_file="/tmp/ue-review-map-result.txt", expect="save_map"))
|
|
|
|
steps.append(run("4/6 captures", ["bash", os.path.join(ROOT, "Tools", "review", "render_screenshots.sh")],
|
|
timeout=2400))
|
|
|
|
if not args.skip_judge:
|
|
steps.append(run("5/6 judge (two models)",
|
|
[sys.executable, os.path.join(ROOT, "Tools", "review", "judge_screenshot.py"), "--shots"],
|
|
timeout=1800))
|
|
else:
|
|
steps.append(("5/6 judge (two models)", True, "skipped"))
|
|
|
|
steps.append(run("6/6 review file",
|
|
[sys.executable, os.path.join(ROOT, "Tools", "review", "build_comparison_md.py")],
|
|
timeout=600))
|
|
|
|
lines = [f"e2e asset check {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}", ""]
|
|
ok = True
|
|
for step, passed, detail in steps:
|
|
lines.append(f"{'PASS' if passed else 'FAIL'} {step}: {detail}")
|
|
ok = ok and passed
|
|
lines += ["", f"VERDICT: {'PASS' if ok else 'FAIL'}",
|
|
"Review: Screenshots/README.md (status table at the top)."]
|
|
os.makedirs(os.path.dirname(REPORT), exist_ok=True)
|
|
with open(REPORT, "w") as handle:
|
|
handle.write("\n".join(lines) + "\n")
|
|
print()
|
|
print("\n".join(lines))
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|