refactor(ppt): 工作目录收进隐藏 .build/ + 反卡片映射 + svg_preview 兜底/gate(bump 0.34.0)
累积一批(承接 ppt生成2 验证 + 用户"缺图形/卡片阵太多/文件夹过多"反馈):
- 工作目录重构:<project_dir> 根原本把"持久源 / 交付物 / 可再生构建产物"混摊。
新增 project_utils.build_dir/svg_final_dir/preview_dir/backup_dir 单一事实源,
把 svg_final→.build/svg_final、preview→.build/preview、backup→.build/backup/latest
(只留最新,不再堆时间戳)。.build 是 dotfile → /v1/files 自动隐藏 → 用户可见面
收敛到 源(sources/images/svg_output/notes/两个 spec)+ 交付物(exports)。改动:
finalize_svg / svg_preview(_collect)/ pptx_discovery('final'→.build/svg_final)/
pptx_cli(backup 路径 + rmtree 清旧)+ SKILL 工作目录约定/命令。端到端实测:根目录
只剩 exports/+svg_output/,.build/ 三子目录就位,导出/预览/backup 全正常。
- 反卡片映射(治"大段大段卡片阵"):executor-base §page_rhythm 的 dense 行去掉
"card grid 是 baseline"的背书;加一段硬映射「先看内容关系再选图形」(系统→
hub_spoke/分层、流程→flow、层级→树/金字塔、循环→环、互依→mind_map、对比→象限、
≥3数据→图表),卡片阵封顶 ~1/3 页、连画两页网格下一关系页必须上示意图,指回 page_charts。
- svg_preview 加 cairosvg 兜底:find_browser 改返回 None 不抛错;无 chromium 时回退
cairosvg,渲前用 embed_icons 预展开 <use data-icon> 成真 path(避 INVALID_MATRIX);
修 --screenshot 相对路径静默失败(改绝对路径 + 暴露 chromium stderr)。
- 扁平 gate 计入 circle/polyline:svg_quality_checker 图形图元加 <circle>(node/venn/
timeline 是真图,修 21-circle roadmap 误判);文字密集 deck ≥60% 页无图形 → ERROR。
架构结论(svg 目录):svg_output(可编辑源)与 svg_final(自包含编译产物)是两态、不能
合并成一个文件,但只暴露一个——现 svg_output 可见、svg_final 进 .build。终态(下一议题)
干掉持久化 svg_final、finalize 内存化 + web 按需预览,牵涉 web 层,本次未做。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13835a315a
commit
5bde2445a0
14
PROGRESS.md
14
PROGRESS.md
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-06-30(ppt skill 加商务红品牌预设 + 配图默认主动提议 + bump 0.33.5)
|
最后更新:2026-07-01(ppt skill 工作目录重构:中间物收进隐藏 .build/ + 反卡片映射 + svg_preview 兜底/gate + bump 0.34.0)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,6 +21,18 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-07-01 / ppt skill 工作目录重构:中间物收进隐藏 .build/(bump 0.34.0)
|
||||||
|
用户反馈"中间产物/文件夹过多"。架构判断:`<project_dir>` 根把三类混摊了——持久源(sources/images/svg_output/notes/两个 spec)、交付物(exports)、**可再生构建产物(svg_final/preview/backup)**;第三类是 build artifact,不该和源平级。修:新增 `project_utils.build_dir/svg_final_dir/preview_dir/backup_dir` 单一事实源,把 svg_final→`.build/svg_final`、preview→`.build/preview`、backup→`.build/backup/latest`(**只留最新**,不再堆时间戳)。`.build` 是 dotfile → `/v1/files` 自动隐藏 → 用户可见面从 ~11 降到"源+交付物"。改动:finalize_svg / svg_preview(_collect)/ pptx_discovery(`final`→`.build/svg_final`)/ pptx_cli(backup 路径 + rmtree 清旧)+ SKILL 工作目录约定/命令。端到端实测:根目录只剩 exports/+svg_output/,`.build/` 三子目录就位,导出/预览/backup 全正常。
|
||||||
|
> 关于"svg现在能 web 预览、要不要收敛成一个 svg 目录":架构上 svg_output(可编辑源:占位符+相对引用)与 svg_final(自包含编译产物:图标展开+图片 base64)是**两态**、不能合并成一个文件(可编辑 vs 浏览器忠实渲染冲突);但只该暴露一个——svg_output 可见、svg_final 进 .build。终态(下一议题):干掉持久化 svg_final,finalize 纯内存化 + web 忠实预览走"按需 finalize 再 serve",磁盘就一个 svg 目录。本次先做隐藏,未做内存化(牵涉 web 层)。
|
||||||
|
|
||||||
|
### 2026-07-01 / ppt skill 验证 ppt生成2 后修复:svg_preview cairosvg 兜底 + gate 计入 circle + 反卡片映射(bump 0.33.x→并入 0.34.0)
|
||||||
|
DB 取证验证「ppt生成2」(用户重跑,商务红+图标):图标 31 个(前 0)、商务红 #C00000、封面 imagegen 配图、扁平 gate 在跑 —— **代码类修复随 bind-mount 全部生效**。但视觉验收卡住:轨迹显示沙箱 `which chromium/cairosvg/rsvg` 全空、`svg_preview.py` 没被调用、模型自己 `pip install cairosvg` 渲 raw svg_output → **6/13 图标页 INVALID_MATRIX 失败**(cairosvg 不认 href-less `<use data-icon>`)。根因:**服务器沙箱镜像旧、没带 chromium 层**(镜像非 bind-mount,`deploy/update.sh` 第 4 步 rebuild 才更新;需服务器执行)。据此两处代码修复(用户选定):
|
||||||
|
- **svg_preview.py 加 cairosvg 兜底**:`find_browser()` 改返回 None 不抛错;无 chromium 时回退 cairosvg,且渲前**用 finalize 的 embed_icons 把 `<use data-icon>` 预展开成真 `<path>`**(避开 INVALID_MATRIX);顺带修上一版遗留的 `--screenshot` 绝对路径 + 保留 chromium 优先(保真更高)。browser happy-path 实测完好。
|
||||||
|
- **扁平 gate 计入 circle/polyline**:`svg_quality_checker` 图形图元加 `<circle>`(node/venn/bubble/timeline 是真图,之前把 21-circle roadmap 误判"无图形");并收紧——文字密集 deck **≥60% 页无图形 → ERROR**(不止"全 deck 0 图形"),40–60% → INFO。实测:ceramic 式(46%)→INFO exit0、多数扁平(75%)→ERROR、极端→ERROR、全 circle→clean。
|
||||||
|
> 部署:视觉验收/PDF/mermaid 的根仍是镜像 —— 服务器跑 `sudo deploy/update.sh`(不加 --skip-build)rebuild `zcbot-sandbox`(Dockerfile 已含 chromium),存量 per-user 容器待 ensure() 用新镜像重建(必要时手动 docker rm 该用户旧容器)。
|
||||||
|
|
||||||
|
同批加 **执行层反卡片映射**(治"大段大段卡片阵"):验证 ppt生成2 发现 SVG 注释自写 "3x2 Card Grid"/"3x3 Grid"——执行模型对"N 个并列项"默认摊成卡片网格。executor-base §page_rhythm:`dense` 行去掉"card grid 是 baseline"的背书;加一段硬映射「先看内容**关系**再选图形」(系统→hub_spoke/分层、流程→flow、层级→树/金字塔、循环→环、互依→mind_map、对比→象限、≥3数据→图表),**卡片阵封顶 ~1/3 页**、连画两页网格下一关系页必须上示意图,并指回 page_charts(strategist 分配了模板就画那个别塌回卡片)。诚实边界:这是执行模型设计本能天花板,prompt 抬下限但不保证每张示意图都漂亮。
|
||||||
|
|
||||||
### 2026-06-30 / ppt skill 加商务红品牌预设 + 配图默认主动提议(bump 0.33.5)
|
### 2026-06-30 / ppt skill 加商务红品牌预设 + 配图默认主动提议(bump 0.33.5)
|
||||||
用户两个需求:(1) 加一款红色主题;(2) 用户没给图时在需要处主动配图。
|
用户两个需求:(1) 加一款红色主题;(2) 用户没给图时在需要处主动配图。
|
||||||
- **商务红品牌预设**:新增 `templates/brands/business-red/design_spec.md`(同 anthropic 格式:#C00000 全色表 + primary-deep/gold/info/positive/alert/surface/border/muted 派生色 + 宋体标题/黑体正文字体栈 + 实心图标偏好 + 政企口吻;无 logo,注明用文字 wordmark / 可后补)+ `brands_index.json` 加条目。**红色承载在 brand 而非 visual-style**(visual-style 不带色)。同时把**商务红设为 strategist §e 默认配色候选**:中文政企/集团/科研商务汇报默认列入 ≥3 候选(红金 #BF9B5F / 红蓝 #2B4C7E 二选一点缀,纯红只压标题/关键数据)。SKILL §默认主题 + 八条对齐 h 行同步指向。
|
- **商务红品牌预设**:新增 `templates/brands/business-red/design_spec.md`(同 anthropic 格式:#C00000 全色表 + primary-deep/gold/info/positive/alert/surface/border/muted 派生色 + 宋体标题/黑体正文字体栈 + 实心图标偏好 + 政企口吻;无 logo,注明用文字 wordmark / 可后补)+ `brands_index.json` 加条目。**红色承载在 brand 而非 visual-style**(visual-style 不带色)。同时把**商务红设为 strategist §e 默认配色候选**:中文政企/集团/科研商务汇报默认列入 ≥3 候选(红金 #BF9B5F / 红蓝 #2B4C7E 二选一点缀,纯红只压标题/关键数据)。SKILL §默认主题 + 八条对齐 h 行同步指向。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.33.5"
|
__version__ = "0.34.0"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx) 文件。✅ 触发:用户
|
||||||
|
|
||||||
**脚本**(host 上用 `.venv/Scripts/python.exe <skill_dir>/scripts/xxx.py ...` 跑;`<skill_dir>` = 本 skill 绝对路径):
|
**脚本**(host 上用 `.venv/Scripts/python.exe <skill_dir>/scripts/xxx.py ...` 跑;`<skill_dir>` = 本 skill 绝对路径):
|
||||||
- `svg_quality_checker.py` —— **SVG 结构质检**(禁用特性 / viewBox / spec_lock 漂移 / 配色越界等)。引擎,自包含
|
- `svg_quality_checker.py` —— **SVG 结构质检**(禁用特性 / viewBox / spec_lock 漂移 / 配色越界等)。引擎,自包含
|
||||||
- `finalize_svg.py` —— **SVG 后处理**(图标内嵌 / 配图裁切内嵌 / tspan 展平 / 圆角矩形转 path)→ 产出 `svg_final/`
|
- `finalize_svg.py` —— **SVG 后处理**(图标内嵌 / 配图裁切内嵌 / tspan 展平 / 圆角矩形转 path)→ 产出 `.build/svg_final/`(隐藏、可再生)
|
||||||
- `svg_to_pptx.py` —— **SVG → 原生 PPTX**(逐元素译 DrawingML;默认嵌演讲者备注 + Office 兼容 PNG 兜底)
|
- `svg_to_pptx.py` —— **SVG → 原生 PPTX**(逐元素译 DrawingML;默认嵌演讲者备注 + Office 兼容 PNG 兜底)
|
||||||
- `total_md_split.py` —— 把 `notes/total.md` 拆成逐页备注(导出前跑)
|
- `total_md_split.py` —— 把 `notes/total.md` 拆成逐页备注(导出前跑)
|
||||||
- `update_spec.py` —— 改 `spec_lock.md` 的颜色/字体后,**一键传播到所有已生成 SVG**(改稿用)
|
- `update_spec.py` —— 改 `spec_lock.md` 的颜色/字体后,**一键传播到所有已生成 SVG**(改稿用)
|
||||||
|
|
@ -52,16 +52,17 @@ description: 生成 PowerPoint 演示文稿 (.pptx) 文件。✅ 触发:用户
|
||||||
├── images/ # 配图(imagegen 生成 / 用户提供 / 公式 PNG);SVG 里用 ../images/ 引用
|
├── images/ # 配图(imagegen 生成 / 用户提供 / 公式 PNG);SVG 里用 ../images/ 引用
|
||||||
├── templates/ # 仅当用户给了模板路径才有(模板 SVG + 其 design_spec)
|
├── templates/ # 仅当用户给了模板路径才有(模板 SVG + 其 design_spec)
|
||||||
├── icons/ # 可选:项目本地图标(没有则 finalize 回退到 skill 的 templates/icons/)
|
├── icons/ # 可选:项目本地图标(没有则 finalize 回退到 skill 的 templates/icons/)
|
||||||
├── svg_output/*.svg # ★ executor 逐页手写的 SVG(视觉真相、改稿对象)
|
├── svg_output/*.svg # ★ executor 逐页手写的 SVG(视觉真相、改稿对象)—— 唯一可见的 svg 目录
|
||||||
├── svg_final/ # finalize 产出(图标/配图已内嵌,供预览)
|
|
||||||
├── notes/total.md # 演讲者备注(逐页),total_md_split 拆分后导出嵌入
|
├── notes/total.md # 演讲者备注(逐页),total_md_split 拆分后导出嵌入
|
||||||
├── preview/ # svg_preview 渲的验收 PNG
|
|
||||||
├── exports/<slug>_<ts>.pptx # ★ 最终产物(原生 DrawingML,可编辑)
|
├── exports/<slug>_<ts>.pptx # ★ 最终产物(原生 DrawingML,可编辑)
|
||||||
├── backup/<ts>/svg_output/ # SVG 源快照(可不跑模型重新导出)
|
├── REVISIONS.md # 修订日志(见 §修订日志)
|
||||||
└── REVISIONS.md # 修订日志(见 §修订日志)
|
└── .build/ # 可再生构建产物(dotfile 隐藏、随时可删;用户文件列表看不到)
|
||||||
|
├── svg_final/ # finalize 产出(图标/配图已内嵌,自包含;供 legacy 导出 + 忠实预览)
|
||||||
|
├── preview/ # svg_preview 渲的验收 PNG
|
||||||
|
└── backup/latest/svg_output/ # SVG 源快照(只留最新一份,可不跑模型重新导出)
|
||||||
```
|
```
|
||||||
|
|
||||||
**所有产物写 `<project_dir>` 下**,不写 cwd / `skills/` / repo 根。
|
**所有产物写 `<project_dir>` 下**,不写 cwd / `skills/` / repo 根。**可见面 = 源 + 交付物**(sources/images/svg_output/notes/exports + 两个 spec + REVISIONS);派生的中间物(svg_final/preview/backup)一律进 `.build/`,由脚本自动落位,**不要手动在根目录建 svg_final/preview/backup**。
|
||||||
|
|
||||||
## 默认主题 — 自由设计(content-driven)
|
## 默认主题 — 自由设计(content-driven)
|
||||||
|
|
||||||
|
|
@ -151,7 +152,7 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
|
||||||
.venv/Scripts/python.exe <skill_dir>/scripts/finalize_svg.py <project_dir>
|
.venv/Scripts/python.exe <skill_dir>/scripts/finalize_svg.py <project_dir>
|
||||||
# 5.3 导出原生 PPTX(默认嵌备注 + Office 兼容 PNG 兜底)
|
# 5.3 导出原生 PPTX(默认嵌备注 + Office 兼容 PNG 兜底)
|
||||||
.venv/Scripts/python.exe <skill_dir>/scripts/svg_to_pptx.py <project_dir>
|
.venv/Scripts/python.exe <skill_dir>/scripts/svg_to_pptx.py <project_dir>
|
||||||
# 产物:exports/<slug>_<ts>.pptx(原生,读 svg_output/)+ backup/<ts>/svg_output/(源快照)
|
# 产物:exports/<slug>_<ts>.pptx(原生,读 svg_output/)+ .build/backup/latest/svg_output/(源快照,只留最新)
|
||||||
```
|
```
|
||||||
- ❌ 别用 `cp` 代替 finalize_svg(它做了多步关键处理);❌ 别加 `--only` / 强制 `-s output`。
|
- ❌ 别用 `cp` 代替 finalize_svg(它做了多步关键处理);❌ 别加 `--only` / 强制 `-s output`。
|
||||||
- 动画可选:`-t fade`(翻页,默认)/ `-a auto`(逐元素入场,**默认 none**,用户要才开)。全表见 animations.md。
|
- 动画可选:`-t fade`(翻页,默认)/ `-a auto`(逐元素入场,**默认 none**,用户要才开)。全表见 animations.md。
|
||||||
|
|
@ -160,9 +161,9 @@ references/visual-styles/<locked-style>.md # 锁定的视觉风格
|
||||||
## 阶段六:验收(渲图肉眼/vision 看)
|
## 阶段六:验收(渲图肉眼/vision 看)
|
||||||
|
|
||||||
```
|
```
|
||||||
.venv/Scripts/python.exe <skill_dir>/scripts/svg_preview.py <project_dir> --pages 1,3,5 -o <project_dir>/preview
|
.venv/Scripts/python.exe <skill_dir>/scripts/svg_preview.py <project_dir> --pages 1,3,5
|
||||||
```
|
```
|
||||||
`read` 渲出的 PNG 亲眼过:封面、一个内容页、一个 breathing 页 —— 看标题层级、卡片过挤/过空、文字是否都正常、节奏是否单调、配图位置。不通过的回阶段三改对应页 SVG 重跑。
|
PNG 默认落 `.build/preview/`(隐藏);优先渲 `.build/svg_final/`(图标/配图已内嵌,最忠实),没有则渲 `svg_output/`(无 chromium 时走 cairosvg 兜底、会就地展开图标)。`read` 渲出的 PNG 亲眼过:封面、一个内容页、一个 breathing 页 —— 看标题层级、卡片过挤/过空、文字是否都正常、节奏是否单调、配图位置。不通过的回阶段三改对应页 SVG 重跑。
|
||||||
|
|
||||||
> svg_preview 渲的是 SVG(视觉真相,与导出的 pptx 1:1),比渲最终 pptx 更早更准暴露观感问题。需要校验"SVG→DrawingML 转换是否保真",再开导出的 pptx 在 PowerPoint 里看。
|
> svg_preview 渲的是 SVG(视觉真相,与导出的 pptx 1:1),比渲最终 pptx 更早更准暴露观感问题。需要校验"SVG→DrawingML 转换是否保真",再开导出的 pptx 在 PowerPoint 里看。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,22 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P<NN>`
|
||||||
| Tag | Layout discipline |
|
| Tag | Layout discipline |
|
||||||
|-----|-------------------|
|
|-----|-------------------|
|
||||||
| `anchor` | Structural page (cover / chapter / TOC / ending). With a template, follow the matching template verbatim. In free design (no template), realize the page's §IX intent — for the cover deliver its `Cover impact` and for a closing page its `Closing impact` (the committed hook / takeaway + composition), never a default centered title + subtitle or a generic "Thank you" sign-off. |
|
| `anchor` | Structural page (cover / chapter / TOC / ending). With a template, follow the matching template verbatim. In free design (no template), realize the page's §IX intent — for the cover deliver its `Cover impact` and for a closing page its `Closing impact` (the committed hook / takeaway + composition), never a default centered title + subtitle or a generic "Thank you" sign-off. |
|
||||||
| `dense` | Information-heavy. Card grids, multi-column layouts, KPI dashboards, tables, and charts are all permitted. This is the baseline behavior. |
|
| `dense` | Information-heavy. Card grids, multi-column layouts, KPI dashboards, tables, and charts are all permitted — but a card grid is **not** the automatic default (see the anti-monotony rule below); pick the structure that fits the content's relationship. |
|
||||||
| `breathing` | Low-density impact page. Avoid **multi-card grid layouts** — do not organize content as multiple parallel rounded containers (3-card row, 4-card KPI grid, 2×2 matrix rendered as cards). Use naked text blocks, dividers, whitespace, or full-bleed imagery as the content structure. Single rounded visual elements (hero image corners, callouts, tags, one emphasis block) are fine — the rule is about grid structure, not about the `rx` attribute. Proportions follow information weight (not a preset ratio). Typical forms: hero quote, single large number with one-line interpretation, full-bleed image with floating caption, section transition. |
|
| `breathing` | Low-density impact page. Avoid **multi-card grid layouts** — do not organize content as multiple parallel rounded containers (3-card row, 4-card KPI grid, 2×2 matrix rendered as cards). Use naked text blocks, dividers, whitespace, or full-bleed imagery as the content structure. Single rounded visual elements (hero image corners, callouts, tags, one emphasis block) are fine — the rule is about grid structure, not about the `rx` attribute. Proportions follow information weight (not a preset ratio). Typical forms: hero quote, single large number with one-line interpretation, full-bleed image with floating caption, section transition. |
|
||||||
|
|
||||||
> Without rhythm variation, every page defaults to card grids (the "AI-generated" look). `page_rhythm` is the only narrative lever that survives context compression.
|
> Without rhythm variation, every page defaults to card grids (the "AI-generated" look). `page_rhythm` is the only narrative lever that survives context compression.
|
||||||
|
|
||||||
|
> 🚧 **Anti-monotony — map the content's RELATIONSHIP before defaulting to a card grid.** "N parallel items → an N×M grid of text cards" is the single biggest source of the AI-deck look. Before drawing cards, ask what the items *are to each other* and pick the structure that shows it:
|
||||||
|
> - **A system of interconnected parts** (六大体系 / 平台架构 / 能力地图) → hub-and-spoke, layered architecture, or module composition (`charts/hub_spoke`, `layered_architecture`, `module_composition`) — the connections ARE the message; a flat grid throws them away.
|
||||||
|
> - **A process / sequence / 历程** → flow with connectors or numbered steps (`charts/process_flow`, `numbered_steps`, `snake_flow`) or a timeline — arrows carry the "then".
|
||||||
|
> - **A hierarchy / breakdown** → tree or pyramid (`charts/top_down_tree`, `pyramid_chart`).
|
||||||
|
> - **A cycle / 闭环** → concentric or segmented wheel (`charts/concentric_circles`, `segmented_wheel`).
|
||||||
|
> - **Interdependent themes** → mind-map / network (`charts/mind_map`, `hub_spoke`).
|
||||||
|
> - **Comparison** → columns / quadrant / matrix (`charts/comparison_columns`, `matrix_2x2`, `quadrant_text_bullets`), not two stacks of cards.
|
||||||
|
> - **≥3 data points** → an actual chart (bar / line / donut …), never text cards of numbers.
|
||||||
|
>
|
||||||
|
> A plain card grid is the right answer ONLY for genuinely independent, unordered items with no relationship to show — and even then, vary card size by weight, add a connecting spine, or give each one icon. **Cap: at most ~1/3 of a deck's content pages may be plain card grids.** If you've just drawn two card-grid pages, the next relational page MUST be a diagram, not a third grid. Pull a `charts/` template's **geometry** as the starting structure (re-skin per §1 — structure not skin) so you are adapting a real diagram, not inventing connectors from scratch. This is why `spec_lock.md page_charts` matters: when Strategist assigned a template for this page, build THAT, don't collapse it back into cards.
|
||||||
|
|
||||||
**Missing `page_rhythm` section** → emit `warning: spec_lock.md missing page_rhythm — defaulting all pages to dense` once, fall back to `dense` for all pages.
|
**Missing `page_rhythm` section** → emit `warning: spec_lock.md missing page_rhythm — defaulting all pages to dense` once, fall back to `dense` for all pages.
|
||||||
|
|
||||||
**Tag not found for current page** → emit `warning: spec_lock.md page_rhythm tag not found for P<NN> — falling back to dense` once per deck (aggregate; do not repeat per page), fall back to `dense`. Do not invent a tag.
|
**Tag not found for current page** → emit `warning: spec_lock.md page_rhythm tag not found for P<NN> — falling back to dense` once per deck (aggregate; do not repeat per page), fall back to `dense`. Do not invent a tag.
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,9 @@ def finalize_project(
|
||||||
compress: Compress images before embedding
|
compress: Compress images before embedding
|
||||||
max_dimension: Downscale images exceeding this dimension
|
max_dimension: Downscale images exceeding this dimension
|
||||||
"""
|
"""
|
||||||
|
from project_utils import svg_final_dir
|
||||||
svg_output = project_dir / 'svg_output'
|
svg_output = project_dir / 'svg_output'
|
||||||
svg_final = project_dir / 'svg_final'
|
svg_final = svg_final_dir(project_dir) # <project>/.build/svg_final (hidden, regenerable)
|
||||||
# Project-first: embed from the deck's own icons/ (synced library icons +
|
# Project-first: embed from the deck's own icons/ (synced library icons +
|
||||||
# any custom icons), falling back to the global library per-icon.
|
# any custom icons), falling back to the global library per-icon.
|
||||||
global_icons_dir = Path(__file__).parent.parent / 'templates' / 'icons'
|
global_icons_dir = Path(__file__).parent.parent / 'templates' / 'icons'
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,39 @@ CANVAS_FORMAT_ALIASES = {
|
||||||
'小红书': 'xiaohongshu',
|
'小红书': 'xiaohongshu',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Regenerable build artifacts live under a single hidden dir ────────────────
|
||||||
|
# svg_output/ is the editable source of truth (stays visible at project root,
|
||||||
|
# sibling to images/ for its `../images/` refs). Everything derived from it —
|
||||||
|
# the self-contained svg_final/, preview PNGs, and source snapshots — is a build
|
||||||
|
# artifact: regenerable and safe to delete. Sweeping them under a dotfile dir
|
||||||
|
# keeps the visible project down to source + deliverable, and the /v1/files API
|
||||||
|
# hides dotfiles so the user never sees the clutter. See SKILL.md 工作目录约定.
|
||||||
|
#
|
||||||
|
# NOTE: the svg_to_pptx package uses the literal ".build/svg_final" /
|
||||||
|
# ".build/backup" to stay import-decoupled from this module — keep them in sync
|
||||||
|
# with BUILD_DIR_NAME here (it is not expected to ever change).
|
||||||
|
BUILD_DIR_NAME = ".build"
|
||||||
|
|
||||||
|
|
||||||
|
def build_dir(project_path) -> Path:
|
||||||
|
"""`<project>/.build` — hidden root for regenerable artifacts."""
|
||||||
|
return Path(project_path) / BUILD_DIR_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def svg_final_dir(project_path) -> Path:
|
||||||
|
"""finalize output (self-contained SVGs: icons + images embedded)."""
|
||||||
|
return build_dir(project_path) / "svg_final"
|
||||||
|
|
||||||
|
|
||||||
|
def preview_dir(project_path) -> Path:
|
||||||
|
"""svg_preview PNGs for visual acceptance."""
|
||||||
|
return build_dir(project_path) / "preview"
|
||||||
|
|
||||||
|
|
||||||
|
def backup_dir(project_path) -> Path:
|
||||||
|
"""svg_output snapshot for re-export. Latest only (no timestamp pile-up)."""
|
||||||
|
return build_dir(project_path) / "backup" / "latest"
|
||||||
|
|
||||||
|
|
||||||
def normalize_canvas_format(format_key: str) -> str:
|
def normalize_canvas_format(format_key: str) -> str:
|
||||||
"""Normalize canvas format key name (supports common aliases)."""
|
"""Normalize canvas format key name (supports common aliases)."""
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,13 @@ _WHICH_NAMES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def find_browser() -> str:
|
def find_browser() -> str | None:
|
||||||
|
"""Return a chromium/chrome/edge executable path, or None if none found.
|
||||||
|
|
||||||
|
Returns None (not raise) so the caller can fall back to the cairosvg
|
||||||
|
renderer when no browser exists (e.g. a sandbox image built without the
|
||||||
|
chromium layer).
|
||||||
|
"""
|
||||||
# Explicit override first (matches rendering/pdf.py's CHROMIUM/CHROME env).
|
# Explicit override first (matches rendering/pdf.py's CHROMIUM/CHROME env).
|
||||||
env = os.environ.get("CHROMIUM") or os.environ.get("CHROME")
|
env = os.environ.get("CHROMIUM") or os.environ.get("CHROME")
|
||||||
if env and (shutil.which(env) or Path(env).exists()):
|
if env and (shutil.which(env) or Path(env).exists()):
|
||||||
|
|
@ -62,11 +68,7 @@ def find_browser() -> str:
|
||||||
p = shutil.which(name)
|
p = shutil.which(name)
|
||||||
if p:
|
if p:
|
||||||
return p
|
return p
|
||||||
raise SystemExit(
|
return None
|
||||||
"[fatal] 未找到 Chrome / Edge / Chromium,无法渲染 SVG 预览。"
|
|
||||||
"沙箱镜像应自带 /usr/bin/chromium;本机可装 Chrome,或设 CHROMIUM 环境变量,"
|
|
||||||
"或用浏览器手动打开 svg_output/*.svg 验收。"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_VIEWBOX_RE = re.compile(r'viewBox\s*=\s*["\']\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*["\']')
|
_VIEWBOX_RE = re.compile(r'viewBox\s*=\s*["\']\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*["\']')
|
||||||
|
|
@ -145,19 +147,21 @@ def render(browser: str, svg_path: Path, out_png: Path, scale: float = 2.0) -> b
|
||||||
|
|
||||||
def _collect(target: Path) -> tuple[list[Path], Path]:
|
def _collect(target: Path) -> tuple[list[Path], Path]:
|
||||||
"""返回 (svg 文件列表, 默认输出目录)。"""
|
"""返回 (svg 文件列表, 默认输出目录)。"""
|
||||||
|
from project_utils import svg_final_dir, preview_dir
|
||||||
if target.is_file() and target.suffix.lower() == ".svg":
|
if target.is_file() and target.suffix.lower() == ".svg":
|
||||||
return [target], target.parent / "preview"
|
return [target], preview_dir(target.parent)
|
||||||
# 目录:优先 svg_final(finalize 后图标/配图已内嵌,渲出来最忠实);
|
# 目录:优先 .build/svg_final(finalize 后图标/配图已内嵌,渲出来最忠实);
|
||||||
# 没 svg_final 就退而渲 svg_output(生成中验收,此时图标仍是占位符不显示)
|
# 没有就退而渲 svg_output(生成中验收 —— cairosvg 兜底会就地展开图标,chromium
|
||||||
if (target / "svg_final").is_dir() and any((target / "svg_final").glob("*.svg")):
|
# 直接渲则图标仍是占位符不显示)。
|
||||||
svg_dir = target / "svg_final"
|
sf = svg_final_dir(target)
|
||||||
|
if sf.is_dir() and any(sf.glob("*.svg")):
|
||||||
|
svg_dir = sf
|
||||||
elif (target / "svg_output").is_dir():
|
elif (target / "svg_output").is_dir():
|
||||||
svg_dir = target / "svg_output"
|
svg_dir = target / "svg_output"
|
||||||
else:
|
else:
|
||||||
svg_dir = target
|
svg_dir = target
|
||||||
files = sorted(svg_dir.glob("*.svg"))
|
files = sorted(svg_dir.glob("*.svg"))
|
||||||
default_out = target / "preview"
|
return files, preview_dir(target)
|
||||||
return files, default_out
|
|
||||||
|
|
||||||
|
|
||||||
def _select(files: list[Path], pages: str | None) -> list[Path]:
|
def _select(files: list[Path], pages: str | None) -> list[Path]:
|
||||||
|
|
@ -171,6 +175,57 @@ def _select(files: list[Path], pages: str | None) -> list[Path]:
|
||||||
return [files[i] for i in idxs if 0 <= i < len(files)]
|
return [files[i] for i in idxs if 0 <= i < len(files)]
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_icons_for_cairo(svg_text: str) -> str:
|
||||||
|
"""Expand `<use data-icon>` placeholders to real `<g><path>` in-memory.
|
||||||
|
|
||||||
|
cairosvg does not understand the `data-icon` placeholder and errors on the
|
||||||
|
href-less `<use>` (observed: CAIRO_STATUS_INVALID_MATRIX on every icon-bearing
|
||||||
|
page). Reuse finalize's exact regex embedder so the fallback rasterizes the
|
||||||
|
same icons the native exporter draws. No-op when the SVG carries no
|
||||||
|
placeholders (e.g. already-finalized svg_final/).
|
||||||
|
"""
|
||||||
|
if 'data-icon' not in svg_text:
|
||||||
|
return svg_text
|
||||||
|
try:
|
||||||
|
from svg_finalize.embed_icons import process_svg_file, DEFAULT_ICONS_DIR
|
||||||
|
except Exception:
|
||||||
|
return svg_text # can't expand — let cairosvg try as-is
|
||||||
|
try:
|
||||||
|
import contextlib
|
||||||
|
import io as _io
|
||||||
|
with tempfile.TemporaryDirectory(prefix="svgprev-ico-") as tmp:
|
||||||
|
f = Path(tmp) / "x.svg"
|
||||||
|
f.write_text(svg_text, encoding="utf-8")
|
||||||
|
# Silence the embedder's per-file "[OK] x.svg (N icons)" chatter —
|
||||||
|
# the temp name is meaningless in preview output.
|
||||||
|
with contextlib.redirect_stdout(_io.StringIO()):
|
||||||
|
process_svg_file(f, DEFAULT_ICONS_DIR, dry_run=False, verbose=False,
|
||||||
|
fallback_dir=DEFAULT_ICONS_DIR)
|
||||||
|
return f.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return svg_text
|
||||||
|
|
||||||
|
|
||||||
|
def render_cairosvg(cairosvg, svg_path: Path, out_png: Path, scale: float = 2.0) -> bool:
|
||||||
|
"""Rasterize one SVG via cairosvg (browser-free fallback). Returns True on success.
|
||||||
|
|
||||||
|
Icons are pre-expanded so cairosvg doesn't choke on `<use data-icon>`.
|
||||||
|
Fidelity is slightly below chromium (no JS, limited filter support), but it
|
||||||
|
lets visual acceptance run in a sandbox image built without chromium.
|
||||||
|
"""
|
||||||
|
svg_text = _expand_icons_for_cairo(
|
||||||
|
svg_path.read_text(encoding="utf-8", errors="ignore"))
|
||||||
|
out_png = out_png.resolve()
|
||||||
|
out_png.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
cairosvg.svg2png(bytestring=svg_text.encode("utf-8"),
|
||||||
|
write_to=str(out_png), scale=scale)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [warn] cairosvg 渲染失败 {svg_path.name}: {e}")
|
||||||
|
return False
|
||||||
|
return out_png.exists() and out_png.stat().st_size > 0
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
ap = argparse.ArgumentParser(description="把 SVG 页渲成 PNG 供肉眼/vision 验收")
|
ap = argparse.ArgumentParser(description="把 SVG 页渲成 PNG 供肉眼/vision 验收")
|
||||||
ap.add_argument("target", type=Path, help="project_dir / svg 目录 / 单个 .svg 文件")
|
ap.add_argument("target", type=Path, help="project_dir / svg 目录 / 单个 .svg 文件")
|
||||||
|
|
@ -187,12 +242,33 @@ def main() -> None:
|
||||||
raise SystemExit(f"[fatal] --pages {args.pages} 没选中任何页(共 {len(_collect(args.target)[0])} 页)")
|
raise SystemExit(f"[fatal] --pages {args.pages} 没选中任何页(共 {len(_collect(args.target)[0])} 页)")
|
||||||
|
|
||||||
out_dir = args.out or default_out
|
out_dir = args.out or default_out
|
||||||
|
|
||||||
|
# Renderer: prefer a real browser (chromium — highest fidelity, matches the
|
||||||
|
# native pptx). Fall back to cairosvg when no browser exists (sandbox image
|
||||||
|
# built without the chromium layer) so visual acceptance still runs.
|
||||||
browser = find_browser()
|
browser = find_browser()
|
||||||
|
cairo = None
|
||||||
|
if browser:
|
||||||
print(f"[svg_preview] browser={browser}")
|
print(f"[svg_preview] browser={browser}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import cairosvg
|
||||||
|
cairo = cairosvg
|
||||||
|
print("[svg_preview] 未找到 chromium → 回退 cairosvg(图标已预展开;"
|
||||||
|
"保真度略低于 chromium。装 chromium 或设 CHROMIUM 可用浏览器渲染)")
|
||||||
|
except Exception:
|
||||||
|
raise SystemExit(
|
||||||
|
"[fatal] 未找到 Chrome / Edge / Chromium,也无 cairosvg 兜底。"
|
||||||
|
"沙箱镜像应自带 /usr/bin/chromium(rebuild sandbox 镜像),"
|
||||||
|
"或 `pip install cairosvg`,或设 CHROMIUM 环境变量。")
|
||||||
|
|
||||||
done = []
|
done = []
|
||||||
for svg in files:
|
for svg in files:
|
||||||
png = out_dir / (svg.stem + ".png")
|
png = out_dir / (svg.stem + ".png")
|
||||||
|
if browser:
|
||||||
render(browser, svg, png, scale=args.scale)
|
render(browser, svg, png, scale=args.scale)
|
||||||
|
else:
|
||||||
|
render_cairosvg(cairo, svg, png, scale=args.scale)
|
||||||
if png.exists():
|
if png.exists():
|
||||||
done.append(png)
|
done.append(png)
|
||||||
print(f" [ok] {svg.name} -> {png}")
|
print(f" [ok] {svg.name} -> {png}")
|
||||||
|
|
|
||||||
|
|
@ -1473,15 +1473,19 @@ class SVGQualityChecker:
|
||||||
def _check_graphic_richness(self, content: str, result: Dict) -> None:
|
def _check_graphic_richness(self, content: str, result: Dict) -> None:
|
||||||
"""Tally graphic primitives per page for the deck-level flat-deck gate.
|
"""Tally graphic primitives per page for the deck-level flat-deck gate.
|
||||||
|
|
||||||
Counts <path>/<polyline>/<polygon>/<image> — the elements that actually
|
Counts <path>/<polyline>/<polygon>/<circle>/<image> — the elements that
|
||||||
draw a diagram, chart, figure, or photo. <rect> and <line> are excluded
|
actually draw a diagram, chart, figure, or photo. <circle> is included
|
||||||
on purpose: they are layout cards, backgrounds, and dividers, and a deck
|
because node / bubble / venn / timeline diagrams are built from circles
|
||||||
built entirely from them is exactly the "text on rectangles" look this
|
(excluding it false-flagged a 21-circle roadmap as "no figure"). <rect>
|
||||||
catches. Per-page nudges stay soft; the hard gate is deck-wide.
|
and <line> stay excluded: they are layout cards, backgrounds, and
|
||||||
|
dividers, and a deck built only from them is the "text on rectangles"
|
||||||
|
look this catches. Icons don't count here — they have their own gate.
|
||||||
|
Per-page nudges stay soft; the hard gate is deck-wide.
|
||||||
"""
|
"""
|
||||||
g = (len(re.findall(r'<path\b', content))
|
g = (len(re.findall(r'<path\b', content))
|
||||||
+ len(re.findall(r'<polyline\b', content))
|
+ len(re.findall(r'<polyline\b', content))
|
||||||
+ len(re.findall(r'<polygon\b', content))
|
+ len(re.findall(r'<polygon\b', content))
|
||||||
|
+ len(re.findall(r'<circle\b', content))
|
||||||
+ len(re.findall(r'<image\b', content)))
|
+ len(re.findall(r'<image\b', content)))
|
||||||
result.setdefault('info', {})['graphic_count'] = g
|
result.setdefault('info', {})['graphic_count'] = g
|
||||||
self._deck_page_count += 1
|
self._deck_page_count += 1
|
||||||
|
|
@ -1493,29 +1497,41 @@ class SVGQualityChecker:
|
||||||
def _print_graphic_summary(self):
|
def _print_graphic_summary(self):
|
||||||
"""Deck-level flat-deck gate.
|
"""Deck-level flat-deck gate.
|
||||||
|
|
||||||
A content-rich deck (>=6 pages, text-heavy) with zero path/polygon/
|
Two hard-error bars for a text-heavy content deck (>=6 pages):
|
||||||
polyline/image deck-wide is the wall-of-text-boxes pathology — hard
|
- ZERO figure primitives deck-wide → the wall-of-text-boxes pathology.
|
||||||
error so it can't ship. Below that bar, surface a soft note when most
|
- >=60% of pages carry no figure → mostly-flat (a few token diagrams
|
||||||
pages carry no figure. Short or sparse decks (<6 pages) are exempt to
|
don't rescue a deck that is otherwise text + boxes + icons).
|
||||||
avoid false-failing minimalist / teaser decks.
|
Between 40% and 60% is a soft INFO nudge. Short decks (<6 pages) are
|
||||||
|
exempt to avoid false-failing minimalist / teaser decks. Figure =
|
||||||
|
path/polygon/polyline/circle/image (see _check_graphic_richness).
|
||||||
"""
|
"""
|
||||||
pages = self._deck_page_count
|
pages = self._deck_page_count
|
||||||
if pages < 6:
|
if pages < 6:
|
||||||
return
|
return
|
||||||
avg_text = self._deck_text_total / pages
|
avg_text = self._deck_text_total / pages
|
||||||
if self._deck_graphic_total == 0 and avg_text >= 10:
|
no_g = len(self._pages_no_graphic)
|
||||||
self.summary['errors'] += 1
|
frac = no_g / pages
|
||||||
print(f"\n[ERROR] Visual richness: {pages} text-heavy pages but ZERO "
|
_fix = ("Map content shape -> a visual: comparison->columns/quadrant, "
|
||||||
"diagram/figure primitives (<path>/<polygon>/<polyline>/<image>) "
|
|
||||||
"deck-wide — the deck is text on rectangles.")
|
|
||||||
print(" Map content shape -> a visual: comparison->columns/quadrant, "
|
|
||||||
"timeline->process, share->donut, trend->line, ≥3 data points->chart "
|
"timeline->process, share->donut, trend->line, ≥3 data points->chart "
|
||||||
"(adapt a templates/charts/ template or draw it), add diagrams/imagery, "
|
"(adapt a templates/charts/ template or draw it), add diagrams/imagery, "
|
||||||
"then re-run.")
|
"then re-run.")
|
||||||
elif len(self._pages_no_graphic) >= max(6, int(pages * 0.7)):
|
if self._deck_graphic_total == 0 and avg_text >= 10:
|
||||||
print(f"\n[INFO] Visual richness: {len(self._pages_no_graphic)}/{pages} pages "
|
self.summary['errors'] += 1
|
||||||
"have no diagram/figure (<path>/<polygon>/<polyline>/<image>) — "
|
print(f"\n[ERROR] Visual richness: {pages} text-heavy pages but ZERO "
|
||||||
"verify dense content pages aren't just text + boxes.")
|
"diagram/figure primitives (<path>/<polygon>/<polyline>/<circle>/"
|
||||||
|
"<image>) deck-wide — the deck is text on rectangles.")
|
||||||
|
print(" " + _fix)
|
||||||
|
elif frac >= 0.6 and avg_text >= 10:
|
||||||
|
self.summary['errors'] += 1
|
||||||
|
print(f"\n[ERROR] Visual richness: {no_g}/{pages} pages carry no diagram/"
|
||||||
|
"figure (<path>/<polygon>/<polyline>/<circle>/<image>) — the deck is "
|
||||||
|
"mostly text + boxes; a few token diagrams don't cover a data/analysis "
|
||||||
|
"deck.")
|
||||||
|
print(" " + _fix)
|
||||||
|
elif frac >= 0.4:
|
||||||
|
print(f"\n[INFO] Visual richness: {no_g}/{pages} pages have no diagram/figure "
|
||||||
|
"(<path>/<polygon>/<polyline>/<circle>/<image>) — verify dense content "
|
||||||
|
"pages aren't just text + boxes.")
|
||||||
|
|
||||||
def print_summary(self):
|
def print_summary(self):
|
||||||
"""Print check summary"""
|
"""Print check summary"""
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog=f'''
|
epilog=f'''
|
||||||
Examples:
|
Examples:
|
||||||
%(prog)s examples/ppt169_demo -s final # Default: native pptx -> exports/, svg_output -> backup/<ts>/
|
%(prog)s examples/ppt169_demo -s final # Default: native pptx -> exports/, svg_output -> .build/backup/latest/
|
||||||
%(prog)s examples/ppt169_demo --svg-snapshot # Also emit SVG-rendered snapshot pptx alongside native in exports/
|
%(prog)s examples/ppt169_demo --svg-snapshot # Also emit SVG-rendered snapshot pptx alongside native in exports/
|
||||||
%(prog)s examples/ppt169_demo --only legacy # Only SVG image version (skips native)
|
%(prog)s examples/ppt169_demo --only legacy # Only SVG image version (skips native)
|
||||||
%(prog)s examples/ppt169_demo -o out.pptx # Explicit path (no backup/)
|
%(prog)s examples/ppt169_demo -o out.pptx # Explicit path (no backup/)
|
||||||
|
|
@ -223,7 +223,7 @@ Recorded narration:
|
||||||
help='Also emit the SVG-rendered snapshot pptx alongside the native pptx in exports/ '
|
help='Also emit the SVG-rendered snapshot pptx alongside the native pptx in exports/ '
|
||||||
'(named <project>_<ts>_svg.pptx). Off by default — the native pptx is the '
|
'(named <project>_<ts>_svg.pptx). Off by default — the native pptx is the '
|
||||||
'canonical output; live preview already provides the SVG visual reference. '
|
'canonical output; live preview already provides the SVG visual reference. '
|
||||||
'Note: the svg_output/ source snapshot is always written to backup/<ts>/ '
|
'Note: the svg_output/ source snapshot is always written to .build/backup/latest/ '
|
||||||
'regardless of this flag.')
|
'regardless of this flag.')
|
||||||
|
|
||||||
def non_negative_float(value: str) -> float:
|
def non_negative_float(value: str) -> float:
|
||||||
|
|
@ -369,11 +369,12 @@ Recorded narration:
|
||||||
exports_dir = project_path / "exports"
|
exports_dir = project_path / "exports"
|
||||||
exports_dir.mkdir(parents=True, exist_ok=True)
|
exports_dir.mkdir(parents=True, exist_ok=True)
|
||||||
native_path = exports_dir / f"{project_name}_{timestamp}.pptx"
|
native_path = exports_dir / f"{project_name}_{timestamp}.pptx"
|
||||||
# svg_output/ snapshot always goes under backup/<ts>/ in default-flow
|
# svg_output/ snapshot goes under the hidden .build/backup/latest/ in
|
||||||
# mode (no -o). --svg-snapshot only controls the optional legacy
|
# default-flow mode (no -o). Latest-only — no timestamp pile-up; the
|
||||||
# SVG-rendered pptx, which now sits alongside the native pptx in
|
# persistent svg_output/ is the real source, this is just a re-export
|
||||||
# exports/ rather than nested inside backup/.
|
# convenience copy. Literal ".build" keeps this package decoupled from
|
||||||
backup_dir = project_path / "backup" / timestamp
|
# project_utils; keep in sync with BUILD_DIR_NAME.
|
||||||
|
backup_dir = project_path / ".build" / "backup" / "latest"
|
||||||
if gen_legacy:
|
if gen_legacy:
|
||||||
legacy_path = exports_dir / f"{project_name}_{timestamp}_svg.pptx"
|
legacy_path = exports_dir / f"{project_name}_{timestamp}_svg.pptx"
|
||||||
|
|
||||||
|
|
@ -656,13 +657,16 @@ Recorded narration:
|
||||||
|
|
||||||
# svg_output/ snapshot — runs once per export in default-flow mode,
|
# svg_output/ snapshot — runs once per export in default-flow mode,
|
||||||
# decoupled from --svg-snapshot. Preserves the AI-generated SVG sources
|
# decoupled from --svg-snapshot. Preserves the AI-generated SVG sources
|
||||||
# under backup/<ts>/svg_output/ for later inspection / re-export.
|
# under .build/backup/latest/svg_output/ for later inspection / re-export.
|
||||||
|
# Latest-only: wipe any prior snapshot so backups don't pile up.
|
||||||
if success and backup_dir is not None:
|
if success and backup_dir is not None:
|
||||||
svg_output_src = project_path / "svg_output"
|
svg_output_src = project_path / "svg_output"
|
||||||
if svg_output_src.is_dir():
|
if svg_output_src.is_dir():
|
||||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
svg_output_dst = backup_dir / "svg_output"
|
svg_output_dst = backup_dir / "svg_output"
|
||||||
try:
|
try:
|
||||||
|
if backup_dir.exists():
|
||||||
|
shutil.rmtree(backup_dir)
|
||||||
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copytree(svg_output_src, svg_output_dst)
|
shutil.copytree(svg_output_src, svg_output_dst)
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f" svg_output backup: {svg_output_dst}")
|
print(f" svg_output backup: {svg_output_dst}")
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,12 @@ def find_svg_files(
|
||||||
Returns:
|
Returns:
|
||||||
(list_of_svg_files, actual_directory_name) tuple.
|
(list_of_svg_files, actual_directory_name) tuple.
|
||||||
"""
|
"""
|
||||||
|
# 'final' lives under the hidden .build/ (regenerable finalize output).
|
||||||
|
# Literal keeps this package import-decoupled; keep in sync with
|
||||||
|
# project_utils.BUILD_DIR_NAME (".build").
|
||||||
dir_map = {
|
dir_map = {
|
||||||
'output': 'svg_output',
|
'output': 'svg_output',
|
||||||
'final': 'svg_final',
|
'final': '.build/svg_final',
|
||||||
}
|
}
|
||||||
|
|
||||||
dir_name = dir_map.get(source, source)
|
dir_name = dir_map.get(source, source)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue