Files
aetherbound-guild/tools/prepare_rsk001_clean_master.py
T

112 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Remove dynamic text/actors from the approved RSK-001 coordinate master."""
from __future__ import annotations
import argparse
from pathlib import Path
import cv2
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = ROOT / "docs/design-authority/pages/RSK-001-risk-board/approved/v001/master.png"
DEFAULT_OUTPUT = ROOT / "runtime/assets/entry/rsk_001_risk_board/risk-board-static-master.png"
def rect(mask: np.ndarray, x0: float, y0: float, x1: float, y1: float) -> None:
height, width = mask.shape
cv2.rectangle(
mask,
(round(x0 * width), round(y0 * height)),
(round(x1 * width), round(y1 * height)),
255,
thickness=-1,
)
def fill_slot(image: np.ndarray, x0: float, y0: float, x1: float, y1: float, sample: tuple[float, float, float]) -> None:
"""Cover a dynamic slot with restrained parchment texture, keeping no glyph edges."""
height, width = image.shape[:2]
left, top = round(x0 * width), round(y0 * height)
right, bottom = round(x1 * width), round(y1 * height)
colour = np.array(sample, dtype=np.float32)
# Keep the warm paper variation without preserving source lettering.
rng = np.random.default_rng(round((x0 + y0) * 100000))
noise = rng.normal(0.0, 1.4, (bottom - top, right - left, 1)).astype(np.float32)
slot = np.clip(colour.reshape(1, 1, 3) + noise, 0, 255).astype(np.uint8)
# Feather the edge so the slot reads as part of the paper rather than a
# hard rectangular sticker.
feather = 0
alpha = np.ones((bottom - top, right - left, 1), dtype=np.float32)
image[top:bottom, left:right] = (image[top:bottom, left:right] * (1.0 - alpha) + slot * alpha).astype(np.uint8)
def build(source: Path, output: Path) -> None:
image = cv2.imread(str(source), cv2.IMREAD_COLOR)
if image is None:
raise SystemExit(f"could not read {source}")
if image.shape[1] != 2048 or image.shape[0] != 1152:
raise SystemExit(f"expected 2048x1152, got {image.shape[1]}x{image.shape[0]}")
mask = np.zeros(image.shape[:2], dtype=np.uint8)
# Header: preserve the plaque/frame, replace all baked title, actors,
# position labels and version badge with a single clean engine-owned slot.
fill_slot(image, 0.255, 0.022, 0.985, 0.275, (224, 217, 201))
rect(mask, 0.255, 0.022, 0.985, 0.275)
# The left plaque is retained as ornament, but its two baked title lines
# are replaced so the localized Runtime title appears exactly once.
fill_slot(image, 0.070, 0.028, 0.270, 0.112, (52, 39, 25))
fill_slot(image, 0.070, 0.078, 0.270, 0.132, (52, 39, 25))
rect(mask, 0.070, 0.028, 0.270, 0.132)
# Option columns: preserve diamond letters, semantic icon circles and
# dividers; all labels and values are rendered by Runtime.
columns = [(0.085, 0.35), (0.365, 0.635), (0.65, 0.92)]
for left, right in columns:
fill_slot(image, left + 0.012, 0.285, right - 0.012, 0.805, (232, 224, 207))
rect(mask, left + 0.012, 0.285, right - 0.012, 0.805)
# Bottom rail: preserve the authored plates and icons while clearing text.
for x0, y0, x1, y1 in [
(0.015, 0.835, 0.28, 0.965),
(0.72, 0.835, 0.985, 0.965),
(0.32, 0.835, 0.68, 0.98),
]:
fill_slot(image, x0, y0, x1, y1, (57, 38, 22))
rect(mask, x0, y0, x1, y1)
cleaned = image
output.parent.mkdir(parents=True, exist_ok=True)
if not cv2.imwrite(str(output), cleaned):
raise SystemExit(f"could not write {output}")
mask_path = output.with_name("dynamic-regions.png")
cv2.imwrite(str(mask_path), mask)
report = output.with_name("static-master-report.md")
report.write_text(
"# RSK-001 Clean Static Master\n\n"
f"- Source: `{source}`\n"
f"- Output: `{output}`\n"
f"- Dynamic-region mask: `{mask_path}`\n"
"- Removed: generated title/party copy, baked header actors and positions, "
"option labels/values, and bottom command labels.\n"
"- Preserved: painted world, panel frames, semantic icon ornaments and rail geometry.\n"
"- Runtime policy: all values, labels, actors, selection and hit areas are engine-owned.\n",
encoding="utf-8",
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
build(args.source, args.output)
if __name__ == "__main__":
main()