feat(#79): IMP-51 image_overrides axis (u1~u11 backend stamp+CLI+CSS inject + frontend drag/resize+persistence + tests)
This commit is contained in:
@@ -3406,6 +3406,7 @@ def run_phase_z2_mvp1(
|
||||
override_frames: Optional[dict[str, str]] = None,
|
||||
override_zone_geometries: Optional[dict[str, dict]] = None,
|
||||
override_section_assignments: Optional[dict[str, list[str]]] = None,
|
||||
override_image_overrides: Optional[dict[str, dict]] = None,
|
||||
) -> Path:
|
||||
"""MVP-1.5b entry — single slide + composition planner v0 + 8 preset vocabulary.
|
||||
|
||||
@@ -3419,6 +3420,15 @@ def run_phase_z2_mvp1(
|
||||
으로 강제. unit_id = "+".join(source_section_ids) (e.g., "03-1"
|
||||
또는 "03-1+03-2"). 매칭 unit 의 v4_candidates 에 있는 entry 면
|
||||
그 entry 의 score / label 도 함께 갱신. 없으면 template_id 만 변경.
|
||||
override_image_overrides : {image_id: {x, y, w, h}} — IMP-51 (#79) u5 axis.
|
||||
image_id = stable id stamped on user-content `<img>` tags by
|
||||
``src/image_id_stamper.py`` (u4). x/y/w/h are percent-of-slide
|
||||
coordinates (0–100, slide-absolute). Forward-compat kwarg: the
|
||||
render-time CSS injection that consumes this mapping lands in
|
||||
u7; until u7 wires the consumer, accepting the kwarg keeps the
|
||||
backend contract (KNOWN_AXES u1 + Vite allowlist u2 + typed
|
||||
client u3 + stamper u4) end-to-end addressable from CLI without
|
||||
diverging the function signature.
|
||||
"""
|
||||
mdx_path = Path(mdx_path)
|
||||
if run_id is None:
|
||||
@@ -5375,6 +5385,36 @@ def run_phase_z2_mvp1(
|
||||
# 7. Render single slide
|
||||
html = render_slide(slide_title, slide_footer, zones_data, layout_preset, layout_css)
|
||||
|
||||
# IMP-51 (#79) u4 + u7 — stamp user-content imgs with stable id /
|
||||
# role attrs, then inject persisted `image_overrides` CSS so the
|
||||
# next render re-applies the user-edited geometry.
|
||||
#
|
||||
# Forward-compat: `stage0_normalized_assets["images"]` is empty in
|
||||
# every current Phase Z run (Q1 = A confirmed at Stage 1), so the
|
||||
# stamper returns an empty `stamped_image_ids` list and the CSS
|
||||
# builder short-circuits to "". The HTML is therefore byte-for-byte
|
||||
# identical to the pre-IMP-51 output until Phase Z starts emitting
|
||||
# user-content imgs (separate axis, out of scope for #79).
|
||||
from src.image_id_stamper import (
|
||||
build_image_overrides_style,
|
||||
inject_image_overrides_style,
|
||||
stamp_user_content_images,
|
||||
)
|
||||
_user_content_image_srcs = [
|
||||
(entry.get("path") or entry.get("src") or "")
|
||||
for entry in (stage0_normalized_assets.get("images") or [])
|
||||
if isinstance(entry, dict)
|
||||
]
|
||||
html, _stamped_image_ids = stamp_user_content_images(
|
||||
html, sources=_user_content_image_srcs,
|
||||
)
|
||||
if override_image_overrides:
|
||||
_image_overrides_css = build_image_overrides_style(
|
||||
override_image_overrides, _stamped_image_ids,
|
||||
)
|
||||
if _image_overrides_css:
|
||||
html = inject_image_overrides_style(html, _image_overrides_css)
|
||||
|
||||
# 8. Write final.html
|
||||
out_path = run_dir / "final.html"
|
||||
out_path.write_text(html, encoding="utf-8")
|
||||
@@ -5844,6 +5884,27 @@ if __name__ == "__main__":
|
||||
"--override-section-assignment bottom=03-2,03-3"
|
||||
),
|
||||
)
|
||||
# IMP-51 (#79) u5 — image override CLI flag. IMAGE_ID = stable id stamped
|
||||
# on user-content `<img>` tags by src/image_id_stamper.py (u4). X,Y,W,H =
|
||||
# percent-of-slide coordinates (0–100, slide-absolute), consistent with
|
||||
# the typed client `ImageOverride` shape (u3, userOverridesApi.ts) and
|
||||
# the persisted `image_overrides` axis (u1, KNOWN_AXES). The render-time
|
||||
# CSS injection consuming this mapping lands in u7; u5 is the CLI surface.
|
||||
parser.add_argument(
|
||||
"--override-image",
|
||||
dest="override_image_overrides",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="IMAGE_ID=X,Y,W,H",
|
||||
help=(
|
||||
"user-content image 의 slide-absolute geometry 강제. IMAGE_ID = "
|
||||
"src/image_id_stamper.py 가 stamp 한 `data-image-id` value "
|
||||
"(e.g., img-1a2b3c4d5e). X,Y,W,H = percent-of-slide (0–100, "
|
||||
"slide-absolute) — typed client ImageOverride shape 와 일치. "
|
||||
"multiple flags: --override-image img-abc=10,15,30,25 "
|
||||
"--override-image img-def=50,15,40,40"
|
||||
),
|
||||
)
|
||||
# IMP-46 u5 — auto-cache opt-in. When set, ``cache.save_proposal``
|
||||
# bypasses the ``user_approved`` gate only (``visual_check_passed``
|
||||
# is never bypassable). Source of truth is
|
||||
@@ -5943,6 +6004,54 @@ if __name__ == "__main__":
|
||||
_seen_sections_across_zones[sid] = zid
|
||||
overrides_section_assignments[zid] = section_ids
|
||||
|
||||
# IMP-51 (#79) u5 — parse --override-image into dict[str, dict[str, float]].
|
||||
# Mirrors --override-zone-geometry parsing pattern: each flag is
|
||||
# IMAGE_ID=X,Y,W,H with 4 floats; multiple flags accumulate. Hard errors
|
||||
# on missing `=` / wrong float count / non-numeric values / empty IMAGE_ID
|
||||
# / duplicate IMAGE_ID. The on-disk schema (u1 KNOWN_AXES) and typed
|
||||
# client (u3 ImageOverride) both expect percent-of-slide values in
|
||||
# 0–100; the CLI accepts floats without range clamping here so the
|
||||
# error remains the user's mistake to read rather than a silent shift.
|
||||
overrides_images: dict[str, dict[str, float]] = {}
|
||||
for ov in args.override_image_overrides:
|
||||
if "=" not in ov:
|
||||
print(
|
||||
f"[error] --override-image must be IMAGE_ID=X,Y,W,H, got: '{ov}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
iid, vals = ov.split("=", 1)
|
||||
iid = iid.strip()
|
||||
if not iid:
|
||||
print(
|
||||
f"[error] --override-image IMAGE_ID must be non-empty, got: '{ov}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
if iid in overrides_images:
|
||||
print(
|
||||
f"[error] --override-image duplicate IMAGE_ID '{iid}' "
|
||||
f"(first assignment kept). Provide each image only once.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
parts = vals.split(",")
|
||||
if len(parts) != 4:
|
||||
print(
|
||||
f"[error] --override-image expects 4 floats X,Y,W,H, got: '{vals}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
try:
|
||||
x, y, w, h = (float(p) for p in parts)
|
||||
except ValueError:
|
||||
print(
|
||||
f"[error] --override-image floats parse fail: '{vals}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
overrides_images[iid] = {"x": x, "y": y, "w": w, "h": h}
|
||||
|
||||
# IMP-52 (#80) u2 — user_overrides.json persistence fallback.
|
||||
# After argparse fully parses CLI flags, fill ONLY the axes the user
|
||||
# did NOT pass on the command line. CLI payload always wins over the
|
||||
@@ -6013,6 +6122,30 @@ if __name__ == "__main__":
|
||||
if _sids:
|
||||
_accepted_sec[_zid] = _sids
|
||||
overrides_section_assignments = _accepted_sec
|
||||
# image_overrides — CLI empty → fill from file (dict[str, dict]).
|
||||
# IMP-51 (#79) u6 — mirrors zone_geometries validation: only accept
|
||||
# mappings of {image_id: {x,y,w,h}} with float-coercible values.
|
||||
if not overrides_images:
|
||||
_file_images = _persisted.get("image_overrides")
|
||||
if isinstance(_file_images, dict):
|
||||
_accepted_img: dict[str, dict] = {}
|
||||
for _iid, _g in _file_images.items():
|
||||
if (
|
||||
isinstance(_iid, str)
|
||||
and _iid
|
||||
and isinstance(_g, dict)
|
||||
and all(k in _g for k in ("x", "y", "w", "h"))
|
||||
):
|
||||
try:
|
||||
_accepted_img[_iid] = {
|
||||
"x": float(_g["x"]),
|
||||
"y": float(_g["y"]),
|
||||
"w": float(_g["w"]),
|
||||
"h": float(_g["h"]),
|
||||
}
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
overrides_images = _accepted_img
|
||||
|
||||
run_phase_z2_mvp1(
|
||||
args.mdx_path,
|
||||
@@ -6021,4 +6154,5 @@ if __name__ == "__main__":
|
||||
override_frames=overrides_frames or None,
|
||||
override_zone_geometries=overrides_geoms or None,
|
||||
override_section_assignments=overrides_section_assignments or None,
|
||||
override_image_overrides=overrides_images or None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user