@lark-apaas/coding-steering 0.1.32-dev.5abff3b → 0.1.32-dev.6ef2b6f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +13 -6
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +121 -30
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +229 -10
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +19 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +4 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +113 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +308 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +13 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +301 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
package/package.json
CHANGED
|
@@ -74,15 +74,15 @@ mkdir -p tmp/pptx-style-extract/<unique-id> && PYTHONDONTWRITEBYTECODE=1 python3
|
|
|
74
74
|
1. 读 `<outdir>/l-out/BRIEF.md` 一次,确定待判断文件、视觉组和页型代表页;需要查候选的全部原始位置、尺寸、重复次数、透明度或近白比例时读 `asset-vision-groups.json`。没有候选图片的页无需做图片判断。
|
|
75
75
|
2. 一次并行看全部 `vision-group-*.jpg` 与 `layout-sheet.png`。每张视觉组拼版都同时给出页面语境和带编号的候选卡;透明或近白候选会同时显示棋盘格和深灰底。除非拼版把图片标成 unreadable,不打开单张素材、不再读旧的 `contact-sheet` / `asset-context-sheet`。
|
|
76
76
|
3. 在 `manifest.yaml` 按每个 `asset_vision_groups` 候选实例填写 `visual_kind`;同一素材在不同页型或位置可分别判断。仅在预算外候选或需要改写既有判断时追加 `asset_decisions`;位置例外写 `box: [x, y, w, h]`,同图同位置跨页型不同时再写 `layout`。可用值固定为 `logo|slogan|background|texture|icon|decorative|illustration|photo|chart|screenshot|footer-copyright|page-number|watermark|content-image|unknown`,不新造值;`package.py` 会把它映射回现有 v2 资产类型,视觉组字段不会进入最终包。
|
|
77
|
-
4. 按页面语境判断用途:本 deck 自己的品牌标志才是 `logo
|
|
77
|
+
4. 按页面语境判断用途:本 deck 自己的品牌标志才是 `logo`;图表、截图、logo 墙,以及用于解释当前页具体产品、案例或信息的图片是 `content-image`;封面、目录、章节或封底中承担构图主视觉的局部图,即使是产品或摄影图,也用 `decorative` 或 `illustration` 保留为版式素材。页码、水印和版权标记用对应的 omit 类值。脚本已直接保留不透明满屏背景和封面主视觉,不为它们补判断;半透明满屏叠加层仍要在视觉组中定性。拿不准的候选用 `unknown`,不要按品牌名、页码或图片数量猜用途。
|
|
78
78
|
5. 用一次 `multi_edit` 批量完成其余 TODO:风格气质,以及 `layout-controls.yaml` 中的页型/文本角色、布局模式和背景规则。普通文本槽默认 `body`,只把确实属于 `title|subtitle|header|footer` 的例外写进 `text_roles`;有 flow 与 slots 两种形态时默认保留 slots,只有样张明确需要内容随高度重排才填 `layout_modes: <页型>: flow`。不补不存在的标题、页眉或页脚。直接编辑草案,不写脚本改 YAML,也不修改 Skill 源码。
|
|
79
|
-
6. `layout-controls.yaml` 的 `bg_rules:`
|
|
79
|
+
6. `layout-controls.yaml` 的 `bg_rules:` 按**背景资产**分组(不是按页型),只编辑草案已有且被页型 `background:` 引用的真实图片背景;没有 `bg_rules` 就不要新增。纯色、渐变、外框、几何装饰和透明叠层不是这里的背景资产。逐张背景填 `text_safe` / `avoid` / `pairing_rule`:看图标清视觉主体、强光斑、深色透明区,打包时自动并入用该背景的所有页型。标题、正文、关键数字、图表、卡片、时间线及其容器的外接矩形都不得压住背景主体,透明容器也不能跨进禁放区。
|
|
80
80
|
|
|
81
81
|
`layout-controls.yaml` 是唯一需要编辑的版式判断文件,只含 `names`、`roles`、`text_roles`、`layout_modes` 和 `bg_rules`。`layouts.yaml` 是兼容用的坐标事实——页型、坐标、字号、色值、对齐、以及各页型自带的 `role: logo` 资产槽全部直读,**不要打开或修改它**。打包时控制文件会覆盖旧文件中的同名判断区。
|
|
82
82
|
|
|
83
83
|
判断口径:
|
|
84
84
|
|
|
85
|
-
-
|
|
85
|
+
- **图片按用途三分**——整幅替换底图的艺术图/摄影图属**背景族**;服务于当前页具体内容的图表、截图、产品说明图是**内容图,不进包**;用于封面、目录、章节或封底构图的局部主视觉不解释内容,即使画面是产品或摄影图,也属**装饰图,标 `decorative` 或 `illustration` 进 assets**;其余纹理、插画、色块、几何点缀同属装饰图。不透明满屏图默认走背景;局部图和半透明满屏叠加层结合候选图与整页语境逐张定性,避免把重要装饰误当内容丢弃。首页、末页优先保留样张,但是否为 `cover` 或 `closing` 仍按页面意图判断。
|
|
86
86
|
- **logo 特指这份 deck 自己的品牌标志**——看图确认是本 PPT 的品牌文字或标志图形才留 `logo`;一页若是 logo 墙(客户/合作方 logo 罗列),那些是内容图、不当风格 logo。拿不准就保留该候选并填 `unknown`,在 `gaps` 写一句;不要删除或合并 `asset_vision_groups` 条目。
|
|
87
87
|
- **anchors 是事实不是结论**——草案里每条只报测到的数(覆盖率、计数、占比),没有「这套风格的特征是 X」这种断言。这一段在 design.md 里读起来像总纲,消费端会照它建全局样式,所以脚本不敢替你下结论:一条 1/8 覆盖率的元素被描述成「跨页不动」,消费端就会每页都摆它。看过重建图后,把真正是这套模板特征的那几条改写成设计要点,不是特征的直接删掉。
|
|
88
88
|
- **数值只改名不改值**——草案里的坐标/色值/字号来自普查;确需推导值(CJK 行高转译、投影尺度上抬)在 manifest 写 `derived:` 声明理由,机检认声明。
|
|
@@ -130,13 +130,20 @@ Read `<pack_dir>/design.md` first, especially `## Usage`, `## Hard Rules`, color
|
|
|
130
130
|
When generating a deck:
|
|
131
131
|
|
|
132
132
|
1. Call `copy_starter_component` with `kind: "deck-stage.js"`.
|
|
133
|
-
2. Build `<deck-stage width="<canvas width>" height="<canvas height>">` using the `canvas` declared in `layouts.md` — source decks are not always 16:9, and a default-sized stage shifts every coordinate on the page. Each slide is one static `<section>`.
|
|
133
|
+
2. Build `<deck-stage width="<canvas width>" height="<canvas height>">` using the `canvas` declared in `layouts.md` — source decks are not always 16:9, and a default-sized stage shifts every coordinate on the page. Each slide is one static `<section data-pptx-layout="<chosen archetype>">`. For a multi-theme pack, choose layouts compatible with `default-theme` by default. When a page intentionally switches theme, add `data-pptx-theme="<theme>"`; the theme must be listed by that layout's `themes`.
|
|
134
134
|
3. Inline CSS variables from `design.md` into the HTML `<style>` block using a `--ppt-*` prefix.
|
|
135
135
|
4. **位移动画用独立的 `translate` 属性**:`@keyframes fadeUp { from{opacity:0; translate:0 24px} to{opacity:1; translate:0 0} }`。`transform` 是单一属性,动画里碰它会覆盖掉元素原有的那条(`left:50%; transform:translateX(-50%)` 的居中就此丢失);`translate` / `rotate` / `scale` 各自独立,与已有 `transform` 叠加。
|
|
136
|
-
5. Map `layouts.md` slots to absolute-positioned elements inside each section: expand `box: [x,y,w,h]` mechanically to `left/top/width/height`, then apply the slot's `css` declaration string unchanged. `box` owns geometry; `css` owns all rendering style, including the template's text padding, typography, alignment, line height, letter spacing, and rotation. Do not reinterpret PPTX fields or replace slot CSS with your own type scale. A slot with `asset` is a fixed image element
|
|
136
|
+
5. Map `layouts.md` slots to absolute-positioned elements inside each section: expand `box: [x,y,w,h]` mechanically to `left/top/width/height`, then apply the slot's `css` declaration string unchanged. `box` owns geometry; `css` owns all rendering style, including the template's text padding, typography, alignment, line height, letter spacing, and rotation. Do not reinterpret PPTX fields or replace slot CSS with your own type scale. A slot with `asset` is a fixed image element: use that exact asset at that box on that archetype only; do not omit or replace it. Do not reflow any of this as generic web grids.
|
|
137
137
|
6. Copy `<pack_dir>/assets/` into the project, for example `assets/pptx-style/<pack-name>/`, and reference those copied files with relative URLs. The scratch `tmp/pptx-style-extract/...` path must never appear in the final HTML.
|
|
138
138
|
7. Use the font stacks and fallback rules from `design.md`; do not install fonts or dependencies at runtime.
|
|
139
|
-
8.
|
|
139
|
+
8. Before the regular slide preflight, verify every background and fixed image asset against the selected archetype:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
PYTHONDONTWRITEBYTECODE=1 python3 -B scripts/verify_layout_assets.py <pack_dir> <index.html> --asset-prefix <copied-assets-prefix>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`PPTX_LAYOUT_ASSETS: FAIL` means fix every reported instance according to `layouts.md`, then rerun the same command. Do not deliver or call `run_commit` until it prints `PPTX_LAYOUT_ASSETS: PASS`.
|
|
146
|
+
9. Run the slide preflight checks: no resource failures, no section overflow, and sampled screenshots follow the package colors, typography, layouts, assets, and Hard Rules.
|
|
140
147
|
|
|
141
148
|
## Export Consumer Attachments
|
|
142
149
|
|
|
@@ -66,6 +66,7 @@ def image_census(shape_recs, bg_images, units):
|
|
|
66
66
|
'crop': r.get('crop'), 'placement': r.get('placement'),
|
|
67
67
|
'w_pct': r.get('w_pct'), 'h_pct': r.get('h_pct'),
|
|
68
68
|
'in_group': bool(r.get('group_path')), 'svg': r.get('media_svg'),
|
|
69
|
+
'visible_instance_blocked': r.get('visible_instance_blocked'),
|
|
69
70
|
})
|
|
70
71
|
for r in shape_recs:
|
|
71
72
|
f = r.get('fill') or {}
|
|
@@ -124,6 +125,8 @@ def image_census(shape_recs, bg_images, units):
|
|
|
124
125
|
'crop_variants': sorted(crop_sigs),
|
|
125
126
|
'stitch_candidate': len(crop_sigs) > 1,
|
|
126
127
|
'svg_companion': next((o.get('svg') for o in occs if o.get('svg')), None),
|
|
128
|
+
'visible_instance_blocked': any(
|
|
129
|
+
o.get('visible_instance_blocked') for o in occs),
|
|
127
130
|
})
|
|
128
131
|
images.sort(key=lambda i: (-i['n'], i['media']))
|
|
129
132
|
|
|
@@ -606,6 +609,8 @@ def layout_inventory(pkg, graph, shapes, bg_by_part):
|
|
|
606
609
|
for lp in pkg.layouts:
|
|
607
610
|
root = pkg.xml(lp)
|
|
608
611
|
cSld = root.find('p:cSld', NS)
|
|
612
|
+
master = graph['master_of_layout'].get(lp)
|
|
613
|
+
background = bg_by_part.get(lp) or bg_by_part.get(master)
|
|
609
614
|
phs = Counter()
|
|
610
615
|
for r in shapes:
|
|
611
616
|
if r['part'] == lp and r.get('ph'):
|
|
@@ -614,11 +619,12 @@ def layout_inventory(pkg, graph, shapes, bg_by_part):
|
|
|
614
619
|
'part': lp,
|
|
615
620
|
'name': cSld.get('name') if cSld is not None else None,
|
|
616
621
|
'type_attr': root.get('type', 'cust'),
|
|
617
|
-
'master':
|
|
622
|
+
'master': master,
|
|
618
623
|
'used_by_slides': graph['slides_per_layout'].get(lp, 0),
|
|
619
624
|
'placeholders': dict(phs),
|
|
620
625
|
'shape_n': sum(1 for r in shapes if r['part'] == lp),
|
|
621
|
-
'background':
|
|
626
|
+
'background': background,
|
|
627
|
+
'background_source': lp if bg_by_part.get(lp) else master,
|
|
622
628
|
'ph_signature': _ph_signature(shapes, lp),
|
|
623
629
|
'guides': [],
|
|
624
630
|
})
|
|
@@ -648,14 +648,11 @@ def probe_image(path):
|
|
|
648
648
|
def needs_asset_judgment(candidate):
|
|
649
649
|
"""局部图和半透明满屏叠加层需要看图定性;不透明满屏图按背景处理。"""
|
|
650
650
|
effective_alpha = candidate.get('effective_alpha_mean')
|
|
651
|
-
if ((candidate.get('probe') or {}).get('near_blank')
|
|
652
|
-
or (effective_alpha is not None and effective_alpha < 13)):
|
|
653
|
-
return False
|
|
654
651
|
if not candidate.get('fullscreen'):
|
|
655
652
|
return True
|
|
656
653
|
alpha = (effective_alpha if effective_alpha is not None
|
|
657
654
|
else (candidate.get('probe') or {}).get('alpha_mean'))
|
|
658
|
-
return alpha is not None and alpha < OPAQUE_ENOUGH
|
|
655
|
+
return alpha is not None and 13 <= alpha < OPAQUE_ENOUGH
|
|
659
656
|
|
|
660
657
|
|
|
661
658
|
def fullscreen_effective_alpha(data, outdir, shapes):
|
|
@@ -844,8 +841,8 @@ def draft_assets(d, outdir, bg_needed=(), cover_media=None, bg_under=None,
|
|
|
844
841
|
canvas_w, canvas_h = d['canvas']['px']
|
|
845
842
|
for c in kept:
|
|
846
843
|
effective_am = c.get('effective_alpha_mean')
|
|
847
|
-
if (c['probe'].get('near_blank')
|
|
848
|
-
|
|
844
|
+
if (c['fullscreen'] and (c['probe'].get('near_blank')
|
|
845
|
+
or (effective_am is not None and effective_am < 13))):
|
|
849
846
|
rejected.append((c, '近全透明(alpha 均值 %.0f/255),PPT 里看不见'
|
|
850
847
|
% (effective_am if effective_am is not None
|
|
851
848
|
else c['probe']['alpha_mean'])))
|
|
@@ -962,6 +959,35 @@ def cover_background_media(archetypes):
|
|
|
962
959
|
if archetype['name'] == 'cover'), None)
|
|
963
960
|
|
|
964
961
|
|
|
962
|
+
def background_decor(background, canvas):
|
|
963
|
+
"""把可直接重放的 PPT 背景声明落到 layouts.md 的最底层装饰。
|
|
964
|
+
|
|
965
|
+
图片背景继续走 `background:` 资产引用;纯色和线性渐变没有可复制的素材文件,
|
|
966
|
+
但同样是页型视觉的一部分,必须随页型输出。path 渐变不能由 CSS 线性渐变准确表达,
|
|
967
|
+
保留给图片/渲染链路而不伪造。
|
|
968
|
+
"""
|
|
969
|
+
if not isinstance(background, dict):
|
|
970
|
+
return None
|
|
971
|
+
if background.get('type') not in ('solid', 'gradient'):
|
|
972
|
+
return None
|
|
973
|
+
if background.get('path'):
|
|
974
|
+
return None
|
|
975
|
+
css = _load_query()._recipe_css(background, None, [], [])
|
|
976
|
+
css = [re.sub(r'\s*\n\s*', ' ', value).strip() for value in css if value]
|
|
977
|
+
if not css:
|
|
978
|
+
return None
|
|
979
|
+
width, height = canvas
|
|
980
|
+
return {'box': [0, 0, width, height], 'geom': 'rect', 'css': '; '.join(css),
|
|
981
|
+
'trace': 'canvas-background'}
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def background_identity(background):
|
|
985
|
+
"""返回可用于聚类和审计的稳定背景标识,不改变背景的原始表达。"""
|
|
986
|
+
if isinstance(background, dict):
|
|
987
|
+
return json.dumps(background, sort_keys=True, separators=(',', ':'))
|
|
988
|
+
return background
|
|
989
|
+
|
|
990
|
+
|
|
965
991
|
# ---------------------------------------------------------------- 版式聚类
|
|
966
992
|
DECOR_MIN = 40.0
|
|
967
993
|
|
|
@@ -1217,7 +1243,21 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1217
1243
|
sample_pages_of_layout = defaultdict(list)
|
|
1218
1244
|
for slide_part, layout_part in lay_of_slide.items():
|
|
1219
1245
|
sample_pages_of_layout[layout_part].append(slide_no(slide_part))
|
|
1246
|
+
sampled_theme_counts = Counter(
|
|
1247
|
+
theme_of_master.get(master_of.get(layout_part))
|
|
1248
|
+
for layout_part in lay_of_slide.values()
|
|
1249
|
+
)
|
|
1250
|
+
sampled_theme_counts.pop(None, None)
|
|
1220
1251
|
default_theme = topo.get('default')
|
|
1252
|
+
if sampled_theme_counts:
|
|
1253
|
+
highest = max(sampled_theme_counts.values())
|
|
1254
|
+
leaders = sorted(
|
|
1255
|
+
theme for theme, count in sampled_theme_counts.items()
|
|
1256
|
+
if count == highest
|
|
1257
|
+
)
|
|
1258
|
+
if default_theme not in leaders:
|
|
1259
|
+
default_theme = leaders[0]
|
|
1260
|
+
topo['default'] = default_theme
|
|
1221
1261
|
multi = len(topo.get('themes') or []) > 1
|
|
1222
1262
|
|
|
1223
1263
|
rows = []
|
|
@@ -1273,7 +1313,17 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1273
1313
|
slots, bgm = instance_override(
|
|
1274
1314
|
shapes, inst, slots, bgm, cW, cH, composites)
|
|
1275
1315
|
taken = {tuple(s['box']) for s in slots}
|
|
1276
|
-
decor =
|
|
1316
|
+
decor = []
|
|
1317
|
+
inherited_background = l.get('background')
|
|
1318
|
+
direct_background = next(
|
|
1319
|
+
(slide.get('background') for slide in d.get('slides') or []
|
|
1320
|
+
if slide.get('part') == inst and slide.get('background')),
|
|
1321
|
+
None)
|
|
1322
|
+
background = direct_background or inherited_background
|
|
1323
|
+
background_layer = background_decor(background, (cW, cH))
|
|
1324
|
+
if background_layer:
|
|
1325
|
+
decor.append(background_layer)
|
|
1326
|
+
decor += collect_decor(shapes, inst or l['part'], taken, (cW, cH))
|
|
1277
1327
|
named_role = role_of_name(l.get('name'))
|
|
1278
1328
|
rows.append({'zh': clean_layout_name(l.get('name')),
|
|
1279
1329
|
'role': named_role or 'content', 'role_guessed': named_role is None,
|
|
@@ -1510,16 +1560,19 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1510
1560
|
image_marks = slide_image_marks(d, overlay_media)
|
|
1511
1561
|
|
|
1512
1562
|
bg_of_slide, layout_of_slide = {}, {}
|
|
1563
|
+
background_of_layout = {
|
|
1564
|
+
row['part']: row.get('background') for row in d.get('layouts') or []
|
|
1565
|
+
}
|
|
1513
1566
|
for s in d.get('slides', []):
|
|
1514
1567
|
bg = s.get('background')
|
|
1515
|
-
bg_of_slide[s['part']] =
|
|
1568
|
+
bg_of_slide[s['part']] = background_identity(bg)
|
|
1516
1569
|
layout_of_slide[s['part']] = s.get('layout')
|
|
1517
1570
|
# 版式层的满屏底图(form=2 常态:底图挂在 layout 上)
|
|
1518
1571
|
composites = d.get('background_composites') or {}
|
|
1519
|
-
|
|
1572
|
+
bg_media_of_layout = {}
|
|
1520
1573
|
for s in shapes:
|
|
1521
1574
|
if s.get('layer') == 'layout' and is_bleed(s) and s.get('media'):
|
|
1522
|
-
|
|
1575
|
+
bg_media_of_layout[s['part']] = s['media']
|
|
1523
1576
|
|
|
1524
1577
|
pages = []
|
|
1525
1578
|
for part, sh in sorted(by_slide.items(), key=lambda kv: slide_no(kv[0])):
|
|
@@ -1527,7 +1580,7 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1527
1580
|
layout_shapes = by_layout.get(layout_part) or []
|
|
1528
1581
|
bg_media = top_bleed_media(sh)
|
|
1529
1582
|
if bg_media is None:
|
|
1530
|
-
bg_media =
|
|
1583
|
+
bg_media = bg_media_of_layout.get(layout_part)
|
|
1531
1584
|
rendered_bg = (composites.get(part)
|
|
1532
1585
|
or composites.get(layout_part)
|
|
1533
1586
|
or bg_media)
|
|
@@ -1571,9 +1624,13 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1571
1624
|
if key not in seen_marks:
|
|
1572
1625
|
seen_marks.add(key)
|
|
1573
1626
|
marks.append(mark)
|
|
1627
|
+
background = next((s.get('background') for s in d.get('slides') or []
|
|
1628
|
+
if s.get('part') == part and s.get('background')), None)
|
|
1629
|
+
background = background or background_of_layout.get(layout_part)
|
|
1574
1630
|
pages.append({'part': part, 'no': slide_no(part), 'bg_media': bg_media,
|
|
1575
1631
|
'rendered_bg': rendered_bg,
|
|
1576
|
-
'bg_color': bg_of_slide.get(part), '
|
|
1632
|
+
'bg_color': bg_of_slide.get(part), 'background': background,
|
|
1633
|
+
'texts': texts, 'pic_n': len(pics),
|
|
1577
1634
|
'marks': marks, 'shape_n': len(visible_shapes), 'layout': layout_part})
|
|
1578
1635
|
|
|
1579
1636
|
# 页型的**角色**(封面 / 章节页 / 内容页……)不在这里判:那是看图才能下的结论,
|
|
@@ -1599,7 +1656,11 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1599
1656
|
# 不替模型下结论。和首页一样,拆组只避免它被聚类代表页吞掉。
|
|
1600
1657
|
groups[('__last__', -2)] = [p]
|
|
1601
1658
|
continue
|
|
1602
|
-
|
|
1659
|
+
background_key = background_identity(
|
|
1660
|
+
p.get('rendered_bg') or p.get('bg_media')
|
|
1661
|
+
or p.get('background') or p.get('bg_color')
|
|
1662
|
+
) or 'none'
|
|
1663
|
+
groups[(background_key, density_band(p))].append(p)
|
|
1603
1664
|
|
|
1604
1665
|
ranked = sorted(groups.items(), key=lambda kv: (-len(kv[1]), kv[1][0]['no']))
|
|
1605
1666
|
# 首页所在的组一定收——deck 的第一页是模板的门面,孤例也不能被名额挤掉。
|
|
@@ -1621,15 +1682,9 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1621
1682
|
leftover = sorted(p['no'] for g in ranked if g not in kept for p in g[1])
|
|
1622
1683
|
|
|
1623
1684
|
archetypes = []
|
|
1624
|
-
for gi, ((
|
|
1685
|
+
for gi, ((_background_key, _band), ps) in enumerate(kept, 1):
|
|
1625
1686
|
rep = max(ps, key=lambda p: len(p['texts']))
|
|
1626
|
-
|
|
1627
|
-
bg_raw = rep['bg_media'] or rep['bg_color'] or 'none'
|
|
1628
|
-
elif bg_raw == '__last__':
|
|
1629
|
-
bg_raw = rep['bg_media'] or rep['bg_color'] or 'none'
|
|
1630
|
-
rendered_bg = rep.get('rendered_bg')
|
|
1631
|
-
if rendered_bg:
|
|
1632
|
-
bg_raw = rendered_bg
|
|
1687
|
+
bg_raw = rep.get('rendered_bg') or rep.get('bg_media')
|
|
1633
1688
|
name = 'layout-%d' % gi
|
|
1634
1689
|
# 标题按「位置 + 跨度」认,不按字号——big-number 类的巨号数值常比标题还大
|
|
1635
1690
|
# 标题 = 该页最靠上的那批文本里最宽的一块。不按「画布前 28%」这类固定比例切:
|
|
@@ -1688,6 +1743,9 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1688
1743
|
round(b.get('w', 0)), round(b.get('h', 0))]})
|
|
1689
1744
|
taken = {tuple(s['box']) for s in slots}
|
|
1690
1745
|
decor = []
|
|
1746
|
+
background_layer = background_decor(rep.get('background'), (cW, cH))
|
|
1747
|
+
if background_layer:
|
|
1748
|
+
decor.append(background_layer)
|
|
1691
1749
|
seen_decor = set()
|
|
1692
1750
|
for source_part in (rep.get('layout'), rep['part']):
|
|
1693
1751
|
for item in collect_decor(shapes, source_part, taken, (cW, cH)):
|
|
@@ -1703,9 +1761,13 @@ def draft_layouts(d, outdir, effective_alpha=None):
|
|
|
1703
1761
|
p['layout'] for p in ps if p.get('layout')
|
|
1704
1762
|
}),
|
|
1705
1763
|
'_source_backgrounds': sorted({
|
|
1706
|
-
|
|
1764
|
+
background_identity(
|
|
1765
|
+
p.get('rendered_bg') or p.get('bg_media')
|
|
1766
|
+
or p.get('background') or p.get('bg_color')
|
|
1767
|
+
)
|
|
1707
1768
|
for p in ps
|
|
1708
|
-
if p.get('rendered_bg') or p.get('bg_media')
|
|
1769
|
+
if (p.get('rendered_bg') or p.get('bg_media')
|
|
1770
|
+
or p.get('background') or p.get('bg_color'))
|
|
1709
1771
|
}),
|
|
1710
1772
|
'_text_n': len(rep['texts']),
|
|
1711
1773
|
'_last_page_candidate': rep['no'] == last_page_no,
|
|
@@ -2338,7 +2400,7 @@ def visual_slot_candidates(candidates, archetypes):
|
|
|
2338
2400
|
return rows
|
|
2339
2401
|
|
|
2340
2402
|
|
|
2341
|
-
def emit_manifest(d, assets, vision_groups, ldir):
|
|
2403
|
+
def emit_manifest(d, assets, vision_groups, ldir, archetypes=()):
|
|
2342
2404
|
L = ['version: alpha',
|
|
2343
2405
|
'name: TODO-style-name # 英文 kebab,体现气质,不要用文件名',
|
|
2344
2406
|
'name_zh: TODO中文名',
|
|
@@ -2346,7 +2408,9 @@ def emit_manifest(d, assets, vision_groups, ldir):
|
|
|
2346
2408
|
' TODO: 一句话说清这套模板的视觉性格(底色 / 主色 / 字形 / 版面骨架),给消费模型定调。']
|
|
2347
2409
|
themes = d['theme_topology'].get('themes') or ['single']
|
|
2348
2410
|
if themes != ['single'] and len(themes) > 1:
|
|
2349
|
-
|
|
2411
|
+
default_theme = d['theme_topology'].get('default') or themes[0]
|
|
2412
|
+
L += ['themes: [%s]' % ', '.join(themes),
|
|
2413
|
+
'default-theme: %s' % default_theme]
|
|
2350
2414
|
if assets:
|
|
2351
2415
|
L.append('assets:')
|
|
2352
2416
|
for a in assets:
|
|
@@ -2381,6 +2445,15 @@ def emit_manifest(d, assets, vision_groups, ldir):
|
|
|
2381
2445
|
' # - {source_media: example.png, visual_kind: chart}',
|
|
2382
2446
|
' # - {source_media: example.png, layout: layout-2, box: [0, 0, 100, 100], visual_kind: decorative}',
|
|
2383
2447
|
]
|
|
2448
|
+
if any(decor.get('trace') == 'canvas-background'
|
|
2449
|
+
for archetype in archetypes
|
|
2450
|
+
for decor in archetype.get('decor') or []):
|
|
2451
|
+
canvas = d['canvas']['px']
|
|
2452
|
+
L += [
|
|
2453
|
+
'derived:',
|
|
2454
|
+
' - value: "[0, 0, %d, %d]"' % (canvas[0], canvas[1]),
|
|
2455
|
+
' reason: "PPT 背景铺满画布"',
|
|
2456
|
+
]
|
|
2384
2457
|
write(os.path.join(ldir, 'manifest.yaml'), '\n'.join(L) + '\n')
|
|
2385
2458
|
|
|
2386
2459
|
|
|
@@ -2727,7 +2800,11 @@ def emit_layout_controls(layout_lines, ldir, text_role_candidates, flow_archetyp
|
|
|
2727
2800
|
]
|
|
2728
2801
|
controls.extend('# %s: flow' % archetype['name'] for archetype in flow_archetypes)
|
|
2729
2802
|
if 'bg_rules' in blocks:
|
|
2730
|
-
controls
|
|
2803
|
+
controls += [
|
|
2804
|
+
'',
|
|
2805
|
+
'# 只编辑本草案已列出的真实图片背景;它们都已被 layouts 中的 background: 引用。',
|
|
2806
|
+
'# 纯色、渐变、外框、几何装饰和透明叠层不新建 bg_rules;没有本段就保持没有。',
|
|
2807
|
+
] + blocks['bg_rules']
|
|
2731
2808
|
write(os.path.join(ldir, 'layout-controls.yaml'), '\n'.join(controls) + '\n')
|
|
2732
2809
|
|
|
2733
2810
|
|
|
@@ -2860,6 +2937,8 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
2860
2937
|
L.append(' %s:' % a['name'])
|
|
2861
2938
|
if a.get('role'):
|
|
2862
2939
|
L.append(' role: %s' % a['role'])
|
|
2940
|
+
if a.get('theme'):
|
|
2941
|
+
L.append(' themes: [%s]' % a['theme'])
|
|
2863
2942
|
if a['bg']:
|
|
2864
2943
|
L.append(' background: %s' % a['bg'])
|
|
2865
2944
|
fl = a.get('flow')
|
|
@@ -2982,6 +3061,10 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
2982
3061
|
% len(archetypes)) if (d.get('form_hint') or {}).get('form') == 3 else
|
|
2983
3062
|
('%d 页样张归纳出 %d 种页型。' % (d['counts']['slides'], len(archetypes))))
|
|
2984
3063
|
L += ['', '## Usage', '',
|
|
3064
|
+
'**生成前必须完整阅读本 `design.md` 和 %s,确认全部页型后再开始搭页。**'
|
|
3065
|
+
'不能只看摘要、前几个页型或 `## Layouts` 清单;后续页型同样可能定义背景、'
|
|
3066
|
+
'安全区、资产和固定元素。' % sidecar,
|
|
3067
|
+
'',
|
|
2985
3068
|
'搭一页 PPT 六步,中间四步的数据都在 %s:' % sidecar, '']
|
|
2986
3069
|
L += ['1. **定画布** —— 舞台按 `layouts.md` 的 `canvas` 设成 %d×%d,'
|
|
2987
3070
|
'别套用默认尺寸:源模板的长宽比不一定是 16:9,套错了整页坐标全偏。'
|
|
@@ -2990,7 +3073,10 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
2990
3073
|
'2. **挑页型** —— 在 %s 里按用途选一个 archetype(清单见下面 Layouts 段)。'
|
|
2991
3074
|
'页数多于页型时,挑最接近的一个原样套用它的 slot:用不到的槽删掉,'
|
|
2992
3075
|
'内容比槽多就按同类槽的间距等距加,**坐标一律沿用该页型给的那套,不要自己另起网格**。'
|
|
2993
|
-
|
|
3076
|
+
'每个生成页面的 `<section>` 都写 `data-pptx-layout="<页型名>"`,'
|
|
3077
|
+
'多主题包默认只选兼容 `default-theme` 的页型;确需切换时,该页同时写 '
|
|
3078
|
+
'`data-pptx-theme="<主题名>"`,且主题必须属于该页型的 `themes`。'
|
|
3079
|
+
'交付前据此核验该页型绑定的背景与图片资产均已使用,且没有跨页型误用。' % sidecar,
|
|
2994
3080
|
'3. **按页型给的形态落元素** —— 页型给 `flow` 就用流式,给 `slots` 就用绝对,'
|
|
2995
3081
|
'两者只会出现一个。'
|
|
2996
3082
|
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin` 是整块的左右边距,'
|
|
@@ -3009,8 +3095,13 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
3009
3095
|
'`[x, y, w, h]`(%dx%d 画布上的绝对像素),机械展开成 `left/top/width/height`;'
|
|
3010
3096
|
'slot 的 `css` 是模板排版属性已转译好的声明串,原样写进 style,不要另选字号、'
|
|
3011
3097
|
'内边距、颜色或对齐。'
|
|
3012
|
-
'带 `asset` 的 slot
|
|
3013
|
-
'
|
|
3098
|
+
'带 `asset` 的 slot 是固定图片实例:元素写 `data-pptx-asset="<asset id>"`,'
|
|
3099
|
+
'引用复制后的原资产,并把 `box` 直接写成 inline '
|
|
3100
|
+
'`position:absolute;left:<x>px;top:<y>px;width:<w>px;height:<h>px`。'
|
|
3101
|
+
'元素必须可见,不得省略或换图,也不得隐藏或只在 CSS 里伪装引用;'
|
|
3102
|
+
'这个页型没有 `asset` 槽,这一页就不出现该资产。'
|
|
3103
|
+
'页型的 `background` 是图片资产时遵循同一实例契约,`box` 使用全画布 '
|
|
3104
|
+
'`[0, 0, %d, %d]`。' % (canvas[0], canvas[1], canvas[0], canvas[1]),
|
|
3014
3105
|
'5. **铺装饰几何** —— 页型的 `decor` 是这一页的图形骨架(图标托底的圆、'
|
|
3015
3106
|
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css` 逐项原样写进 '
|
|
3016
3107
|
'style;没有 `border-radius` 就按 `0`。只有 `geom: ellipse` 另加 '
|
|
@@ -3357,7 +3448,7 @@ def main(argv=None):
|
|
|
3357
3448
|
exceptions.append('源 deck 第 %s 页是单页孤例,没有归纳成 archetype;需要类似构图时按最接近的页型改。'
|
|
3358
3449
|
% '、'.join(map(str, leftover)))
|
|
3359
3450
|
|
|
3360
|
-
emit_manifest(d, assets, selected_vision_groups, ldir)
|
|
3451
|
+
emit_manifest(d, assets, selected_vision_groups, ldir, archetypes)
|
|
3361
3452
|
emit_frontmatter(d, tokens, fonts, roles, anchors, gaps, ldir)
|
|
3362
3453
|
# 每张背景量一次局部对比度,作为「哪里不能压文字」的客观依据摆进判断单。
|
|
3363
3454
|
# 只报测到的数,不替人填 avoid——哪块算主体、要不要避让,是看图才能定的。
|