Add Phase Z B4 source-shape-aware placement

- enable B1/B2/B4 source-shape-aware F13 placement behind env flag
- align F13 placement_trace with mapper top_bullets cardinality
- preserve canonical render output when flag is off
This commit is contained in:
2026-05-07 05:26:57 +09:00
parent 8a201337f7
commit 761a43da5e
4 changed files with 151 additions and 23 deletions

View File

@@ -62,6 +62,7 @@ class InternalRegion:
— B2 v0 에서 kind="frame_match" / frame_id=None /
display_strategy="inline_full" 고정.
실제 frame 결정은 Step 9 / B4 책임.
source_shape_index : positional index from B1 source_shape split (Option 1, optional)
"""
region_id: str
@@ -70,6 +71,7 @@ class InternalRegion:
ratio_estimate: float
content_unit_ids: list[str]
frame_match_strategy: dict
source_shape_index: Optional[int] = None
@dataclass
@@ -146,6 +148,75 @@ _TYPE_ORDER_PRIORITY: dict[str, int] = {
}
# ─── Option 1 source_shape-aware planner ─────────────────────────
def _plan_by_source_shape_index(
content_objects: list[ContentObject],
section_id: str,
) -> ZoneRegionPlan:
"""source_shape_index 기준 *positional* region grouping (Option 1).
같은 source_shape_index 끼리 1 region. mapper 의 split_source 와 cardinality align —
F13 의 top_bullets 3 개 → 3 region 으로 mapper pillar_1/2/3 와 1:1 positional.
"""
groups: dict[int, list[ContentObject]] = {}
for obj in content_objects:
if obj.source_shape_index is None:
continue
groups.setdefault(obj.source_shape_index, []).append(obj)
sorted_indices = sorted(groups.keys())
# size proxy + ratio (positional region 내부 size_estimate 합산)
index_sizes: dict[int, float] = {idx: sum(_size_proxy(o) for o in groups[idx]) for idx in sorted_indices}
total_size = sum(index_sizes.values())
if total_size <= 0:
equal_share = 1.0 / max(len(sorted_indices), 1)
index_sizes = {idx: equal_share for idx in sorted_indices}
total_size = sum(index_sizes.values()) or 1.0
regions: list[InternalRegion] = []
for ord_idx, sidx in enumerate(sorted_indices, start=1):
objs = groups[sidx]
# role / content_type : group 내 첫 obj 의 type 기반 (Option 1 pilot = text_block 동질)
primary_obj = objs[0]
ctype = primary_obj.type
regions.append(
InternalRegion(
region_id=f"{section_id}.region-{ord_idx}",
role=_TYPE_ROLE.get(ctype, "primary"),
content_type=ctype,
ratio_estimate=round(index_sizes[sidx] / total_size, 4),
content_unit_ids=[o.id for o in objs],
frame_match_strategy={
"kind": "frame_match",
"frame_id": None,
"display_strategy": "inline_full",
},
source_shape_index=sidx,
)
)
region_count = len(regions)
if region_count == 1:
layout_type = "region-single"
placement = "single"
else:
layout_type = "region-vertical-stack"
placement = "vertical"
region_order = [r.region_id for r in regions]
return ZoneRegionPlan(
internal_regions=regions,
region_layout=RegionLayout(
region_layout_type=layout_type,
region_order=region_order,
region_placement=placement,
),
)
# ─── Public entry ────────────────────────────────────────────────
@@ -186,6 +257,11 @@ def plan_internal_regions(
if not content_objects:
return ZoneRegionPlan()
# Option 1 source_shape-aware path : ContentObjects 가 source_shape_index 보유 시 *positional*
# grouping. 같은 index 끼리 1 region. mapper 의 split_source 와 cardinality align.
if any(o.source_shape_index is not None for o in content_objects):
return _plan_by_source_shape_index(content_objects, section_id)
# 1. type 별 grouping
groups = _group_by_type_preserving_order(content_objects)