Remaster twenty-one original pictures and preserve complete review evidence
This commit is contained in:
1 parent
6b1a2629a0
commit
e0d114e657
237 files changed
+576164
-73
No files matched your search
@@ -0,0 +1,180 @@
|
||||
"""Replay a checked batch of existing boss cleanup recipes without new pixel math.
|
||||
|
||||
Each frame is passed to the hash-bound hd_boss_cleanup.run_recipe unchanged.
|
||||
This dispatcher records actual subruns and binds one outer portable recipe.
|
||||
It does not approve artwork or register assets.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
METHOD = "derived-generated-boss-source-geometry-batch-v1"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def digest(value):
|
||||
return (type(value) is str and len(value) == 64
|
||||
and all(c in "0123456789abcdef" for c in value))
|
||||
|
||||
|
||||
def contained(base, value):
|
||||
if type(value) is not str or not value or "\\" in value:
|
||||
raise ValueError("expected portable relative path")
|
||||
path = Path(value)
|
||||
if path.is_absolute() or any(part in (".", "..") for part in path.parts):
|
||||
raise ValueError("expected contained relative path")
|
||||
result = (base / path).resolve()
|
||||
if not result.is_relative_to(base.resolve()) or not result.is_file():
|
||||
raise ValueError("missing or escaped input")
|
||||
return result
|
||||
|
||||
|
||||
def checked_ref(base, value):
|
||||
if type(value) is not dict or set(value) != {"path", "sha256"} or not digest(value["sha256"]):
|
||||
raise ValueError("expected path/sha256 binding")
|
||||
path = contained(base, value["path"])
|
||||
if sha(path) != value["sha256"]:
|
||||
raise ValueError("input hash mismatch")
|
||||
return path
|
||||
|
||||
|
||||
def plan(recipe_path, project):
|
||||
recipe_path = Path(recipe_path).resolve()
|
||||
recipe = json.loads(recipe_path.read_bytes())
|
||||
if (type(recipe) is not dict or set(recipe) != {
|
||||
"format_version", "kind", "method", "cleanup_tool", "decoder", "frames"}
|
||||
or type(recipe["format_version"]) is not int or recipe["format_version"] != 1
|
||||
or recipe["kind"] != "boss_cleanup_reproduce_batch" or recipe["method"] != METHOD):
|
||||
raise ValueError("unsupported batch recipe")
|
||||
project = Path(project).resolve()
|
||||
for field, expected in [("cleanup_tool", "tools/hd_boss_cleanup.py"),
|
||||
("decoder", "tools/hd_frame_policy.py")]:
|
||||
if type(recipe[field]) is not dict or recipe[field].get("path") != expected:
|
||||
raise ValueError("unsupported cleanup implementation")
|
||||
checked_ref(project, recipe[field])
|
||||
frames = recipe["frames"]
|
||||
if type(frames) is not list or not 1 <= len(frames) <= 32:
|
||||
raise ValueError("expected bounded nonempty frame list")
|
||||
planned, seen = [], set()
|
||||
for frame in frames:
|
||||
if type(frame) is not dict or set(frame) != {"asset", "recipe", "expected_output_sha256"}:
|
||||
raise ValueError("invalid batch frame")
|
||||
asset = frame["asset"]
|
||||
if (type(asset) is not str or not 1 <= len(asset) <= 8
|
||||
or any(c not in "0123456789abcdef" for c in asset) or asset in seen
|
||||
or not digest(frame["expected_output_sha256"])):
|
||||
raise ValueError("invalid or duplicate asset/output binding")
|
||||
seen.add(asset)
|
||||
subpath = checked_ref(recipe_path.parent, frame["recipe"])
|
||||
sub = json.loads(subpath.read_bytes())
|
||||
if type(sub) is not dict or sub.get("asset") != asset:
|
||||
raise ValueError("subrecipe asset mismatch")
|
||||
# Validate all declared input bodies before creating any output directory.
|
||||
for key in ("source", "palette", "parent_raw"):
|
||||
value = sub.get(key)
|
||||
if type(value) is not dict or set(value) != {"path", "sha256"} or not digest(value["sha256"]):
|
||||
raise ValueError("invalid subrecipe input binding")
|
||||
p = Path(value["path"])
|
||||
if not p.is_absolute():
|
||||
p = subpath.parent / p
|
||||
if not p.is_file() or sha(p) != value["sha256"]:
|
||||
raise ValueError("subrecipe input hash mismatch")
|
||||
planned.append((frame, subpath, sub))
|
||||
return recipe, planned
|
||||
|
||||
|
||||
def run_batch(recipe_path, output, project):
|
||||
recipe_path, output, project = Path(recipe_path).resolve(), Path(output), Path(project).resolve()
|
||||
if output.exists():
|
||||
raise ValueError("output exists; do not overwrite old outputs")
|
||||
initial_recipe_sha256 = sha(recipe_path)
|
||||
recipe, planned = plan(recipe_path, project)
|
||||
if sha(recipe_path) != initial_recipe_sha256:
|
||||
raise ValueError("batch recipe changed during planning")
|
||||
sys.path.insert(0, str(project / "tools"))
|
||||
cleanup = importlib.import_module("hd_boss_cleanup")
|
||||
decoder = importlib.import_module("hd_frame_policy")
|
||||
if (Path(cleanup.__file__).resolve() != project / "tools/hd_boss_cleanup.py"
|
||||
or Path(decoder.__file__).resolve() != project / "tools/hd_frame_policy.py"
|
||||
or sha(cleanup.__file__) != recipe["cleanup_tool"]["sha256"]
|
||||
or sha(decoder.__file__) != recipe["decoder"]["sha256"]):
|
||||
raise ValueError("loaded implementation differs from bound tools")
|
||||
output.mkdir(parents=True)
|
||||
subruns, sources, parents, outputs = [], {}, set(), {}
|
||||
|
||||
def verify_pinned_inputs():
|
||||
if sha(recipe_path) != initial_recipe_sha256:
|
||||
raise ValueError("batch recipe changed during execution")
|
||||
checked_ref(project, recipe["cleanup_tool"])
|
||||
checked_ref(project, recipe["decoder"])
|
||||
for pinned_frame, pinned_path, pinned_sub in planned:
|
||||
if sha(pinned_path) != pinned_frame["recipe"]["sha256"]:
|
||||
raise ValueError("subrecipe changed during execution")
|
||||
for key in ("source", "palette", "parent_raw"):
|
||||
value = pinned_sub[key]
|
||||
path = Path(value["path"])
|
||||
if not path.is_absolute():
|
||||
path = pinned_path.parent / path
|
||||
if sha(path) != value["sha256"]:
|
||||
raise ValueError("subrecipe input changed during execution")
|
||||
|
||||
verify_pinned_inputs()
|
||||
for frame, subpath, sub in planned:
|
||||
verify_pinned_inputs()
|
||||
asset = frame["asset"]
|
||||
destination = output / asset
|
||||
report = cleanup.run_recipe(subpath, destination)
|
||||
png = destination / f"{asset}-derived.png"
|
||||
actual = sha(png)
|
||||
if actual != frame["expected_output_sha256"]:
|
||||
raise ValueError(f"accepted PNG not reproduced: {asset}")
|
||||
expected_execution = {"status": "success",
|
||||
"method": "derived-generated-boss-source-geometry-rgb-preserved-v1",
|
||||
"recipe_sha256": frame["recipe"]["sha256"],
|
||||
"tool_sha256": recipe["cleanup_tool"]["sha256"],
|
||||
"source_sha256": {asset: sub["source"]["sha256"]},
|
||||
"parent_raw_sha256": [sub["parent_raw"]["sha256"]],
|
||||
"output_sha256": {asset: actual}}
|
||||
if type(report) is not dict or report.get("execution") != expected_execution:
|
||||
raise ValueError("subrun execution receipt differs from pinned inputs")
|
||||
verify_pinned_inputs()
|
||||
sources[asset] = sub["source"]["sha256"]
|
||||
parents.add(sub["parent_raw"]["sha256"])
|
||||
outputs[asset] = actual
|
||||
provenance = destination / "derived-provenance.json"
|
||||
subruns.append({"asset": asset, "recipe": frame["recipe"],
|
||||
"execution": report["execution"],
|
||||
"provenance": {"path": f"{asset}/derived-provenance.json", "sha256": sha(provenance)}})
|
||||
verify_pinned_inputs()
|
||||
result = {"format_version": 1, "kind": "boss_cleanup_actual_batch_execution",
|
||||
"execution": {"status": "success", "method": METHOD,
|
||||
"recipe_sha256": initial_recipe_sha256, "tool_sha256": sha(__file__),
|
||||
"source_sha256": sources, "parent_raw_sha256": sorted(parents),
|
||||
"output_sha256": outputs},
|
||||
"actual_dispatcher_calls": 1, "actual_cleanup_subruns": len(subruns),
|
||||
"subruns": subruns, "cleanup_tool": recipe["cleanup_tool"], "decoder": recipe["decoder"],
|
||||
"scope": "Bookkeeping replay only; existing cleanup pixel algorithm unchanged; no approval/registration."}
|
||||
(output / "batch-execution.json").write_text(json.dumps(result, indent=2, sort_keys=True) + "\n")
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("recipe", type=Path)
|
||||
parser.add_argument("output_new_directory", type=Path)
|
||||
parser.add_argument("project", type=Path)
|
||||
args = parser.parse_args()
|
||||
report = run_batch(args.recipe, args.output_new_directory, args.project)
|
||||
print(json.dumps(report["execution"], sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Private derived-RGB singleton cleanup with declared Original functional cells.
|
||||
|
||||
The reviewed DSHP renderer preserves broad parent RGB and supplies SourceScale4
|
||||
coverage. Only explicitly listed painted logical cells then receive actual
|
||||
Original Scale4 RGBA. This is derived artwork, never untouched generation.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
R = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(R / 'tools'))
|
||||
import hd_dshp20_cleanup as base
|
||||
|
||||
BASE_SHA = 'c6123e9591f0a169846677019ddcb944bf9c2b4c9184a7d8ab02a08546de409b'
|
||||
DECODER_SHA = '66184a4758027dadbadb5e20fe90b2add2b9fc218b7810304b1a1ea2de74f58e'
|
||||
METHOD = 'derived-parent-full-RGB-SourceScale4-geometry-explicit-functional-Original-v1'
|
||||
|
||||
def digest(b): return hashlib.sha256(b).hexdigest()
|
||||
|
||||
def dependency_pins():
|
||||
tool = Path(base.__file__).resolve()
|
||||
decoder = Path(base.hd_frame_policy.__file__).resolve()
|
||||
assert digest(tool.read_bytes()) == BASE_SHA
|
||||
assert digest(decoder.read_bytes()) == DECODER_SHA
|
||||
return dict(RGB_geometry_tool=dict(path=str(tool), sha256=BASE_SHA),
|
||||
Source_decoder=dict(path=str(decoder), sha256=DECODER_SHA))
|
||||
|
||||
def retain_functional_original(rgba, source, palette, cells):
|
||||
"""Copy the whole actual Scale4 kernel footprint, including transparency."""
|
||||
h, w = source.shape
|
||||
if not isinstance(cells, list) or not 1 <= len(cells) <= 64:
|
||||
raise ValueError('need1..64 explicit functional Original cells')
|
||||
for xy in cells:
|
||||
if (not isinstance(xy, list) or len(xy) != 2 or any(type(v) is not int for v in xy)
|
||||
or not 0 <= xy[0] < w or not 0 <= xy[1] < h or not 0 <= source[xy[1], xy[0]] < 256):
|
||||
raise ValueError('functional cell must be exact integer inbounds painted Source')
|
||||
if cells != sorted(cells) or len({tuple(xy) for xy in cells}) != len(cells):
|
||||
raise ValueError('functional cells must be sorted and unique')
|
||||
if rgba.shape != (h*4, w*4, 4) or rgba.dtype != np.uint8:
|
||||
raise ValueError('need complete native-times4 uint8 RGBA canvas')
|
||||
s4 = base.scale2(base.scale2(source))
|
||||
mask = np.zeros(s4.shape, bool)
|
||||
for x, y in cells: mask[y*4:(y+1)*4, x*4:(x+1)*4] = True
|
||||
original = np.zeros_like(rgba)
|
||||
painted = s4 < 256
|
||||
original[:,:,:3][painted] = palette[s4[painted]]
|
||||
original[:,:,3][painted] = 255
|
||||
out = rgba.copy()
|
||||
out[mask] = original[mask]
|
||||
assert np.array_equal(out[~mask], rgba[~mask])
|
||||
assert np.array_equal(out[mask], original[mask])
|
||||
return out, dict(cells=cells, Source_indices=[int(source[y,x]) for x,y in cells],
|
||||
actual_Original_Scale4_RGBA_samples=int(mask.sum()),
|
||||
actual_Original_transparent_samples=int((mask & ~painted).sum()),
|
||||
RGBA_changes=int(np.any(out != rgba, axis=2).sum()),
|
||||
outside_explicit_footprints_RGBA_exact=True,
|
||||
retained_Original_not_generated_HD=True)
|
||||
|
||||
def run_recipe(recipe_path, output):
|
||||
recipe_path, output = Path(recipe_path), Path(output)
|
||||
if output.exists(): raise ValueError('fresh absent output directory required')
|
||||
dependencies = dependency_pins()
|
||||
rb = recipe_path.read_bytes(); recipe = json.loads(rb)
|
||||
if (not isinstance(recipe, dict) or set(recipe) != {'format_version','geometry_recipe','functional_original_cells'}
|
||||
or type(recipe['format_version']) is not int or recipe['format_version'] != 1):
|
||||
raise ValueError('exact wrapper recipe fields/version required')
|
||||
geometry = recipe['geometry_recipe']
|
||||
if not isinstance(geometry, dict) or not isinstance(geometry.get('frames'), list) or len(geometry['frames']) != 1:
|
||||
raise ValueError('this private wrapper supports one complete singleton per recipe')
|
||||
arrays, report = base.render_family(geometry, recipe_path.resolve().parent)
|
||||
frame = geometry['frames'][0]
|
||||
_, payload = base.checked(frame['source'], recipe_path.resolve().parent)
|
||||
w,h,cells = base._indices(payload)
|
||||
source = np.asarray(cells,np.uint16).reshape(h,w)
|
||||
_, pb = base.checked(geometry['palette'], recipe_path.resolve().parent)
|
||||
p6 = np.frombuffer(pb,np.uint8).reshape(256,3).astype(np.uint16)
|
||||
palette = ((p6 << 2) | (p6 >> 4)).astype(np.uint8)
|
||||
rgba, functional = retain_functional_original(arrays[0], source, palette, recipe['functional_original_cells'])
|
||||
encoded = io.BytesIO(); Image.fromarray(rgba).save(encoded, format='PNG', compress_level=9)
|
||||
png = encoded.getvalue(); asset = frame['asset']
|
||||
execution = dict(status='success', method=METHOD, recipe_sha256=digest(rb),
|
||||
tool_sha256=digest(Path(__file__).read_bytes()), source_sha256={asset:frame['source']['sha256']},
|
||||
parent_raw_sha256=[frame['raw_png']['sha256']], output_sha256={asset:digest(png)})
|
||||
report.update(method=METHOD, geometry_method=base.METHOD, dependencies=dependencies,
|
||||
functional_Original=functional, execution=execution,
|
||||
output=dict(asset=asset, file=asset+'-derived.png', sha256=digest(png),
|
||||
RGBA_sha256=digest(rgba.tobytes()), size=[w*4,h*4]),
|
||||
scope='DERIVED broad generated RGB, SourceScale4 geometry and explicit retained Original functional cells; no game-context or art acceptance.')
|
||||
output.mkdir(parents=True)
|
||||
(output/(asset+'-derived.png')).write_bytes(png)
|
||||
(output/'derived-provenance.json').write_text(json.dumps(report,indent=2,sort_keys=True)+'\n')
|
||||
(output/'normalized-execution.json').write_text(json.dumps(execution,indent=2,sort_keys=True)+'\n')
|
||||
return report
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('recipe',type=Path);parser.add_argument('output_new_directory',type=Path)
|
||||
args=parser.parse_args(); result=run_recipe(args.recipe,args.output_new_directory)
|
||||
print(json.dumps(result['execution'],sort_keys=True))
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Synthetic dispatcher tests; never execute cleanup pixel processing."""
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location("batch", Path(__file__).with_name("hd_boss_cleanup_reproduce.py"))
|
||||
batch = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(batch)
|
||||
|
||||
|
||||
class BatchTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.project = self.root / "project"
|
||||
(self.project / "tools").mkdir(parents=True)
|
||||
self.base = self.root / "job"
|
||||
self.base.mkdir()
|
||||
for n in ["hd_boss_cleanup.py", "hd_frame_policy.py"]:
|
||||
(self.project / "tools" / n).write_text("# synthetic bound implementation\n")
|
||||
for n in ["source", "raw", "palette"]:
|
||||
(self.base / n).write_bytes(n.encode())
|
||||
def ref(p, base): return {"path": str(p.relative_to(base)), "sha256": batch.sha(p)}
|
||||
self.doc = {"format_version": 1, "kind": "boss_cleanup_reproduce_batch", "method": batch.METHOD,
|
||||
"cleanup_tool": ref(self.project / "tools/hd_boss_cleanup.py", self.project),
|
||||
"decoder": ref(self.project / "tools/hd_frame_policy.py", self.project), "frames": []}
|
||||
for asset in ["abc", "abd"]:
|
||||
sub = {"asset": asset, "source": ref(self.base / "source", self.base),
|
||||
"parent_raw": ref(self.base / "raw", self.base), "palette": ref(self.base / "palette", self.base)}
|
||||
p = self.base / f"{asset}.json"
|
||||
p.write_text(json.dumps(sub))
|
||||
self.doc["frames"].append({"asset": asset, "recipe": ref(p, self.base),
|
||||
"expected_output_sha256": __import__('hashlib').sha256(asset.encode()).hexdigest()})
|
||||
self.recipe = self.base / "batch.json"
|
||||
self.save()
|
||||
|
||||
def save(self): self.recipe.write_text(json.dumps(self.doc))
|
||||
|
||||
def test_preflight_rejects_bad_tool_before_any_output(self):
|
||||
self.doc["cleanup_tool"]["sha256"] = "0" * 64
|
||||
self.save()
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
||||
self.assertFalse((self.base / "out").exists())
|
||||
|
||||
def test_duplicate_asset_rejected(self):
|
||||
self.doc["frames"][1] = self.doc["frames"][0].copy()
|
||||
self.save()
|
||||
with self.assertRaises(ValueError): batch.plan(self.recipe, self.project)
|
||||
|
||||
def test_escaped_recipe_rejected(self):
|
||||
self.doc["frames"][0]["recipe"]["path"] = "../outside.json"
|
||||
self.save()
|
||||
with self.assertRaises(ValueError): batch.plan(self.recipe, self.project)
|
||||
|
||||
def test_wrong_subrecipe_asset_rejected(self):
|
||||
self.doc["frames"][0]["asset"] = "abe"
|
||||
self.save()
|
||||
with self.assertRaises(ValueError): batch.plan(self.recipe, self.project)
|
||||
|
||||
def test_changed_source_rejected_before_output(self):
|
||||
(self.base / "source").write_bytes(b"changed")
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
||||
self.assertFalse((self.base / "out").exists())
|
||||
|
||||
def fake_cleanup(self, subpath, output):
|
||||
sub = json.loads(subpath.read_text()); asset = sub["asset"]
|
||||
output.mkdir()
|
||||
(output / f"{asset}-derived.png").write_bytes(asset.encode())
|
||||
result = {"execution": {"status": "success", "method": "derived-generated-boss-source-geometry-rgb-preserved-v1", "recipe_sha256": batch.sha(subpath),
|
||||
"tool_sha256": self.doc["cleanup_tool"]["sha256"], "source_sha256": {asset: sub["source"]["sha256"]},
|
||||
"parent_raw_sha256": [sub["parent_raw"]["sha256"]],
|
||||
"output_sha256": {asset: batch.sha(output / f"{asset}-derived.png")}}}
|
||||
(output / "derived-provenance.json").write_text(json.dumps(result))
|
||||
return result
|
||||
|
||||
def fake_import(self, name):
|
||||
return SimpleNamespace(__file__=str(self.project / "tools" / f"{name}.py"), run_recipe=self.fake_cleanup)
|
||||
|
||||
def test_actual_aggregate_uses_unique_parent_and_per_asset_outputs(self):
|
||||
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import):
|
||||
result = batch.run_batch(self.recipe, self.base / "out", self.project)
|
||||
e = result["execution"]
|
||||
self.assertEqual(result["actual_cleanup_subruns"], 2)
|
||||
self.assertEqual(e["recipe_sha256"], batch.sha(self.recipe))
|
||||
self.assertEqual(e["tool_sha256"], batch.sha(batch.__file__))
|
||||
self.assertEqual(e["parent_raw_sha256"], [batch.sha(self.base / "raw")])
|
||||
self.assertEqual(set(e["source_sha256"]), {"abc", "abd"})
|
||||
self.assertEqual(e["output_sha256"], {r["asset"]: r["expected_output_sha256"] for r in self.doc["frames"]})
|
||||
|
||||
def test_wrong_accepted_png_does_not_write_success_receipt(self):
|
||||
self.doc["frames"][0]["expected_output_sha256"] = "0" * 64
|
||||
self.save()
|
||||
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import):
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
||||
self.assertFalse((self.base / "out/batch-execution.json").exists())
|
||||
|
||||
def test_existing_output_is_never_overwritten(self):
|
||||
output = self.base / "out"; output.mkdir(); sentinel = output / "old"; sentinel.write_text("old")
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, output, self.project)
|
||||
self.assertEqual(sentinel.read_text(), "old")
|
||||
|
||||
def test_all_subrun_execution_fields_must_match_pinned_inputs(self):
|
||||
original = self.fake_cleanup
|
||||
fields = ["status", "method", "recipe_sha256", "tool_sha256", "source_sha256", "parent_raw_sha256", "output_sha256"]
|
||||
for number, field in enumerate(fields):
|
||||
with self.subTest(field=field):
|
||||
def contradictory(path, output):
|
||||
report = original(path, output)
|
||||
report['execution'][field] = "contradiction"
|
||||
return report
|
||||
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import), patch.object(self, "fake_cleanup", side_effect=contradictory):
|
||||
output = self.base / f"bad-{number}"
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, output, self.project)
|
||||
self.assertFalse((output / 'batch-execution.json').exists())
|
||||
|
||||
def test_batch_recipe_edit_during_subrun_prevents_success(self):
|
||||
original = self.fake_cleanup
|
||||
def changed(path, output):
|
||||
report = original(path, output)
|
||||
self.recipe.write_text(self.recipe.read_text() + ' ')
|
||||
return report
|
||||
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import), patch.object(self, "fake_cleanup", side_effect=changed):
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
||||
self.assertFalse((self.base / 'out/batch-execution.json').exists())
|
||||
|
||||
def test_subrecipe_edit_during_subrun_prevents_success(self):
|
||||
original = self.fake_cleanup
|
||||
def changed(path, output):
|
||||
report = original(path, output)
|
||||
path.write_text(path.read_text() + ' ')
|
||||
return report
|
||||
with patch.object(batch.importlib, "import_module", side_effect=self.fake_import), patch.object(self, "fake_cleanup", side_effect=changed):
|
||||
with self.assertRaises(ValueError): batch.run_batch(self.recipe, self.base / "out", self.project)
|
||||
self.assertFalse((self.base / 'out/batch-execution.json').exists())
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,69 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import tempfile
|
||||
import unittest
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
spec=importlib.util.spec_from_file_location('other_cleanup',Path(__file__).with_name('hd_other_functional_cleanup.py'))
|
||||
c=importlib.util.module_from_spec(spec);spec.loader.exec_module(c)
|
||||
|
||||
class FunctionalOriginalTests(unittest.TestCase):
|
||||
def palette(self):
|
||||
p=np.arange(768,dtype=np.uint16).reshape(256,3)%64
|
||||
return ((p<<2)|(p>>4)).astype(np.uint8)
|
||||
def pattern(self,h,w):
|
||||
y,x=np.indices((h*4,w*4));a=np.zeros((h*4,w*4,4),np.uint8)
|
||||
a[:,:,:3]=np.stack([(x//4)*33+25,(y//4)*27+40,(x*7+y*3)%230],axis=-1);a[:,:,3]=255
|
||||
return a
|
||||
def test_only_declared_ordinary31_and_gray15_footprints_replace_full_RGB(self):
|
||||
s=np.full((4,5),15,np.uint16);s[:,:2]=31;a=self.pattern(4,5);p=self.palette()
|
||||
out,report=c.retain_functional_original(a,s,p,[[1,1],[3,2]])
|
||||
mask=np.zeros((16,20),bool);mask[4:8,4:8]=True;mask[8:12,12:16]=True
|
||||
self.assertTrue(np.array_equal(out[~mask],a[~mask]));self.assertTrue(np.array_equal(a,self.pattern(4,5)))
|
||||
self.assertTrue(np.all(out[4:8,4:8,:3]==p[31]));self.assertTrue(np.all(out[8:12,12:16,:3]==p[15]))
|
||||
self.assertEqual(report['actual_Original_Scale4_RGBA_samples'],32)
|
||||
def test_kernel_footprint_retains_real_Original_transparency_not_NN_point_color(self):
|
||||
s=np.array([[256,256,1],[256,31,1],[1,1,1]],np.uint16);a=self.pattern(3,3)
|
||||
out,r=c.retain_functional_original(a,s,self.palette(),[[1,1]])
|
||||
s4=c.base.scale2(c.base.scale2(s));expected=np.zeros((4,4,4),np.uint8);q=s4[4:8,4:8];painted=q<256
|
||||
expected[:,:,:3][painted]=self.palette()[q[painted]];expected[:,:,3][painted]=255
|
||||
self.assertTrue(np.array_equal(out[4:8,4:8],expected));self.assertGreater(r['actual_Original_transparent_samples'],0)
|
||||
self.assertLess(int((out[4:8,4:8,3]>0).sum()),16)
|
||||
def test_zero_typed_out_of_bounds_transparent_duplicate_or_unsorted_selectors_rejected(self):
|
||||
s=np.full((4,5),10,np.uint16);s[0,0]=256;a=self.pattern(4,5)
|
||||
invalid=[[],[[True,1]],[[1.0,1]],[[5,1]],[[-1,1]],[[1,4]],[[0,0]],[[1,1],[1,1]],[[3,2],[1,1]]]
|
||||
for cells in invalid:
|
||||
with self.subTest(cells=cells),self.assertRaises(ValueError):c.retain_functional_original(a,s,self.palette(),cells)
|
||||
self.assertTrue(np.array_equal(a,self.pattern(4,5)))
|
||||
def test_broad_gradient_and_panel_edges_are_exact_outside_small_retention(self):
|
||||
s=np.full((10,12),10,np.uint16);s[9,2]=31;a=self.pattern(10,12)
|
||||
out,_=c.retain_functional_original(a,s,self.palette(),[[2,9]])
|
||||
self.assertTrue(np.array_equal(out[:36],a[:36]));self.assertTrue(np.array_equal(out[36:,:8],a[36:,:8]))
|
||||
self.assertGreater(len(np.unique(out[:36,:,:3].reshape(-1,3),axis=0)),100)
|
||||
def test_reviewed_base_and_decoder_dependencies_are_bound(self):
|
||||
d=c.dependency_pins();self.assertEqual(d['RGB_geometry_tool']['sha256'],c.BASE_SHA);self.assertEqual(d['Source_decoder']['sha256'],c.DECODER_SHA)
|
||||
def test_once_output_normalized_lineage_input_immutability_and_deterministic_PNG(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root=Path(td);source=np.full((3,4),10,np.uint16);source[1,1]=31
|
||||
b=bytearray(struct.pack('<5i',0,1,31,4,3))
|
||||
for y in range(3):b.extend(struct.pack('<4i',0,y,y*4,4));b.extend(bytes(source[y].tolist()))
|
||||
b.extend(struct.pack('<4i',0,0,-1,0));(root/'source.pic').write_bytes(b)
|
||||
(root/'palette').write_bytes(bytes((np.arange(768)%64).astype(np.uint8)))
|
||||
Image.fromarray(self.pattern(3,4)).save(root/'parent.png')
|
||||
def bind(n):return dict(path=n,sha256=hashlib.sha256((root/n).read_bytes()).hexdigest())
|
||||
geometry=dict(format_version=1,palette=bind('palette'),output_scale=4,max_fill_distance_pixels=12,shared_backing_anchor=0,
|
||||
frames=[dict(asset='102f7',source=bind('source.pic'),raw_png=bind('parent.png'),viewport=[0,0,16,12],sampling_offset_native=[0,0],origin=[0,0])])
|
||||
recipe=dict(format_version=1,geometry_recipe=geometry,functional_original_cells=[[1,1]])
|
||||
rp=root/'recipe.json';rp.write_text(json.dumps(recipe));before={p:p.read_bytes()for p in root.iterdir()}
|
||||
report=c.run_recipe(rp,root/'out1');c.run_recipe(rp,root/'out2')
|
||||
self.assertEqual((root/'out1/102f7-derived.png').read_bytes(),(root/'out2/102f7-derived.png').read_bytes())
|
||||
self.assertEqual(set(report['execution']),{'status','method','recipe_sha256','tool_sha256','source_sha256','parent_raw_sha256','output_sha256'})
|
||||
self.assertEqual(report['functional_Original']['Source_indices'],[31])
|
||||
for p,data in before.items():self.assertEqual(p.read_bytes(),data)
|
||||
with self.assertRaisesRegex(ValueError,'fresh absent'):c.run_recipe(rp,root/'out1')
|
||||
|
||||
if __name__=='__main__':unittest.main(verbosity=2)
|
||||
Reference in new issue
Block a user