Compare commits

...

45 Commits

Author SHA1 Message Date
songtianyi.theo
455ff07527 feat(svglide): 完成 beautiful 模板全量 page-family 更新
补齐 34 套 beautiful family 的 renderer、golden、smoke、gallery 证据。

刷新 351 页 current deck render 和 production review gallery。

强化 contract_compile 的 raw Satori lowering,覆盖 line、path、matrix 等协议风险。

修复模板侧可见元信息泄漏、XML-like primitive、canvas bounds 等问题。

保持仅 blue-professional 为 production/default_selectable,其余继续 needs_review。
2026-06-28 13:48:18 +08:00
songtianyi.theo
1ba2a36ea3 修复 SVGlide 可编辑文本 lowering
- 聚合 raw Satori 文本并转换 baseline 到文本框坐标
- 映射 role font 到 Slide 可用字体并保留 source metadata
- 增加 preflight、quality、readback、editability 碎片化守护
- 验证 Diablo II 链路到 snapshot_visual_fidelity
2026-06-27 13:45:04 +08:00
songtianyi.theo
f1e08bb920 复用未失效的 SVG 生成产物
当 generate_svg stage 记录被上游剪掉但既有 receipt、输入 hash 和生成输出仍然 current 时,runner 记录 cache_hit 并复用现有产物。

避免下游 gate 抖动导致不必要的 renderer 重跑,审计文档同步更新 M2 状态。
2026-06-27 01:30:04 +08:00
songtianyi.theo
8cfbced316 补充 SVGlide 调试耗时审计
新增 debug-time-audit.json,分离 runner 执行时间、事件间 gap、显式 debug bucket 与未归属耗时。

保持 timing receipt 语义不变,避免再用 runner stage time 误代表完整人工调试耗时。
2026-06-27 01:26:25 +08:00
songtianyi.theo
02559cc0d4 拆分当前产物视觉完整性证据
新增 current-deck-visual-integrity receipt,用于承接当前 deck 发布链路中的 bounded fidelity warning。

quality gate 在接受 template-fidelity passed_with_warnings 前,必须校验 current deck 视觉完整性证据,避免把模板晋升口径和当前发布口径混用。
2026-06-27 01:23:44 +08:00
songtianyi.theo
5a3a0049dc 修复 SVGlide 链路重跑与校验边界
修复 composite gate 遇到 stale 子检查时直接阻断的问题,改为由父 gate 剪枝并重跑子检查。

明确 generate_svg 的真实输入边界,避免下游 quality gate receipt 变化反向污染生成阶段。

补充 contract_compile、prepare、PPE、readback、editability、beautiful template 复刻和生产评审相关链路产物与回归测试。
2026-06-27 01:18:47 +08:00
songtianyi.theo
55d1166d31 fix: 支持本地 beautiful 来源截图审查
新增本地 source screenshot 生成器,用于从 beautiful-html-templates 的 template.html 生成审查页左侧基准图。\n\ngallery 优先读取本地生成的 source-page-screenshots,并记录 bytes,避免回退到封面图造成误判。\n\n截图目录和 receipt 标记为本地审查缓存,不随代码上传。
2026-06-25 00:21:58 +08:00
songtianyi.theo
2a3783794f fix: 修正 beautiful 审查页截图回退
缺少第 N 页源截图时不再回退到封面参考图。

审查页改为显示 source screenshot missing,占位信息写入 gallery manifest 和 family JSON。

保持 production/default_selectable 不变,仍只有 blue-professional。
2026-06-24 23:31:34 +08:00
songtianyi.theo
96ea87a6db feat: 增加 beautiful 模板人工审查闭环
新增 34 套 family 的本地人工审查面板和 JSON 导出。

新增 human review receipt apply 脚本,生成可追溯的 pending/pass/needs_fix/reject 结果。

人工 receipt 不直接修改 production/default_selectable,当前默认链路仍只有 blue-professional。
2026-06-24 23:07:13 +08:00
songtianyi.theo
e8c1d53b86 feat: 生成 beautiful 全量审查画廊
为 34 套 beautiful family 生成 deck-level review manifest、family JSON、family HTML contact sheet 和 beautiful-34 gallery receipt。

gallery 和 batch receipt 明确标记为 review input / not promotion receipt;production/default 仍只保留 blue-professional,其余 33 套保持 blocked/needs_review。

补充 TDD 与 lint,阻断把 gallery 当作 production_review_receipt,并要求 passed smoke evidence 必须具备多页 deck、receipt、coverage 和 contact sheet。
2026-06-24 22:30:52 +08:00
songtianyi.theo
b6dd77264d feat: 增强 beautiful 模板生产审查门禁
补齐 blue-professional 10/10 page-family smoke 覆盖,新增 production review receipt 校验,确保 production/default 必须绑定 renderer、golden、fidelity、smoke、visual contract 和审查凭证。

新增 34 套 beautiful family 的 production review gallery 作为审查入口,但不作为晋级凭证;其余 33 套继续保持 needs_review,默认链路不被污染。

同步 quality gate 测试夹具到 10 variant contract,并补充 stale receipt、缺失 variant、gallery 非 promotion 的 TDD 用例。
2026-06-24 22:07:00 +08:00
songtianyi.theo
08687d1fa2 feat: 强化 SVGlide 模板复刻与视觉门禁
补齐 beautiful template 字体、排版、文本样式契约与 page-family smoke gate。

收紧 snapshot visual fidelity:要求两页可追踪 render 证据、PNG 可解码、receipt provenance 和 per-fixture 对齐。

完善 artboard renderer、quality gate、project runner 及相关 TDD 覆盖。
2026-06-24 21:35:16 +08:00
songtianyi.theo
8e23a21d68 feat: 推进 SVGlide beautiful 模板复刻契约
补齐 beautiful 模板的 visual contract、字体/排版/文本样式策略字段,并将 executable matrix、fidelity schema、fidelity receipt、golden fixture 与 Satori artboard 渲染链路对齐。

更新 template fidelity check、quality gate、prepare/project runner 等校验,确保未具备真实 renderer/golden/fidelity 证据的模板不会进入默认生产链路。

本提交是阶段性进展,保留已有 dedicated renderer、golden spec 和 fidelity receipt,后续继续收口 Slide 侧 snapshot/parser 验证与最终发布门禁。
2026-06-24 20:30:29 +08:00
songtianyi.theo
099b432336 feat: 收紧 beautiful 模板默认链路门禁
落地 M9 范围校正:34 套 family 只进入 candidate matrix,不再以 34 套全部 production/default_selectable 作为目标。

新增 blue-professional -> executive-dashboard 的真实闭环样板,包括 dedicated renderer、golden fixture、可复验 fidelity receipt、font role manifest,以及 selector/runtime/quality gate 的证据校验。

同步 project runner 的 template_fidelity stage,并补齐 runtime、quality gate、selection lint、artboard golden 和 diversity gate 的 TDD 覆盖,防止旧 P1/legacy 模板进入默认生产链路。
2026-06-24 12:54:45 +08:00
songtianyi.theo
8007c4e51a feat: 落地 SVGlide 设计资产组合路由
- 新增 deck recipe/style pack/semantic route registry 与 42 条路由用例,支持未知主题 fail-closed。

- 将 select_style 接入 recipe -> template family -> style_pack -> palette/component/image treatment,并回填 selection metadata 与 style_lock。

- 新增 diversity_gate 并接入 runner/quality_gate,阻断 style drift 和 template/style/layout/component 组合复用。

- 更新 SVG route 文档、schema 与私有索引,并补充 selector/review/preflight/quality/runner 测试。
2026-06-23 21:09:48 +08:00
songtianyi.theo
809d7d1a5b docs: 整合 SVGlide 设计资产组合总纲
新增设计资产组合系统执行总纲,整合 TDD 计划、并行团队、防偏移审查和最终验收标准。同步登记到 SVG 私有文档规则和兼容 manifest,便于后续按统一入口读取。
2026-06-23 20:17:14 +08:00
songtianyi.theo
6318eaba64 feat(svglide): 完善 create-svg PPE 提交流程
新增 ppe-profile,固定注入 PPE 三元组。

支持 append-to-presentation 和 revision-id。

补充 SVG rasterize/safe rewrite 辅助脚本和测试。

runner 增加 ppe-create-probe 与 ppe-image-probe gate。

readback 复用固定 PPE header 白名单。

验证:go test ./shortcuts/slides ./cmd/api 通过。

验证:ppe/readback Python 单测 19 个通过。

验证:runner live-create 目标单测 5 个通过。
2026-06-23 17:18:55 +08:00
songtianyi.theo
a56ef7f7c4 feat: 升级 beautiful-html 二档模板资产
将 19 个 beautiful-html-template family 提升为 SVGlide production template/theme,并补齐 absorption record、promotion gate、Satori renderer、golden fixture 与 selector 匹配。

同时强化 runtime selectable、selection lint/review 和 quality gate,阻止 source_inventory_only 或 promotion gate 未通过的模板进入生产链路。

验证:python3 -m unittest beautiful_template_knowledge_absorption_test.py svglide_selection_metadata_lint_test.py svglide_selection_review_test.py svglide_quality_gate_test.py svglide_theme_test.py svglide_theme_template_selector_test.py svglide_artboard_template_golden_test.py;pnpm --dir skills/lark-slides/scripts/artboard_renderer build;python3 -m json.tool beautiful-html-template-families.json。
2026-06-23 16:55:53 +08:00
songtianyi.theo
8eb4358586 feat(svglide): 隔离老旧资产并加固生产门禁
- 默认 production registry 仅暴露可信 theme/template/palette
- 将旧 P0 模板、baseline layout/image/chart 降级为 fixture/debug
- 收窄“链路”架构误触发,并过滤 planner legacy context
- 阻断 legacy/fallback/include_legacy_debug registry 进入 production gate
- 补充 TDD 回归与 fixture/debug 兼容测试

验证:
- 423 tests OK
- git diff --check
- JSON registry 校验通过
2026-06-23 15:24:47 +08:00
songtianyi.theo
872cfd4281 feat(svglide): 携带 SVG 图片完整元信息 2026-06-23 14:18:18 +08:00
songtianyi.theo
480d4e2fbb feat(svglide): 完成 beautiful 模板知识吸收闭环
补齐 34 个 beautiful-html-template family 的 cjk_policy、family_usage_policy、extension_grammar 和 cover/mid/late benchmark roles。

接入 matcher、planner、theme selector、preflight、quality gate 与 dry-run receipt,阻断跨 family 混用、CJK fake italic、未授权 recolor、source inventory claim escalation 等 M15 问题。

验证:M15 targeted 150 tests OK;full unittest discover 736 tests OK;dry-run internal-review / zhipu-minimax passed;json/schema ok;git diff --check OK。
2026-06-23 00:16:26 +08:00
songtianyi.theo
985089291f feat(svglide): 落地 beautiful 模板家族资产链路
- 新增 beautiful template family、component、asset strategy、asset slot、cleanup map 等结构化资产
- 将 matcher、planner、renderer、preflight、quality gate 迁移到 template_family_selection / template_variant / semantic_blocks / component_selection / asset_strategy
- 删除旧 style preset、visual recipe、手写 theme/template/component/palette registry,并把 provenance 内联到 family registry
- 加强图片 slot、真实图片、未归属装饰 primitive、contract compile 和路由防污染校验
- 补充 dry-run、schema、preflight、contract compile 及字体第一档相关测试
2026-06-22 23:41:25 +08:00
songtianyi.theo
52439e6f96 chore: 补充 SVGlide 开源合规边界
补充 OSS 来源清单和第三方 notice,明确参考抽象与运行时依赖的分发规则。

将 artboard renderer 中的 satori 保持为 external,并增加 package check/单测阻断 bundled Satori 回归。
2026-06-22 22:13:03 +08:00
songtianyi.theo
78b130c1bc chore(svglide): 完成 artboard 清理并加固预览验收门禁
本次提交完成 artboard-satori 清理计划的核心落地:补齐参考资产抽象、模板/主题/调色板/图片策略注册表,并加入对应 golden fixture。

同时加固 local_real_preview 到 visual_acceptance 的验收边界:显式 source_url 不再被 no-image-search 阻断,预览锚点必须真实存在,page-level preview hash 会被 runner 和 pre-submit 强制校验。

验证:P0/P1 cleanup check 通过;selector、metadata lint、runtime review、artboard renderer、quality gate、assets、asset injector、visual_acceptance、pre-submit review、project runner 测试均通过;reference_absorption_wave2 通过 local_real_preview visual_acceptance 回归。
2026-06-22 18:18:14 +08:00
songtianyi.theo
551f333563 fix(slides): gate vf5 trusted benchmark route 2026-06-22 01:53:47 +08:00
songtianyi.theo
a9b2a416b4 feat(slides): add svglide visual acceptance gates 2026-06-22 01:41:05 +08:00
songtianyi.theo
d6c060279d docs: add svglide visual acceptance follow-up 2026-06-21 23:45:34 +08:00
songtianyi.theo
c4a272d28d fix(svglide): polish artboard assets and layout fallbacks 2026-06-21 23:23:55 +08:00
songtianyi.theo
8502e8c433 feat(svglide): complete artboard follow-up pipeline 2026-06-21 22:05:02 +08:00
songtianyi.theo
4a37a2fe7b test(svglide): close theme system p0 plan evidence 2026-06-21 21:37:31 +08:00
songtianyi.theo
842e2d44de feat(svglide): land artboard theme system gates
Implement SVGlide artboard Satori scaffolding, ThemeSpec validation/adherence gates, package and pre-submit checks, runner integration, quality gate freshness binding, and regression fixtures.

Tests: python3 -m unittest discover skills/lark-slides/scripts -p '*_test.py'; go test ./cmd/skill ./cmd/api
2026-06-21 21:07:20 +08:00
songtianyi.theo
94c5eb5b25 feat(svglide): add visual identity distinctness gate 2026-06-20 16:23:31 +08:00
songtianyi.theo
2af5dd41b9 feat(svglide): inject local assets into SVG preview 2026-06-20 00:12:31 +08:00
songtianyi.theo
c98b7719bf feat(svglide): migrate online-first source and asset gates 2026-06-19 19:40:10 +08:00
songtianyi.theo
071ec01247 chore(svglide): checkpoint svg private pipeline 2026-06-19 19:29:44 +08:00
songtianyi.theo
b99975bd5d feat(slides): gate SVGlide preview quality 2026-06-12 14:14:03 +08:00
songtianyi.theo
4aa6c1f56d feat(slides): isolate SVGlide private route knowledge 2026-06-11 21:26:42 +08:00
songtianyi.theo
07e57d7857 支持 SVGlide 图表 spec 标记 2026-06-11 19:51:06 +08:00
songtianyi.theo
566f6cfd47 feat(slides): support SVGlide chart markers 2026-06-11 15:56:24 +08:00
songtianyi.theo
cd0854e931 中文化 SVGlide 视觉规则文档
将 visual recipe 和 aesthetic review 两个执行入口改为中文说明,保留字段名、枚举、命令和 JSON 契约。
2026-06-11 15:23:49 +08:00
songtianyi.theo
be8a67b894 完善 SVGlide 视觉生成与预检门禁
新增 style presets、visual recipe 和 aesthetic review gate,并接入 lark-slides skill 执行链路。

扩展 create-svg 图片处理、协议说明与 svg_preflight 校验,补充对应测试。
2026-06-11 15:23:49 +08:00
songtianyi.theo
f5502416c8 docs: harden svglide workflow guidance 2026-06-11 15:22:23 +08:00
songtianyi.theo
182c541ea6 docs: strengthen svglide generation guidance 2026-06-11 15:22:23 +08:00
songtianyi.theo
d980e54ab8 docs: add svglide deck density planning 2026-06-11 15:22:23 +08:00
songtianyi.theo
85965e41e4 feat: add svglide create-svg shortcut 2026-06-11 15:22:22 +08:00
1609 changed files with 612520 additions and 62 deletions

View File

@@ -48,6 +48,37 @@ jobs:
exit 1
fi
svglide-artboard-macos-x64-runtime:
needs: fast-gate
runs-on: macos-13
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
with:
node-version: '20'
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.x'
- name: Install artboard renderer dependencies
run: |
corepack enable
corepack prepare pnpm@9.15.4 --activate
pnpm --dir skills/lark-slides/scripts/artboard_renderer install --frozen-lockfile
- name: Validate macOS x64 runtime
run: |
mkdir -p .artifacts/svglide-artboard-package-check
python3 skills/lark-slides/scripts/svglide_artboard_package_check.py \
--require-system Darwin \
--require-arch x64 \
--output-dir .artifacts/svglide-artboard-package-check \
--pretty
- name: Upload SVGlide artboard runtime evidence
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: svglide-artboard-macos-x64-runtime-${{ github.run_number }}
path: .artifacts/svglide-artboard-package-check
retention-days: 30
# ── Layer 2: Quality Gate ──────────────────────────────────────────
unit-test:
needs: fast-gate

8
.gitignore vendored
View File

@@ -6,6 +6,8 @@ bin/
# Node
node_modules/
!skills/lark-slides/scripts/artboard_renderer/dist/
!skills/lark-slides/scripts/artboard_renderer/dist/render.mjs
# OS
@@ -43,4 +45,10 @@ app.log
.tmp/
cover*.out
# SVGlide local review caches
skills/lark-slides/references/production-review/beautiful/source-page-screenshots/
skills/lark-slides/references/production-review/beautiful/current-svglide-decks/
skills/lark-slides/references/receipts/production-review/beautiful-34-source-page-screenshots.json
skills/lark-slides/references/receipts/production-review/beautiful-34-current-svglide-decks.json
lark-env.sh

View File

@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
@@ -42,6 +43,7 @@ type APIOptions struct {
JqExpr string
DryRun bool
File string
Headers []string
}
var urlPrefixRe = regexp.MustCompile(`https?://[^/]+(/open-apis/.+)`)
@@ -94,6 +96,7 @@ func NewCmdApiWithContext(ctx context.Context, f *cmdutil.Factory, runF func(*AP
cmd.Flags().StringVarP(&opts.JqExpr, "jq", "q", "", "jq expression to filter JSON output")
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "print request without executing")
cmd.Flags().StringVar(&opts.File, "file", "", "file to upload as multipart/form-data ([field=]path, supports - for stdin)")
cmd.Flags().StringArrayVar(&opts.Headers, "request-header", nil, "internal request header for controlled lanes; repeat key=value, only Env=Pre_release, x-tt-env=ppe_pure_svg, x-use-ppe=1 are allowed")
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
@@ -140,6 +143,13 @@ func buildAPIRequest(opts *APIOptions) (client.RawApiRequest, *cmdutil.FileUploa
Params: params,
As: opts.As,
}
headers, err := parseAPIRequestHeaders(opts.Headers)
if err != nil {
return client.RawApiRequest{}, nil, err
}
if len(headers) > 0 {
request.ExtraOpts = append(request.ExtraOpts, larkcore.WithHeaders(headers))
}
if opts.File != "" {
// File upload path: build formdata.
@@ -187,6 +197,41 @@ func buildAPIRequest(opts *APIOptions) (client.RawApiRequest, *cmdutil.FileUploa
return request, nil, nil
}
func parseAPIRequestHeaders(values []string) (http.Header, error) {
headers := http.Header{}
for _, raw := range values {
item := strings.TrimSpace(raw)
if item == "" {
return nil, output.ErrValidation("--request-header cannot be empty")
}
key, value, ok := strings.Cut(item, "=")
if !ok {
return nil, output.ErrValidation("--request-header %q must use key=value", item)
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
canonicalKey, canonicalValue, ok := allowedAPIInternalHeader(key)
if !ok || value != canonicalValue {
return nil, output.ErrValidation("--request-header %q is not supported; allowed internal headers are Env=Pre_release, x-tt-env=ppe_pure_svg, x-use-ppe=1", item)
}
headers.Set(canonicalKey, value)
}
return headers, nil
}
func allowedAPIInternalHeader(key string) (string, string, bool) {
switch {
case strings.EqualFold(key, "Env"):
return "Env", "Pre_release", true
case strings.EqualFold(key, "x-tt-env"):
return "x-tt-env", "ppe_pure_svg", true
case strings.EqualFold(key, "x-use-ppe"):
return "x-use-ppe", "1", true
default:
return "", "", false
}
}
func apiRun(opts *APIOptions) error {
f := opts.Factory
opts.As = f.ResolveAs(opts.Ctx, opts.Cmd, opts.As)

View File

@@ -88,6 +88,57 @@ func TestApiCmd_BotMode(t *testing.T) {
}
}
func TestApiCmd_RequestHeaderPassesToCall(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
stub := &httpmock.Stub{
URL: "/open-apis/test",
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{"result": "success"}},
}
reg.Register(stub)
cmd := NewCmdApi(f, nil)
cmd.SetArgs([]string{
"GET", "/open-apis/test", "--as", "user",
"--request-header", "Env=Pre_release",
"--request-header", "x-tt-env=ppe_pure_svg",
"--request-header", "x-use-ppe=1",
})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := stub.CapturedHeaders.Get("Env"); got != "Pre_release" {
t.Fatalf("Env header = %q, want Pre_release", got)
}
if got := stub.CapturedHeaders.Get("x-tt-env"); got != "ppe_pure_svg" {
t.Fatalf("x-tt-env header = %q, want ppe_pure_svg", got)
}
if got := stub.CapturedHeaders.Get("x-use-ppe"); got != "1" {
t.Fatalf("x-use-ppe header = %q, want 1", got)
}
if !strings.Contains(stdout.String(), "success") {
t.Error("expected 'success' in output")
}
}
func TestApiCmd_RequestHeaderRejectsUnsupportedKey(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdApi(f, nil)
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "user", "--request-header", "authorization=secret"})
err := cmd.Execute()
if err == nil {
t.Fatal("expected unsupported request header error")
}
if !strings.Contains(err.Error(), "allowed internal headers") {
t.Fatalf("err = %v, want supported-header message", err)
}
}
func TestApiCmd_MissingArgs(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,

View File

@@ -50,7 +50,8 @@ func NewCmdSkill(f *cmdutil.Factory) *cobra.Command {
Short: "Read embedded skill content (list / read)",
Long: "Read agent-readable skill content (SKILL.md and reference files) embedded in " +
"the CLI binary at build time, so it stays in sync with the CLI version. " +
"Machine resources such as assets/ and scripts/ are not embedded.",
"Selected lark-slides prompts, scripts, and artboard renderer package files are also embedded; " +
"runtime dependency folders such as node_modules/ and generated artifacts are not embedded.",
}
// Risk is set on each leaf (GetRisk does not walk parents); the group has none.
cmdutil.DisableAuthCheck(cmd)

View File

@@ -251,6 +251,14 @@ func (ctx *RuntimeContext) CallAPI(method, url string, params map[string]interfa
return HandleApiResult(result, err, "API call failed")
}
// CallAPIWithHeaders is CallAPI plus request-scoped HTTP headers. Keep this for
// shortcuts that need a narrow, audited transport lane rather than global CLI
// header injection.
func (ctx *RuntimeContext) CallAPIWithHeaders(method, url string, params map[string]interface{}, data interface{}, headers http.Header) (map[string]interface{}, error) {
result, err := ctx.callRawWithHeaders(method, url, params, data, headers)
return HandleApiResult(result, err, "API call failed")
}
// CallAPITyped is the typed-only replacement for CallAPI: it performs the same
// SDK request (buildRequest → APIClient.DoAPI → DoSDKRequest, identical
// transport and query model to CallAPI) and returns the "data" object, but
@@ -422,11 +430,19 @@ func (ctx *RuntimeContext) buildRequest(method, url string, params map[string]in
}
func (ctx *RuntimeContext) callRaw(method, url string, params map[string]interface{}, data interface{}) (interface{}, error) {
return ctx.callRawWithHeaders(method, url, params, data, nil)
}
func (ctx *RuntimeContext) callRawWithHeaders(method, url string, params map[string]interface{}, data interface{}, headers http.Header) (interface{}, error) {
ac, err := ctx.getAPIClient()
if err != nil {
return nil, err
}
return ac.CallAPI(ctx.ctx, ctx.buildRequest(method, url, params, data))
req := ctx.buildRequest(method, url, params, data)
if len(headers) > 0 {
req.ExtraOpts = append(req.ExtraOpts, larkcore.WithHeaders(headers))
}
return ac.CallAPI(ctx.ctx, req)
}
// DoAPI executes a raw Lark SDK request with automatic auth handling.
@@ -1000,6 +1016,10 @@ func runShortcut(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bo
}
}
if wantDryRun, _ := cmd.Flags().GetBool("dry-run"); wantDryRun && s.DryRun != nil {
return runShortcutDryRunLocal(cmd, f, s, botOnly)
}
as, err := resolveShortcutIdentity(cmd, f, s)
if err != nil {
return err
@@ -1050,6 +1070,49 @@ func runShortcut(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bo
return rctx.outputErr
}
func runShortcutDryRunLocal(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, botOnly bool) error {
asFlag, _ := cmd.Flags().GetString("as")
as := core.Identity(strings.TrimSpace(asFlag))
if as == "" || as == "auto" {
as = core.AsUser
}
if botOnly {
as = core.AsBot
}
if !shortcutSupportsIdentity(as, s.AuthTypes) {
return f.CheckIdentity(as, s.AuthTypes)
}
config := &core.CliConfig{}
rctx := newLocalDryRunRuntimeContext(cmd, f, s, config, as, botOnly)
if err := validateEnumFlags(rctx, s.Flags); err != nil {
return err
}
if err := resolveInputFlags(rctx, s.Flags); err != nil {
return err
}
if err := output.ValidateJqFlags(rctx.JqExpr, "", rctx.Format); err != nil {
return err
}
if s.Validate != nil {
if err := s.Validate(rctx.ctx, rctx); err != nil {
return err
}
}
return handleShortcutDryRun(f, rctx, s)
}
func shortcutSupportsIdentity(as core.Identity, authTypes []string) bool {
if len(authTypes) == 0 {
return true
}
for _, raw := range authTypes {
if core.Identity(raw) == as {
return true
}
}
return false
}
func resolveShortcutIdentity(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) (core.Identity, error) {
// Step 1: determine identity (--as > default-as > auto-detect).
asFlag, _ := cmd.Flags().GetString("as")
@@ -1104,6 +1167,21 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf
return rctx, nil
}
func newLocalDryRunRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, config *core.CliConfig, as core.Identity, botOnly bool) *RuntimeContext {
ctx := cmd.Context()
ctx = cmdutil.ContextWithShortcut(ctx, s.Service+":"+s.Command, uuid.New().String())
rctx := &RuntimeContext{ctx: ctx, Config: config, Cmd: cmd, botOnly: botOnly, resolvedAs: as, Factory: f}
rctx.apiClientFunc = sync.OnceValues(func() (*client.APIClient, error) {
return nil, fmt.Errorf("API client is not available during local dry-run")
})
rctx.botInfoFunc = sync.OnceValues(func() (*BotInfo, error) {
return nil, fmt.Errorf("bot info is not available during local dry-run")
})
rctx.Format = rctx.Str("format")
rctx.JqExpr, _ = cmd.Flags().GetString("jq")
return rctx
}
// stripUTF8BOM removes a leading UTF-8 byte-order mark from content read from a
// file or stdin. A BOM that survives into a CSV cell corrupts the first value
// (e.g. "\ufeffNorth", which then makes a MAXIFS/lookup miss it), and a BOM at the

View File

@@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common"
func Shortcuts() []common.Shortcut {
return []common.Shortcut{
SlidesCreate,
SlidesCreateSVG,
SlidesMediaUpload,
SlidesReplaceSlide,
}

View File

@@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"
@@ -124,35 +125,19 @@ var SlidesCreate = common.Shortcut{
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
title := effectiveTitle(runtime.Str("title"))
content := buildPresentationXML(title)
slidesStr := runtime.Str("slides")
// Step 1: Create presentation
data, err := runtime.CallAPITyped(
"POST",
"/open-apis/slides_ai/v1/xml_presentations",
nil,
map[string]interface{}{
"xml_presentation": map[string]interface{}{
"content": content,
},
},
)
presentationID, revisionID, err := createEmptyPresentation(runtime, title)
if err != nil {
return err
}
presentationID := common.GetString(data, "xml_presentation_id")
if presentationID == "" {
return errs.NewInternalError(errs.SubtypeInvalidResponse, "slides create returned no xml_presentation_id")
}
result := map[string]interface{}{
"xml_presentation_id": presentationID,
"title": title,
}
if revisionID := common.GetFloat(data, "revision_id"); revisionID > 0 {
result["revision_id"] = int(revisionID)
if revisionID > 0 {
result["revision_id"] = revisionID
}
// Step 2: Add slides if provided
@@ -197,6 +182,9 @@ var SlidesCreate = common.Shortcut{
if sid := common.GetString(slideData, "slide_id"); sid != "" {
slideIDs = append(slideIDs, sid)
}
if latest := common.GetFloat(slideData, "revision_id"); latest > 0 {
result["revision_id"] = int(latest)
}
}
result["slide_ids"] = slideIDs
@@ -204,19 +192,7 @@ var SlidesCreate = common.Shortcut{
}
}
// Build the presentation URL locally from the token. The brand-standard
// host transparently redirects to the tenant domain (same fallback used by
// drive +upload / wiki +node-create). This avoids the prior best-effort
// drive metas/batch_query call, which needed an extra drive scope and 403'd
// for users who only authorized slides scopes — without ever blocking an
// otherwise-successful creation.
if url := common.BuildResourceURL(runtime.Config.Brand, "slides", presentationID); url != "" {
result["url"] = url
}
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, presentationID, "slides"); grant != nil {
result["permission_grant"] = grant
}
fillPresentationResult(runtime, presentationID, result)
runtime.Out(result, nil)
return nil
@@ -243,6 +219,49 @@ func buildPresentationXML(title string) string {
)
}
func createEmptyPresentation(runtime *common.RuntimeContext, title string) (string, int, error) {
return createEmptyPresentationWithHeaders(runtime, title, nil)
}
func createEmptyPresentationWithHeaders(runtime *common.RuntimeContext, title string, headers http.Header) (string, int, error) {
data, err := runtime.CallAPIWithHeaders(
"POST",
"/open-apis/slides_ai/v1/xml_presentations",
nil,
map[string]interface{}{
"xml_presentation": map[string]interface{}{
"content": buildPresentationXML(title),
},
},
headers,
)
if err != nil {
return "", 0, err
}
presentationID := common.GetString(data, "xml_presentation_id")
if presentationID == "" {
return "", 0, errs.NewInternalError(errs.SubtypeInvalidResponse, "slides create returned no xml_presentation_id")
}
revisionID := 0
if rev := common.GetFloat(data, "revision_id"); rev > 0 {
revisionID = int(rev)
}
return presentationID, revisionID, nil
}
func fillPresentationResult(runtime *common.RuntimeContext, presentationID string, result map[string]interface{}) {
// Build the presentation URL locally from the token. The brand-standard
// host transparently redirects to the tenant domain. This avoids the prior
// best-effort drive metas/batch_query call, which needed an extra drive scope.
if url := common.BuildResourceURL(runtime.Config.Brand, "slides", presentationID); url != "" {
result["url"] = url
}
if grant := common.AutoGrantCurrentUserDrivePermission(runtime, presentationID, "slides"); grant != nil {
result["permission_grant"] = grant
}
}
// uploadSlidesPlaceholders uploads each unique placeholder path against the
// presentation and returns the path→file_token map. The second return value is
// the number of files successfully uploaded before any error, so callers can

View File

@@ -0,0 +1,380 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package slides
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
// SlidesCreateSVG creates a new Lark Slides presentation from one or more
// SVGlide SVG files by adding each page through the existing XML slide route.
var SlidesCreateSVG = common.Shortcut{
Service: "slides",
Command: "+create-svg",
Description: "Create a Lark Slides presentation from SVG",
Risk: "write",
AuthTypes: []string{"user", "bot"},
Scopes: []string{
"slides:presentation:create",
"slides:presentation:write_only",
"docs:document.media:upload",
},
Flags: []common.Flag{
{Name: "title", Desc: "presentation title"},
{
Name: "file",
Type: "string_array",
Required: true,
Desc: "SVG file path; repeat for multiple pages",
},
{Name: "assets", Desc: "optional assets.json path mapping SVG @path placeholders to uploaded file tokens"},
{Name: "font-family", Desc: "optional supported font family to apply to SVGlide text; custom slide-font-* fonts are not supported"},
{
Name: "svg-rasterize-effects",
Default: "off",
Enum: []string{"off", "auto", "strict", "force-page"},
Desc: "Rasterize unsupported rich SVG effects before upload: off|auto|strict|force-page",
},
{
Name: "svg-rasterize-scale",
Type: "int",
Default: "2",
Desc: "PNG raster scale; default 2",
},
{Name: "svg-rasterize-report", Desc: "optional raster report output path"},
{Name: "ppe-profile", Default: "none", Enum: []string{"none", "ppe_pure_svg"}, Desc: "internal SVGlide PPE profile"},
{Name: "request-header", Type: "string_array", Desc: "internal request header for SVGlide live lanes; repeat key=value, only Env=Pre_release, x-tt-env=ppe_pure_svg, x-use-ppe=1 are allowed"},
{Name: "append-to-presentation", Desc: "existing xml_presentation_id or /slides/ URL to append SVG pages into instead of creating a new presentation"},
{Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision for append/add-slide calls (-1 = latest)"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if err := validateSVGFileInputs(runtime, runtime.StrArray("file")); err != nil {
return err
}
if err := validateSVGRasterizeFlags(runtime); err != nil {
return err
}
if err := validateSVGAssetsPath(runtime, runtime.Str("assets")); err != nil {
return err
}
if _, err := normalizeSVGFontFamily(runtime.Str("font-family")); err != nil {
return err
}
if _, err := parseSVGRequestHeaders(runtime.Str("ppe-profile"), runtime.StrArray("request-header")); err != nil {
return err
}
_, err := appendPresentationID(runtime.Str("append-to-presentation"))
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
title := effectiveTitle(runtime.Str("title"))
fontFamily, err := normalizeSVGFontFamily(runtime.Str("font-family"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
requestHeaders, err := parseSVGRequestHeaders(runtime.Str("ppe-profile"), runtime.StrArray("request-header"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
appendID, err := appendPresentationID(runtime.Str("append-to-presentation"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
svgs, prepareReport, err := prepareSVGFilesForCreate(
runtime,
runtime.StrArray("file"),
svgPrepareOptionsFromRuntime(runtime, true),
)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
assets, err := parseSVGAssets(runtime, runtime.Str("assets"))
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
if err := validateSVGRasterAssetConflicts(assets, prepareReport); err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
inputPaths := runtime.StrArray("file")
pages, uploadPaths, err := dryRunRewriteSVGImagePlaceholders(runtime, svgs, assets)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
dry := common.NewDryRunAPI()
createSteps := 1
presentationID := "<xml_presentation_id>"
if appendID != "" {
createSteps = 0
presentationID = appendID
}
total := createSteps + len(uploadPaths) + len(pages)
descSuffix := ""
if len(uploadPaths) > 0 {
descSuffix = fmt.Sprintf(" + upload %d image(s)", len(uploadPaths))
}
if appendID == "" {
dry.Desc(fmt.Sprintf("Create presentation from %d SVG page(s)%s", len(pages), descSuffix)).
POST("/open-apis/slides_ai/v1/xml_presentations").
Desc(fmt.Sprintf("[1/%d] Create presentation", total)).
Body(map[string]interface{}{
"xml_presentation": map[string]interface{}{"content": buildPresentationXML(title)},
})
} else {
dry.Desc(fmt.Sprintf("Append %d SVG page(s) to presentation %s%s", len(pages), appendID, descSuffix))
}
for i, path := range uploadPaths {
appendSlidesUploadDryRun(dry, path, presentationID, createSteps+i+1)
}
slideStepStart := createSteps + len(uploadPaths) + 1
pageProofs := make([]svgPageProof, 0, len(pages))
for i, page := range pages {
content := page.Content
if fontFamily != "" {
content = applySVGlideFontFamily(content, fontFamily)
}
content, injectErr := injectSVGTransportAssetMetadata(content, page.Assets)
if injectErr != nil {
return common.NewDryRunAPI().Set("error", injectErr.Error())
}
pageProofs = append(pageProofs, summarizeSVGPageContent(svgSourcePath(inputPaths, i), i+1, content))
dry.POST(fmt.Sprintf("/open-apis/slides_ai/v1/xml_presentations/%s/slide", presentationID)).
Desc(fmt.Sprintf("[%d/%d] Add SVG page %d", slideStepStart+i, total, i+1)).
Params(map[string]interface{}{"revision_id": runtime.Int("revision-id")}).
Body(buildCreateSVGBody(content))
}
if appendID == "" && runtime.IsBot() {
dry.Desc("After creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new presentation.")
}
if prepareReport != nil {
dry.Set("svg_rasterize_report", prepareReport)
}
if len(requestHeaders) > 0 {
dry.Set("request_headers", svgRequestHeadersForOutput(requestHeaders))
}
if fontFamily != "" {
dry.Set("font_family", fontFamily)
}
if appendID != "" {
dry.Set("append_to_presentation", appendID)
}
dry.Set("svg_pages", pageProofs)
return dry.Set("title", title)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
title := effectiveTitle(runtime.Str("title"))
fontFamily, err := normalizeSVGFontFamily(runtime.Str("font-family"))
if err != nil {
return err
}
requestHeaders, err := parseSVGRequestHeaders(runtime.Str("ppe-profile"), runtime.StrArray("request-header"))
if err != nil {
return err
}
appendID, err := appendPresentationID(runtime.Str("append-to-presentation"))
if err != nil {
return err
}
svgs, prepareReport, err := prepareSVGFilesForCreate(
runtime,
runtime.StrArray("file"),
svgPrepareOptionsFromRuntime(runtime, false),
)
if err != nil {
return err
}
assets, err := parseSVGAssets(runtime, runtime.Str("assets"))
if err != nil {
return err
}
if err := validateSVGRasterAssetConflicts(assets, prepareReport); err != nil {
return err
}
inputPaths := runtime.StrArray("file")
presentationID := appendID
revisionID := runtime.Int("revision-id")
created := appendID == ""
if created {
presentationID, revisionID, err = createEmptyPresentationWithHeaders(runtime, title, requestHeaders)
if err != nil {
return err
}
}
result := map[string]interface{}{
"xml_presentation_id": presentationID,
"title": title,
}
if !created {
result["append_to_presentation"] = presentationID
}
if prepareReport != nil {
result["svg_rasterize_report"] = prepareReport
}
if revisionID > 0 {
result["revision_id"] = revisionID
}
if len(requestHeaders) > 0 {
result["request_headers"] = svgRequestHeadersForOutput(requestHeaders)
}
if fontFamily != "" {
result["font_family"] = fontFamily
}
pages, uploaded, err := rewriteSVGImagePlaceholders(runtime, presentationID, svgs, assets)
if err != nil {
action := "was created"
if !created {
action = "was selected for append"
}
return output.Errorf(output.ExitAPI, "api_error",
"image upload failed: %v (presentation %s %s; %d image(s) uploaded before failure)",
err, presentationID, action, uploaded)
}
if uploaded > 0 {
result["images_uploaded"] = uploaded
}
slideURL := fmt.Sprintf(
"/open-apis/slides_ai/v1/xml_presentations/%s/slide",
validate.EncodePathSegment(presentationID),
)
var slideIDs []string
pageProofs := make([]svgPageProof, 0, len(pages))
for i, page := range pages {
content := page.Content
if fontFamily != "" {
content = applySVGlideFontFamily(content, fontFamily)
}
content, err := injectSVGTransportAssetMetadata(content, page.Assets)
if err != nil {
action := "was created"
if !created {
action = "was selected for append"
}
return output.Errorf(output.ExitValidation, "validation",
"page %d/%d failed before API call: %v (presentation %s %s; %d slide(s) added; slide_ids=%s)",
i+1, len(pages), err, presentationID, action, len(slideIDs), strings.Join(slideIDs, ","))
}
proof := summarizeSVGPageContent(svgSourcePath(inputPaths, i), i+1, content)
slideData, err := runtime.CallAPIWithHeaders(
"POST",
slideURL,
map[string]interface{}{"revision_id": revisionID},
buildCreateSVGBody(content),
requestHeaders,
)
if err != nil {
action := "was created"
if !created {
action = "was selected for append"
}
return output.Errorf(output.ExitAPI, "api_error",
"page %d/%d failed: %v%s (presentation %s %s; %d slide(s) added; slide_ids=%s)",
i+1, len(pages), err, formatSVGlideErrorSuffix(err), presentationID, action, len(slideIDs), strings.Join(slideIDs, ","))
}
if sid := common.GetString(slideData, "slide_id"); sid != "" {
slideIDs = append(slideIDs, sid)
}
pageProofs = append(pageProofs, proof)
if latest := common.GetFloat(slideData, "revision_id"); latest > 0 {
revisionID = int(latest)
result["revision_id"] = revisionID
}
}
result["slide_ids"] = slideIDs
result["slides_added"] = len(slideIDs)
result["svg_pages"] = pageProofs
fillPresentationResult(runtime, presentationID, result)
runtime.Out(result, nil)
return nil
},
}
func appendPresentationID(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", nil
}
ref, err := parsePresentationRef(raw)
if err != nil {
return "", err
}
if ref.Kind != "slides" {
return "", output.ErrValidation("--append-to-presentation must be an xml_presentation_id or /slides/ URL")
}
return ref.Token, nil
}
func parseSVGRequestHeaders(profile string, values []string) (http.Header, error) {
headers := http.Header{}
switch strings.TrimSpace(profile) {
case "", "none":
case "ppe_pure_svg":
headers.Set("Env", "Pre_release")
headers.Set("x-tt-env", "ppe_pure_svg")
headers.Set("x-use-ppe", "1")
default:
return nil, output.ErrValidation("--ppe-profile must be one of none, ppe_pure_svg")
}
for _, raw := range values {
item := strings.TrimSpace(raw)
if item == "" {
return nil, output.ErrValidation("--request-header cannot be empty")
}
key, value, ok := strings.Cut(item, "=")
if !ok {
return nil, output.ErrValidation("--request-header %q must use key=value", item)
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
canonicalKey, canonicalValue, ok := allowedSVGPPEHeader(key)
if !ok || value != canonicalValue {
return nil, output.ErrValidation("--request-header %q is not supported; allowed SVGlide PPE headers are Env=Pre_release, x-tt-env=ppe_pure_svg, x-use-ppe=1", item)
}
if existing := headers.Get(canonicalKey); existing != "" && existing != value {
return nil, output.ErrValidation("--request-header %s conflicts with --ppe-profile", canonicalKey)
}
headers.Set(canonicalKey, value)
}
return headers, nil
}
func allowedSVGPPEHeader(key string) (string, string, bool) {
switch {
case strings.EqualFold(key, "Env"):
return "Env", "Pre_release", true
case strings.EqualFold(key, "x-tt-env"):
return "x-tt-env", "ppe_pure_svg", true
case strings.EqualFold(key, "x-use-ppe"):
return "x-use-ppe", "1", true
default:
return "", "", false
}
}
func svgRequestHeadersForOutput(headers http.Header) map[string]string {
out := map[string]string{}
if value := headers.Get("Env"); value != "" {
out["Env"] = value
}
if value := headers.Get("x-tt-env"); value != "" {
out["x-tt-env"] = value
}
if value := headers.Get("x-use-ppe"); value != "" {
out["x-use-ppe"] = value
}
return out
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,599 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package slides
import (
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"os"
"reflect"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/shortcuts/common"
)
func TestExtractSVGImagePlaceholderPaths(t *testing.T) {
t.Parallel()
svgs := []string{
`<svg><image slide:role="image" href="@./hero.png"/><a href="@./link.png"/></svg>`,
`<svg><image xlink:href='@./hero.png'/><image href = "@./other.png"/></svg>`,
}
got := extractSVGImagePlaceholderPaths(svgs, svgAssetMap{"@./other.png": {Token: "boxcn_other"}})
want := []string{"./hero.png"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestRewriteSVGImagePlaceholdersWithTokens(t *testing.T) {
t.Parallel()
in := `<svg><image slide:role="image" href="@./hero.png"/><image xlink:href='@./logo.png'/><image data-href="@./ignored.png"/><a href="@./link.png">link</a><image href="https://example.com/noop.png"/></svg>`
got, assets := rewriteSVGImagePlaceholdersWithTokens(in, svgAssetMap{
"./hero.png": {Token: "boxcn_hero", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360},
"./logo.png": {Token: "boxcn_logo", Name: "logo.png", MimeType: "image/png", Size: 5678, Width: 320, Height: 180},
})
for _, want := range []string{`href="boxcn_hero"`, `href="boxcn_logo"`} {
if !strings.Contains(got, want) {
t.Fatalf("rewritten SVG missing %s: %s", want, got)
}
}
if strings.Contains(got, "xlink:href") {
t.Fatalf("rewritten SVG must not retain xlink:href: %s", got)
}
if !strings.Contains(got, `<a href="@./link.png">`) {
t.Fatalf("non-image href should be untouched: %s", got)
}
if !strings.Contains(got, `data-href="@./ignored.png"`) {
t.Fatalf("non-href image attribute should be untouched: %s", got)
}
wantAssets := []svgAssetMeta{
{Token: "boxcn_hero", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360},
{Token: "boxcn_logo", Name: "logo.png", MimeType: "image/png", Size: 5678, Width: 320, Height: 180},
}
if !reflect.DeepEqual(assets, wantAssets) {
t.Fatalf("assets = %v, want %v", assets, wantAssets)
}
}
func TestInjectSVGTransportAssetMetadata(t *testing.T) {
t.Parallel()
in := `<?xml version="1.0"?><!DOCTYPE svg><!-- lead --><svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect/></svg>`
got, err := injectSVGTransportAssetMetadata(in, []svgAssetMeta{
{Token: "boxcn_a", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360},
{Token: "boxcn_b", Name: "logo.jpg", MimeType: "image/jpeg", Size: 5678, Width: 320, Height: 180},
{Token: "boxcn_a", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
rootIdx := strings.Index(got, "<svg")
metaIdx := strings.Index(got, `<metadata data-svglide-assets="svglide-assets/v1">`)
if rootIdx < 0 || metaIdx < rootIdx {
t.Fatalf("metadata should be injected inside root <svg>, got: %s", got)
}
if strings.Count(got, `src="boxcn_a"`) != 1 {
t.Fatalf("boxcn_a should be deduped, got: %s", got)
}
if !strings.Contains(got, `src="boxcn_b"`) {
t.Fatalf("boxcn_b missing, got: %s", got)
}
for _, want := range []string{
`<img xmlns="" src="boxcn_a" name="hero.png" mimeType="image/png" size="1234" width="640" height="360" />`,
`<img xmlns="" src="boxcn_b" name="logo.jpg" mimeType="image/jpeg" size="5678" width="320" height="180" />`,
} {
if !strings.Contains(got, want) {
t.Fatalf("metadata missing %s, got: %s", want, got)
}
}
}
func TestInjectSVGTransportAssetMetadataMergesExisting(t *testing.T) {
t.Parallel()
in := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><metadata data-svglide-assets="svglide-assets/v1"><img xmlns="" src="boxcn_a" name="hero.png" mimeType="image/png" size="1234" width="640" height="360" /></metadata><image href="boxcn_a"/></svg>`
got, err := injectSVGTransportAssetMetadata(in, []svgAssetMeta{
{Token: "boxcn_a", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360},
{Token: "boxcn_b", Name: "logo.png", MimeType: "image/png", Size: 5678, Width: 320, Height: 180},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if strings.Count(got, `<metadata data-svglide-assets="svglide-assets/v1">`) != 1 {
t.Fatalf("should keep a single transport metadata block, got: %s", got)
}
if strings.Count(got, `src="boxcn_a"`) != 1 {
t.Fatalf("boxcn_a should remain deduped, got: %s", got)
}
if !strings.Contains(got, `src="boxcn_b" name="logo.png" mimeType="image/png" size="5678" width="320" height="180"`) {
t.Fatalf("boxcn_b should be appended, got: %s", got)
}
}
func TestInjectSVGTransportAssetMetadataUpgradesLegacyBlock(t *testing.T) {
t.Parallel()
in := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><metadata data-svglide-assets="true"><img src="boxcn_a" /></metadata><image href="boxcn_a"/></svg>`
got, err := injectSVGTransportAssetMetadata(in, []svgAssetMeta{
{Token: "boxcn_a", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if strings.Contains(got, `data-svglide-assets="true"`) {
t.Fatalf("legacy asset metadata marker should be upgraded, got: %s", got)
}
for _, want := range []string{
`<metadata data-svglide-assets="svglide-assets/v1">`,
`<img xmlns="" src="boxcn_a" name="hero.png" mimeType="image/png" size="1234" width="640" height="360" />`,
} {
if !strings.Contains(got, want) {
t.Fatalf("upgraded metadata missing %s, got: %s", want, got)
}
}
}
func TestParseSVGAssetsSupportsStringAndObjectValues(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("assets.json", []byte(`{
"@./token-only.png": "boxcn_token_only",
"@./hero.png": {
"token": "boxcn_hero",
"name": "hero.png",
"mimeType": "image/png",
"size": 1234,
"width": 640,
"height": 360
}
}`), 0o644); err != nil {
t.Fatalf("write assets.json: %v", err)
}
assets, err := parseSVGAssets(testSlidesRuntime(t), "assets.json")
if err != nil {
t.Fatalf("parse assets: %v", err)
}
if got := assets["@./token-only.png"]; got != (svgAssetMeta{Token: "boxcn_token_only"}) {
t.Fatalf("token-only asset = %#v", got)
}
want := svgAssetMeta{Token: "boxcn_hero", Name: "hero.png", MimeType: "image/png", Size: 1234, Width: 640, Height: 360}
if got := assets["@./hero.png"]; got != want {
t.Fatalf("object asset = %#v, want %#v", got, want)
}
}
func TestParseSVGAssetsRejectsObjectWithoutToken(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("assets.json", []byte(`{"@./hero.png":{"name":"hero.png","mimeType":"image/png","size":1234,"width":640,"height":360}}`), 0o644); err != nil {
t.Fatalf("write assets.json: %v", err)
}
_, err := parseSVGAssets(testSlidesRuntime(t), "assets.json")
if err == nil {
t.Fatal("expected missing token to fail")
}
if !strings.Contains(err.Error(), "must include token") {
t.Fatalf("err = %v, want token guidance", err)
}
}
func testSlidesRuntime(t *testing.T) *common.RuntimeContext {
t.Helper()
cfg := slidesTestConfig(t, "")
f, _, _, _ := cmdutil.TestFactory(t, cfg)
return common.TestNewRuntimeContextForAPI(context.Background(), &cobra.Command{Use: "slides"}, cfg, f, core.AsUser)
}
func TestEnsureSVGlideRootContractVersionInjectsMissingVersion(t *testing.T) {
t.Parallel()
in := `<?xml version="1.0"?><!DOCTYPE svg><!-- lead --><svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></svg>`
got, err := ensureSVGlideRootContractVersion(in, "page.svg")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(got, `slide:contract-version="svglide-authoring-contract/v1"`) {
t.Fatalf("contract version missing after normalization: %s", got)
}
if strings.Index(got, `slide:contract-version`) > strings.Index(got, `><rect`) {
t.Fatalf("contract version should be injected on the root open tag: %s", got)
}
if err := validateSVGlideSVG(got, "page.svg"); err != nil {
t.Fatalf("normalized SVG should pass validation: %v", err)
}
}
func TestEnsureSVGlideRootContractVersionRejectsWrongVersion(t *testing.T) {
t.Parallel()
in := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="old"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></svg>`
_, err := ensureSVGlideRootContractVersion(in, "page.svg")
if err == nil {
t.Fatal("expected wrong contract-version to fail")
}
if !strings.Contains(err.Error(), `slide:contract-version="svglide-authoring-contract/v1"`) {
t.Fatalf("error = %v, want contract-version guidance", err)
}
}
func TestNormalizeSVGFontFamily(t *testing.T) {
t.Parallel()
got, err := normalizeSVGFontFamily(" Noto Serif SC, Arial ")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "Noto Serif SC, Arial" {
t.Fatalf("font family = %q, want normalized list", got)
}
for _, raw := range []string{
"slide-font-0123456789abcdef0123456789abcdef",
"Noto Sans; color:red",
"Noto Sans,",
} {
if _, err := normalizeSVGFontFamily(raw); err == nil {
t.Fatalf("normalizeSVGFontFamily(%q) should fail", raw)
}
}
}
func TestApplySVGlideFontFamilyOnlyRewritesTextForeignObjects(t *testing.T) {
t.Parallel()
in := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` +
`<rect slide:role="shape" x="0" y="0" width="100" height="50" style="font-family:Inter;fill:#fff;"/>` +
`<foreignObject slide:role="shape" slide:shape-type="text" x="0" y="0" width="200" height="80" style="color:#111;font-family:Inter;">` +
`<div xmlns="http://www.w3.org/1999/xhtml"><span style="font-family:Arial;color:#333;" font-family="Arial">hello</span></div>` +
`</foreignObject></svg>`
got := applySVGlideFontFamily(in, "Noto Serif SC")
if !strings.Contains(got, `style="font-family:Inter;fill:#fff;"`) {
t.Fatalf("non-text shape font-family should stay untouched: %s", got)
}
for _, want := range []string{
`style="color:#111;font-family:Noto Serif SC;"`,
`style="font-family:Noto Serif SC;color:#333;"`,
`font-family="Noto Serif SC"`,
} {
if !strings.Contains(got, want) {
t.Fatalf("rewritten SVG missing %s: %s", want, got)
}
}
for _, notWant := range []string{`font-family:Arial`, `font-family="Arial"`} {
if strings.Contains(got, notWant) {
t.Fatalf("rewritten SVG should not contain %s: %s", notWant, got)
}
}
}
func TestApplySVGlideFontFamilyEmptyIsNoop(t *testing.T) {
t.Parallel()
in := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` +
`<foreignObject slide:role="shape" slide:shape-type="text" x="0" y="0" width="200" height="80" style="font-family:Inter;">` +
`<div xmlns="http://www.w3.org/1999/xhtml"><span style="font-family:Arial;">hello</span></div>` +
`</foreignObject></svg>`
if got := applySVGlideFontFamily(in, ""); got != in {
t.Fatalf("empty font family should be no-op:\n got %s\nwant %s", got, in)
}
}
func TestValidateSVGlideSVGRecursiveChildren(t *testing.T) {
t.Parallel()
tests := []struct {
name string
svg string
wantErr string
}{
{
name: "supported shape rect",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></svg>`,
},
{
name: "supported text foreignObject",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><foreignObject slide:role="shape" slide:shape-type="text" x="0" y="0" width="200" height="80"><p xmlns="http://www.w3.org/1999/xhtml">hello</p></foreignObject></svg>`,
},
{
name: "supported image href",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><image slide:role="image" href="boxcn_img" x="0" y="0" width="100" height="60"/></svg>`,
},
{
name: "supported image xlink href before rewrite",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><image slide:role="image" xlink:href="@./hero.png" x="0" y="0" width="100" height="60"/></svg>`,
},
{
name: "supported editable line role",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><line slide:role="line" x1="0" y1="0" x2="100" y2="60" stroke="#123456"/></svg>`,
},
{
name: "supported path commands",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><path slide:role="shape" d="M1e-3 0 L80 0 H120 V40 C120 60 100 80 80 80 Q40 80 20 40 Z" fill="#123456"/></svg>`,
},
{
name: "defs and metadata are ignored",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><defs><rect id="r"/></defs><metadata data-svglide-assets="true"><img src="boxcn_img"/></metadata><circle slide:role="shape" cx="50" cy="50" r="20"/></svg>`,
},
{
name: "group container with role-fixed child",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g fill="#112233" transform="translate(10 20)"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></g></svg>`,
},
{
name: "nested svg container with role-fixed child",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><svg viewBox="0 0 100 100"><circle slide:role="shape" cx="50" cy="50" r="20"/></svg></svg>`,
},
{
name: "group container ignores its own role",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g slide:role="shape"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></g></svg>`,
},
{
name: "nested svg container ignores its own role",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><svg slide:role="shape" viewBox="0 0 100 100"><circle slide:role="shape" cx="50" cy="50" r="20"/></svg></svg>`,
},
{
name: "root chart marker with inline payload",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + `</svg>`,
},
{
name: "style and nested defs are ignored",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><style>.primary{fill:#123456}</style><g><defs><linearGradient id="g"><stop offset="0%" stop-color="#fff"/><stop offset="100%" stop-color="#000"/></linearGradient></defs></g><rect slide:role="shape" class="primary" x="0" y="0" width="100" height="60" fill="url(#g)"/></svg>`,
},
{
name: "filter and shadow styles are preserved",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><style>.card{filter:drop-shadow(2px 4px 8px rgba(0,0,0,.2));box-shadow:0 8px 20px rgba(0,0,0,.18)}</style><g><defs><filter id="shadow"><feDropShadow dx="2" dy="3" stdDeviation="5" flood-color="#000" flood-opacity=".25"/></filter></defs></g><rect slide:role="shape" class="card" x="0" y="0" width="100" height="60" filter="url(#shadow)"/></svg>`,
},
{
name: "foreignObject XHTML subtree is not role-validated",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><foreignObject slide:role="shape" slide:shape-type="text" x="0" y="0" width="200" height="80"><div xmlns="http://www.w3.org/1999/xhtml"><span>hello</span></div></foreignObject></svg>`,
},
{
name: "foreignObject XHTML br is allowed",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><foreignObject slide:role="shape" slide:shape-type="text" x="0" y="0" width="200" height="80"><div xmlns="http://www.w3.org/1999/xhtml">hello<br />world</div></foreignObject></svg>`,
},
{
name: "namespaced root is rejected with precise message",
svg: `<svg:svg xmlns:svg="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></svg:svg>`,
wantErr: `root element must be non-namespaced <svg>`,
},
{
name: "root child missing role",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect x="0" y="0" width="100" height="60"/></svg>`,
wantErr: `<rect> must include slide:role="shape", slide:role="image", slide:role="line", or slide:role="text"`,
},
{
name: "group child missing role is rejected",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g><rect x="0" y="0" width="100" height="60"/></g></svg>`,
wantErr: `<rect> must include slide:role="shape", slide:role="image", slide:role="line", or slide:role="text"`,
},
{
name: "unsupported text element remains rejected",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><text slide:role="shape" x="0" y="20">bad</text></svg>`,
wantErr: `<text slide:role="shape"> is not supported by SVGlide`,
},
{
name: "rect shape requires geometry",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="shape" x="0" y="0" height="60"/></svg>`,
wantErr: `<rect slide:role="shape"> missing required attribute "width"`,
},
{
name: "path shape requires d",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><path slide:role="shape" fill="#123456"/></svg>`,
wantErr: `<path slide:role="shape"> missing required attribute "d"`,
},
{
name: "rect rejects percent geometry",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="shape" x="0" y="0" width="50%" height="60"/></svg>`,
wantErr: `attribute "width" must be a number or px length`,
},
{
name: "rect rejects calc geometry",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="shape" x="calc(10px)" y="0" width="100" height="60"/></svg>`,
wantErr: `attribute "x" must be a number or px length`,
},
{
name: "container transform rejects percent argument",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g transform="translate(10% 20)"><rect slide:role="shape" x="0" y="0" width="100" height="60"/></g></svg>`,
wantErr: `transform translate() argument must be a number or px length`,
},
{
name: "path rejects arc command",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><path slide:role="shape" d="M0 0 A10 10 0 0 1 20 20" fill="#123456"/></svg>`,
wantErr: `unsupported path command or character "A"`,
},
{
name: "path rejects smooth command",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><path slide:role="shape" d="M0 0 S10 10 20 20" fill="#123456"/></svg>`,
wantErr: `unsupported path command or character "S"`,
},
{
name: "plain metadata support node is ignored",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><metadata><desc>not transport metadata</desc></metadata></svg>`,
},
{
name: "whiteboard role is explicitly rejected",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g slide:role="whiteboard" x="0" y="0" width="100" height="60"/></svg>`,
wantErr: `slide:role="whiteboard" is not supported`,
},
{
name: "legacy whiteboard marker metadata is explicitly rejected",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><metadata data-svglide-whiteboard="svglide-whiteboard-inline/v1">abc</metadata></svg>`,
wantErr: `legacy SVGlide whiteboard marker metadata is not supported`,
},
{
name: "foreignObject shape requires text type",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><foreignObject slide:role="shape"><p xmlns="http://www.w3.org/1999/xhtml">hello</p></foreignObject></svg>`,
wantErr: `<foreignObject slide:role="shape"> must include slide:shape-type="text"`,
},
{
name: "image role must be image tag",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="image" href="boxcn_img"/></svg>`,
wantErr: `<rect slide:role="image"> is not supported`,
},
{
name: "image requires href",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><image slide:role="image" x="0" y="0" width="100" height="60"/></svg>`,
wantErr: `<image slide:role="image"> must include href`,
},
{
name: "image requires geometry",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><image slide:role="image" href="boxcn_img" x="0" y="0" height="60"/></svg>`,
wantErr: `<image slide:role="image"> missing required attribute "width"`,
},
{
name: "image rejects external href",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><image slide:role="image" href="https://images.unsplash.com/photo.jpg" x="0" y="0" width="100" height="60"/></svg>`,
wantErr: `<image slide:role="image"> must not use external http(s) or data href`,
},
{
name: "unsupported role",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><rect slide:role="decor"/></svg>`,
wantErr: `unsupported slide:role="decor"`,
},
{
name: "nested chart marker is rejected",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g>` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + `</g></svg>`,
wantErr: `<g slide:role="chart"> must be a direct child of root <svg>`,
},
{
name: "chart marker requires ref",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g slide:role="chart" x="0" y="0" width="100" height="60">` + testSVGlideChartMetadata(testSVGlideChartSpecJSON()) + `</g></svg>`,
wantErr: `missing required attribute "slide:chart-ref"`,
},
{
name: "chart marker rejects bad bbox",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide"><g slide:role="chart" slide:chart-ref="chart-1" x="10%" y="0" width="100" height="60">` + testSVGlideChartMetadata(testSVGlideChartSpecJSON()) + `</g></svg>`,
wantErr: `attribute "x" must be a number or px length`,
},
{
name: "chart marker requires single metadata",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())+testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + `</svg>`,
wantErr: `must contain exactly one metadata child`,
},
{
name: "chart marker rejects duplicate chart refs",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideChartSpecJSON())) + testSVGlideChartMarker(testSVGlideChartMetadata(testSVGlideLineChartSpecJSON())) + `</svg>`,
wantErr: `duplicate slide:chart-ref "chart-1"`,
},
{
name: "chart marker rejects bad base64url",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(`<metadata data-svglide-chart="svglide-chart-inline/v1" data-format="svglide-chart-spec-v1" data-encoding="base64url-json" data-payload-hash="sha256:`+strings.Repeat("0", 64)+`">bad+payload</metadata>`) + `</svg>`,
wantErr: `payload must be base64url`,
},
{
name: "chart marker rejects old sxsd chart format",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(`<metadata data-svglide-chart="svglide-chart-inline/v1" data-format="sxsd-chart-v1" data-encoding="base64url" data-payload-hash="sha256:`+strings.Repeat("0", 64)+`">`+base64.RawURLEncoding.EncodeToString([]byte(`<chart />`))+`</metadata>`) + `</svg>`,
wantErr: `data-format="svglide-chart-spec-v1"`,
},
{
name: "chart marker rejects hash mismatch",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadataWithHash(testSVGlideChartSpecJSON(), "sha256:"+strings.Repeat("0", 64))) + `</svg>`,
wantErr: `data-payload-hash does not match`,
},
{
name: "chart marker decoded payload must be json",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(`<chart />`)) + `</svg>`,
wantErr: `decoded payload must be valid svglide-chart-spec-v1 JSON`,
},
{
name: "chart marker rejects unsupported chart type",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(`{"version":"svglide-chart-spec/v1","chartType":"pie","data":{"categories":["Q1"],"series":[{"name":"Revenue","values":[12]}]}}`)) + `</svg>`,
wantErr: `chartType must be one of bar,line`,
},
{
name: "chart marker rejects values length mismatch",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(`{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12]}]}}`)) + `</svg>`,
wantErr: `values length must match data.categories length`,
},
{
name: "chart marker rejects nonnumeric values",
svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide">` + testSVGlideChartMarker(testSVGlideChartMetadata(`{"version":"svglide-chart-spec/v1","chartType":"line","data":{"categories":["Q1"],"series":[{"name":"Revenue","values":["12"]}]}}`)) + `</svg>`,
wantErr: `must be a finite number`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := validateSVGlideSVG(withTestSVGlideContractVersion(tt.svg), "page.svg")
if tt.wantErr == "" {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
return
}
if err == nil {
t.Fatalf("expected error containing %q", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error = %q, want to contain %q", err.Error(), tt.wantErr)
}
})
}
}
func testSVGlideChartMarker(metadata string) string {
return `<g slide:role="chart" slide:chart-ref="chart-1" x="80" y="96" width="420" height="260">` + metadata + `</g>`
}
func testSVGlideChartSpecJSON() string {
return `{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12.5,18]}]}}`
}
func testSVGlideLineChartSpecJSON() string {
return `{"version":"svglide-chart-spec/v1","chartType":"line","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12.5,18]}]}}`
}
func testSVGlideChartMetadata(chartJSON string) string {
sum := sha256.Sum256([]byte(chartJSON))
return testSVGlideChartMetadataWithHash(chartJSON, fmt.Sprintf("sha256:%x", sum))
}
func testSVGlideChartMetadataWithHash(chartJSON, hash string) string {
payload := base64.RawURLEncoding.EncodeToString([]byte(chartJSON))
return fmt.Sprintf(
`<metadata data-svglide-chart="svglide-chart-inline/v1" data-format="svglide-chart-spec-v1" data-encoding="base64url-json" data-payload-hash="%s">%s</metadata>`,
hash,
payload,
)
}
func withTestSVGlideContractVersion(svg string) string {
if strings.Contains(svg, `slide:contract-version=`) {
return svg
}
return strings.Replace(svg, `slide:role="slide"`, `slide:role="slide" slide:contract-version="svglide-authoring-contract/v1"`, 1)
}
func TestExtractSVGlideErrorJSON(t *testing.T) {
t.Parallel()
err := errors.New(`api error: SVGLIDE_ERROR_JSON:{"type":"svg_validation_error","page_index":0,"tag_name":"foreignObject","hint":"Use supported elements"}`)
got := extractSVGlideErrorJSON(err)
if got["type"] != "svg_validation_error" {
t.Fatalf("type = %v", got["type"])
}
if got["tag_name"] != "foreignObject" {
t.Fatalf("tag_name = %v", got["tag_name"])
}
suffix := formatSVGlideErrorSuffix(err)
for _, want := range []string{"svglide_error=", "svg_validation_error", "foreignObject"} {
if !strings.Contains(suffix, want) {
t.Fatalf("suffix = %q, want %q", suffix, want)
}
}
}

View File

@@ -0,0 +1,366 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package slides
import (
"bytes"
"context"
"encoding/json"
"fmt"
"image/png"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/larksuite/cli/extension/fileio"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
)
const (
svgRasterizerSkillPath = "lark-slides/scripts/svg_rasterize_effects.py"
svgRasterizerSourcePath = "skills/" + svgRasterizerSkillPath
svgRasterizedOutputRoot = ".lark-slides/rasterized"
maxSVGRasterPNGBytes int64 = 20 * 1024 * 1024
)
var svgRasterizerEmbeddedSkillPaths = []string{
"lark-slides/scripts/svg_rasterize_effects.py",
"lark-slides/scripts/svg_effect_classifier.py",
"lark-slides/scripts/svg_safe_rewrite.py",
"lark-slides/scripts/svg_raster_renderer.py",
}
type svgRasterRuntime struct {
PythonPath string
}
type svgRasterizerInvocation struct {
PythonPath string
ScriptPath string
Args []string
}
var (
svgRasterizeResolveRuntime = resolveSVGRasterRuntime
svgRasterizeRunScript = runSVGRasterizerScript
)
func rasterizeRichSVGEffects(
runtime *common.RuntimeContext,
svgs []string,
paths []string,
opts svgPrepareOptions,
) ([]string, *svgPrepareReport, error) {
if len(svgs) != len(paths) {
return nil, nil, output.ErrValidation("internal svg rasterization error: SVG count %d does not match path count %d", len(svgs), len(paths))
}
if opts.RasterizeScale == 0 {
opts.RasterizeScale = 2
}
scriptPath, err := resolveSVGRasterizerScript(runtime)
if err != nil {
return nil, nil, err
}
rasterRuntime, err := svgRasterizeResolveRuntime(contextFromRuntime(runtime))
if err != nil {
return nil, nil, err
}
baseDir, err := runtime.ResolveSavePath(".")
if err != nil {
return nil, nil, output.ErrValidation("resolve current working directory for SVG rasterization: %v", err)
}
runID := newSVGRasterRunID()
runDir := filepath.ToSlash(filepath.Join(svgRasterizedOutputRoot, runID))
if err := ensureSVGRasterOutputDir(runtime, runDir); err != nil {
return nil, nil, err
}
report := &svgPrepareReport{
Version: "1",
Mode: string(opts.RasterizeMode),
RunID: runID,
BaseDir: baseDir,
Quality: svgPrepareQuality{
GatePassed: true,
},
Pages: make([]svgPreparePageReport, 0, len(svgs)),
}
prepared := make([]string, 0, len(svgs))
for i, svg := range svgs {
pageNo := i + 1
inputPath := filepath.ToSlash(filepath.Join(runDir, fmt.Sprintf("page-%03d.rich.svg", pageNo)))
outputPath := filepath.ToSlash(filepath.Join(runDir, fmt.Sprintf("page-%03d.safe.svg", pageNo)))
pageReportPath := filepath.ToSlash(filepath.Join(runDir, fmt.Sprintf("page-%03d-raster-report.json", pageNo)))
if _, err := runtime.FileIO().Save(inputPath, fileio.SaveOptions{ContentType: "image/svg+xml", ContentLength: int64(len(svg))}, strings.NewReader(svg)); err != nil {
return nil, report, common.WrapSaveErrorTyped(err)
}
invocation := svgRasterizerInvocation{
PythonPath: rasterRuntime.PythonPath,
ScriptPath: scriptPath,
Args: []string{
"--mode", string(opts.RasterizeMode),
"--scale", strconv.Itoa(opts.RasterizeScale),
"--input", inputPath,
"--output", outputPath,
"--asset-dir", runDir,
"--base-dir", baseDir,
"--report", pageReportPath,
},
}
start := time.Now()
if err := svgRasterizeRunScript(contextFromRuntime(runtime), invocation); err != nil {
return nil, report, err
}
renderMS := time.Since(start).Milliseconds()
data, err := cmdutil.ReadInputFile(runtime.FileIO(), outputPath)
if err != nil {
return nil, report, common.WrapInputStatError(err, fmt.Sprintf("raster safe SVG %s", outputPath))
}
safeSVG := string(data)
pageReport := readSVGRasterPageReport(runtime, pageReportPath)
if pageReport.SourcePath == "" {
pageReport.SourcePath = paths[i]
}
pageReport.SafePath = outputPath
if pageReport.Mode == "" {
pageReport.Mode = string(opts.RasterizeMode)
}
if opts.RasterizeMode == svgRasterizeForcePage && pageReport.FallbackReason == "" {
pageReport.FallbackReason = "force-page"
}
pngs := extractSVGImagePlaceholderPaths([]string{safeSVG}, nil)
if len(pngs) == 0 {
pngs = pageReport.PNGs
}
pngs = dedupeStrings(pngs)
pageReport.PNGs = pngs
if len(pageReport.Islands) == 0 {
pageReport.Islands = islandsFromRasterPNGs(pngs, opts.RasterizeScale, renderMS)
}
if err := validateSVGRasterPNGs(runtime, pngs); err != nil {
return nil, report, err
}
for _, pngPath := range pngs {
report.GeneratedAssets = append(report.GeneratedAssets, pngPath)
}
report.RasterImageCount += len(pngs)
report.RasterTotalMS += renderMS
if pageReport.FallbackReason != "" {
report.FullPageFallbackCount++
}
for _, island := range pageReport.Islands {
report.RasterTotalBytes += island.Bytes
}
report.Pages = append(report.Pages, pageReport)
prepared = append(prepared, safeSVG)
}
report.GeneratedAssets = dedupeStrings(report.GeneratedAssets)
if err := writeSVGRasterDeckReport(runtime, report, runDir, opts.ReportPath); err != nil {
return nil, report, err
}
return prepared, report, nil
}
func resolveSVGRasterizerScript(runtime *common.RuntimeContext) (string, error) {
if _, err := runtime.FileIO().Stat(svgRasterizerSourcePath); err == nil {
return svgRasterizerSourcePath, nil
}
if runtime.Factory == nil || runtime.Factory.SkillContent == nil {
return "", output.ErrValidation("svg rasterization requires bundled lark-slides raster scripts; rebuild CLI with scripts embedded")
}
dir, err := os.MkdirTemp("", "lark-cli-svg-rasterizer-*") //nolint:forbidigo // extracting embedded runtime script to process-local temp dir for execution.
if err != nil {
return "", output.ErrValidation("extract SVG rasterizer script: %v", err)
}
for _, skillPath := range svgRasterizerEmbeddedSkillPaths {
data, err := fs.ReadFile(runtime.Factory.SkillContent, skillPath)
if err != nil {
return "", output.ErrValidation("svg rasterization requires bundled lark-slides raster script %s; rebuild CLI with scripts embedded", skillPath)
}
target := filepath.Join(dir, filepath.Base(skillPath))
if err := os.WriteFile(target, data, 0o600); err != nil { //nolint:forbidigo // writes embedded scripts into the temp dir created above.
return "", output.ErrValidation("extract SVG rasterizer script %s: %v", skillPath, err)
}
}
return filepath.Join(dir, "svg_rasterize_effects.py"), nil
}
func resolveSVGRasterRuntime(ctx context.Context) (svgRasterRuntime, error) {
pythonPath, err := exec.LookPath("python3")
if err != nil {
return svgRasterRuntime{}, output.ErrValidation("svg rasterization requires python3 on PATH")
}
cmd := exec.CommandContext(ctx, pythonPath, "-c", "import playwright") //nolint:gosec // fixed interpreter probe, no user-controlled code.
if out, err := cmd.CombinedOutput(); err != nil {
return svgRasterRuntime{}, output.ErrValidation("svg rasterization requires Python package 'playwright' and installed Chromium; run `python3 -m pip install playwright && python3 -m playwright install chromium` (%s)", strings.TrimSpace(string(out)))
}
return svgRasterRuntime{PythonPath: pythonPath}, nil
}
func runSVGRasterizerScript(ctx context.Context, invocation svgRasterizerInvocation) error {
args := append([]string{invocation.ScriptPath}, invocation.Args...)
cmd := exec.CommandContext(ctx, invocation.PythonPath, args...) //nolint:gosec // script path is resolved from source or embedded skill content; args are fixed CLI flags.
out, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(out))
if msg == "" {
msg = err.Error()
}
return output.ErrValidation("svg rasterization failed: %s", msg)
}
return nil
}
func ensureSVGRasterOutputDir(runtime *common.RuntimeContext, runDir string) error {
keep := filepath.ToSlash(filepath.Join(runDir, ".keep"))
if _, err := runtime.FileIO().Save(keep, fileio.SaveOptions{ContentType: "text/plain", ContentLength: 0}, strings.NewReader("")); err != nil {
return common.WrapSaveErrorTyped(err)
}
return nil
}
func readSVGRasterPageReport(runtime *common.RuntimeContext, path string) svgPreparePageReport {
data, err := cmdutil.ReadInputFile(runtime.FileIO(), path)
if err != nil || len(bytes.TrimSpace(data)) == 0 {
return svgPreparePageReport{}
}
var page svgPreparePageReport
if json.Unmarshal(data, &page) == nil && (page.SafePath != "" || len(page.PNGs) > 0 || len(page.Islands) > 0) {
return page
}
var deck svgPrepareReport
if json.Unmarshal(data, &deck) == nil && len(deck.Pages) > 0 {
return deck.Pages[0]
}
return svgPreparePageReport{}
}
func islandsFromRasterPNGs(pngs []string, scale int, renderMS int64) []svgPrepareIslandReport {
islands := make([]svgPrepareIslandReport, 0, len(pngs))
for i, pngPath := range pngs {
islands = append(islands, svgPrepareIslandReport{
ID: fmt.Sprintf("page-island-%03d", i+1),
Reason: "script-generated",
OutputPNG: pngPath,
Scale: scale,
RenderMS: renderMS,
})
}
return islands
}
func validateSVGRasterPNGs(runtime *common.RuntimeContext, paths []string) error {
for _, path := range paths {
if err := validateSVGRasterPNGPath(path); err != nil {
return err
}
stat, err := runtime.FileIO().Stat(path)
if err != nil {
return common.WrapInputStatError(err, fmt.Sprintf("raster PNG %s", path))
}
if stat.Size() <= 0 {
return output.ErrValidation("raster PNG %s is empty", path)
}
if stat.Size() > maxSVGRasterPNGBytes {
return output.ErrValidation("raster PNG %s size %s exceeds %s limit", path, common.FormatSize(stat.Size()), common.FormatSize(maxSVGRasterPNGBytes))
}
if err := validateSVGRasterPNGContent(runtime, path); err != nil {
return err
}
}
return nil
}
func validateSVGRasterPNGPath(path string) error {
clean := filepath.ToSlash(filepath.Clean(path))
if strings.HasPrefix(path, "/private/tmp/") {
return nil
}
if filepath.IsAbs(path) {
return output.ErrValidation("raster PNG %s must use a cwd-relative @./ path for upload", path)
}
if !strings.HasPrefix(clean, ".lark-slides/rasterized/") {
return output.ErrValidation("raster PNG %s must be generated under .lark-slides/rasterized", path)
}
if strings.Contains(clean, "../") || clean == ".." {
return output.ErrValidation("raster PNG %s cannot escape the raster output directory", path)
}
return nil
}
func validateSVGRasterPNGContent(runtime *common.RuntimeContext, path string) error {
f, err := runtime.FileIO().Open(path)
if err != nil {
return common.WrapInputStatError(err, fmt.Sprintf("raster PNG %s", path))
}
defer f.Close()
img, err := png.Decode(f)
if err != nil {
if err == io.ErrUnexpectedEOF {
return output.ErrValidation("raster PNG %s is truncated", path)
}
return output.ErrValidation("raster PNG %s is not a valid PNG: %v", path, err)
}
bounds := img.Bounds()
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
return output.ErrValidation("raster PNG %s has invalid dimensions %dx%d", path, bounds.Dx(), bounds.Dy())
}
allTransparent := true
for y := bounds.Min.Y; y < bounds.Max.Y && allTransparent; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
_, _, _, a := img.At(x, y).RGBA()
if a != 0 {
allTransparent = false
break
}
}
}
if allTransparent {
return output.ErrValidation("raster PNG %s is fully transparent", path)
}
return nil
}
func writeSVGRasterDeckReport(runtime *common.RuntimeContext, report *svgPrepareReport, runDir, requestedPath string) error {
data, err := json.MarshalIndent(report, "", " ")
if err != nil {
return output.ErrValidation("marshal SVG raster report: %v", err)
}
defaultPath := filepath.ToSlash(filepath.Join(runDir, "raster-report.json"))
if _, err := runtime.FileIO().Save(defaultPath, fileio.SaveOptions{ContentType: "application/json", ContentLength: int64(len(data))}, bytes.NewReader(data)); err != nil {
return common.WrapSaveErrorTyped(err)
}
if strings.TrimSpace(requestedPath) == "" || filepath.Clean(requestedPath) == filepath.Clean(defaultPath) {
return nil
}
if _, err := runtime.FileIO().Save(requestedPath, fileio.SaveOptions{ContentType: "application/json", ContentLength: int64(len(data))}, bytes.NewReader(data)); err != nil {
return common.WrapSaveErrorTyped(err)
}
return nil
}
func newSVGRasterRunID() string {
id := strings.ReplaceAll(uuid.NewString(), "-", "")
return time.Now().UTC().Format("20060102-150405") + "-" + id[:8]
}
func contextFromRuntime(runtime *common.RuntimeContext) context.Context {
if runtime == nil || runtime.Ctx() == nil {
return context.Background()
}
return runtime.Ctx()
}

View File

@@ -0,0 +1,288 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package slides
import (
"bytes"
"context"
"encoding/json"
"image"
"image/color"
"image/png"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
"testing/fstest"
"github.com/spf13/cobra"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/shortcuts/common"
)
func TestSlidesCreateSVGFlagsExposeRasterOptions(t *testing.T) {
byName := map[string]common.Flag{}
for _, fl := range SlidesCreateSVG.Flags {
byName[fl.Name] = fl
}
if got := byName["svg-rasterize-effects"]; got.Default != "off" || strings.Join(got.Enum, ",") != "off,auto,strict,force-page" {
t.Fatalf("svg-rasterize-effects flag = %+v", got)
}
if got := byName["svg-rasterize-scale"]; got.Type != "int" || got.Default != "2" {
t.Fatalf("svg-rasterize-scale flag = %+v", got)
}
if _, ok := byName["svg-rasterize-report"]; !ok {
t.Fatal("missing svg-rasterize-report flag")
}
}
func TestPrepareSVGFilesForCreateOffKeepsNativeReadPath(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("page.svg", []byte(testSVGlidePage1), 0o644); err != nil {
t.Fatalf("write page.svg: %v", err)
}
runtime := newSVGRasterTestRuntime(t, nil)
got, report, err := prepareSVGFilesForCreate(runtime, []string{"page.svg"}, svgPrepareOptions{RasterizeMode: svgRasterizeOff})
if err != nil {
t.Fatalf("prepare off: %v", err)
}
if report != nil {
t.Fatalf("report = %+v, want nil in off mode", report)
}
if len(got) != 1 || !strings.Contains(got[0], `slide:contract-version="svglide-authoring-contract/v1"`) {
t.Fatalf("prepared SVG = %#v", got)
}
}
func TestPrepareSVGFilesForCreateForcePageRunsScriptAndGatesSafeSVG(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("page.svg", []byte(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" viewBox="0 0 960 540"><defs><filter id="glow"/></defs><rect filter="url(#glow)" x="0" y="0" width="100" height="60"/></svg>`), 0o644); err != nil {
t.Fatalf("write page.svg: %v", err)
}
restore := stubSVGRasterizer(t)
defer restore()
runtime := newSVGRasterTestRuntime(t, embeddedSVGRasterizerTestFS())
got, report, err := prepareSVGFilesForCreate(runtime, []string{"page.svg"}, svgPrepareOptions{
RasterizeMode: svgRasterizeForcePage,
RasterizeScale: 2,
})
if err != nil {
t.Fatalf("prepare force-page: %v", err)
}
if len(got) != 1 || strings.Contains(got[0], "<filter") || !strings.Contains(got[0], `href="@./.lark-slides/rasterized/`) {
t.Fatalf("safe SVG did not pass through rasterizer: %s", got[0])
}
if report == nil || report.Mode != "force-page" || len(report.Pages) != 1 || !report.Pages[0].RuntimeGateOK {
t.Fatalf("report = %+v", report)
}
if len(report.GeneratedAssets) != 1 {
t.Fatalf("GeneratedAssets = %v, want one PNG", report.GeneratedAssets)
}
if gotPaths := extractSVGImagePlaceholderPaths(got, nil); len(gotPaths) != 1 || gotPaths[0] != report.GeneratedAssets[0] {
t.Fatalf("placeholder paths = %v, generated = %v", gotPaths, report.GeneratedAssets)
}
}
func TestSlidesCreateSVGForcePageDryRunIncludesRasterReportAndV1Metadata(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
if err := os.WriteFile("page.svg", []byte(testSVGlidePage1), 0o644); err != nil {
t.Fatalf("write page.svg: %v", err)
}
restore := stubSVGRasterizer(t)
defer restore()
f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
f.SkillContent = embeddedSVGRasterizerTestFS()
err := runSlidesCreateSVGShortcut(t, f, stdout, []string{
"+create-svg",
"--file", "page.svg",
"--title", "raster dry",
"--svg-rasterize-effects", "force-page",
"--as", "user",
"--dry-run",
})
if err != nil {
t.Fatalf("dry-run force-page: %v", err)
}
out := stdout.String()
for _, want := range []string{
"svg_rasterize_report",
".lark-slides/rasterized/",
"uploaded_file_token:",
} {
if !strings.Contains(out, want) {
t.Fatalf("dry-run output missing %q: %s", want, out)
}
}
content := dryRunSlideContent(t, stdout)
for _, want := range []string{
`<metadata data-svglide-assets="svglide-assets/v1">`,
`name="page-001-island-001.png"`,
} {
if !strings.Contains(content, want) {
t.Fatalf("dry-run slide content missing %q: %s", want, content)
}
}
}
func TestValidateSVGRasterizeFlagsRejectsLowScale(t *testing.T) {
runtime := newSVGRasterTestRuntime(t, nil)
runtime.Cmd.Flags().Set("svg-rasterize-effects", "force-page")
runtime.Cmd.Flags().Set("svg-rasterize-scale", "1")
err := validateSVGRasterizeFlags(runtime)
if err == nil || !strings.Contains(err.Error(), "svg-rasterize-scale") {
t.Fatalf("err = %v, want scale validation", err)
}
}
func TestValidateSafeSVGNoResidualRichEffectsRejectsHardTags(t *testing.T) {
err := validateSafeSVGNoResidualRichEffects(`<svg><defs><filter id="f"/></defs><rect filter="url(#f)"/></svg>`, "safe.svg")
if err == nil || !strings.Contains(err.Error(), "safe SVG") {
t.Fatalf("err = %v, want safe SVG hard-tag rejection", err)
}
}
func TestResolveSVGRasterizerScriptUsesSourceThenEmbedded(t *testing.T) {
dir := t.TempDir()
withSlidesTestWorkingDir(t, dir)
sourcePath := filepath.Join("skills", "lark-slides", "scripts")
if err := os.MkdirAll(sourcePath, 0o755); err != nil {
t.Fatalf("mkdir source: %v", err)
}
if err := os.WriteFile(filepath.Join(sourcePath, "svg_rasterize_effects.py"), []byte("# source"), 0o644); err != nil {
t.Fatalf("write source: %v", err)
}
runtime := newSVGRasterTestRuntime(t, nil)
got, err := resolveSVGRasterizerScript(runtime)
if err != nil {
t.Fatalf("resolve source: %v", err)
}
if got != svgRasterizerSourcePath {
t.Fatalf("script path = %s, want source path", got)
}
dir = t.TempDir()
withSlidesTestWorkingDir(t, dir)
runtime = newSVGRasterTestRuntime(t, embeddedSVGRasterizerTestFS())
got, err = resolveSVGRasterizerScript(runtime)
if err != nil {
t.Fatalf("resolve embedded: %v", err)
}
data, err := os.ReadFile(got)
if err != nil {
t.Fatalf("read extracted script: %v", err)
}
if string(data) != "# embedded" {
t.Fatalf("extracted script = %q", data)
}
}
func TestValidateSVGRasterAssetConflicts(t *testing.T) {
report := &svgPrepareReport{GeneratedAssets: []string{".lark-slides/rasterized/run/page.png"}}
err := validateSVGRasterAssetConflicts(svgAssetMap{"@.lark-slides/rasterized/run/page.png": {Token: "boxcn_existing"}}, report)
if err == nil || !strings.Contains(err.Error(), "--assets conflicts") {
t.Fatalf("err = %v, want conflict", err)
}
}
func newSVGRasterTestRuntime(t *testing.T, skills fs.FS) *common.RuntimeContext {
t.Helper()
f, _, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, ""))
f.SkillContent = skills
cmd := &cobra.Command{Use: "test"}
for _, fl := range SlidesCreateSVG.Flags {
switch fl.Type {
case "int":
cmd.Flags().Int(fl.Name, 0, "")
if fl.Default != "" {
cmd.Flags().Set(fl.Name, fl.Default)
}
case "string_array":
cmd.Flags().StringArray(fl.Name, nil, "")
default:
cmd.Flags().String(fl.Name, fl.Default, "")
}
}
return &common.RuntimeContext{
Config: slidesTestConfig(t, ""),
Cmd: cmd,
Factory: f,
}
}
func embeddedSVGRasterizerTestFS() fstest.MapFS {
return fstest.MapFS{
"lark-slides/scripts/svg_rasterize_effects.py": {Data: []byte("# embedded")},
"lark-slides/scripts/svg_effect_classifier.py": {Data: []byte("# classifier")},
"lark-slides/scripts/svg_safe_rewrite.py": {Data: []byte("# rewrite")},
"lark-slides/scripts/svg_raster_renderer.py": {Data: []byte("# renderer")},
}
}
func stubSVGRasterizer(t *testing.T) func() {
t.Helper()
origResolve := svgRasterizeResolveRuntime
origRun := svgRasterizeRunScript
svgRasterizeResolveRuntime = func(context.Context) (svgRasterRuntime, error) {
return svgRasterRuntime{PythonPath: "python3"}, nil
}
svgRasterizeRunScript = func(_ context.Context, invocation svgRasterizerInvocation) error {
args := map[string]string{}
for i := 0; i+1 < len(invocation.Args); i += 2 {
args[invocation.Args[i]] = invocation.Args[i+1]
}
out := args["--output"]
assetDir := args["--asset-dir"]
reportPath := args["--report"]
pngPath := "./" + filepath.ToSlash(filepath.Join(assetDir, "page-001-island-001.png"))
if err := writeTestRasterPNG(pngPath); err != nil {
return err
}
safe := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" viewBox="0 0 960 540"><image slide:role="image" href="@` + pngPath + `" x="0" y="0" width="960" height="540"/></svg>`
if err := os.WriteFile(out, []byte(safe), 0o644); err != nil {
return err
}
report := svgPreparePageReport{
Mode: "force-page",
FallbackReason: "force-page",
PNGs: []string{pngPath},
Islands: []svgPrepareIslandReport{{
ID: "page-001-island-001",
Reason: "force-page",
OutputPNG: pngPath,
Scale: 2,
Bytes: 1,
RenderMS: 1,
}},
}
data, err := json.Marshal(report)
if err != nil {
return err
}
return os.WriteFile(reportPath, data, 0o644)
}
return func() {
svgRasterizeResolveRuntime = origResolve
svgRasterizeRunScript = origRun
}
}
func writeTestRasterPNG(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
img := image.NewRGBA(image.Rect(0, 0, 4, 4))
img.Set(0, 0, color.RGBA{R: 255, A: 255})
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return err
}
return os.WriteFile(path, buf.Bytes(), 0o644)
}

View File

@@ -1,7 +1,7 @@
---
name: lark-slides
version: 1.0.0
description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill不要因为域名不是飞书而回退到 WebFetch路由依据是 URL 路径模式和 token而不是域名。不负责云文档内容编辑走 lark-doc、云文档里的独立画板对象走 lark-whiteboard注意 slide 内嵌的流程图/架构图仍属本 skill、上传或下载普通文件走 lark-drive。"
version: 1.0.2
description: "飞书幻灯片:创建和编辑幻灯片,接口通过 XML/SVG 协议通信。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill不要因为域名不是飞书而回退到 WebFetch路由依据是 URL 路径模式和 token而不是域名。不负责云文档内容编辑走 lark-doc、云文档里的独立画板对象走 lark-whiteboard注意 slide 内嵌的流程图/架构图仍属本 skill、上传或下载普通文件走 lark-drive。"
metadata:
requires:
bins: ["lark-cli"]
@@ -15,27 +15,40 @@ metadata:
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|----------|----------|-----------------|
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md``visual-planning.md``asset-planning.md``slides +create` |
| AI 生成 SVG 创建 PPT | 先做 SVG route admission命中后按 SVG 私有规则加载专属文档,优先用 runner 走到 quality gate再调用 `slides +create-svg` | `svglide-route-admission.md``svglide-svg-private.rules.json``svg-private-manifest.json``svglide_project_runner.py``slides +create-svg` |
| 大幅改写页面 | 先回读现有 XML写入新 plan再替换或重建相关页面 | `xml_presentations.get``+replace-slide``lark-slides-edit-workflows.md` |
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide``lark-slides-replace-slide.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token回读全文或单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `xml_presentations.get``xml_presentation.slide.get` |
| 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`,或 `+create --slides``@./path` 占位符 |
| 在 slide 中绘制柱/条/折线/面积/雷达/饼等有数据序列的图表 | 使用原生 `<chart>` 元素 | `xml-schema-quick-ref.md` |
| 在 slide 中绘制流程图、时序图、架构图、散点图、漏斗图或装饰图案 | 必须先用 Read 工具读取参考文档,再生成 `<whiteboard>` 元素 | [`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md) |
| 上传或使用图片 | XML 路径先做资产元数据规划;创建时用 `slides +media-upload` 或 XML 图片占位符上传/替换 | `asset-planning.md``slides +media-upload``lark-slides-media-upload.md` |
| 在 slide 中绘制柱/条/折线等 MVP 支持的数据图表 | XML 路径使用原生 `<chart>`SVG 路径必须先通过 route admission | `xml-schema-quick-ref.md``svglide-route-admission.md` |
| 在 slide 中绘制流程图、时序图、架构图、散点图、漏斗图或装饰图案 | XML 路径必须先用 Read 工具读取参考文档,再生成 `<whiteboard>` 元素SVG 路径必须先通过 route admission | [`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md)`svglide-route-admission.md` |
| 使用语义图标 | 先检索 IconPark再写 `<icon iconType="...">` | `iconpark_tool.py search → resolve``iconpark.md` |
| 用户提到模板、主题、版式 | 先检索模板,再摘要,必要时裁切骨架 | `template_tool.py search → summarize → extract` |
| SVG route 需要主题/模板家族 | 先用 beautiful template family matcher 选择 deck-level family、variants、components、asset strategy | `beautiful_template_matcher.py``beautiful-html-template-families.json``component-registry.json``asset-strategy-registry.json` |
| SVG route 需要从主题提示词选择设计资产 | 先运行 design asset routingrecipe -> template family -> style pack -> palette/component/image treatment并由 diversity gate 防同质化 | `svglide-design-asset-routing.md``svglide_recipe_selector.py``svglide_diversity_gate.py` |
| 创建失败、空白页、3350001、布局异常 | 先回读状态,再按排障清单修复,不假设原操作原子成功 | `troubleshooting.md``validation-checklist.md` |
**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),认证、权限和全局参数均以 lark-shared 为准。**
**CRITICAL — 生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构**
**CRITICAL — route admission走 XML 创建/编辑路径时,只读取 XML schema、XML create/edit/validation 文档。只有当用户显式要求 SVG / SVGlide / `slides +create-svg`、输入 root 为 `<svg slide:role="slide">`,或 plan 声明 SVG route 时,才读取 [svglide-route-admission.md](references/svglide-route-admission.md)。SVG route 激活后,私有文档列表以 [svglide-svg-private.rules.json](references/svglide-svg-private.rules.json) 为准,[svg-private-manifest.json](references/svg-private-manifest.json) 仅作兼容索引XML route 不得读取**
**CRITICAL — 新建演示文稿或大幅改写页面时MUST 先生成 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。先创建对应目录规划层规则和中间产物生命周期见 [planning-layer.md](references/planning-layer.md)。仅替换一个标题、插入一个块等小型已有页编辑可豁免**
**CRITICAL — SVG route 激活后,生成前 MUST 在 `slide_plan.json` 记录 `loaded_rule_set`、`art_direction`、`quality_gates` 和必要的 `business_claims`。`loaded_rule_set` 必须覆盖 manifest 中的 SVG 设计与验证文档;`art_direction` 必须说明封面、章节/节奏页、结尾页、deck motif 和至少 3 个 SVG-native moments可见业务数字或推导性商业声明必须记录来源或假设**
**CRITICAL — Planner Ownership当前执行者必须亲自完成 Deck Planner / Slide Planner / Canvas Planner 的推理和产物生成。不得为了完成当前链路再调用另一个 agent、subagent、`codex exec`、`claude`、Tika、AIME、BitsAI 或任何外部 planner 来生成 deck plan / slide plan / canvas spec除非用户在当前请求中明确要求验证“CLI 无人值守自动调用模型/provider”。允许使用普通工具做文件读取、事实检索、素材获取、渲染、校验和导出这些工具不得接管 planner 决策。若使用 reviewerreviewer 只审查证据,不生成 planning 产物。**
**CRITICAL — 走 XML 创建/编辑路径时,生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。走 SVG 创建路径时,先完成 route admission再读取 SVG 私有协议和创建文档。**
**CRITICAL — 新建演示文稿或大幅改写页面时MUST 先生成 plan再生成 XML 或已准入的 SVG route 产物。XML 路径使用 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`SVG route 准入后使用 `.lark-slides/plan/<deck-or-task-id>/02-plan/slide_plan.json`。先创建对应目录XML 规划层规则和中间产物生命周期见 [planning-layer.md](references/planning-layer.md)SVG 扩展规划只在 route admission 后加载。仅替换一个标题、插入一个块等小型已有页编辑可豁免。**
**CRITICAL — 新建演示文稿或大幅改写页面时,生成 XML 前 MUST 读取 [visual-planning.md](references/visual-planning.md),确保 `layout_type`、`visual_focus`、`text_density` 实际改变页面几何、主视觉和文本量。**
**CRITICAL — 新建演示文稿或大幅改写页面时,规划 `asset_need` MUST 遵循 [asset-planning.md](references/asset-planning.md):只做元数据规划,必须有 `fallback_if_missing`,不得要求真实搜索、下载或上传素材。**
**CRITICAL — 创建或大幅改写后MUST 按 [validation-checklist.md](references/validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素、检查空白/破损页、明显溢出、布局风险XML 语法和文本重叠静态检查优先使用 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py)。**
**CRITICAL — 创建或大幅改写后MUST 按 [validation-checklist.md](references/validation-checklist.md) 做显式验证:回读全文 XML、核对页数和关键元素、检查空白/破损页、明显溢出、布局风险XML 语法和文本重叠静态检查优先使用 [`scripts/xml_text_overlap_lint.py`](scripts/xml_text_overlap_lint.py)。SVG route 的额外验证只在 route admission 后加载。**
**CRITICAL — SVG route 创建前 MUST 先通过 [`scripts/svg_preflight.py`](scripts/svg_preflight.py) 的 plan/source gate再对本地 HTML/SVG preview 运行 [`scripts/svg_preview_lint.py`](scripts/svg_preview_lint.py),并运行 [`scripts/svglide_semantic_review.py`](scripts/svglide_semantic_review.py) 校验中文、页型、章节、内容厚度和 SVG 可见文本来源。preview 中不得展示 safe-area/debug guide文本溢出、大数字窄框、明显重叠、英文 plan、缺页型或 generator 硬编码文本都会阻断 `slides +create-svg`。live create 后仍需 readback gateHTML preview 不能替代服务端转换后的验证。**
**CRITICAL — SVG route 需要主题、模板、版式或用户只给主题时MUST 先按 [`references/svglide-design-asset-routing.md`](references/svglide-design-asset-routing.md) 运行 design asset routing`svglide_recipe_selector.py` 写入 `deck_recipe_selection`、`template_family_selection`、`style_pack_selection`、`density_mode_selection`、`component_variant_selection`、`image_treatment_selection` 和 deck-level `style_lock``beautiful_template_matcher.py` 仅作为 template family/component/asset registry 的补充入口。模板家族来源是 [`references/beautiful-html-template-families.json`](references/beautiful-html-template-families.json);旧 baseline theme、旧 style preset、旧 visual recipe、旧 beautiful preset 不得作为默认生成入口。需要真实图片的页面必须声明 `image_slots`,并由 `svg_preflight.py` 挡住缺图、图文不一致和 AI/local generated bitmap 冒充真实图。**
**CRITICAL — 创建前自检或失败排障时MUST 按 [troubleshooting.md](references/troubleshooting.md) 检查 XML 转义、结构、shell 截断、图片 token、3350001 和布局风险。**
@@ -80,7 +93,7 @@ lark-cli auth login --domain slides
按需再读:
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)
- 创建:[`lark-slides-create.md`](references/lark-slides-create.md)SVG 创建先读 [`svglide-route-admission.md`](references/svglide-route-admission.md),准入后按 [`svglide-svg-private.rules.json`](references/svglide-svg-private.rules.json) 加载私有文档,旧 [`svg-private-manifest.json`](references/svg-private-manifest.json) 仅用于兼容检查
- 编辑:[`lark-slides-edit-workflows.md`](references/lark-slides-edit-workflows.md)、[`lark-slides-replace-slide.md`](references/lark-slides-replace-slide.md)
- 图片:[`lark-slides-media-upload.md`](references/lark-slides-media-upload.md)
- 流程图 / 时序图 / 架构图 / 装饰图案:[`lark-slides-whiteboard.md`](references/lark-slides-whiteboard.md)
@@ -104,7 +117,7 @@ lark-cli auth login --domain slides
- **背景一致性**:先确定全 deck 的背景策略,默认保持同一明暗基调和底色体系;只有分节、转场或强调页才有意改变背景,并必须通过相同主色、纹理、边栏或 motif 让变化看起来属于同一套设计。无论深浅,都要保证正文、图标和线条对比充足。
- **统一 motif**:选择一个可复用视觉母题贯穿全文,例如粗侧边栏、圆形图标底、半出血图片区、编号节点、卡片左上角色块或大号数字。不要每页换一套装饰语言。
每页至少要有一个视觉元素:图片、图标、图表、表格、流程、对比结构、大号数字、示意图或由 shape 组成的抽象视觉。文本框本身不算主视觉。
每页至少要有一个视觉元素:图片、图标、图表、表格、流程、对比结构、大号数字、示意图或由 shape 组成的抽象视觉。文本框本身不算主视觉。展示型、宣传型、产品型和案例型 deck 不能全程纯矢量,必须包含真实图片资产作为封面、半出血主视觉、案例场景、产品截图或材质背景。
可优先考虑这些页面形态:
@@ -128,7 +141,7 @@ lark-cli auth login --domain slides
- 不要所有页面复用同一种标题 + 三 bullets 版式。
- 不要用低对比文字或低对比图标,例如浅灰字压在浅色背景上。
- 不要让装饰线穿过文字,或让页脚、来源、编号挤压主体内容。
- 不要把素材缺失表现为空白图片框;必须按 `fallback_if_missing` 生成 XML-native 视觉。
- 不要把素材缺失表现为空白图片框;XML 路径必须按 `fallback_if_missing` 生成可见的 XML-native 视觉,并在结果中说明
- 不要留下模板占位文案、示例公司名、示例日期或与用户主题无关的原模板内容。
### 创建方式选择
@@ -137,6 +150,7 @@ lark-cli auth login --domain slides
|------|----------|
| 简单 XML1-3 页、结构简单、几乎无复杂中文和特殊字符) | `slides +create --slides '[...]'` 一步创建 |
| 复杂 XML多页、含中文、大段文本、复杂布局、嵌套引号、特殊字符较多 | **两步创建**:先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide create` 逐页添加 |
| AI 生成 SVGlide SVG希望减少 shell XML 转义、按文件逐页创建) | `slides +create-svg --file page1.svg --file page2.svg --title "<标题>"` |
| 已有 PPT 继续追加或插入页面 | 使用 `xml_presentation.slide create`,必要时配合 `before_slide_id` |
> [!WARNING]
@@ -157,18 +171,18 @@ python3 skills/lark-slides/scripts/template_tool.py extract --template <template
```text
Step 1: 需求澄清 & 读取知识
- 澄清主题、受众、页数、风格;模板需求按“模板与脚本优先流程”处理
- 澄清主题、受众、页数、风格;如用户要求 SVG / SVGlide / `slides +create-svg`,先执行 route admission模板需求按“模板与脚本优先流程”处理
- 读取 xml-schema-quick-ref.md新建 / 大幅改写时还要读取 planning-layer.md、visual-planning.md、asset-planning.md
Step 2: 生成大纲 → 用户确认 → 写入 slide_plan.json
- 生成结构化大纲供用户确认;如使用模板,标明基于哪个模板改写
- 新建 / 大幅改写必须先创建目录并写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`
- 新建 / 大幅改写必须先创建目录并写入 planXML 路径写 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`SVG route 写 `.lark-slides/plan/<deck-or-task-id>/02-plan/slide_plan.json`
- plan 字段、路径命名、模板边界和 `asset_need` 结构按 planning-layer.md / asset-planning.md 执行
Step 3: 按 slide_plan.json 生成 XML → 创建
Step 3: 按 slide_plan.json 生成 XML 或 SVGlide SVG → 创建
- 逐页消费 plankey_message 定主结论layout_type 定几何visual_focus 定主视觉text_density 定文本量
- 缺少真实素材时必须用 `fallback_if_missing` 生成 XML-native 兜底视觉;不要留空
- 创建方式按“创建方式选择”判断;图片、复杂 XML、转义和 3350001 排查按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行
- XML 路径按 lark-slides-create.md、media-upload.md、troubleshooting.md 执行SVG route 准入后按私有清单执行,产物是 `.svg` 文件而不是 Slides XML使用同一个 run root 下的 `02-plan/slide_plan.json`
Step 4: 审查 & 交付
- 创建完成后,必须用 xml_presentations.get 读取全文 XML并按 validation-checklist.md 做显式验证记录,包括 XML 文本重叠检查
@@ -264,6 +278,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli slides +<verb> [flags]`
| Shortcut | 说明 |
|----------|------|
| [`+create`](references/lark-slides-create.md) | 创建 PPT可选 `--slides` 一步添加页面,支持 `<img src="@./local.png">` 占位符自动上传) |
| [`+create-svg`](references/lark-slides-create-svg.md) | 从一个或多个 SVGlide SVG 文件创建 PPT`--file` 顺序逐页调用现有 `/slide` 路由 |
| [`+media-upload`](references/lark-slides-media-upload.md) | 上传本地图片到指定演示文稿,返回 `file_token`(用作 `<img src="...">`),最大 20 MB |
| [`+replace-slide`](references/lark-slides-replace-slide.md) | 对已有幻灯片页面进行块级替换/插入(`block_replace` / `block_insert`),自动注入 id 和 `<content/>`,不改变页序 |
@@ -278,13 +293,31 @@ lark-cli slides <resource> <method> [flags] # 调用 API
## 核心规则
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`;模板、风格和大纲只能作为规划输入,不能绕过规划层
2. **创建流程**:简单短 XML1-3 页、结构简单、特殊字符少)可用 `slides +create --slides '[...]'` 一步创建;复杂内容、含图片/中文大段文本/嵌套引号/较多特殊字符,或超过 10 页时,默认先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide.create` 逐页添加
1. **先规划再写 XML**:新建演示文稿或大幅改写页面时,XML 路径必须先写入 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`SVG route 准入后写入 `.lark-slides/plan/<deck-or-task-id>/02-plan/slide_plan.json`模板、风格和大纲只能作为规划输入,不能绕过规划层
2. **创建流程**:简单短 XML1-3 页、结构简单、特殊字符少)可用 `slides +create --slides '[...]'` 一步创建;复杂内容、含图片/中文大段文本/嵌套引号/较多特殊字符,或超过 10 页时,默认先 `slides +create` 创建空白 PPT再用 `xml_presentation.slide.create` 逐页添加AI SVG 路径使用 `slides +create-svg`,不要把 SVG 塞进 `--slides`
3. **`<slide>` 直接子元素只有 `<style>``<data>``<note>`**:文本和图形必须放在 `<data>`
4. **文本通过 `<content>` 表达**:必须用 `<content><p>...</p></content>`,不能把文字直接写在 shape 内
5. **保存关键 ID**:后续操作需要 `xml_presentation_id``slide_id``revision_id`
6. **删除谨慎**:删除操作不可逆,且至少保留一页幻灯片
7. **编辑已有页面优先块级替换**:修改单个 shape/img 用 `+replace-slide``block_replace` / `block_insert`),不要整页重建;只有需要替换整页结构时才用 `slide.delete` + `slide.create`
8. **`<img src>` 只能用上传到飞书 drive 的 `file_token`,禁止使用 http(s) 外链 URL**:飞书 slides 渲染端不会代理外链图片,外链 src 在 PPT 里通常不显示或显示破图。流程必须是「先把图存到本地 → 用 `slides +media-upload` 上传或 `+create --slides``@./path` 占位符自动上传 → 拿 `file_token` 写进 `<img src>`」。如果用户给了网图链接,先 `curl`/下载到 CWD 内再走上传流程,不要直接把外链 URL 塞进 `src`。**图片最大 20 MB**slides upload API 不支持分片上传)。
8. **图片资产必须可见**XML 路径使用 `<img src="...">` 或本地占位符上传;如果没有可用素材,必须按 `asset-planning.md``fallback_if_missing` 生成可见兜底视觉,不要留下空图片框。**图片最大 20 MB**slides upload API 不支持分片上传)。
> **注意**:如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致,以后两者为准。
## 权限速查
| 方法 | 所需 scope |
|------|-----------|
| `slides +create` | `slides:presentation:create`, `slides:presentation:write_only`(含 `@` 占位符时还需 `docs:document.media:upload` |
| `slides +create-svg` | `slides:presentation:create`, `slides:presentation:write_only`, `docs:document.media:upload` |
| `slides +media-upload` | `docs:document.media:upload`wiki URL 解析还需 `wiki:node:read` |
| `slides +replace-slide` | `slides:presentation:update`wiki URL 解析还需 `wiki:node:read` |
| `xml_presentations.get` | `slides:presentation:read` |
| `xml_presentation.slide.create` | `slides:presentation:update``slides:presentation:write_only` |
| `xml_presentation.slide.delete` | `slides:presentation:update``slides:presentation:write_only` |
| `xml_presentation.slide.get` | `slides:presentation:read` |
| `xml_presentation.slide.replace` | `slides:presentation:update` |
> **注意**XML 路径如果 md 内容与 `slides_xml_schema_definition.xml` 或 `lark-cli schema slides.<resource>.<method>` 输出不一致以后两者为准SVG route 文档只在 route admission 后加载。
## SVG Route
`slides +create-svg` 只作为命令入口出现在顶层。出现 SVG / SVGlide / `slides +create-svg` 需求时,先读取 [`svglide-route-admission.md`](references/svglide-route-admission.md),命中后再按 [`svglide-svg-private.rules.json`](references/svglide-svg-private.rules.json) 加载 SVG 私有协议、规划、验证和排障文档,旧 [`svg-private-manifest.json`](references/svg-private-manifest.json) 仅作为兼容索引保留。新建或大幅改写 SVG deck 时,优先使用 [`svglide_project_runner.py`](scripts/svglide_project_runner.py) 管理控制面:`init` 建目录,`source` 归一化 `source/evidence.json` 并写入 source receipt`select_style` 执行 recipe/style_pack/palette/template routing`plan` 将 selection receipts 回填到 `slide_plan.json``strategy_review` 先锁定语言、受众、页型、章节、内容厚度和 `visual_identity``confirm_plan` 仅作为兼容/人工审批面保留不是默认必经节点runner 再负责 `assets -> generate_svg -> contract_compile -> prepare -> preview -> preflight -> preview_lint -> aesthetic_review -> chart_verify -> semantic_review -> runtime_review -> visual_distinctness_review -> diversity_gate -> theme_adherence -> quality_gate`。本地内容验收使用 `run .lark-slides/plan/<deck-id> --profile preview_only` 默认停在 `quality_gate``assets` 负责资产契约和 token/local-file 审计,`generate_svg``artboard_satori` 模式只产 `04-artboard/raw` raw visual/semantic artifacts`contract_compile` 才注入 SVGlide/Slides 私有协议并写入 `04-svg/page-###.svg`、contract report/manifest 和资产注入摘要,`prepare` 只消费 contract manifest 校验过的新鲜 canonical SVG。`chart_verify` 只在页面声明 required/exact chart contract 时强制,`semantic_review` 负责 `semantic-review.json``text-inventory.json``runtime_review` 负责 renderer/layout 多样性,`visual_distinctness_review` 负责阻断不同主题复用同一套 palette、cover treatment 和 renderer/layout 序列,`diversity_gate` 负责阻断 template/style/layout/component 组合过度复用和 style lock 漂移。live create 前必须有新鲜的 `quality_gate.status=passed``dry-run.json``ppe-proof.json`live create 后必须进入 readback stage。XML route 不得读取 SVG 私有清单中的策略正文。

View File

@@ -0,0 +1,30 @@
# SVGlide Canvas Planner Prompt
## Contract
- Input bundle: `02-plan/slide-plan.json`, `svglide-template-registry.json`, `themes/registry.json`, `svglide-canvas-spec.schema.json`, golden CanvasSpec examples.
- Output schema: `skills/lark-slides/references/svglide-canvas-plan.schema.json`.
- Output path: `02-plan/slide_plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Canvas Planner turns each slide plan into the final `generation_mode=artboard_satori` `slide_plan.json`. Every slide must include a full `canvas_spec` with:
- `version`
- `canvas`
- `safe_area`
- `template_id`
- `theme_id`
- `theme`
- `content`
- `semantic_elements`
- `quality_constraints`
The output must pass `svglide-plan.schema.json`, `svglide-canvas-plan.schema.json`, CanvasSpec validation, Template Registry binding, Theme Registry binding, and template text-budget/max-items checks before Satori is invoked.
## Forbidden Outputs
Do not output free HTML, CSS, SVG, JSX, TSX, Markdown prose, raw Satori SVG, foreignObject snippets, or arbitrary inline style. Use structured CanvasSpec JSON only.

View File

@@ -0,0 +1,25 @@
# SVGlide Deck Planner Prompt
## Contract
- Input bundle: `user_topic`, `audience`, `target_slide_count`, `source_policy`, `available_template_registry`, `available_theme_registry`.
- Output schema: `skills/lark-slides/references/svglide-deck-plan.schema.json`.
- Output path: `02-plan/deck-plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Deck Planner defines the narrative system for the whole deck:
- objective
- audience
- target slide count
- narrative arc
- theme direction
- per-slide role, key message, content goal, and visual goal
## Forbidden Outputs
Do not output free HTML, CSS, SVG, JSX, TSX, Markdown prose, base64 image data, or rendered visual markup. Do not create page geometry here. Do not invent numeric claims; mark missing facts as `pending_confirmation`.

View File

@@ -0,0 +1,24 @@
# SVGlide Repair Planner Prompt
## Contract
- Input bundle: validation receipt, target planner JSON, schema issue list, template fit issue list.
- Output schema: `skills/lark-slides/references/svglide-repair-plan.schema.json`.
- Output path: `02-plan/repair-plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Repair Planner outputs scoped JSON Patch operations only. Each patch must target one precise field, such as:
- `/slides/0/canvas_spec/content/title`
- `/slides/1/canvas_spec/content/right_points/2`
- `/slides/2/canvas_spec/semantic_elements/0/bbox/width`
Every patch must include a short `reason` tied to a validation issue.
## Forbidden Outputs
Do not rewrite the full deck. Do not output `slides`, full `canvas_spec`, full `deck_plan`, free HTML, CSS, SVG, JSX, TSX, Markdown prose, or unscoped patch paths such as `/`, `/slides`, `/slides/0`, or `/slides/0/canvas_spec`.

View File

@@ -0,0 +1,20 @@
# SVGlide Slide Planner Prompt
## Contract
- Input bundle: `02-plan/deck-plan.json`, `svglide-template-registry.json`, `themes/registry.json`, `svglide-layout-archetypes.json`, `svglide-component-registry.json`.
- Output schema: `skills/lark-slides/references/svglide-slide-plan.schema.json`.
- Output path: `02-plan/slide-plan.json`.
- Validation command: `python3 skills/lark-slides/scripts/svglide_planner_contracts.py <project>`.
## Output Rules
Return JSON only. Do not wrap the answer in Markdown fences.
The Slide Planner chooses registered templates and themes for each slide. It may define structured content requirements, but it must not write CanvasSpec yet.
Every slide must choose a `template_id` and `theme_id` from the registries. Keep content short enough for the selected template budgets.
## Forbidden Outputs
Do not output free HTML, CSS, SVG, JSX, TSX, Markdown prose, raw Satori SVG, or unregistered template/theme IDs. Do not bypass Template Registry or Theme Registry.

View File

@@ -0,0 +1,31 @@
# Third Party Notices
This file records open-source projects referenced or used by the SVGlide artboard/Satori work.
Reference absorption means SVGlide records provenance and reimplements the usable pattern with SVGlide-owned data, templates, tokens, or renderer primitives. It does not permit embedding upstream HTML, CSS, JavaScript, screenshots, or renderer source unless a specific absorption record marks that portion as copied or adapted and carries the required notice.
## Reference Sources
| Project | Repository | License | Notice | SVGlide usage |
| --- | --- | --- | --- | --- |
| beautiful-html-templates | https://github.com/zarazhangrui/beautiful-html-templates.git | MIT | Copyright (c) 2026 Zara Zhang | Template family, layout, component, and planner-selection signal extraction. |
| ppt-master | https://github.com/hugohe3/ppt-master.git | MIT | Copyright (c) 2025-2026 Hugo He | Slide workflow, planning, visual QA, and artifact discipline reference. |
| PosterGen | https://github.com/Y-Research-SBU/PosterGen.git | MIT | Copyright (c) 2025 Y-Research @SBU | Poster-style composition and visual hierarchy reference. |
| og-images-generator | https://github.com/gracile-web/og-images-generator.git | ISC | Copyright (c) 2024 Julian Cataldo - https://www.juliancataldo.com | Renderer pipeline and OG-image generation boundary reference. |
| open-design | https://github.com/nexu-io/open-design.git | Apache-2.0 | Apache License Version 2.0 | Design-generation vocabulary and planning structure reference. |
## Runtime Dependency
| Project | Repository | License | Notice | SVGlide usage |
| --- | --- | --- | --- | --- |
| satori | https://github.com/vercel/satori.git | MPL-2.0 | Mozilla Public License Version 2.0; package author Shu Ding <g@shud.in> | External runtime dependency for HTML/CSS-like tree to SVG rendering. |
Satori must remain external to `skills/lark-slides/scripts/artboard_renderer/dist/render.mjs`. The bundle build externalizes `satori`, and the package check rejects bundled Satori markers. If a future distribution embeds Satori source or compiled Satori code, that change must first add the required MPL-2.0 source and notice handling.
## Distribution Rules
- Keep `skills/lark-slides/references/oss-source-manifest.json` updated whenever a referenced upstream project, license, HEAD, or usage type changes.
- For MIT or ISC copied/adapted portions, retain the upstream copyright notice, permission notice, and warranty disclaimer with the distributed SVGlide artifact.
- For Apache-2.0 copied/adapted portions, retain the license text and any applicable NOTICE content, and mark local modifications when required.
- For MPL-2.0 dependencies, do not bundle covered source into SVGlide artifacts unless MPL source availability and notice obligations are implemented and reviewed.
- For pure reference absorption, keep a provenance record and verify the output does not embed upstream source assets.

View File

@@ -0,0 +1,10 @@
# SVGlide Reference Absorptions
This directory stores reference absorption records.
Each record must describe an SVGlide-owned abstraction, not copied runtime HTML,
CSS, SVG, JS, or source repository code. A record is only valid when it links a
source inventory item to concrete SVGlide target assets or rules and includes at
least one fixture or receipt proof field.
Use the schema in `skills/lark-slides/references/svglide-reference-abstraction.schema.json`.

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "8-bit-orbit",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/8-bit-orbit/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/8-bit-orbit/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/8-bit-orbit-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/8-bit-orbit-6.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/8-bit-orbit-5.png"
}
],
"svglide_asset_ids": [
"template.pixel-orbit-console",
"theme.8-bit-orbit",
"layout.pixel_orbit_console",
"component.title_block",
"component.metric_card",
"component.mini_chart",
"component.action_list"
],
"template_id": "pixel-orbit-console",
"theme_id": "8-bit-orbit"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "biennale-yellow",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/biennale-yellow/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/biennale-yellow/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/biennale-yellow-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/biennale-yellow-5.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/biennale-yellow-8.png"
}
],
"svglide_asset_ids": [
"template.biennale-programme-poster",
"theme.biennale-yellow",
"layout.biennale_programme_poster",
"component.title_block",
"component.section_label",
"component.timeline",
"component.action_list"
],
"template_id": "biennale-programme-poster",
"theme_id": "biennale-yellow"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "block-frame",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/block-frame/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/block-frame/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/block-frame-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/block-frame-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/block-frame-8.png"
}
],
"svglide_asset_ids": [
"template.block-frame-grid",
"theme.block-frame",
"layout.block_frame_grid",
"component.title_block",
"component.metric_card",
"component.comparison_matrix",
"component.finding_callout"
],
"template_id": "block-frame-grid",
"theme_id": "block-frame"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "capsule",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/capsule/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/capsule/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/capsule-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/capsule-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/capsule-8.png"
}
],
"svglide_asset_ids": [
"template.capsule-card-system",
"theme.capsule",
"layout.capsule_card_system",
"component.title_block",
"component.metric_card",
"component.process_flow",
"component.action_list"
],
"template_id": "capsule-card-system",
"theme_id": "capsule"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "coral",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/coral/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/coral/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/coral-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/coral-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/coral-8.png"
}
],
"svglide_asset_ids": [
"template.coral-magazine-feature",
"theme.coral",
"layout.coral_magazine_feature",
"component.title_block",
"component.finding_callout",
"component.metric_card"
],
"template_id": "coral-magazine-feature",
"theme_id": "coral"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "creative-mode",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/creative-mode/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/creative-mode/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/creative-mode-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/creative-mode-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/creative-mode-6.png"
}
],
"svglide_asset_ids": [
"template.creative-mode-grid",
"theme.creative-mode",
"layout.creative_mode_grid",
"component.title_block",
"component.finding_callout",
"component.process_flow",
"component.comparison_matrix"
],
"template_id": "creative-mode-grid",
"theme_id": "creative-mode"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "daisy-days",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/daisy-days/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/daisy-days/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/daisy-days-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/daisy-days-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/daisy-days-8.png"
}
],
"svglide_asset_ids": [
"template.daisy-workshop-playbook",
"theme.daisy-days",
"layout.daisy_workshop_playbook",
"component.title_block",
"component.action_list",
"component.finding_callout"
],
"template_id": "daisy-workshop-playbook",
"theme_id": "daisy-days"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "editorial-tri-tone",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/editorial-tri-tone/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/editorial-tri-tone/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/editorial-tri-tone-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/editorial-tri-tone-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/editorial-tri-tone-3.png"
}
],
"svglide_asset_ids": [
"template.tritone-editorial-spread",
"theme.editorial-tri-tone",
"layout.tritone_editorial_spread",
"component.title_block",
"component.finding_callout",
"component.metric_card"
],
"template_id": "tritone-editorial-spread",
"theme_id": "editorial-tri-tone"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "emerald-editorial",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/emerald-editorial/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/emerald-editorial/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/emerald-editorial-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/emerald-editorial-3.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/emerald-editorial-6.png"
}
],
"svglide_asset_ids": [
"template.emerald-editorial-cover",
"theme.emerald-editorial",
"layout.emerald_editorial_cover",
"component.title_block",
"component.metric_card",
"component.finding_callout"
],
"template_id": "emerald-editorial-cover",
"theme_id": "emerald-editorial"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "grove",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/grove/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/grove/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/grove-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/grove-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/grove-8.png"
}
],
"svglide_asset_ids": [
"template.grove-organic-brief",
"theme.grove",
"layout.grove_organic_brief",
"component.title_block",
"component.finding_callout",
"component.metric_card"
],
"template_id": "grove-organic-brief",
"theme_id": "grove"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "mat",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/mat/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/mat/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/mat-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/mat-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/mat-8.png"
}
],
"svglide_asset_ids": [
"template.mat-midcentury-board",
"theme.mat",
"layout.mat_midcentury_board",
"component.title_block",
"component.comparison_matrix",
"component.timeline",
"component.metric_card"
],
"template_id": "mat-midcentury-board",
"theme_id": "mat"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "peoples-platform",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/peoples-platform/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/peoples-platform/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/peoples-platform-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/peoples-platform-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/peoples-platform-8.png"
}
],
"svglide_asset_ids": [
"template.people-platform-manifesto",
"theme.peoples-platform",
"layout.people_platform_manifesto",
"component.title_block",
"component.finding_callout",
"component.action_list"
],
"template_id": "people-platform-manifesto",
"theme_id": "peoples-platform"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "pink-script",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/pink-script/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/pink-script/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/pink-script-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/pink-script-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/pink-script-8.png"
}
],
"svglide_asset_ids": [
"template.pink-nocturne-feature",
"theme.pink-script",
"layout.pink_nocturne_feature",
"component.title_block",
"component.finding_callout",
"component.action_list"
],
"template_id": "pink-nocturne-feature",
"theme_id": "pink-script"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "playful",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/playful/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/playful/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/playful-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/playful-6.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/playful-8.png"
}
],
"svglide_asset_ids": [
"template.playful-indie-launch",
"theme.playful",
"layout.playful_indie_launch",
"component.title_block",
"component.metric_card",
"component.action_list"
],
"template_id": "playful-indie-launch",
"theme_id": "playful"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "retro-zine",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/retro-zine/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/retro-zine/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/retro-zine-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/retro-zine-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/retro-zine-8.png"
}
],
"svglide_asset_ids": [
"template.retro-zine-spread",
"theme.retro-zine",
"layout.retro_zine_spread",
"component.title_block",
"component.finding_callout",
"component.action_list"
],
"template_id": "retro-zine-spread",
"theme_id": "retro-zine"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "scatterbrain",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/scatterbrain/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/scatterbrain/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/scatterbrain-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/scatterbrain-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/scatterbrain-8.png"
}
],
"svglide_asset_ids": [
"template.sticky-workshop-board",
"theme.scatterbrain",
"layout.sticky_workshop_board",
"component.title_block",
"component.finding_callout",
"component.action_list",
"component.timeline"
],
"template_id": "sticky-workshop-board",
"theme_id": "scatterbrain"
}

View File

@@ -0,0 +1,58 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "soft-editorial",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/soft-editorial/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/soft-editorial/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/soft-editorial-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/soft-editorial-6.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/soft-editorial-10.png"
}
],
"svglide_asset_ids": [
"template.soft-editorial-feature",
"theme.soft-editorial",
"layout.soft_editorial_feature",
"component.title_block",
"component.finding_callout",
"component.evidence_table"
],
"template_id": "soft-editorial-feature",
"theme_id": "soft-editorial"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "stencil-tablet",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/stencil-tablet/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/stencil-tablet/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/stencil-tablet-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/stencil-tablet-3.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/stencil-tablet-8.png"
}
],
"svglide_asset_ids": [
"template.stencil-field-manual",
"theme.stencil-tablet",
"layout.stencil_field_manual",
"component.title_block",
"component.evidence_table",
"component.process_flow",
"component.timeline"
],
"template_id": "stencil-field-manual",
"theme_id": "stencil-tablet"
}

View File

@@ -0,0 +1,59 @@
{
"absorbed_as": [
"template_candidate",
"theme_candidate",
"layout_archetype",
"component_variant",
"planner_selection_signal"
],
"created_at": "2026-06-23",
"runtime_contract": {
"external_html_css_js": "forbidden",
"forbidden_svg_features": [
"filter",
"pattern",
"foreignObject",
"image",
"use",
"linearGradient",
"radialGradient"
],
"python_generic_fallback": "forbidden",
"runtime": "owned_satori_template"
},
"schema_version": "svglide-beautiful-template-absorption/v1",
"source_family": "vellum",
"source_trace": [
{
"evidence": "source_design_md",
"source": "beautiful-html-templates/templates/vellum/design.md"
},
{
"evidence": "source_template_json",
"source": "beautiful-html-templates/templates/vellum/template.json"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/vellum-1.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/vellum-4.png"
},
{
"evidence": "source_screenshot",
"source": "beautiful-html-templates/screenshots/vellum-8.png"
}
],
"svglide_asset_ids": [
"template.vellum-scholar-brief",
"theme.vellum",
"layout.vellum_scholar_brief",
"component.title_block",
"component.finding_callout",
"component.evidence_table",
"component.metric_card"
],
"template_id": "vellum-scholar-brief",
"theme_id": "vellum"
}

View File

@@ -0,0 +1,32 @@
{
"source_item_id": "svglide-baseline.chart_strategies",
"absorbed_as": [
"owned_baseline_runtime_asset",
"chart_strategy"
],
"svglide_asset_ids": [
"chart_strategy.data-story-bar-series",
"chart_strategy.risk-row-stack",
"chart_strategy.roadmap-lane-progress",
"chart_strategy.timeline-step-sequence"
],
"non_copying_transform": "Existing SVGlide strategy catalog entries are recorded as owned baseline strategy abstractions with fixture-only claim scope.",
"forbidden_usage": [
"do_not_relabel_baseline_assets_as_external_reference_absorption",
"do_not_import_raw_external_runtime_artifacts",
"do_not_claim_backend_readback_without_receipt"
],
"template_guardrail_records": [
"skills/lark-slides/references/svglide-chart-strategies.json"
],
"canvas_spec_fixtures": [
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/data-story.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/roadmap-lanes.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/timeline-steps.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/risk-alert.canvas-spec.json"
],
"quality_receipts": [
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"review_notes": "Baseline strategy records do not claim trusted image provider or backend chart readback evidence."
}

View File

@@ -0,0 +1,57 @@
{
"source_item_id": "svglide-baseline.components",
"absorbed_as": [
"owned_baseline_runtime_asset",
"component_variant"
],
"svglide_asset_ids": [
"component.AccentShape",
"component.AffiliationMarker",
"component.ArchitectureNode",
"component.ArchitectureNodeBlock",
"component.Badge",
"component.BarMark",
"component.BarSeries",
"component.Callout",
"component.Chip",
"component.DividerRule",
"component.FigurePlaceholder",
"component.ImageCaptionBlock",
"component.ImageFrame",
"component.IndexNumber",
"component.LegendDot",
"component.MetricTile",
"component.PosterSection",
"component.ProcessStep",
"component.ProcessStepCard",
"component.QuoteBlock",
"component.ResearchSectionBlock",
"component.RiskBanner",
"component.RiskSeverityPill",
"component.RoadmapLane",
"component.RoadmapProgressLine",
"component.SectionHeader",
"component.StatCard",
"component.Subtitle",
"component.TextBlock",
"component.TimelineNode",
"component.TimelineStepMarker",
"component.Title"
],
"non_copying_transform": "Existing SVGlide renderer primitives and baseline component variants are recorded as owned runtime abstractions, not as imported reference code.",
"forbidden_usage": [
"do_not_relabel_baseline_assets_as_external_reference_absorption",
"do_not_import_raw_external_runtime_artifacts",
"do_not_claim_backend_readback_without_receipt"
],
"template_guardrail_records": [
"skills/lark-slides/references/svglide-component-registry.json"
],
"canvas_spec_fixtures": [
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/cover-hero.canvas-spec.json"
],
"quality_receipts": [
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"review_notes": "Baseline components are exercised through existing golden templates and renderer tests."
}

View File

@@ -0,0 +1,29 @@
{
"source_item_id": "svglide-baseline.image_strategies",
"absorbed_as": [
"owned_baseline_runtime_asset",
"image_strategy"
],
"svglide_asset_ids": [
"image_strategy.bounded-figure-placeholder",
"image_strategy.captioned-evidence-frame",
"image_strategy.research-key-visual-slot"
],
"non_copying_transform": "Existing SVGlide strategy catalog entries are recorded as owned baseline strategy abstractions with fixture-only claim scope.",
"forbidden_usage": [
"do_not_relabel_baseline_assets_as_external_reference_absorption",
"do_not_import_raw_external_runtime_artifacts",
"do_not_claim_backend_readback_without_receipt"
],
"template_guardrail_records": [
"skills/lark-slides/references/svglide-image-strategies.json"
],
"canvas_spec_fixtures": [
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/image-feature.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/research-poster.canvas-spec.json"
],
"quality_receipts": [
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"review_notes": "Baseline strategy records do not claim trusted image provider or backend chart readback evidence."
}

View File

@@ -0,0 +1,52 @@
{
"source_item_id": "svglide-baseline.layouts",
"absorbed_as": [
"owned_baseline_runtime_asset",
"layout_archetype"
],
"svglide_asset_ids": [
"layout.agenda-list",
"layout.architecture-blueprint",
"layout.data-story-bars",
"layout.full-bleed-image-story",
"layout.hero-cover",
"layout.image-text-split",
"layout.metric-dashboard",
"layout.process-flow",
"layout.quote-claim",
"layout.research-poster-3col",
"layout.section-divider",
"layout.timeline-horizontal",
"layout.two-column-comparison"
],
"non_copying_transform": "Existing SVGlide layout archetypes are recorded as owned planner/layout abstractions with positive golden fixture coverage.",
"forbidden_usage": [
"do_not_relabel_baseline_assets_as_external_reference_absorption",
"do_not_import_raw_external_runtime_artifacts",
"do_not_claim_backend_readback_without_receipt"
],
"template_guardrail_records": [
"skills/lark-slides/references/svglide-layout-archetypes.json"
],
"canvas_spec_fixtures": [
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/cover-hero.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/section-title.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/agenda-list.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/comparison-cards.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/timeline-steps.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/process-flow.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/metric-dashboard.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/data-story.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/image-feature.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/research-poster.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/architecture-blueprint.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/roadmap-lanes.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/risk-alert.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/quote-focus.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/summary-final.canvas-spec.json"
],
"quality_receipts": [
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"review_notes": "Baseline layouts keep existing template-to-layout mapping and are not attributed to external repositories."
}

View File

@@ -0,0 +1,55 @@
{
"source_item_id": "svglide-baseline.templates",
"absorbed_as": [
"owned_baseline_runtime_asset",
"template_candidate"
],
"svglide_asset_ids": [
"template.agenda-list",
"template.architecture-blueprint",
"template.comparison-cards",
"template.cover-hero",
"template.data-story",
"template.image-feature",
"template.metric-dashboard",
"template.process-flow",
"template.quote-focus",
"template.research-poster",
"template.risk-alert",
"template.roadmap-lanes",
"template.section-title",
"template.summary-final",
"template.timeline-steps"
],
"non_copying_transform": "Existing SVGlide artboard templates are recorded as owned baseline assets. The record preserves reverse traceability without attributing them to external reference repositories.",
"forbidden_usage": [
"do_not_relabel_baseline_assets_as_external_reference_absorption",
"do_not_import_raw_external_runtime_artifacts",
"do_not_claim_backend_readback_without_receipt"
],
"template_guardrail_records": [
"skills/lark-slides/references/svglide-template-registry.json",
"skills/lark-slides/references/svglide-template-guardrails.json"
],
"canvas_spec_fixtures": [
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/cover-hero.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/comparison-cards.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/summary-final.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/section-title.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/agenda-list.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/timeline-steps.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/process-flow.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/metric-dashboard.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/quote-focus.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/image-feature.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/research-poster.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/data-story.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/risk-alert.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/roadmap-lanes.canvas-spec.json",
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/architecture-blueprint.canvas-spec.json"
],
"quality_receipts": [
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"review_notes": "Baseline templates predate this reference absorption wave and keep their existing golden CanvasSpec fixtures."
}

View File

@@ -0,0 +1,35 @@
{
"source_item_id": "svglide-baseline.themes",
"absorbed_as": [
"owned_baseline_runtime_asset",
"theme_rule"
],
"svglide_asset_ids": [
"theme.blueprint-technical",
"theme.cobalt-grid",
"theme.dark-clarity",
"theme.editorial-tritone",
"theme.finance-dark",
"theme.forest-signal",
"theme.glass-neon",
"theme.paper-research",
"theme.swiss-red",
"theme.warm-editorial"
],
"non_copying_transform": "Existing SVGlide theme token files are recorded as owned baseline token abstractions, retaining local color/spacing/typography contracts.",
"forbidden_usage": [
"do_not_relabel_baseline_assets_as_external_reference_absorption",
"do_not_import_raw_external_runtime_artifacts",
"do_not_claim_backend_readback_without_receipt"
],
"template_guardrail_records": [
"skills/lark-slides/scripts/artboard_renderer/themes/registry.json"
],
"canvas_spec_fixtures": [
"skills/lark-slides/scripts/fixtures/svglide_artboard/golden/cover-hero.canvas-spec.json"
],
"quality_receipts": [
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"review_notes": "Baseline themes are validated by theme registry loading and existing artboard fixture renders."
}

View File

@@ -4,6 +4,66 @@
本文件只定义轻量资产规划。不要把它理解成素材采集流程。
## SVG Route Asset Slot Contract
XML route still uses `asset_need` as metadata. SVGlide/SVG route adds a stricter contract for pages that require real images because local preview and live submit must not silently drop assets.
When a page requires real images, add both `asset_strategy` and `image_slots`:
```json
{
"asset_strategy": {
"strategy_id": "real_image_required",
"expected_asset_count": 2,
"no_fake_data": true
},
"image_slots": [
{
"slot_id": "company_logo",
"semantic_subject": "company identity",
"asset_type": "logo",
"required": true,
"real_image_required": true,
"shared_asset_allowed": false
},
{
"slot_id": "product_screenshot",
"semantic_subject": "product UI screenshot",
"asset_type": "screenshot",
"required": true,
"real_image_required": true,
"shared_asset_allowed": false
}
]
}
```
Each acquired image must bind back to one slot through `asset_contract.binds_slot` or `asset_contract.image_slot_id`:
```json
{
"asset_id": "product_screenshot_asset",
"binds_slot": "product_screenshot",
"source_type": "public_url",
"semantic_subject": "product UI screenshot",
"retrieval_query": "product UI screenshot",
"license": "preview_unverified",
"href": "https://example.com/product.png",
"usage_page": 1,
"source_url": "https://example.com/product.png"
}
```
Rules for the SVG route validation gate:
- `expected_asset_count` must be satisfied by required `image_slots`.
- Required slots cannot be partially filled.
- One image reused across slots fails unless every reused slot has `shared_asset_allowed=true`.
- `source_type=ai_generated_bitmap`, `generated_bitmap`, or local generated image cannot satisfy `real_image_required`.
- `semantic_subject` / `retrieval_query` must match the slot subject; mismatches fail with `semantic_mismatch`.
- The prepared SVG must render at least one `<image>` per required image slot before local preview or live submit.
- If the user explicitly asks for no images, use `asset_strategy.strategy_id="none_required"` with `user_override=true`.
## Core Rules
- `asset_need` is metadata only. It can guide page design, but it must not require web search, local download, media upload, or external tools.

View File

@@ -0,0 +1,43 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/asset-slot-contract.schema.json",
"title": "SVGlide image asset slot contract",
"type": "object",
"required": ["asset_strategy", "image_slots"],
"additionalProperties": true,
"properties": {
"asset_strategy": {
"type": "object",
"required": ["strategy_id"],
"additionalProperties": true,
"properties": {
"strategy_id": {"type": "string", "minLength": 1},
"expected_asset_count": {"type": "integer", "minimum": 0},
"source_type_allowlist": {
"type": "array",
"items": {"enum": ["web_search_preview", "user_provided", "uploaded_file"]}
},
"generated_bitmap_allowed_as_real_image": {"type": "boolean"},
"preview_required": {"type": "boolean"},
"live_submit_requires_file_token": {"type": "boolean"},
"user_override": {"type": "boolean"}
}
},
"image_slots": {
"type": "array",
"items": {
"type": "object",
"required": ["slot_id", "semantic_subject", "asset_type", "required"],
"additionalProperties": true,
"properties": {
"slot_id": {"type": "string", "minLength": 1},
"semantic_subject": {"type": "string", "minLength": 1},
"asset_type": {"enum": ["photo", "logo", "screenshot", "chart", "illustration"]},
"required": {"type": "boolean"},
"shared_asset_allowed": {"type": "boolean"},
"fallback_if_missing": {"type": "string"}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
{
"version": "svglide-asset-strategy-registry/v1",
"strategies": [
{
"strategy_id": "real_image_required",
"trigger_signals": ["company", "product", "person", "brand", "case study", "food", "place", "event"],
"allowed_asset_types": ["logo", "screenshot", "photo"],
"fallback_if_missing": "Render a structured identity panel or structured semantic card group. Do not leave empty image boxes.",
"source_type_allowlist": ["web_search_preview", "user_provided", "uploaded_file"],
"generated_bitmap_allowed_as_real_image": false
},
{
"strategy_id": "identity_structured_fallback",
"trigger_signals": ["company", "product", "brand"],
"allowed_asset_types": ["none"],
"fallback_if_missing": "Render text badge, product/category labels, and source-visible identity metadata without fake logos.",
"source_type_allowlist": [],
"generated_bitmap_allowed_as_real_image": false
},
{
"strategy_id": "chart_when_quantified",
"trigger_signals": ["同比", "环比", "增长", "下降", "占比", "排名", "trend", "share"],
"allowed_asset_types": ["chart"],
"fallback_if_missing": "Use a clearly marked qualitative comparison table or unlabeled trend skeleton; do not fabricate numbers.",
"no_fake_data": true
},
{
"strategy_id": "structured_fallback",
"trigger_signals": ["abstract", "no verifiable media", "no data"],
"allowed_asset_types": ["none"],
"fallback_if_missing": "Use native structured cards, labels, and qualitative layout. Mark fallback explicitly.",
"no_fake_data": true
},
{
"strategy_id": "none_required",
"trigger_signals": ["user requested no images", "pure vector"],
"allowed_asset_types": ["none"],
"fallback_if_missing": "No image fallback is required because the user explicitly disabled image assets.",
"requires_user_override": true
}
]
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/asset-strategy-registry.schema.json",
"title": "SVGlide asset strategy registry",
"type": "object",
"required": ["version", "strategies"],
"additionalProperties": true,
"properties": {
"version": {"const": "svglide-asset-strategy-registry/v1"},
"strategies": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["strategy_id", "trigger_signals", "allowed_asset_types", "fallback_if_missing"],
"additionalProperties": true
}
}
}
}

View File

@@ -0,0 +1,223 @@
{
"version": "beautiful-html-template-cleanup-map/v1",
"policy": "default_delete_extract_unique_signal_only",
"cleanup_candidates": [
{
"target": "skills/lark-slides/references/style-presets.json",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"no runtime code imports style-presets.json or style-presets.md",
"golden preview cases pass with family tokens only"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/style-presets.md",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"no prompt, planner, matcher, or renderer imports style-presets.md",
"banned low-level SVG instruction patterns have zero runtime references"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/svg-visual-recipes.md",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"no prompt, planner, matcher, or renderer imports svg-visual-recipes.md",
"path_flow, connector_flow, svg_effects, required_primitives, and explicit path/line prompt rules have zero runtime references"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/scripts/artboard_renderer/themes",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"theme cache can be generated from beautiful-html-template-families.json",
"renderer tests pass without hand-written theme source files"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/svglide-template-registry.json",
"action": "extract_minimal_signal_then_delete",
"deletion_allowed": true,
"unique_signal_evidence": [
"page_role to variant mapping that cannot be reconstructed from beautiful-html-template metadata"
],
"extract_to": [
"skills/lark-slides/references/beautiful-html-template-families.json::families[].variants"
],
"delete_after": [
"template family matcher covers old template selection golden cases",
"no runtime code imports svglide-template-registry.json"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/svglide-component-registry.json",
"action": "extract_minimal_signal_then_delete",
"deletion_allowed": true,
"unique_signal_evidence": [
"semantic block to component binding not already represented in component-registry.json"
],
"extract_to": [
"skills/lark-slides/references/component-registry.json"
],
"delete_after": [
"component selection tests pass with unified component-registry.json",
"no runtime code imports svglide-component-registry.json"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/svglide-palette-registry.json",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"family palette roles and protected brand override cover runtime palette selection",
"no runtime code imports svglide-palette-registry.json"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/absorptions/beautiful-html-templates",
"action": "extract_minimal_signal_then_delete",
"deletion_allowed": true,
"unique_signal_evidence": [
"source_item_id, source_context_refs, svglide_asset_ids, absorbed_as provenance"
],
"extract_to": [
"skills/lark-slides/references/beautiful-html-template-families.json::families[].source",
"skills/lark-slides/references/beautiful-html-template-families.json::families[].svglide_mapping"
],
"delete_after": [
"all 15 existing absorption records are represented in family registry provenance",
"no runtime code imports absorption JSON directly"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/beautiful-html-template-presets.json",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"beautiful-html-template-families.json is the only beautiful template runtime entrypoint",
"no compatibility alias or fallback preset index remains"
],
"runtime_import_allowed": false
},
{
"target": "skills/lark-slides/references/beautiful-html-template-presets.md",
"action": "default_delete",
"deletion_allowed": true,
"unique_signal_evidence": [],
"extract_to": [],
"delete_after": [
"SKILL.md points to family registry and matcher, not presets",
"no compatibility alias or fallback preset doc remains"
],
"runtime_import_allowed": false
}
],
"content_cleanup_candidates": [
{
"target": "skills/lark-slides/references/svglide-canvas-plan.schema.json",
"action": "remove_required_low_level_svg_fields",
"delete_fields": [
"style_preset",
"style_selection_reason",
"style_system",
"visual_recipe",
"visual_signature",
"svg_effects",
"required_primitives",
"svg_primitives"
],
"keep_fields": [
"template_family_selection",
"template_variant",
"semantic_blocks",
"component_selection",
"asset_strategy",
"image_slots"
],
"runtime_import_allowed": true
},
{
"target": "skills/lark-slides/references/svglide-visual-planning.md",
"action": "partial_rewrite",
"delete_patterns": [
"SVG-native advantage",
"path composition",
"dense geometry",
"dashboard frame",
"technical_texture"
],
"keep_scope": [
"text safety",
"box model",
"safe area",
"overflow prevention"
],
"runtime_import_allowed": true
},
{
"target": "skills/lark-slides/references/visual-planning.md",
"action": "partial_rewrite",
"delete_patterns": [
"spine",
"connected by arrows or lines",
"timeline line",
"process arrow"
],
"keep_scope": [
"layout intent",
"text density",
"title area",
"page role"
],
"runtime_import_allowed": true
}
],
"banned_low_level_svg_instruction_patterns": [
"path_flow",
"connector_flow",
"svg_effects",
"required_primitives",
"svg_primitives",
"explicit path/line",
"curved route path",
"connector density",
"SVG-native advantage",
"dashboard frame",
"technical_texture"
],
"protected_assets": [
{"target": "satori", "reason": "renderer capability reference"},
{"target": "og-images-generator", "reason": "pipeline reference"},
{"target": "skills/lark-slides/scripts/svglide_contract_compile.py", "reason": "protocol injection boundary"},
{"target": "skills/lark-slides/scripts/svg_preflight.py", "reason": "validation boundary"},
{"target": "skills/lark-slides/references/svglide-generate-svg.contract.md", "reason": "generate_svg stage contract"},
{"target": "skills/lark-slides/references/safe-native-v1.profile.json", "reason": "renderer safety profile"},
{"target": "skills/lark-slides/references/svglide-brand-palette-registry.json", "reason": "brand override layer"},
{"target": "ppt-master", "reason": "external reference for chart and deck structure"},
{"target": "skills/lark-slides/assets/templates", "reason": "legacy XML entrypoints may serve non-SVGlide flows"}
]
}

View File

@@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/beautiful-html-template-cleanup-map.schema.json",
"title": "SVGlide beautiful template cleanup map",
"type": "object",
"required": ["version", "policy", "cleanup_candidates", "content_cleanup_candidates", "banned_low_level_svg_instruction_patterns", "protected_assets"],
"additionalProperties": true,
"properties": {
"version": {"const": "beautiful-html-template-cleanup-map/v1"},
"policy": {"const": "default_delete_extract_unique_signal_only"},
"cleanup_candidates": {
"type": "array",
"items": {
"type": "object",
"required": ["target", "action", "deletion_allowed", "unique_signal_evidence", "extract_to", "delete_after", "runtime_import_allowed"],
"additionalProperties": true,
"properties": {
"action": {"enum": ["default_delete", "extract_minimal_signal_then_delete"]},
"deletion_allowed": {"const": true},
"runtime_import_allowed": {"const": false}
}
}
},
"content_cleanup_candidates": {"type": "array"},
"banned_low_level_svg_instruction_patterns": {"type": "array", "items": {"type": "string"}},
"protected_assets": {"type": "array", "items": {"type": "object", "required": ["target", "reason"], "additionalProperties": true}}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,188 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/beautiful-html-template-families.schema.json",
"title": "SVGlide beautiful-html-template families",
"type": "object",
"required": ["version", "source", "families"],
"additionalProperties": true,
"properties": {
"version": {"const": "beautiful-html-template-families/v1"},
"source": {
"type": "object",
"required": ["repo", "template_count"],
"additionalProperties": true,
"properties": {
"repo": {"const": "beautiful-html-templates"},
"template_count": {"type": "integer", "minimum": 1}
}
},
"families": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"template_id",
"source",
"status",
"claim_level",
"runtime_policy",
"font_policy",
"cjk_policy",
"family_usage_policy",
"extension_grammar",
"semantic_fit",
"design_tokens",
"visual_dna",
"component_candidates",
"layout_variants",
"variants"
],
"additionalProperties": true,
"properties": {
"template_id": {"type": "string", "minLength": 1},
"source": {
"type": "object",
"required": ["source_template_json", "source_design_md", "source_template_html", "source_screenshots"],
"additionalProperties": true
},
"status": {"enum": ["source_inventoried", "absorbed", "blocked", "reference_only", "active", "deprecated"]},
"claim_level": {"enum": ["source_inventory_only", "reference_asset_only", "svglide_absorbed"]},
"runtime_policy": {
"type": "object",
"required": ["direct_satori_svg_allowed", "requires_contract_compile", "requires_visual_qa"],
"additionalProperties": true,
"properties": {
"direct_satori_svg_allowed": {"const": false},
"requires_contract_compile": {"const": true},
"requires_visual_qa": {"const": true}
}
},
"font_policy": {
"type": "object",
"required": ["fallback_stack", "font_role_map"],
"additionalProperties": true
},
"cjk_policy": {
"type": "object",
"required": [
"strategy",
"display_font_cn",
"body_font_cn",
"runtime_font_policy",
"emphasis_policy",
"italic_policy",
"letter_spacing_policy",
"mixed_run_spacing",
"known_degradation",
"source_section_sha256"
],
"additionalProperties": true,
"properties": {
"runtime_font_policy": {"const": "system_font_only_no_remote_dependency"},
"mixed_run_spacing": {"enum": ["pangu_spacing", "none_required"]}
}
},
"family_usage_policy": {
"type": "object",
"required": [
"closed_visual_system",
"cross_family_layout_mix_allowed",
"recolor_allowed",
"font_substitution_allowed",
"extend_missing_layout_policy"
],
"additionalProperties": true,
"properties": {
"closed_visual_system": {"const": true},
"cross_family_layout_mix_allowed": {"const": false},
"recolor_allowed": {"const": false},
"font_substitution_allowed": {"const": false},
"extend_missing_layout_policy": {
"type": "object",
"required": ["same_fonts", "same_palette", "same_spacing_rhythm", "same_component_grammar", "same_decorative_vocabulary", "same_chrome"],
"additionalProperties": true
}
}
},
"extension_grammar": {
"type": "object",
"required": [
"layout_rhythm",
"spacing_rhythm",
"component_grammar",
"chrome_rules",
"decorative_vocabulary",
"allowed_new_layouts",
"forbidden_mutations",
"source_basis"
],
"additionalProperties": true
},
"semantic_fit": {
"type": "object",
"required": ["best_for", "industries", "tones", "avoid_when"],
"additionalProperties": true
},
"design_tokens": {
"type": "object",
"required": ["colors", "typography", "spacing", "radii", "components", "css_variables", "css_class_names"],
"additionalProperties": true
},
"visual_dna": {
"type": "object",
"required": ["palette_roles", "typography_roles", "decorative_motifs", "visual_effects", "screenshot_benchmarks"],
"additionalProperties": true,
"properties": {
"screenshot_benchmarks": {
"type": "array",
"minItems": 3,
"items": {
"type": "object",
"required": ["path", "role", "slide_number", "why_selected", "visual_targets", "acceptance_use"],
"additionalProperties": true,
"properties": {
"role": {"enum": ["cover_reference", "mid_deck_reference", "late_deck_reference"]},
"slide_number": {"type": "integer", "minimum": 1},
"visual_targets": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"acceptance_use": {"type": "array", "minItems": 1, "items": {"type": "string"}}
}
}
},
"visual_effects": {
"type": "array",
"items": {
"type": "object",
"required": ["effect_id", "lowering_policy"],
"additionalProperties": true,
"properties": {
"lowering_policy": {"enum": ["native_svg", "css_to_satori", "approximate", "reference_only"]}
}
}
}
}
},
"component_candidates": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"layout_variants": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["variant_id", "layout_intents", "required_slots"],
"additionalProperties": true
}
},
"variants": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["variant_id", "page_roles", "layout_intents", "required_slots"],
"additionalProperties": true
}
}
}
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,125 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SVGlide Beautiful Template Fidelity Receipt",
"type": "object",
"required": [
"schema_version",
"stage",
"status",
"template_id",
"reference_screenshot",
"render_screenshot",
"reference_selection",
"score",
"threshold",
"metrics",
"issues",
"generated_by",
"generator_version",
"command",
"reference_sha256",
"render_sha256"
],
"properties": {
"schema_version": {"const": "svglide-template-fidelity/v1"},
"stage": {"const": "template_fidelity"},
"status": {"enum": ["passed", "failed"]},
"template_id": {"type": "string", "minLength": 1},
"page_type": {"type": "string"},
"reference_screenshot": {"type": "string", "minLength": 1},
"render_screenshot": {"type": "string", "minLength": 1},
"generated_by": {"const": "beautiful_template_fidelity_check.py"},
"generator_version": {"type": "string", "minLength": 1},
"command": {
"type": "array",
"items": {"type": "string"}
},
"reference_sha256": {
"anyOf": [
{"type": "string", "pattern": "^[0-9a-f]{64}$"},
{"type": "null"}
]
},
"render_sha256": {
"anyOf": [
{"type": "string", "pattern": "^[0-9a-f]{64}$"},
{"type": "null"}
]
},
"reference_selection": {
"type": "object",
"required": ["rule", "path"],
"properties": {
"rule": {"type": "string", "minLength": 1},
"path": {"type": "string", "minLength": 1}
},
"additionalProperties": true
},
"score": {"type": "number", "minimum": 0, "maximum": 1},
"threshold": {"type": "number", "minimum": 0, "maximum": 1},
"metrics": {
"type": "object",
"required": [
"color_distribution",
"layout_structure",
"edge_density",
"whitespace",
"dominant_region",
"color_complexity",
"primary_color_alignment",
"layout_region",
"decorative_density",
"typographic_hierarchy"
],
"properties": {
"color_distribution": {"type": "number", "minimum": 0, "maximum": 1},
"layout_structure": {"type": "number", "minimum": 0, "maximum": 1},
"edge_density": {"type": "number", "minimum": 0, "maximum": 1},
"whitespace": {"type": "number", "minimum": 0, "maximum": 1},
"dominant_region": {"type": "number", "minimum": 0, "maximum": 1},
"color_complexity": {"type": "number", "minimum": 0, "maximum": 1},
"primary_color_alignment": {"type": "number", "minimum": 0, "maximum": 1},
"layout_region": {"type": "number", "minimum": 0, "maximum": 1},
"decorative_density": {"type": "number", "minimum": 0, "maximum": 1},
"typographic_hierarchy": {"type": "number", "minimum": 0, "maximum": 1}
},
"additionalProperties": true
},
"issues": {
"type": "array",
"items": {
"type": "object",
"required": ["code", "message"],
"properties": {
"code": {"type": "string", "minLength": 1},
"message": {"type": "string", "minLength": 1}
},
"additionalProperties": true
}
},
"role_consumption": {
"type": "object",
"required": ["source", "font_roles", "typography_roles", "text_style_roles"],
"properties": {
"source": {"type": "string", "minLength": 1},
"font_roles": {
"type": "object",
"required": ["display", "body", "label", "metric"],
"additionalProperties": true
},
"typography_roles": {
"type": "object",
"required": ["display", "body", "label", "metric"],
"additionalProperties": true
},
"text_style_roles": {
"type": "object",
"required": ["bold", "italic", "underline", "line_through", "emphasis", "text_decoration_policy"],
"additionalProperties": true
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}

View File

@@ -0,0 +1,26 @@
{
"version": "beautiful-template-issue-codes/v1",
"phase_0_contract_freeze": [
"asset_slot_unfilled",
"preview_missing_required_image",
"generated_bitmap_not_real_image",
"semantic_mismatch",
"asset_slot_shared_without_permission",
"asset_slot_count_mismatch",
"asset_source_type_not_allowed",
"live_submit_missing_file_token",
"unowned_decorative_primitive",
"decorative_motif_overuse"
],
"phase_1_knowledge_absorption": [
"cross_family_layout_mix",
"missing_extension_grammar",
"remote_font_dependency",
"cjk_fake_italic",
"cjk_letter_spacing_inherited",
"cjk_mixed_run_spacing_missing",
"family_recolor_without_override",
"source_inventoried_claim_escalation",
"missing_screenshot_benchmark_role"
]
}

View File

@@ -0,0 +1,276 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "svglide/beautiful-template-visual-contract.schema.json",
"title": "SVGlide Beautiful Template Visual Contract",
"type": "object",
"required": [
"version",
"family_id",
"runtime_template_id",
"source",
"layout",
"typography",
"palette",
"decorative",
"image",
"component",
"page_type",
"page_family",
"page_variants",
"satori",
"font_strategy",
"typography_strategy",
"text_style_strategy",
"do_not_simplify"
],
"properties": {
"version": {
"const": "svglide-beautiful-template-visual-contract/v1"
},
"family_id": {
"type": "string",
"minLength": 1
},
"runtime_template_id": {
"type": "string",
"minLength": 1
},
"source": {
"type": "object",
"required": [
"source_template_html",
"source_design_md",
"source_template_json",
"reference_screenshot"
]
},
"layout": {
"type": "object",
"minProperties": 1
},
"typography": {
"type": "object",
"minProperties": 1
},
"palette": {
"type": "object",
"minProperties": 1
},
"decorative": {
"type": "object",
"minProperties": 1
},
"image": {
"type": "object",
"minProperties": 1
},
"component": {
"type": "object",
"minProperties": 1
},
"page_type": {
"type": "object",
"minProperties": 1
},
"page_family": {
"type": "object",
"required": [
"source_slide_count",
"core_page_roles",
"production_minimum_roles"
],
"properties": {
"source_slide_count": {
"type": "integer",
"minimum": 1
},
"core_page_roles": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"production_minimum_roles": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
}
}
},
"page_variants": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"type": "object",
"required": [
"source_class",
"page_role",
"required_slots",
"source_refs",
"extraction_confidence"
],
"properties": {
"source_class": {
"type": "string",
"minLength": 1
},
"source_slide_index": {
"type": "integer",
"minimum": 1
},
"page_role": {
"type": "string",
"minLength": 1
},
"required_slots": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"optional_slots": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"reference_screenshot": {
"type": "string",
"minLength": 1
},
"source_refs": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["path", "selector_or_token", "raw_value"],
"properties": {
"path": {
"type": "string",
"minLength": 1
},
"selector_or_token": {
"type": "string",
"minLength": 1
},
"raw_value": {
"type": "string",
"minLength": 1
}
}
}
},
"extraction_confidence": {
"enum": [
"direct_from_design_md",
"css_extracted_from_template_html",
"inferred_from_layout",
"absent_use_default"
]
}
}
}
},
"satori": {
"type": "object",
"minProperties": 1
},
"font_strategy": {
"type": "object",
"required": [
"source_fonts",
"slide_native_preferred",
"adobe_or_embedded_fallback",
"cjk_fallback",
"role_mapping",
"forbidden",
"mapping_reason"
],
"properties": {
"source_fonts": {
"type": "array",
"minItems": 1
},
"slide_native_preferred": {
"type": "array",
"minItems": 1
},
"adobe_or_embedded_fallback": {
"type": "array",
"minItems": 1
},
"cjk_fallback": {
"type": "string",
"minLength": 1
},
"role_mapping": {
"type": "object",
"required": ["display", "body", "label", "metric"]
},
"forbidden": {
"type": "array",
"minItems": 1
},
"mapping_reason": {
"type": "string",
"minLength": 1
}
}
},
"typography_strategy": {
"type": "object",
"required": [
"source_typography_tokens",
"role_mapping",
"font_size_scale",
"font_weight_scale",
"line_height_scale",
"letter_spacing_scale",
"word_spacing",
"paragraph_spacing",
"text_transform_policy",
"hierarchy_ratio",
"max_lines",
"measure",
"alignment",
"wrapping_policy",
"text_direction",
"writing_mode",
"cjk_typography_adjustment",
"mapping_reason",
"extraction_confidence",
"source_refs"
]
},
"text_style_strategy": {
"type": "object",
"required": [
"bold",
"italic",
"underline",
"line_through",
"emphasis",
"text_decoration_policy",
"forbidden",
"extraction_confidence",
"source_refs"
]
},
"do_not_simplify": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
}
}
}

View File

@@ -0,0 +1,140 @@
{
"version": "svglide-component-registry/v1",
"components": [
{
"component_id": "title_block",
"fits_semantic_blocks": ["title", "context", "section"],
"required_data": ["title"],
"optional_data": ["subtitle", "eyebrow"],
"allowed_asset_types": ["none"],
"density": "low",
"compile_notes": "Use native text boxes and simple family-owned chrome."
},
{
"component_id": "section_label",
"fits_semantic_blocks": ["section", "chapter", "agenda_marker"],
"required_data": ["label"],
"optional_data": ["index", "subtitle"],
"allowed_asset_types": ["none"],
"density": "low",
"compile_notes": "Use compact family typography and owned divider treatment."
},
{
"component_id": "metric_card",
"fits_semantic_blocks": ["metric", "kpi", "delta", "target_progress"],
"required_data": ["label", "value"],
"optional_data": ["delta", "period", "source_note"],
"allowed_asset_types": ["chart", "none"],
"density": "medium",
"compile_notes": "Use text, rect, and optional mini chart. Do not fabricate numeric data."
},
{
"component_id": "finding_callout",
"fits_semantic_blocks": ["finding", "insight", "hypothesis"],
"required_data": ["statement"],
"optional_data": ["evidence", "owner"],
"allowed_asset_types": ["none", "screenshot"],
"density": "medium",
"compile_notes": "Use family surface and emphasis token, not ad hoc callout styling."
},
{
"component_id": "evidence_table",
"fits_semantic_blocks": ["evidence", "table", "source"],
"required_data": ["rows"],
"optional_data": ["columns", "source_note"],
"allowed_asset_types": ["none"],
"density": "high",
"compile_notes": "Use structured table-like native shapes; keep source notes visible."
},
{
"component_id": "comparison_matrix",
"fits_semantic_blocks": ["comparison", "competitor", "before_after"],
"required_data": ["items", "dimensions"],
"optional_data": ["verdict", "source_note"],
"allowed_asset_types": ["logo", "screenshot", "none"],
"density": "medium-high",
"compile_notes": "Use qualitative cells unless numeric data is provided."
},
{
"component_id": "timeline",
"fits_semantic_blocks": ["timeline", "roadmap", "milestone"],
"required_data": ["steps"],
"optional_data": ["dates", "owners"],
"allowed_asset_types": ["none"],
"density": "medium",
"compile_notes": "Use family variant semantics; do not require explicit path/line primitives."
},
{
"component_id": "process_flow",
"fits_semantic_blocks": ["process", "workflow", "journey"],
"required_data": ["steps"],
"optional_data": ["inputs", "outputs"],
"allowed_asset_types": ["none"],
"density": "medium",
"compile_notes": "Represent process as semantic steps; family decides visual form."
},
{
"component_id": "image_panel",
"fits_semantic_blocks": ["company", "product", "person", "case", "food", "place"],
"required_data": ["semantic_subject"],
"optional_data": ["caption", "source_note"],
"allowed_asset_types": ["photo", "logo", "screenshot"],
"density": "medium",
"compile_notes": "Must bind to image_slots when real_image_required is selected."
},
{
"component_id": "logo_strip",
"fits_semantic_blocks": ["company", "partner", "ecosystem"],
"required_data": ["logos"],
"optional_data": ["labels"],
"allowed_asset_types": ["logo"],
"density": "medium",
"compile_notes": "Logo assets must use logo slots; do not replace with photos."
},
{
"component_id": "mini_chart",
"fits_semantic_blocks": ["metric", "trend", "share"],
"required_data": ["data"],
"optional_data": ["axis_labels", "source_note"],
"allowed_asset_types": ["chart"],
"density": "medium",
"compile_notes": "Only draw concrete values when user or source data provides them."
},
{
"component_id": "qualitative_radar",
"fits_semantic_blocks": ["capability", "evaluation", "comparison"],
"required_data": ["dimensions", "items"],
"optional_data": ["scores", "source_note"],
"allowed_asset_types": ["chart", "none"],
"density": "medium-high",
"compile_notes": "Use qualitative labels unless numeric scores are provided by source data."
},
{
"component_id": "action_list",
"fits_semantic_blocks": ["action", "next_step", "owner"],
"required_data": ["actions"],
"optional_data": ["owners", "deadline"],
"allowed_asset_types": ["none"],
"density": "medium",
"compile_notes": "Use native text list with family token hierarchy."
},
{
"component_id": "risk_matrix",
"fits_semantic_blocks": ["risk", "dependency", "decision"],
"required_data": ["risks"],
"optional_data": ["severity", "mitigation"],
"allowed_asset_types": ["none"],
"density": "medium-high",
"compile_notes": "Use structured cells; do not invent probability values."
},
{
"component_id": "architecture_diagram",
"fits_semantic_blocks": ["architecture", "system", "dependency", "module"],
"required_data": ["nodes"],
"optional_data": ["edges", "risks", "owners"],
"allowed_asset_types": ["none", "screenshot"],
"density": "medium-high",
"compile_notes": "Represent modules and dependencies as semantic boxes; do not require decorative connector primitives."
}
]
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://larksuite.local/component-registry.schema.json",
"title": "SVGlide semantic component registry",
"type": "object",
"required": ["version", "components"],
"additionalProperties": true,
"properties": {
"version": {"const": "svglide-component-registry/v1"},
"components": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["component_id", "fits_semantic_blocks", "required_data", "allowed_asset_types", "compile_notes"],
"additionalProperties": true
}
}
}
}

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-1" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">cover</div></foreignObject>
<rect id="hero-panel" slide:role="shape" x="64" y="150" width="520" height="230" fill="#ffffff" stroke="#111827" />
</svg>

After

Width:  |  Height:  |  Size: 685 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-2" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">agenda</div></foreignObject>
<circle id="capability-glyph-0" slide:role="shape" cx="96" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-1" slide:role="shape" cx="266" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-2" slide:role="shape" cx="436" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-3" slide:role="shape" cx="96" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-4" slide:role="shape" cx="266" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-5" slide:role="shape" cx="436" cy="270" r="22" fill="#2563EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-3" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">context overview</div></foreignObject>
<rect id="metric-bar-1" slide:role="shape" x="120" y="214" width="180" height="18" fill="#2563EB" />
<rect id="metric-bar-2" slide:role="shape" x="120" y="258" width="140" height="18" fill="#2563EB" />
<rect id="metric-bar-3" slide:role="shape" x="120" y="302" width="210" height="18" fill="#2563EB" />
<rect id="metric-bar-4" slide:role="shape" x="120" y="346" width="120" height="18" fill="#2563EB" />
<foreignObject id="metric-label-3" slide:role="shape" slide:shape-type="text" x="120" y="150" width="260" height="34"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">metric structure</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-4" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">metric dashboard</div></foreignObject>
<rect id="dashboard-card-0" slide:role="shape" x="80" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-0" slide:role="shape" x="98" y="230" width="90" height="16" fill="#2563EB" />
<rect id="dashboard-card-1" slide:role="shape" x="285" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-1" slide:role="shape" x="303" y="230" width="102" height="16" fill="#2563EB" />
<rect id="dashboard-card-2" slide:role="shape" x="490" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-2" slide:role="shape" x="508" y="230" width="114" height="16" fill="#2563EB" />
<rect id="dashboard-card-3" slide:role="shape" x="695" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-3" slide:role="shape" x="713" y="230" width="126" height="16" fill="#2563EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<defs><linearGradient id="depth-gradient" x1="0" x2="1"><stop offset="0%" stop-color="#2563EB"/><stop offset="100%" stop-color="#F2D4CF"/></linearGradient></defs>
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-5" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">problem analysis</div></foreignObject>
<rect id="gradient-slab" slide:role="shape" x="90" y="150" width="740" height="230" fill="url(#depth-gradient)" />
</svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-6" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">cause analysis</div></foreignObject>
<rect id="spotlight-focus-panel" slide:role="shape" x="96" y="158" width="360" height="180" fill="#ffffff" stroke="#2563EB" />
<foreignObject id="annotation-label-6" slide:role="shape" slide:shape-type="text" x="520" y="200" width="260" height="58"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:22px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">annotation focus</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 999 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-7" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">comparison</div></foreignObject>
<rect id="brand-system-panel" slide:role="shape" x="90" y="150" width="740" height="230" fill="#ffffff" stroke="#111827" />
<foreignObject id="brand-system-7" slide:role="shape" slide:shape-type="text" x="130" y="210" width="260" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">summary</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 979 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-8" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">case evidence</div></foreignObject>
<rect id="hero-panel" slide:role="shape" x="64" y="150" width="520" height="230" fill="#ffffff" stroke="#111827" />
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-9" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">action plan</div></foreignObject>
<circle id="capability-glyph-0" slide:role="shape" cx="96" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-1" slide:role="shape" cx="266" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-2" slide:role="shape" cx="436" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-3" slide:role="shape" cx="96" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-4" slide:role="shape" cx="266" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-5" slide:role="shape" cx="436" cy="270" r="22" fill="#2563EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-10" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">Closing summary</div></foreignObject>
<rect id="brand-system-panel" slide:role="shape" x="90" y="150" width="740" height="230" fill="#ffffff" stroke="#111827" />
<foreignObject id="brand-system-10" slide:role="shape" slide:shape-type="text" x="130" y="210" width="260" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">summary</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@@ -0,0 +1,10 @@
<!doctype html><html><body><img src="page-001.svg" />
<img src="page-002.svg" />
<img src="page-003.svg" />
<img src="page-004.svg" />
<img src="page-005.svg" />
<img src="page-006.svg" />
<img src="page-007.svg" />
<img src="page-008.svg" />
<img src="page-009.svg" />
<img src="page-010.svg" /></body></html>

View File

@@ -0,0 +1,30 @@
{
"version": "beautiful-template-e2e-dry-run/v1",
"case_id": "internal-review",
"query": "internal business review for management with metrics evidence and action plan",
"selected_template_id": "blue-professional",
"candidate_template_ids": [
"blue-professional",
"signal",
"emerald-editorial"
],
"slide_count": 10,
"template_variant_count": 10,
"component_count": 9,
"required_image_slots": 0,
"rendered_image_count": 0,
"required_image_fill_rate": 1.0,
"unowned_decorative_primitive_count": 0,
"preflight_summary": {
"file_count": 10,
"error_count": 0,
"warning_count": 0,
"plan_count": 1
},
"status": "passed",
"artifacts": {
"plan": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/internal-review/slide_plan.json",
"preview": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/internal-review/preview.html",
"receipt": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/internal-review/receipt.json"
}
}

View File

@@ -0,0 +1,657 @@
{
"page_count": 10,
"output_mode": "svglide-svg",
"plan_path": ".lark-slides/plan/internal-review/slide_plan.json",
"loaded_rule_set": [
"skills/lark-slides/references/lark-slides-create-svg.md",
"skills/lark-slides/references/svg-aesthetic-review.md",
"skills/lark-slides/references/svg-protocol.md",
"skills/lark-slides/references/svglide-artifacts.spec.md",
"skills/lark-slides/references/svglide-assets.contract.md",
"skills/lark-slides/references/svglide-checks.checklist.md",
"skills/lark-slides/references/svglide-create-svg.contract.md",
"skills/lark-slides/references/svglide-generate-svg.contract.md",
"skills/lark-slides/references/svglide-lock.contract.md",
"skills/lark-slides/references/svglide-plan.contract.md",
"skills/lark-slides/references/svglide-planning-layer.md",
"skills/lark-slides/references/svglide-ppt-master-migration.matrix.md",
"skills/lark-slides/references/svglide-preview.spec.md",
"skills/lark-slides/references/svglide-readback.contract.md",
"skills/lark-slides/references/svglide-route-admission.md",
"skills/lark-slides/references/svglide-svg-private.rules.json",
"skills/lark-slides/references/svglide-validation-checklist.md",
"skills/lark-slides/references/svglide-visual-planning.md",
"skills/lark-slides/references/svglide-workflow.spec.md"
],
"quality_gates": {
"no_text_overflow": true,
"no_debug_guides": true,
"no_xml_like_pages": true
},
"art_direction": {
"cover_treatment": "template-family cover with one dominant structured visual block",
"section_divider_treatment": "template-family section rhythm when section pages exist",
"closing_treatment": "closing summary mirrors the selected family motif",
"deck_motif": "beautiful-html-template family translated into native SVG structure",
"svg_native_moments": [
"cover structure",
"comparison grid",
"closing motif"
]
},
"template_family_selection": {
"enabled": true,
"source": "beautiful-html-template-families",
"selected_template_id": "blue-professional",
"candidate_template_ids": [
"blue-professional",
"signal",
"emerald-editorial"
],
"selection_reason": "business review fit; preferred analytical family; business review semantic fit"
},
"svg_files": [
{
"page": 1,
"path": "page-001.svg"
},
{
"page": 2,
"path": "page-002.svg"
},
{
"page": 3,
"path": "page-003.svg"
},
{
"page": 4,
"path": "page-004.svg"
},
{
"page": 5,
"path": "page-005.svg"
},
{
"page": 6,
"path": "page-006.svg"
},
{
"page": 7,
"path": "page-007.svg"
},
{
"page": 8,
"path": "page-008.svg"
},
{
"page": 9,
"path": "page-009.svg"
},
{
"page": 10,
"path": "page-010.svg"
}
],
"slides": [
{
"page": 1,
"title": "cover",
"renderer_id": "hero_1",
"layout_family": "hero",
"density": "medium",
"visual_intent": "show cover with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "cover rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "cover",
"semantic_blocks": [
{
"block_id": "title_1",
"type": "title",
"content": "cover key message"
},
{
"block_id": "hero_finding_1",
"type": "finding",
"content": "internal business review for management with metrics evidence and action plan"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_1"
]
},
{
"component_id": "finding_callout",
"binds": [
"hero_finding_1"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 2,
"title": "agenda",
"renderer_id": "capability_map_2",
"layout_family": "capability_map",
"density": "medium",
"visual_intent": "show agenda with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "agenda rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "agenda",
"semantic_blocks": [
{
"block_id": "title_2",
"type": "title",
"content": "agenda key message"
},
{
"block_id": "agenda_2",
"type": "agenda",
"content": "Key sections and decision flow"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_2"
]
},
{
"component_id": "action_list",
"binds": [
"agenda_2"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 3,
"title": "context overview",
"renderer_id": "scorecard_3",
"layout_family": "scorecard",
"density": "medium",
"visual_intent": "show context_overview with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "context_overview rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "context_overview",
"semantic_blocks": [
{
"block_id": "title_3",
"type": "title",
"content": "context_overview key message"
},
{
"block_id": "finding_3",
"type": "finding",
"content": "context_overview finding"
},
{
"block_id": "evidence_3",
"type": "evidence",
"content": "Source-backed evidence table"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_3"
]
},
{
"component_id": "finding_callout",
"binds": [
"finding_3"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_3"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 4,
"title": "metric dashboard",
"renderer_id": "dashboard_4",
"layout_family": "dashboard",
"density": "medium",
"visual_intent": "show metric_dashboard with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "metric_dashboard rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "metric_dashboard",
"semantic_blocks": [
{
"block_id": "title_4",
"type": "title",
"content": "metric_dashboard key message"
},
{
"block_id": "metric_4",
"type": "metric",
"content": "Metric requires provided data"
},
{
"block_id": "kpi_4",
"type": "kpi",
"content": "KPI context without fabricated numbers"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_4"
]
},
{
"component_id": "metric_card",
"binds": [
"metric_4"
]
},
{
"component_id": "metric_card",
"binds": [
"kpi_4"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 5,
"title": "problem analysis",
"renderer_id": "depth_5",
"layout_family": "depth",
"density": "medium",
"visual_intent": "show problem_analysis with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "problem_analysis rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "problem_analysis",
"semantic_blocks": [
{
"block_id": "title_5",
"type": "title",
"content": "problem_analysis key message"
},
{
"block_id": "finding_5",
"type": "finding",
"content": "problem_analysis finding"
},
{
"block_id": "evidence_5",
"type": "evidence",
"content": "Source-backed evidence table"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_5"
]
},
{
"component_id": "finding_callout",
"binds": [
"finding_5"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_5"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 6,
"title": "cause analysis",
"renderer_id": "annotation_6",
"layout_family": "annotation",
"density": "medium",
"visual_intent": "show cause_analysis with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "cause_analysis rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "cause_analysis",
"semantic_blocks": [
{
"block_id": "title_6",
"type": "title",
"content": "cause_analysis key message"
},
{
"block_id": "finding_6",
"type": "finding",
"content": "cause_analysis finding"
},
{
"block_id": "evidence_6",
"type": "evidence",
"content": "Source-backed evidence table"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_6"
]
},
{
"component_id": "finding_callout",
"binds": [
"finding_6"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_6"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 7,
"title": "comparison",
"renderer_id": "brand_7",
"layout_family": "brand",
"density": "medium",
"visual_intent": "show comparison with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "comparison rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "comparison",
"semantic_blocks": [
{
"block_id": "title_7",
"type": "title",
"content": "comparison key message"
},
{
"block_id": "comparison_7",
"type": "comparison",
"content": "Compare entities, claims, and constraints"
},
{
"block_id": "company_7",
"type": "company",
"content": "internal business review for management with metrics evidence and action plan"
},
{
"block_id": "evidence_7",
"type": "evidence",
"content": "Evidence slot tied to source material"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_7"
]
},
{
"component_id": "comparison_matrix",
"binds": [
"comparison_7"
]
},
{
"component_id": "image_panel",
"binds": [
"company_7"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_7"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 8,
"title": "case evidence",
"renderer_id": "hero_8",
"layout_family": "hero",
"density": "medium",
"visual_intent": "show case_evidence with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "case_evidence rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "case_evidence",
"semantic_blocks": [
{
"block_id": "title_8",
"type": "title",
"content": "case_evidence key message"
},
{
"block_id": "comparison_8",
"type": "comparison",
"content": "Compare entities, claims, and constraints"
},
{
"block_id": "company_8",
"type": "company",
"content": "internal business review for management with metrics evidence and action plan"
},
{
"block_id": "evidence_8",
"type": "evidence",
"content": "Evidence slot tied to source material"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_8"
]
},
{
"component_id": "comparison_matrix",
"binds": [
"comparison_8"
]
},
{
"component_id": "image_panel",
"binds": [
"company_8"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_8"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 9,
"title": "action plan",
"renderer_id": "capability_map_9",
"layout_family": "capability_map",
"density": "medium",
"visual_intent": "show action_plan with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "action_plan rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "action_plan",
"semantic_blocks": [
{
"block_id": "title_9",
"type": "title",
"content": "action_plan key message"
},
{
"block_id": "action_9",
"type": "action",
"content": "Owner, next action, and deadline"
},
{
"block_id": "process_9",
"type": "process",
"content": "Execution sequence"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_9"
]
},
{
"component_id": "action_list",
"binds": [
"action_9"
]
},
{
"component_id": "process_flow",
"binds": [
"process_9"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 10,
"title": "Closing summary",
"renderer_id": "brand_10",
"layout_family": "brand",
"density": "medium",
"visual_intent": "show risk_dependency with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "risk_dependency rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "risk_dependency",
"semantic_blocks": [
{
"block_id": "title_10",
"type": "title",
"content": "risk_dependency key message"
},
{
"block_id": "risk_10",
"type": "risk",
"content": "Risk and dependency register"
},
{
"block_id": "action_10",
"type": "action",
"content": "Mitigation owner"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_10"
]
},
{
"component_id": "risk_matrix",
"binds": [
"risk_10"
]
},
{
"component_id": "action_list",
"binds": [
"action_10"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
}
]
}

View File

@@ -0,0 +1,67 @@
{
"version": "beautiful-template-e2e-dry-run-summary/v1",
"status": "passed",
"receipt_count": 2,
"receipts": [
{
"version": "beautiful-template-e2e-dry-run/v1",
"case_id": "internal-review",
"query": "internal business review for management with metrics evidence and action plan",
"selected_template_id": "blue-professional",
"candidate_template_ids": [
"blue-professional",
"signal",
"emerald-editorial"
],
"slide_count": 10,
"template_variant_count": 10,
"component_count": 9,
"required_image_slots": 0,
"rendered_image_count": 0,
"required_image_fill_rate": 1.0,
"unowned_decorative_primitive_count": 0,
"preflight_summary": {
"file_count": 10,
"error_count": 0,
"warning_count": 0,
"plan_count": 1
},
"status": "passed",
"artifacts": {
"plan": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/internal-review/slide_plan.json",
"preview": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/internal-review/preview.html",
"receipt": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/internal-review/receipt.json"
}
},
{
"version": "beautiful-template-e2e-dry-run/v1",
"case_id": "zhipu-minimax",
"query": "Zhipu and MiniMax product comparison with company identity and real image slots",
"selected_template_id": "blue-professional",
"candidate_template_ids": [
"blue-professional",
"raw-grid",
"cartesian"
],
"slide_count": 10,
"template_variant_count": 6,
"component_count": 7,
"required_image_slots": 2,
"rendered_image_count": 2,
"required_image_fill_rate": 1.0,
"unowned_decorative_primitive_count": 0,
"preflight_summary": {
"file_count": 10,
"error_count": 0,
"warning_count": 2,
"plan_count": 1
},
"status": "passed",
"artifacts": {
"plan": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/zhipu-minimax/slide_plan.json",
"preview": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/zhipu-minimax/preview.html",
"receipt": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/zhipu-minimax/receipt.json"
}
}
]
}

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-1" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">cover</div></foreignObject>
<rect id="hero-panel" slide:role="shape" x="64" y="150" width="520" height="230" fill="#ffffff" stroke="#111827" />
<image id="required-real-image" slide:role="image" href="https://example.com/zhipu-minimax-product.png" x="600" y="150" width="260" height="160" />
<rect id="image-overlay" slide:role="shape" x="600" y="150" width="260" height="160" fill="#111827" opacity="0.18" />
</svg>

After

Width:  |  Height:  |  Size: 955 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-2" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">agenda</div></foreignObject>
<circle id="capability-glyph-0" slide:role="shape" cx="96" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-1" slide:role="shape" cx="266" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-2" slide:role="shape" cx="436" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-3" slide:role="shape" cx="96" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-4" slide:role="shape" cx="266" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-5" slide:role="shape" cx="436" cy="270" r="22" fill="#2563EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-3" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">context overview</div></foreignObject>
<rect id="metric-bar-1" slide:role="shape" x="120" y="214" width="180" height="18" fill="#2563EB" />
<rect id="metric-bar-2" slide:role="shape" x="120" y="258" width="140" height="18" fill="#2563EB" />
<rect id="metric-bar-3" slide:role="shape" x="120" y="302" width="210" height="18" fill="#2563EB" />
<rect id="metric-bar-4" slide:role="shape" x="120" y="346" width="120" height="18" fill="#2563EB" />
<foreignObject id="metric-label-3" slide:role="shape" slide:shape-type="text" x="120" y="150" width="260" height="34"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:18px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">metric structure</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-4" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">comparison</div></foreignObject>
<rect id="dashboard-card-0" slide:role="shape" x="80" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-0" slide:role="shape" x="98" y="230" width="90" height="16" fill="#2563EB" />
<rect id="dashboard-card-1" slide:role="shape" x="285" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-1" slide:role="shape" x="303" y="230" width="102" height="16" fill="#2563EB" />
<rect id="dashboard-card-2" slide:role="shape" x="490" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-2" slide:role="shape" x="508" y="230" width="114" height="16" fill="#2563EB" />
<rect id="dashboard-card-3" slide:role="shape" x="695" y="150" width="170" height="110" fill="#ffffff" stroke="#CBD5E1" />
<rect id="dashboard-bar-3" slide:role="shape" x="713" y="230" width="126" height="16" fill="#2563EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<defs><linearGradient id="depth-gradient" x1="0" x2="1"><stop offset="0%" stop-color="#2563EB"/><stop offset="100%" stop-color="#F2D4CF"/></linearGradient></defs>
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-5" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">action plan</div></foreignObject>
<rect id="gradient-slab" slide:role="shape" x="90" y="150" width="740" height="230" fill="url(#depth-gradient)" />
</svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-6" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">closing</div></foreignObject>
<rect id="spotlight-focus-panel" slide:role="shape" x="96" y="158" width="360" height="180" fill="#ffffff" stroke="#2563EB" />
<foreignObject id="annotation-label-6" slide:role="shape" slide:shape-type="text" x="520" y="200" width="260" height="58"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:22px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">annotation focus</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-7" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">cover</div></foreignObject>
<rect id="brand-system-panel" slide:role="shape" x="90" y="150" width="740" height="230" fill="#ffffff" stroke="#111827" />
<foreignObject id="brand-system-7" slide:role="shape" slide:shape-type="text" x="130" y="210" width="260" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">summary</div></foreignObject>
<image id="required-real-image" slide:role="image" href="https://example.com/zhipu-minimax-product.png" x="600" y="150" width="260" height="160" />
<rect id="image-overlay" slide:role="shape" x="600" y="150" width="260" height="160" fill="#111827" opacity="0.18" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-8" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">agenda</div></foreignObject>
<rect id="hero-panel" slide:role="shape" x="64" y="150" width="520" height="230" fill="#ffffff" stroke="#111827" />
</svg>

After

Width:  |  Height:  |  Size: 686 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-9" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">context overview</div></foreignObject>
<circle id="capability-glyph-0" slide:role="shape" cx="96" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-1" slide:role="shape" cx="266" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-2" slide:role="shape" cx="436" cy="160" r="22" fill="#2563EB" />
<circle id="capability-glyph-3" slide:role="shape" cx="96" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-4" slide:role="shape" cx="266" cy="270" r="22" fill="#2563EB" />
<circle id="capability-glyph-5" slide:role="shape" cx="436" cy="270" r="22" fill="#2563EB" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" slide:contract-version="svglide-authoring-contract/v1" width="960" height="540" viewBox="0 0 960 540">
<rect slide:role="shape" x="0" y="0" width="960" height="540" fill="#F5F5F5" />
<foreignObject id="title-10" slide:role="shape" slide:shape-type="text" x="64" y="44" width="620" height="90"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:44px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">Closing summary</div></foreignObject>
<rect id="brand-system-panel" slide:role="shape" x="90" y="150" width="740" height="230" fill="#ffffff" stroke="#111827" />
<foreignObject id="brand-system-10" slide:role="shape" slide:shape-type="text" x="130" y="210" width="260" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="font-size:24px;font-weight:800;font-family:Arial;color:#111827;line-height:1.16;">summary</div></foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@@ -0,0 +1,10 @@
<!doctype html><html><body><img src="page-001.svg" />
<img src="page-002.svg" />
<img src="page-003.svg" />
<img src="page-004.svg" />
<img src="page-005.svg" />
<img src="page-006.svg" />
<img src="page-007.svg" />
<img src="page-008.svg" />
<img src="page-009.svg" />
<img src="page-010.svg" /></body></html>

View File

@@ -0,0 +1,30 @@
{
"version": "beautiful-template-e2e-dry-run/v1",
"case_id": "zhipu-minimax",
"query": "Zhipu and MiniMax product comparison with company identity and real image slots",
"selected_template_id": "blue-professional",
"candidate_template_ids": [
"blue-professional",
"raw-grid",
"cartesian"
],
"slide_count": 10,
"template_variant_count": 6,
"component_count": 7,
"required_image_slots": 2,
"rendered_image_count": 2,
"required_image_fill_rate": 1.0,
"unowned_decorative_primitive_count": 0,
"preflight_summary": {
"file_count": 10,
"error_count": 0,
"warning_count": 2,
"plan_count": 1
},
"status": "passed",
"artifacts": {
"plan": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/zhipu-minimax/slide_plan.json",
"preview": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/zhipu-minimax/preview.html",
"receipt": "/Users/bytedance/bd-projects/workspaces/SVGlide/.worktrees/cli-svglide-svg-private/skills/lark-slides/references/examples/beautiful-template-dry-runs/zhipu-minimax/receipt.json"
}
}

View File

@@ -0,0 +1,668 @@
{
"page_count": 10,
"output_mode": "svglide-svg",
"plan_path": ".lark-slides/plan/zhipu-minimax/slide_plan.json",
"loaded_rule_set": [
"skills/lark-slides/references/lark-slides-create-svg.md",
"skills/lark-slides/references/svg-aesthetic-review.md",
"skills/lark-slides/references/svg-protocol.md",
"skills/lark-slides/references/svglide-artifacts.spec.md",
"skills/lark-slides/references/svglide-assets.contract.md",
"skills/lark-slides/references/svglide-checks.checklist.md",
"skills/lark-slides/references/svglide-create-svg.contract.md",
"skills/lark-slides/references/svglide-generate-svg.contract.md",
"skills/lark-slides/references/svglide-lock.contract.md",
"skills/lark-slides/references/svglide-plan.contract.md",
"skills/lark-slides/references/svglide-planning-layer.md",
"skills/lark-slides/references/svglide-ppt-master-migration.matrix.md",
"skills/lark-slides/references/svglide-preview.spec.md",
"skills/lark-slides/references/svglide-readback.contract.md",
"skills/lark-slides/references/svglide-route-admission.md",
"skills/lark-slides/references/svglide-svg-private.rules.json",
"skills/lark-slides/references/svglide-validation-checklist.md",
"skills/lark-slides/references/svglide-visual-planning.md",
"skills/lark-slides/references/svglide-workflow.spec.md"
],
"quality_gates": {
"no_text_overflow": true,
"no_debug_guides": true,
"no_xml_like_pages": true
},
"art_direction": {
"cover_treatment": "template-family cover with one dominant structured visual block",
"section_divider_treatment": "template-family section rhythm when section pages exist",
"closing_treatment": "closing summary mirrors the selected family motif",
"deck_motif": "beautiful-html-template family translated into native SVG structure",
"svg_native_moments": [
"cover structure",
"comparison grid",
"closing motif"
]
},
"template_family_selection": {
"enabled": true,
"source": "beautiful-html-template-families",
"selected_template_id": "blue-professional",
"candidate_template_ids": [
"blue-professional",
"raw-grid",
"cartesian"
],
"selection_reason": "supports image panel; company/product comparison fit"
},
"svg_files": [
{
"page": 1,
"path": "page-001.svg"
},
{
"page": 2,
"path": "page-002.svg"
},
{
"page": 3,
"path": "page-003.svg"
},
{
"page": 4,
"path": "page-004.svg"
},
{
"page": 5,
"path": "page-005.svg"
},
{
"page": 6,
"path": "page-006.svg"
},
{
"page": 7,
"path": "page-007.svg"
},
{
"page": 8,
"path": "page-008.svg"
},
{
"page": 9,
"path": "page-009.svg"
},
{
"page": 10,
"path": "page-010.svg"
}
],
"slides": [
{
"page": 1,
"title": "cover",
"renderer_id": "hero_1",
"layout_family": "hero",
"density": "medium",
"visual_intent": "show cover with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "cover rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": [
{
"asset_id": "zhipu-minimax-product-image",
"binds_slot": "company-product-image",
"source_type": "web_search_preview",
"semantic_subject": "Zhipu and MiniMax product identity",
"retrieval_query": "Zhipu AI MiniMax product identity screenshot",
"license": "preview_unverified",
"href": "https://example.com/zhipu-minimax-product.png",
"usage_page": 1,
"source_url": "https://example.com/zhipu-minimax-product.png"
}
],
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "cover",
"semantic_blocks": [
{
"block_id": "title_1",
"type": "title",
"content": "cover key message"
},
{
"block_id": "hero_finding_1",
"type": "finding",
"content": "Zhipu and MiniMax product comparison with company identity and real image slots"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_1"
]
},
{
"component_id": "finding_callout",
"binds": [
"hero_finding_1"
]
}
],
"asset_strategy": {
"strategy_id": "real_image_required",
"expected_asset_count": 1
},
"image_slots": [
{
"slot_id": "company-product-image",
"semantic_subject": "Zhipu and MiniMax product identity",
"asset_type": "screenshot",
"required": true,
"real_image_required": true,
"shared_asset_allowed": false
}
]
},
{
"page": 2,
"title": "agenda",
"renderer_id": "capability_map_2",
"layout_family": "capability_map",
"density": "medium",
"visual_intent": "show agenda with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "agenda rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "agenda",
"semantic_blocks": [
{
"block_id": "title_2",
"type": "title",
"content": "agenda key message"
},
{
"block_id": "agenda_2",
"type": "agenda",
"content": "Key sections and decision flow"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_2"
]
},
{
"component_id": "action_list",
"binds": [
"agenda_2"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 3,
"title": "context overview",
"renderer_id": "scorecard_3",
"layout_family": "scorecard",
"density": "medium",
"visual_intent": "show context_overview with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "context_overview rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "context_overview",
"semantic_blocks": [
{
"block_id": "title_3",
"type": "title",
"content": "context_overview key message"
},
{
"block_id": "finding_3",
"type": "finding",
"content": "context_overview finding"
},
{
"block_id": "evidence_3",
"type": "evidence",
"content": "Source-backed evidence table"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_3"
]
},
{
"component_id": "finding_callout",
"binds": [
"finding_3"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_3"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 4,
"title": "comparison",
"renderer_id": "dashboard_4",
"layout_family": "dashboard",
"density": "medium",
"visual_intent": "show comparison with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "comparison rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "comparison",
"semantic_blocks": [
{
"block_id": "title_4",
"type": "title",
"content": "comparison key message"
},
{
"block_id": "comparison_4",
"type": "comparison",
"content": "Compare entities, claims, and constraints"
},
{
"block_id": "company_4",
"type": "company",
"content": "Zhipu and MiniMax product comparison with company identity and real image slots"
},
{
"block_id": "evidence_4",
"type": "evidence",
"content": "Evidence slot tied to source material"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_4"
]
},
{
"component_id": "comparison_matrix",
"binds": [
"comparison_4"
]
},
{
"component_id": "image_panel",
"binds": [
"company_4"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_4"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 5,
"title": "action plan",
"renderer_id": "depth_5",
"layout_family": "depth",
"density": "medium",
"visual_intent": "show action_plan with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "action_plan rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "action_plan",
"semantic_blocks": [
{
"block_id": "title_5",
"type": "title",
"content": "action_plan key message"
},
{
"block_id": "action_5",
"type": "action",
"content": "Owner, next action, and deadline"
},
{
"block_id": "process_5",
"type": "process",
"content": "Execution sequence"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_5"
]
},
{
"component_id": "action_list",
"binds": [
"action_5"
]
},
{
"component_id": "process_flow",
"binds": [
"process_5"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 6,
"title": "closing",
"renderer_id": "annotation_6",
"layout_family": "annotation",
"density": "medium",
"visual_intent": "show closing with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "closing rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "closing",
"semantic_blocks": [
{
"block_id": "title_6",
"type": "title",
"content": "closing key message"
},
{
"block_id": "finding_6",
"type": "finding",
"content": "closing key message"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_6"
]
},
{
"component_id": "finding_callout",
"binds": [
"finding_6"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 7,
"title": "cover",
"renderer_id": "brand_7",
"layout_family": "brand",
"density": "medium",
"visual_intent": "show cover with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "cover rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": [
{
"asset_id": "zhipu-minimax-product-image",
"binds_slot": "company-product-image",
"source_type": "web_search_preview",
"semantic_subject": "Zhipu and MiniMax product identity",
"retrieval_query": "Zhipu AI MiniMax product identity screenshot",
"license": "preview_unverified",
"href": "https://example.com/zhipu-minimax-product.png",
"usage_page": 7,
"source_url": "https://example.com/zhipu-minimax-product.png"
}
],
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "cover",
"semantic_blocks": [
{
"block_id": "title_7",
"type": "title",
"content": "cover key message"
},
{
"block_id": "hero_finding_7",
"type": "finding",
"content": "Zhipu and MiniMax product comparison with company identity and real image slots"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_7"
]
},
{
"component_id": "finding_callout",
"binds": [
"hero_finding_7"
]
}
],
"asset_strategy": {
"strategy_id": "real_image_required",
"expected_asset_count": 1
},
"image_slots": [
{
"slot_id": "company-product-image",
"semantic_subject": "Zhipu and MiniMax product identity",
"asset_type": "screenshot",
"required": true,
"real_image_required": true,
"shared_asset_allowed": false
}
]
},
{
"page": 8,
"title": "agenda",
"renderer_id": "hero_8",
"layout_family": "hero",
"density": "medium",
"visual_intent": "show agenda with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "agenda rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "agenda",
"semantic_blocks": [
{
"block_id": "title_8",
"type": "title",
"content": "agenda key message"
},
{
"block_id": "agenda_8",
"type": "agenda",
"content": "Key sections and decision flow"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_8"
]
},
{
"component_id": "action_list",
"binds": [
"agenda_8"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 9,
"title": "context overview",
"renderer_id": "capability_map_9",
"layout_family": "capability_map",
"density": "medium",
"visual_intent": "show context_overview with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "context_overview rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "context_overview",
"semantic_blocks": [
{
"block_id": "title_9",
"type": "title",
"content": "context_overview key message"
},
{
"block_id": "finding_9",
"type": "finding",
"content": "context_overview finding"
},
{
"block_id": "evidence_9",
"type": "evidence",
"content": "Source-backed evidence table"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_9"
]
},
{
"component_id": "finding_callout",
"binds": [
"finding_9"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_9"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
},
{
"page": 10,
"title": "Closing summary",
"renderer_id": "brand_10",
"layout_family": "brand",
"density": "medium",
"visual_intent": "show comparison with the selected template family",
"visual_focal_point": "main structured visual block",
"visual_signature": "comparison rendered through family variant structure",
"content_density_contract": "medium-density structured template page",
"asset_contract": "none_required",
"risk_flags": [],
"source_policy": "Use prompt-provided content only; do not fabricate numbers.",
"template_variant": "comparison",
"semantic_blocks": [
{
"block_id": "title_10",
"type": "title",
"content": "comparison key message"
},
{
"block_id": "comparison_10",
"type": "comparison",
"content": "Compare entities, claims, and constraints"
},
{
"block_id": "company_10",
"type": "company",
"content": "Zhipu and MiniMax product comparison with company identity and real image slots"
},
{
"block_id": "evidence_10",
"type": "evidence",
"content": "Evidence slot tied to source material"
}
],
"component_selection": [
{
"component_id": "title_block",
"binds": [
"title_10"
]
},
{
"component_id": "comparison_matrix",
"binds": [
"comparison_10"
]
},
{
"component_id": "image_panel",
"binds": [
"company_10"
]
},
{
"component_id": "evidence_table",
"binds": [
"evidence_10"
]
}
],
"asset_strategy": {
"strategy_id": "structured_fallback",
"no_fake_data": true
}
}
]
}

View File

@@ -0,0 +1,23 @@
{
"version": "beautiful-template-plan/v1",
"target_slide_count": 10,
"template_family_selection": {
"enabled": true,
"source": "beautiful-html-template-families",
"selected_template_id": "blue-professional",
"candidate_template_ids": ["blue-professional", "emerald-editorial", "signal"],
"selection_reason": "Formal internal review with metrics, evidence, problem analysis, and action plan."
},
"slides": [
{"page": 1, "template_variant": "cover", "semantic_blocks": [{"block_id": "cover_title", "type": "finding", "content": "内部业务复盘"}], "component_selection": [{"component_id": "title_block", "binds": ["cover_title"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 2, "template_variant": "agenda", "semantic_blocks": [{"block_id": "agenda_items", "type": "finding", "content": "关键结论、问题、原因、动作"}], "component_selection": [{"component_id": "finding_callout", "binds": ["agenda_items"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 3, "template_variant": "context_overview", "semantic_blocks": [{"block_id": "context", "type": "finding", "content": "背景与目标"}], "component_selection": [{"component_id": "finding_callout", "binds": ["context"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 4, "template_variant": "metric_dashboard", "semantic_blocks": [{"block_id": "metric_1", "type": "metric", "content": "指标占位,等待真实数据"}], "component_selection": [{"component_id": "metric_card", "binds": ["metric_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 5, "template_variant": "problem_analysis", "semantic_blocks": [{"block_id": "problem_1", "type": "finding", "content": "核心问题"}], "component_selection": [{"component_id": "finding_callout", "binds": ["problem_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 6, "template_variant": "cause_analysis", "semantic_blocks": [{"block_id": "cause_1", "type": "process", "content": "原因链路"}], "component_selection": [{"component_id": "process_flow", "binds": ["cause_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 7, "template_variant": "comparison", "semantic_blocks": [{"block_id": "compare_1", "type": "comparison", "content": "方案对比"}], "component_selection": [{"component_id": "comparison_matrix", "binds": ["compare_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 8, "template_variant": "case_evidence", "semantic_blocks": [{"block_id": "evidence_1", "type": "evidence", "content": "证据与案例"}], "component_selection": [{"component_id": "evidence_table", "binds": ["evidence_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 9, "template_variant": "action_plan", "semantic_blocks": [{"block_id": "action_1", "type": "action", "content": "后续动作"}], "component_selection": [{"component_id": "action_list", "binds": ["action_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 10, "template_variant": "closing", "semantic_blocks": [{"block_id": "close_1", "type": "finding", "content": "总结与决策请求"}], "component_selection": [{"component_id": "title_block", "binds": ["close_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}}
]
}

View File

@@ -0,0 +1,23 @@
{
"version": "beautiful-template-plan/v1",
"target_slide_count": 10,
"template_family_selection": {
"enabled": true,
"source": "beautiful-html-template-families",
"selected_template_id": "blue-professional",
"candidate_template_ids": ["blue-professional", "signal", "emerald-editorial"],
"selection_reason": "Internal analytical comparison of Zhipu and MiniMax with product, model, commercialization, and action implications."
},
"slides": [
{"page": 1, "template_variant": "cover", "semantic_blocks": [{"block_id": "cover_title", "type": "finding", "content": "智谱和 MiniMax"}], "component_selection": [{"component_id": "title_block", "binds": ["cover_title"]}], "asset_strategy": {"strategy_id": "real_image_required", "expected_asset_count": 2, "source_type_allowlist": ["web_search_preview", "user_provided", "uploaded_file"], "generated_bitmap_allowed_as_real_image": false, "preview_required": true}, "image_slots": [{"slot_id": "zhipu_identity", "semantic_subject": "智谱", "asset_type": "logo", "required": true, "shared_asset_allowed": false}, {"slot_id": "minimax_identity", "semantic_subject": "MiniMax", "asset_type": "logo", "required": true, "shared_asset_allowed": false}]},
{"page": 2, "template_variant": "agenda", "semantic_blocks": [{"block_id": "agenda_items", "type": "finding", "content": "公司、模型、产品、商业化、判断"}], "component_selection": [{"component_id": "finding_callout", "binds": ["agenda_items"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 3, "template_variant": "context_overview", "semantic_blocks": [{"block_id": "company_context", "type": "company", "content": "两家公司定位"}], "component_selection": [{"component_id": "image_panel", "binds": ["company_context"]}], "asset_strategy": {"strategy_id": "identity_structured_fallback", "fallback_if_missing": "Use structured identity panels if verifiable logos/screenshots are unavailable.", "no_fake_data": true}},
{"page": 4, "template_variant": "comparison", "semantic_blocks": [{"block_id": "model_compare", "type": "comparison", "content": "模型能力对比"}], "component_selection": [{"component_id": "comparison_matrix", "binds": ["model_compare"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 5, "template_variant": "case_evidence", "semantic_blocks": [{"block_id": "product_evidence", "type": "product", "content": "产品矩阵与使用场景"}], "component_selection": [{"component_id": "image_panel", "binds": ["product_evidence"]}], "asset_strategy": {"strategy_id": "real_image_required", "expected_asset_count": 2, "source_type_allowlist": ["web_search_preview", "user_provided", "uploaded_file"], "generated_bitmap_allowed_as_real_image": false, "preview_required": true}, "image_slots": [{"slot_id": "zhipu_product", "semantic_subject": "智谱产品", "asset_type": "screenshot", "required": true, "shared_asset_allowed": false}, {"slot_id": "minimax_product", "semantic_subject": "MiniMax 产品", "asset_type": "screenshot", "required": true, "shared_asset_allowed": false}]},
{"page": 6, "template_variant": "metric_dashboard", "semantic_blocks": [{"block_id": "commercial_metric", "type": "metric", "content": "商业化指标需要真实输入"}], "component_selection": [{"component_id": "metric_card", "binds": ["commercial_metric"]}], "asset_strategy": {"strategy_id": "structured_fallback", "fallback_if_missing": "Use qualitative cards unless source data provides concrete numbers.", "no_fake_data": true}},
{"page": 7, "template_variant": "problem_analysis", "semantic_blocks": [{"block_id": "risk_1", "type": "risk", "content": "采用风险"}], "component_selection": [{"component_id": "risk_matrix", "binds": ["risk_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 8, "template_variant": "action_plan", "semantic_blocks": [{"block_id": "action_1", "type": "action", "content": "后续验证动作"}], "component_selection": [{"component_id": "action_list", "binds": ["action_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 9, "template_variant": "risk_dependency", "semantic_blocks": [{"block_id": "dependency_1", "type": "risk", "content": "依赖与决策点"}], "component_selection": [{"component_id": "risk_matrix", "binds": ["dependency_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}},
{"page": 10, "template_variant": "closing", "semantic_blocks": [{"block_id": "close_1", "type": "finding", "content": "结论与建议"}], "component_selection": [{"component_id": "title_block", "binds": ["close_1"]}], "asset_strategy": {"strategy_id": "structured_fallback", "no_fake_data": true}}
]
}

View File

@@ -0,0 +1,12 @@
{
"version": "svglide-font-fallback-policy/v1",
"default_stack": "system-sans-cjk",
"stacks": {
"system-sans-cjk": ["Inter", "Arial", "PingFang SC", "Microsoft YaHei", "sans-serif"],
"system-sans-cjk-heavy": ["Inter", "Arial", "PingFang SC", "Microsoft YaHei", "sans-serif"],
"system-sans-cjk-medium": ["Inter", "Arial", "PingFang SC", "Microsoft YaHei", "sans-serif"],
"system-sans-cjk-regular": ["Inter", "Arial", "PingFang SC", "Microsoft YaHei", "sans-serif"],
"system-mono": ["SFMono-Regular", "Menlo", "Consolas", "monospace"]
},
"forbidden": ["fonts.googleapis.com", "@font-face", "remote_font_upload"]
}

View File

@@ -0,0 +1,18 @@
# SVGlide Font Fallback Policy
SVGlide beautiful template families record original font families for provenance, but runtime generation must lower them to system font roles.
Allowed first-tier stacks:
- `system-sans-cjk`
- `system-sans-cjk-heavy`
- `system-sans-cjk-medium`
- `system-sans-cjk-regular`
- `system-mono`
Forbidden runtime dependencies:
- Google Fonts
- scattered `@font-face`
- custom font upload
- template-specific web font loading

View File

@@ -0,0 +1,697 @@
# slides +create-svg
从一个或多个 SVGlide SVG 文件创建飞书幻灯片:
> 兼容说明:新建或大幅重生成 SVG deck 时,调用本命令前先使用 `svglide_project_runner.py` 和 `svglide-artifacts.spec.md` 的分阶段产物目录。本页保留为最终 create 步骤的命令级契约。
```bash
lark-cli slides +create-svg \
--as user \
--title "Demo" \
--file page1.svg \
--file page2.svg
```
## 适用场景
- AI 已经能生成符合 [svg-protocol.md](svg-protocol.md) 的 SVGlide SVG。
- 希望按文件逐页创建,避免把大段 XML/SVG 塞进 shell 参数。
- 需要 SVG 内本地图片占位符自动上传并替换为 file token。
- 需要把原生 chart 的 canonical JSON spec 作为 root chart spec marker 透传给服务端。
不适用:
- 你只有普通 SVG且没有 `slide:role` 协议标记。
- 复杂普通 SVG 不能直接提交;需要把实际可渲染元素标成 SVGlide role。`g` / 嵌套 `svg` 容器可以保留,但不能代替子元素 role。
- 你想通过 SVG 路径提交 whiteboard marker`slide:role="whiteboard"` 和旧 `data-svglide-whiteboard` marker 会被 CLI 拒绝。
- 你需要插入到指定页前MVP 只创建新 presentation 并按顺序追加页面。
## Flags
| Flag | 说明 |
|------|------|
| `--title` | presentation 标题,省略时为 `Untitled` |
| `--file` | SVG 文件路径;可重复,页面顺序就是 flag 顺序 |
| `--assets` | 可选 `assets.json`,把 SVG `@path` 映射到已上传 file token |
| `--dry-run` | 展示创建空白 presentation + N 次 `/slide` 调用,不真实创建 |
## 请求链路
CLI 先创建空白 presentation
```http
POST /open-apis/slides_ai/v1/xml_presentations
```
随后对每个 SVG 文件调用现有 slide create 路由:
```http
POST /open-apis/slides_ai/v1/xml_presentations/{xml_presentation_id}/slide?revision_id=-1
```
body
```json
{
"slide": {
"content": "<svg ...>...</svg>"
}
}
```
不会新增 `/svg_slide` 路由,也不会把 `file_meta_map` 当成 CLI 到服务端的契约。
chart spec marker 也不新增 API。CLI 不会上传 chart 资源,也不会调用任何 chart 创建接口;它只把通过 marker 外壳、hash 和 JSON spec 基础校验的 marker 留在同一个 `slide.content` SVG 中。
## 图片处理
SVG 内本地图片写成:
```xml
<image slide:role="image" href="@./hero.png" x="0" y="0" width="320" height="180" />
```
`<image>` 可以位于 `g` / 嵌套 `svg` 容器中CLI 会全局扫描 `<image href="@...">``<image xlink:href="@...">` 并替换为 canonical `href="file_token"`
CLI 会:
1. 上传本地图片到新 presentation。
2.`href="@./hero.png"``xlink:href="@./hero.png"` 替换为 canonical `href="file_token"`
3. 注入 transport metadata`<metadata data-svglide-assets="true"><img src="file_token" /></metadata>`
预上传资源可用 `--assets`
```json
{
"@./hero.png": "boxcn..."
}
```
## Chart Spec Marker
`slides +create-svg` 支持一种最小 chart marker用于透传 canonical JSON chart spec。payload 不是 SXSD `<chart>` XML也不是 chart snapshot/staticData服务端会在 SVGlide parser 内部把 spec 转成 chart 创建所需数据:
```xml
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:slide="https://slides.bytedance.com/ns"
slide:role="slide"
slide:contract-version="svglide-authoring-contract/v1"
width="960" height="540" viewBox="0 0 960 540">
<g slide:role="chart"
slide:chart-ref="chart-sales-001"
x="80" y="96" width="420" height="260">
<metadata
data-svglide-chart="svglide-chart-inline/v1"
data-format="svglide-chart-spec-v1"
data-encoding="base64url-json"
data-payload-hash="sha256:<64 hex>"
>BASE64URL_PAYLOAD</metadata>
</g>
</svg>
```
Decoded canonical JSON shape:
```json
{"version":"svglide-chart-spec/v1","chartType":"bar","data":{"categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[12.5,18]}]}}
```
CLI 校验范围只包括:
- marker 必须是 root `<svg>` 直系 `<g slide:role="chart">`
- `slide:chart-ref``x/y/width/height` bbox 必填bbox 只接受数字或 `px`
- marker 内必须且只能有一个 `<metadata>`
- metadata 必须使用 `data-svglide-chart="svglide-chart-inline/v1"``data-format="svglide-chart-spec-v1"``data-encoding="base64url-json"`
- payload 必须是无 padding base64url`data-payload-hash` 必须匹配 decoded canonical JSON bytes 的 sha256不要对 base64 文本计算 hash。
- decoded payload 必须是 JSON object且包含 `version="svglide-chart-spec/v1"``chartType``data.categories``data.series[].name``data.series[].values`
- MVP 只支持 `chartType="bar"` / `"line"``categories` 和每个 `values` 数组长度必须一致;`values` 只能是有限 JSON number。
`sxsd-chart-v1` / `base64url` 的 SXSD `<chart>` XML payload 不属于 SVGlide chart marker 协议面,会被 CLI 拒绝。`slide:role="whiteboard"` 和旧的 `data-svglide-whiteboard` marker 明确不属于 `+create-svg` 协议面。
## 生成质量规则
这些规则用于生成阶段主动规避服务端降级、近似和泛化错误。几何数值、path 命令、role/必填属性、图片 href 等基础约束已由 CLI 强校验;版式、美观和文本溢出仍需要生成器或人工复核。
### 与现有规划层对齐
SVG 创建不使用单独的规划目录。新建或大幅改写 SVG deck 时,仍然复用 [planning-layer.md](planning-layer.md) 规定的 `.lark-slides/plan/<deck-or-task-id>/02-plan/slide_plan.json`,不要另建 `.lark-slides/svg-plan` 或只保留散落的 `.svg` 文件。
在通用 plan 字段基础上SVG deck 还应补充这些 SVG 专属字段:
```json
{
"output_mode": "svglide-svg",
"canvas": {"width": 960, "height": 540, "viewBox": "0 0 960 540"},
"safe_area": {"x": 48, "y": 40, "width": 864, "height": 460},
"template_family_selection": {
"enabled": true,
"source": "beautiful-html-template-families",
"selected_template_id": "blue-professional",
"candidate_template_ids": ["blue-professional", "signal", "cobalt-grid"],
"selection_reason": "internal review with metrics, evidence, and actions"
},
"svg_constraints": {
"text_element": "foreignObject slide:role=shape slide:shape-type=text",
"path_commands": "M/L/H/V/C/Q/Z only",
"image_href": "@./path or file token only",
"css": "explicit font-size/font-weight/color/line-height/text-align; no font shorthand"
},
"svg_files": [
{"page": 1, "path": ".lark-slides/plan/<deck-id>/04-svg/prepared/page-001.svg"}
],
"preflight": {
"command": "python3 skills/lark-slides/scripts/svg_preflight.py --plan .lark-slides/plan/<deck-id>/02-plan/slide_plan.json --input .lark-slides/plan/<deck-id>/04-svg/prepared/page-001.svg",
"status": "pending"
},
"readback_verification": {
"status": "pending",
"checks": ["page_count", "blank_page", "canvas_bounds", "text_overlap", "asset_tokens", "closing_slide"]
}
}
```
模板也复用现有 `template_tool.py search -> summarize -> extract` 路由。模板摘要只用于选择主题、页面流、视觉节奏和布局骨架;生成 SVG 时要把模板结构翻译成 template family / variant / components / asset strategy不要照搬模板 XML也不要读取完整模板 XML。
SVG deck 的 `slides[]` 还必须包含这些可校验字段,避免生成结果虽然能创建但内容千篇一律、信息量不足或在资料缺失时编造事实:
```json
{
"page": 3,
"page_type": "content",
"renderer_id": "dashboard_scorecard",
"layout_family": "dashboard",
"template_variant": "metric_dashboard",
"semantic_blocks": [
{"block_id": "kpi_1", "type": "metric", "content": "DAU 同比增长 18%"},
{"block_id": "finding_1", "type": "finding", "content": "新增主要来自渠道 A"}
],
"component_selection": [
{"component_id": "metric_card", "binds": ["kpi_1"]},
{"component_id": "finding_callout", "binds": ["finding_1"]}
],
"asset_strategy": {
"strategy_id": "chart_when_quantified",
"decision": "render_chart_if_data_provided_else_structured_fallback",
"no_fake_data": true
},
"density": "high",
"density_structure": "dashboard with four metric cards, trend line, and source note",
"content_density_contract": "dashboard >= 4 metrics",
"asset_contract": "none_required | {mode: preview|production, retrieval_query, source_type, license, local_path_or_href, usage_page, source_url/generated_by, replacement_required}",
"risk_flags": ["text_overflow", "image_license", "conversion_dasharray"],
"source_status": "source_verified | attachment_missing | user_prompt_only",
"source_policy": "when attachment_missing, show 待从附件补齐 / 来源缺失 and avoid numeric claims",
"layout_guardrails": [
"renderer_id must change actual geometry, not only the name",
"template_variant must map to a real family layout",
"main text and chart labels stay inside safe area",
"dense page uses a structured visual carrier, not a long bullet box",
"avoid XML-like card layout unless the page has real SVG-native visual structure"
]
}
```
### Template Family Catalog
SVGlide 高质量生成必须先从 [beautiful-html-template-families.json](beautiful-html-template-families.json) 选择 deck-level `template_family_selection`。该选择决定统一视觉语言、可用 variants、组件倾向和图片/图表策略。
生成前还必须读取 [component-registry.json](component-registry.json)、[asset-strategy-registry.json](asset-strategy-registry.json) 和 [asset-slot-contract.schema.json](asset-slot-contract.schema.json)。这三者分别负责 semantic block 到组件、真实图片/图表策略、preview/live 图片 slot 一致性。
生成顺序:
```text
semantic plan
-> template_family_selection
-> template_variant
-> semantic_blocks + component_selection
-> asset_strategy + image_slots
-> layout boxes
-> raw SVG/visual artifact
-> contract_compile
-> svg_preflight.py --plan
```
每页必须声明:
- `template_variant`: 这一页在所选 family 内使用的布局变体。
- `semantic_blocks`: 页面内容的语义块,而不是低层几何指令。
- `component_selection`: 每个语义块绑定的组件。
- `asset_strategy`: 真实图片、图表或结构化 fallback 策略。
`svg_preflight.py` 会校验 template family 字段是否完整、图片 slot 是否被满足、可见文本是否泄漏 source token/tool/path以及未归属装饰 primitive 是否进入 SVG。
### 生成阶段 Fail-Fast Gate
`slide_plan.json` 不是说明文档,而是生成阶段的硬契约。生成器必须先通过 plan gate再渲染 SVG本地 `svg_preflight.py --plan` 失败时禁止调用 live API。
每页 SVG plan 必填:
| Field | 作用 | 失败后处理 |
|---|---|---|
| `renderer_id` | 标识具体渲染器/几何结构 | 换真实 renderer不用 `two_column_1` 这类假命名 |
| `layout_family` | 做 deck 级版式多样性检查 | 相邻页重复时换阅读方向、主视觉位置或信息结构 |
| `template_variant` | family 内页型变体 | 从 family variants 选择,不能自造无渲染支持的变体 |
| `semantic_blocks` | 页面语义块 | 每页至少有标题/内容/证据/行动等可绑定块 |
| `component_selection` | 语义块到组件的绑定 | 组件必须来自 component registry |
| `asset_strategy` | 图片/图表/结构化 fallback 决策 | 图片页必须声明 image_slots图表页必须声明数据来源 |
| `content_density_contract` | 信息密度硬契约 | 高密度页必须量化,例如 `dashboard >= 4 metrics` |
| `asset_contract` | 图片/素材来源与许可契约 | 无图写 `none_required`Preview 网络图必须记录 `retrieval_query` / `source_url`,授权未确认可写 `license=preview_unverified` 且不阻断;正式交付必须补 source/license/local path 或替换 |
| `risk_flags` | 生成风险显式登记 | 无风险用空数组;不要省略字段 |
| `source_policy` | 缺数据/数字声明处理策略 | 防止自动扩写时编造业务数字 |
deck 级硬门禁:
- 用户未说明页数,或只说“一份 slide / 一份 PPT / 做个 slide / 生成一个 slide”这类模糊表达时默认 `page_count=10`;不要仅因页数缺失而停下来追问。明确“一页 / 单页 / onepage / one slide / 只要封面”才按 `page_count=1`。默认 10 页必须包含 closing slide并满足 10 页 deck 的 layout / renderer 多样性门禁。
- 8 页以上必须有明确 closing slide。
- 10 页以上至少 5 种 `layout_family`
- 不允许连续 3 页使用同一 `layout_family`
- 8 页以上至少 6 种 `template_variant` 或明确的 family variant 节奏。
- 10 页以上至少 5 种真实 `renderer_id`
- 高密度页必须有量化 `content_density_contract`,不能只写“信息丰富”。
量化密度契约建议:
```text
matrix/table >= 6 cells
timeline >= 4 nodes
dashboard >= 4 metrics
flow >= 4 stages
risk_grid >= 4 items
comparison >= 4 rows or columns
```
如果 SVG source 无法满足对应数量,`svg_preflight.py` 会报 `plan_content_density_contract_not_met`,生成器必须补真实结构,不要只改字段名。
### 生成前强约束
以下规则来自实际 SVGlide live 生成、回读和修复经验,生成器必须先满足这些规则,再追求视觉复杂度。
- MUST: 默认使用 Lark Slides 当前回读画布 `960 x 540`,即 root 写成 `width="960" height="540" viewBox="0 0 960 540"`。不要默认用 `1280 x 720`,否则服务端回读后可能整页偏大并裁切。
- MUST: 主体元素使用安全区,建议 `safe = x:48 y:40 w:864 h:460`。除全屏背景外,文本、卡片、图表、标签、节点和图例都必须落在安全区内。
- MUST: 多页 deck 应包含明确的 closing slide。8 页以上讲解/汇报型 deck 不要把 roadmap / next-playbook 当作结束页;最后一页应包含 `closing``summary``Q&A``Thanks` 或下一步联系信息。
- MUST: `slides[]` 必须记录 `renderer_id`,且它要对应真实几何结构,而不是 `two-column-1` / `two-column-2` 这种名字变化。10 页以上 deck 至少 5 种 renderer/layout family不得连续 3 页使用同一 renderer。
- MUST: `slides[]` 必须记录 `layout_family``template_variant``semantic_blocks``component_selection``asset_strategy``content_density_contract``risk_flags``source_policy`。图片页必须记录 slot 级 `asset_contract`MVP 阶段普通非图片页缺失只 warning。
- MUST: `component_selection` 必须来自 component registry且每个绑定的 semantic block 在页面内容中存在。`renderer_id` 不能替代 `template_variant`
- MUST: 8 页以上 SVG deck 至少使用 5 种 visual recipe family不能整套 deck 都是卡片、双栏或普通 dashboard。
- MUST: 高密度页必须声明 `density_structure` 和量化 `content_density_contract`,例如 `matrix/table >= 6 cells``timeline >= 4 nodes``dashboard >= 4 metrics``flow >= 4 stages``risk_grid >= 4 items`。只有“大标题 + 大图 + 2-3 个短 chip”不算高密度。
- MUST: 来源不足、附件缺失、用户未提供数据时,必须在 plan 中写 `source_status``source_policy`,并在页面上显式表达“待从附件补齐 / 来源缺失 / no numeric claims”。不要编造客户、排名、真实论文数据、金额、占比、链接、logo 或引用。
- MUST: `foreignObject` 文本样式使用显式 CSS`font-size``font-weight``font-family``color``line-height``text-align`。不要用 `font:` shorthand 表达关键字号和加粗。
- MUST: 白色或接近白色的文字必须完整落在深色 shape 承载底上。标题、封面副标题、CTA、页脚等不能跨出深色底压到浅色图片、白色蒙层或白底上需要时扩大色块、加深色背板/遮罩,或改用深色文字。
- MUST: 圆形/椭圆节点只承载短标签,不承载解释句。节点内 `foreignObject` bbox 必须小于节点 bbox微解释、指标、下一步和注释放到独立说明卡、图例、机制表或外侧 callout。
- MUST: 提交前和 live 回读后都检查边界和重叠:非背景元素不得越过 `960 x 540`,第 2/3 页等信息密集页必须额外检查 text bbox overlap。
- SHOULD: 如果本地预览使用更大画布,例如 `1280 x 720`,必须在输出给 `slides +create-svg` 前按比例换算为 `960 x 540`,而不是只改 root viewBox。
### 生成器实现约束与 Preflight
生成器必须先把高概率错误拦在本地,再调用 `lark-cli`。不要依赖 live 创建后的人工修补来发现基础问题。
实现约束:
- MUST: SVG 生成 helper 的返回类型保持一致。推荐统一返回 `string`,或统一返回 `string[]` 后在页面末尾 `flat().filter(Boolean).join("\n")`;不要混用 `...items.map(...).join("\n")`,这会把已拼好的 SVG 标签按字符展开,生成非法 XML。
- MUST: 所有组件都从稳定布局盒推导坐标,避免散点手调。文本、标签、图例、曲线端点和卡片内容应有明确的父盒和对齐规则。
- MUST: 生成脚本要先写 deck plan / asset list再写页面不能边补坐标边生成最终 SVG。
- MUST: 生成器要把 preflight 规则前移为本地 assert。写 SVG 前先由实际组件 manifest 反推出 semantic blocks、component bindings、asset slots 和密度结构,再检查 `content_density_contract` 数量、主体 safe area、文本 bbox 和最小文本框高度;断言失败时修组件或布局,不要只改 `slide_plan.json` 字段。
- MUST: 高密度结构要由组件实际数量驱动,例如 `scorecard >= 4 metrics` 必须生成 4 个能被识别为 metric/bar/card 的元素;`timeline >= 4 nodes` 必须生成 4 个真实节点和标签;不要用文字描述冒充结构。
- MUST: 文本组件要按字号、行高和预估行数计算最小 `foreignObject` 高度。卡片、节点、脚注、图例的正文框不得出现 0、高度个位数或明显低于一行文字的 bbox。
- MUST: 主体文本、卡片、图表、标签、节点和图例必须落在 safe area全画布背景、边缘承载底、图片遮罩和装饰边框可以超出 safe area但应只承担背景/承载作用,不承载关键文本。
- SHOULD: 对高风险页面使用更保守的留白:标题与图表标签至少相隔 24px曲线端点标签不要压在标题/图例区域,卡片内文字与边框至少留 10-14px。
- SHOULD: 把每页的 `safe``titleBox``visualBox``textBox` 等布局盒保存为可检查数据,便于自动计算越界和重叠。
推荐生成顺序:
```text
deck/page plan
-> layout boxes
-> components with emitted primitive manifest
-> generator asserts: recipe/primitives/density/text/safe-area
-> write SVG + slide_plan.json from the same manifest
-> svg_preflight.py --plan ...
-> dry-run / live create / readback
```
### 本地 HTML 预览(建议)
HTML 预览是生成阶段的轻量质检,不是 SVGlide 协议或 CLI API 的硬依赖。
- SHOULD: 生成 SVGlide deck 后、调用 `slides +create-svg` 前,生成本地 `05-preview/preview.html`,把每页 SVG 按 16:9 画布嵌入,并展示页码、标题、`renderer_id` / `template_variant`、图片资产状态、preview-only 图片来源和明显 warning。
- SHOULD: 如果当前 agent、IDE 或浏览器工具支持打开本地文件,打开 `05-preview/preview.html` 进行人工或截图式预览,优先检查:
- 页面是否空白、明显裁切或整体偏大。
- 标题、正文、图片和装饰元素是否重叠。
- 白色/浅色文字是否压到浅色背景或图片亮部。
- 相邻页面是否版式过度重复。
- 信息密度是否明显不足,尤其是高密度页是否真的有 matrix/table/timeline/dashboard/flow/risk grid。
- 结尾页是否存在。
- 图片是否显示,是否有破图、空图片框、图片过少或 preview-only 来源未记录。
- SHOULD: 在最终产物目录记录 `05-preview/preview.html` 路径;如果未生成或无法打开,说明原因,并继续执行 preflight / dry-run / readback。
- MUST NOT: 用 HTML 预览替代 `svg_preflight.py``slides +create-svg --dry-run` 或 live readback。HTML 预览主要提前发现审美、布局和素材问题服务端转换后的字体、path bbox、图片 token 和部分 SVG 效果仍必须通过 readback 验证。
打开预览后必须按 [svg-aesthetic-review.md](svg-aesthetic-review.md) 做一次人工或截图式审查。重点看所有页面的标题区、装饰线、badge、文本框、图片框、safe area、重复版式和 SVG 视觉优势;如果多页出现同类问题,修生成规则后重新生成,不要只逐页微调坐标。
本地 preflight 必须在 `slides +create-svg` 前执行,失败即停:
- `python3 skills/lark-slides/scripts/svg_preflight.py --plan .lark-slides/plan/<deck-id>/02-plan/slide_plan.json --input .lark-slides/plan/<deck-id>/04-svg/prepared/page-*.svg` 通过;如果脚本不可用,再退回 `xmllint --noout page-*.svg` 加人工检查。
- root 是 `width="960" height="540" viewBox="0 0 960 540"`
- root / leaf `slide:role` 完整,所有 leaf 有几何必填属性。
- plan 中每页 `layout_family``template_variant``semantic_blocks``component_selection``asset_strategy``content_density_contract``risk_flags``source_policy` 完整。图片页的 `asset_contract``image_slots` 必须满足;非图片页可声明 `none_required`
- 禁止 SVG 退化成 XML-like 卡片页:如果页面基本只有 `rect + foreignObject`,且没有 path、gradient、image overlay、annotation、micro chart、icon、texture、spotlight、flow 等 SVG-native primitivepreflight 必须失败。
- 禁止零尺寸元素;文本框、图片、卡片和圆/椭圆必须有正向宽高,不能生成 `height="0"` 的隐藏说明。
- `<image opacity="...">` 或图片 style 里写 `opacity:` 在 MVP 阶段只 warning当前转换链路不会稳定保留到 readback `<img>`。需要淡化图片时,优先把透明度预合成进 PNG/JPG或在图片上方加半透明 `rect` 遮罩。
- 禁止白色/浅色文字跨出深色承载底;如果 preflight 报 `light_text_without_dark_backing`,优先扩大深色背景或加文本背板,不要只缩小字号。
- 禁止把解释文字塞进圆形/椭圆节点;如果 preflight 报 `node_text_overflow`,节点内只保留短标签,把说明迁移到旁边卡片、表格或图例。
- 警惕 `circle` / `ellipse``stroke-width`;当前转换链路可能只保留 border color 而丢失 width。关键圆环、节点外圈和粗描边用双层填充圆/椭圆模拟,或改成 path/rect。
- 禁止关键路线、闭环、流程连接、timeline rail 使用 `stroke-dasharray`;普通装饰虚线也会 warning。关键路线必须用显式短线段或小圆点 markers 组成,不要把虚线作为唯一视觉表达。
- 禁止 `font:` shorthand 和空图片框。MVP 阶段 http(s) / data URL 图片、未下载远程图片只 warning正式交付和可见性要求高的 deck 仍应下载到本地并走 `@./path` 上传或使用 file token。
- 禁止 unsupported path command`path d` 只含 `M/L/H/V/C/Q/Z`
- 非背景元素不得越界;主体元素应在 safe area 内。
- 文本框做 bbox overlap 近似检查,尤其是目录、痛点、竞品表、案例图表和总结页。
- 图片资产文件存在、大小合理,或 http(s)/data URL 能在 preview 中显示。Preview 阶段来源/授权不完整只 warning但必须用 `asset_contract.license=preview_unverified``risk_flags=["image_preview_only"]` 显式标记;正式交付再补齐来源/授权或替换。
- deck plan 通过 renderer 多样性、layout family 多样性、closing slide、高密度结构、资产契约、来源保护六类校验。
创建顺序:
```text
generate deck plan -> user confirms plan -> assets -> generate_svg
-> prepare -> 05-preview/preview.html and browser preview when supported
-> local preflight with --plan -> preview lint -> aesthetic review -> quality gate
-> lark-cli slides +create-svg --dry-run
-> live create -> xml_presentations get readback
-> readback bbox / text overlap / closing slide checks
```
readback 不能省略。服务端会把 SVGlide 转成 Slides XML文字 bbox、path bounds 和图片 token 可能和本地 SVG 预估不同;本地 preflight 负责拦住确定错误readback 负责发现转换后的版式漂移。
### Deck 级密度规划
生成多页 SVG deck 前,先写 deck-level plan。页面类型只定义叙事职责密度由 `deck_type`、受众、页面目的和节奏共同决定,不要把某个 page type 永久绑定为固定密度。
最小 plan schema
```json
{
"deck_type": "explain | decision | product | brand | technical | education | report",
"audience": "who will read it",
"goal": "what the deck should make the audience understand or decide",
"density_strategy": "how low/medium/high density pages are distributed",
"asset_strategy": "which query/topic-related web images should be searched and fetched, where they will be used, preview source/url/license risk, and production replacement plan if needed",
"visual_rhythm": "how layout, imagery, charts, and text density vary across pages",
"slides": [
{
"page": 1,
"page_type": "cover",
"density": "low",
"density_mode": "visual-dense",
"takeaway": "one sentence the audience should remember",
"evidence": [],
"visual_structure": "full-bleed image with title overlay",
"layout_guardrails": ["large hero title", "no dense body copy"]
}
]
}
```
常用 `page_type`
```text
cover, opener, agenda, section-divider, context, problem, opportunity,
executive-summary, content, data, comparison, process, case-study, demo,
architecture, system, roadmap, timeline, decision, recommendation,
risk, tradeoff, summary, closing, q-and-a, appendix
```
密度规则:
- MUST: 每页都要有明确 `takeaway`,即使是封面、分隔页和结束页。
- MUST: 每个 SVG deck 默认都要包含真实图片资产,不要全程只用矢量 shape 冒充“配图”。Preview 阶段应优先根据用户 query、deck 标题和页面主题去网络检索并拉取强相关图片,再补充产品截图、网页截图、场景图、材质纹理、图鉴图和 AI 生成图增强视觉冲击;展示型、宣传型、产品型、品牌型和案例型 deck 至少包含 3 处图片使用,其中至少 1 页使用全幅或半出血图片主视觉。
- MUST: 高密度页必须有承载信息的视觉结构,例如矩阵、流程、地图、时间线、标注图、案例卡或手绘微图表,不能只有装饰图形。
- MUST: 生成器必须先扩写页面“结构信息”,再绘制 SVG。信息密度不足时优先补结构化解释层例如编号标签、微解释、比较维度、轴线、图例、阶段、来源状态、下一步而不是把同一句话换写成多个 chip。
- MUST: 流程页、闭环页、机制页和产品体系页不能只有“4 个圆节点 + 短标签”。至少补 1 层结构化信息例如机制表、KPI 标签、触发条件、责任/频率、输入输出、风险提示或下一步动作。
- SHOULD: 高密度内容页通常包含 3-6 个信息块和若干可读细节,但 executive brief、品牌页、产品视觉页、短汇报可以降低数量只保留强结论、关键证据和视觉锚点。
- SHOULD NOT: 不要让所有高密度页长成同一种“主结论 + 3-6 卡片 + 3 个 callout”模板。
- MUST NOT: 缺少素材或数据时不要编造数字、客户名、logo、排名、引用或真实案例用 qualitative label、relative scale、hypothesis/assumption 标注兜底。
### 结构示例
8-10 页讲解型 deck 可参考这个节奏,但不要把它当成唯一模板;如果 deck 已经包含 roadmap / playbook仍建议再补一页 closing
```text
cover -> opener/context -> agenda/map -> content -> data/comparison
-> process/system breakdown -> case-study/demo -> content/implications
-> summary -> closing
```
5 页决策汇报优先前置结论:
```text
cover -> executive-summary -> options/comparison -> recommendation/risk -> next steps
```
6 页产品/品牌 deck 可以强化视觉叙事:
```text
cover -> value proposition -> user scenario -> feature map/demo
-> proof/roadmap -> closing
```
边界处理:
- 3-5 页短 deck 可以省略 agenda把 summary 并入 closing。
- 15 页以上长 deck 应增加 section-divider 或 recap避免连续高密度阅读疲劳。
- 技术方案要混合 architecture、process、tradeoff、risk不要连续堆文字。
- 教学讲解要前置 context / concept map逐步增加密度。
- 素材不足时用抽象视觉系统、定性矩阵、annotated wireframe、scenario card 兜底,并标明假设。
### 先定义布局盒
不要直接手写散点坐标。每页先定义稳定布局盒,再把文字、图形、图例和图片放进盒内:
```text
page = 960 x 540
safe = x:48 y:40 w:864 h:460
titleBox = x:54 y:52 w:600 h:96
visualBox = x:516 y:176 w:350 h:260
notesGrid = x:54 y:430 w:760 h:48
```
生成后检查:
- 关键元素必须在 safe area 内。
- 同组元素使用同一个父盒推导坐标。
- 图例、标签、指标不能浮在不上不下的位置,必须相对主视觉左/右/下边对齐。
- 如果页面有圆、节点、卡片或框体,内容中心应和外框中心基本一致,不靠手调 `x + 10``y + 10` 维持观感。
- 不要把 1280x720 的坐标直接提交给 `slides +create-svg`。当前服务端回读画布通常是 960x540错误坐标系会表现为每页偏大、右侧卡片裁切、底部标签越界。
### 文本安全余量
`foreignObject` 文本优先使用显式 CSS。为了服务端转换后保留样式字号、加粗、颜色、行距和对齐必须写成独立属性不要把关键样式藏在 `font:` shorthand 或只写在复杂外层 wrapper 上:
```xml
<foreignObject slide:role="shape" slide:shape-type="text" x="54" y="62" width="600" height="42">
<div xmlns="http://www.w3.org/1999/xhtml"
style="margin:0;padding:0;font-size:30px;font-weight:900;font-family:Arial,'Source Han Sans SC';color:#111827;line-height:1.12;text-align:left;letter-spacing:0;">
关键结论:增长来自三件事
</div>
</foreignObject>
```
中文和混排字体要留安全高度:
- subtitle 不小于 64px。
- note / chip 单行文本盒不小于 20px。
- 小型标签文本盒不小于 14px。
- 多行文字要按行高预估高度,再额外留 8-12px。
- 右侧图例或矩阵格里的文字不得贴边,水平 padding 至少 10-14px。
- 白色/浅色文字的 bbox 必须完全落在深色 rect/card/overlay 内;封面标题如果跨出色块,应优先扩大色块或改成深色字,不要让白字压在浅色图片或白色蒙层上。
- 圆形/椭圆节点内只放短标签,解释文字移动到节点外的 callout、legend 或机制表;不要让圆内文本框宽度超过圆形直径。
- 服务端支持 `foreignObject` 内的 `<br />`。为了本地预览和标题排版稳定,标题/大段文本优先使用多个块级 `div``p` 控制行高,不要只靠 `<br />` 调整复杂布局。
- 如果需要垂直居中,优先通过更准确的文本框高度、段落行高和 y 坐标解决;布局 wrapper 可以使用,但实际文字节点仍要带显式 `font-size` / `font-weight` / `color`
### 几何与 path 安全线
leaf 几何属性必须写数字或 `px`,不要生成百分比、`em/rem``calc(...)`
```xml
<rect slide:role="shape" x="80" y="96" width="420px" height="240px" />
```
`path d` 只生成 `M/L/H/V/C/Q/Z` 命令。不要生成 `A``S``T` 等命令;需要圆角或弧线时,用 `C` / `Q` 近似,或改用 `circle` / `ellipse` / `rect`
Transform 参数同样使用数字或 `px`。不要写 `translate(10%, 20%)`,先在布局盒里换算成绝对坐标。
### 版式节奏
同一 deck 不能连续复用同一种“暗色网格 + 左文案 + 右卡片 + 底部 chips”。10 页左右的讲解型 deck 至少混用这些结构:
- 封面 / 全幅图片背景页。
- 目录矩阵页或行业地图页。
- 左文右图 / 左图右文双栏页。
- 全幅路线图或时间线页。
- 2x2 / 2x4 总结矩阵页。
- 数据仪表页、流程页、对比页或案例页。
相邻页面至少改变一个主结构维度:主视觉位置、网格列数、图片用法、文本密度或阅读方向。
### 图片使用与 Preview Image Mode
默认必须规划和使用图片资产。用户可见 preview / `local_real_preview` 的目标是验证真实 SVGlide 视觉上限,因此图片必须来自可审计线上来源,不能用本地生成图、程序化纹理、无来源本地文件或 `preview_unverified` 凑数。推荐先从用户 query、deck 标题、章节标题和页面 takeaway 生成 2-5 个图片检索词,去网络检索并拉取主题强相关图片;可使用公开图库、百科/开放素材、官网/产品页截图、新闻图或内部资产服务。必须在 plan / asset manifest 里记录 `retrieval_query``source_url``license``retrieved_at` 和使用页。
最稳流程是先从线上来源下载到项目缓存,同时保留 `source_url` 和 license provenance再写成本地占位符
```xml
<image slide:role="image" href="@./assets/hero.jpg" x="0" y="0" width="960" height="540" />
```
推荐的网络拉图流程:
1. 从用户 query、deck title、page takeaway、章节标题中提取 `retrieval_query`,优先使用具体名词、场景、人物、作品、产品、地点、历史事件或学科对象,避免只搜抽象词。
2. 对封面、章节过渡页、案例页、教学解释页和产品/品牌页优先执行网络图片搜索或网页截图获取,选择和主题直接相关的真实图片,不用无关风景图凑数。
3. 能下载时先保存到 `assets/` 并用 `@./assets/...` 引用;来不及下载时可以保留 http(s) URL 进入 preview但 Asset Gate 仍要求 `source_url` 和 license。
4. 每张图在 `asset_contract` 记录 `retrieval_query``source_type``source_url``retrieved_at`、明确 `license``usage_page``attribution`
5. 网络不可用或无法找到强相关图片时,用户可见 preview 必须 fail-closed 并要求补资产AI 生成图、程序化纹理或纯 SVG 视觉只能用于 debug/fixture不能宣称真实预览完成。
图片不只用于局部卡片背景,也可以作为整页背景、半出血主视觉、材质纹理、案例示例、产品截图、数据仪表截图、网页/应用界面截图、人物/场景图、图鉴封面、历史/艺术/科学素材或产品细节局部。作为整页背景时,必须叠加半透明遮罩或暗角,保证标题和正文对比度。
图片数量与用法建议:
- MUST: 在 `asset_strategy` 或 asset manifest 中记录图片检索词、图片来源、授权/许可类型、下载 URL、署名要求和使用页用户可见 profile 中 `license=preview_unverified`、本地生成图或无 `source_url` 必须阻断。
- MUST: 5 页以上 deck 至少使用 2 张真实图片8 页以上 deck 至少使用 4 张;宣传/产品/品牌/案例/教学型 deck 至少使用 5 张或至少 40% 页面含图片。
- MUST: 封面优先使用图片或图片+抽象图形混合主视觉,不要只用网格、光效和几何背景。
- MUST: 案例页优先使用行业场景图、产品截图、仪表盘截图或真实质感背景,并叠加数据 callout。
- MUST: 同一 deck 中混用全幅背景、半出血图片、卡片图、纹理/材质背景、标注型截图、图鉴式小图和局部裁切特写,避免所有图片都只是小卡片背景。
- SHOULD: 对教育、历史、艺术、医学、产品讲解等主题,优先用图片建立具象认知:人物、器物、场景、局部特写、对比图、流程截图、资料封面或时间背景图。
- MUST NOT: 保留空图片框、破图、`data:` 图片、无来源本地图片或本地生成图。用户可见 preview 必须让 Asset Gate 通过后才能展示为完成。
用户可见 preview 优先使用这些来源来快速获得丰富视觉,并在获取时记录可审计 provenance
| Source | 适合用途 | Preview 规则 |
|--------|----------|------|
| Web image search / topic query | 和用户 query、页面主题、作品/人物/地点/产品直接相关的真实图片 | 优先使用;记录 `retrieval_query`、图片页 URL、实际图片 URL、license/attribution |
| Unsplash / Pexels / Pixabay | 高质量摄影、封面背景、场景图 | 结合主题 query 检索;记录图片页 URL、作者、平台 license |
| Openverse / Wikimedia Commons | 百科、历史、技术、公共领域素材 | 记录单图 URL、作者、license、署名要求 |
| The Met / Smithsonian / NASA Open Access | 艺术、科学、历史、航天视觉 | 记录条目 URL、Open Access 条款或第三方权利说明 |
| 官网 / 产品页 / 新闻图 / 搜索图 | 产品截图、竞品页、事件背景、真实语境 | 只用于事实展示或内部评审;记录页面 URL 和使用风险,不得造成商业背书误导 |
| 内部资产服务 | 公司/团队已有授权图、产品截图、品牌资产 | 使用 `internal://...` 或 http(s) 资产 URL记录资产 id、授权范围和来源 owner |
素材清单建议字段:
```json
{
"local_path": "./assets/hero.jpg",
"source": "Unsplash",
"retrieval_query": "Beethoven Symphony No. 5 concert hall orchestra",
"source_url": "https://...",
"retrieved_at": "2026-06-08",
"license": "unsplash",
"commercial_use": "allowed_by_source_terms",
"replacement_required": false,
"attribution_required": false,
"usage_page": 1,
"notes": "Preview-only visual placeholder; replace or verify license before production delivery"
}
```
### 信息密度与图鉴感
短 note 不要占一个很宽胶囊。优先写成“编号/标签 + 主句 + 微解释/数值”:
```text
03 GRID ENERGY 86% | storage demand peaks before grid balancing
```
内容页可以用三种方式提高密度,不要把高密度等同于堆文字:
- `text-dense`: 多解释、多证据、多注释,适合背景分析和概念讲解。
- `chart-dense`: SVG shape 手绘矩阵、流程、时间线、微柱状、雷达、散点、标尺;如果需要原生 bar/line chart使用 root chart spec marker不要把外部图表截图当成唯一方案。
- `visual-dense`: 高级视觉图案或图片上叠加标注层、数据 callout、局部标签、对比线和图例。
视觉区要补足可读细节,避免只有装饰符号:
- 局部标注、刻度、坐标轴、图例。
- 行业标签、材料纹理、指标卡。
- 路线节点、连接线、层级分区。
- 流程/闭环图旁边补机制表或说明卡,例如“触发条件 / 运营动作 / 衡量指标”,不要把说明句塞进圆形节点内部。
- 小型表格、雷达/柱状/散点等微图表。
### 转换稳定性经验
这些规则来自 live 创建后对比 source SVG 与 readback XML 的结果,属于生成侧必须规避的转换差异:
- `image opacity` 不稳定:本地 SVG 里的 `<image opacity="0.18">` / `<image opacity="0.22">` 可能会在 readback `<img>` 中丢失透明度。MVP preflight 只 warning生成器仍应把淡化效果烘焙进图片本身或使用半透明 shape 遮罩。
- shape opacity 稳定:`rect``circle``path` 等 shape 的 `opacity` 会转换为 XML `alpha`,可用于蒙层、暗角和装饰层。
- circle / ellipse stroke width 不稳定:圆形/椭圆描边可能只保留颜色、不保留宽度。关键外圈使用“外层有色圆 + 内层背景圆”的双 shape ring或用 path 绘制;不要用单个 stroked circle 承载关键视觉。
- dashed stroke 不稳定:`stroke-dasharray` 可能降级,尤其是自定义 path 的虚线闭环。关键路线用短 line segment 或 filled dot markers 手工排布;普通装饰虚线也要经 readback 复核。
- path 会转换为 `type="custom"` 并做 bbox 内坐标归一化,这是预期行为;只要 readback bbox 和视觉位置正确,不算差异。
- 字体会被转换为服务端支持字体,例如 `Noto Sans` / `思源黑体`,因此生成阶段要给 `foreignObject` 留足高度,不要按浏览器本地字体做极限排版。
### 生成后检查
生成脚本或人工复核必须检查:
- 是否已执行本地 preflight且所有 SVG 通过 XML、协议、资产、bbox 和文本重叠检查。
- 是否已执行 `slides +create-svg --dry-run`,确认请求链路是创建 presentation + 按页追加 SVG。
- live 创建后是否已用 `xml_presentations get` 读回,重新检查画布、页数、越界、文本重叠和 closing slide。
- root / leaf role 是否完整。
- 每个 leaf 是否有 [svg-protocol.md](svg-protocol.md) 中列出的几何必填属性。
- 几何属性和 transform 参数是否只使用数字或 `px`
- `path d` 是否只包含 `M/L/H/V/C/Q/Z`
- 文本是否截断、重叠或贴边。
- 内容是否在 safe area 内,关键图例和外框是否对齐。
- 相邻页面是否明显换版式。
- 每页是否有明确 takeaway高密度页的视觉结构是否承载信息而不只是装饰。
- 内容页是否避免了“大标题 + 大图 + 2-3 个短 chip”的低信息布局。
- 自称数据、排名、客户、引用、logo 或案例时,是否有来源;没有来源时是否改为定性或假设表达。
- 图片是否足够丰富并可见;如果 Preview/MVP 阶段暂时保留 http(s) / data URL 或 `preview_unverified` 来源,要记录 warning、确认 live/readback 可见,并在正式交付前列出替换项。
验证记录建议写回 `.lark-slides/plan/<deck-or-task-id>/08-readback/readback-check.json`,并在最终回复中简述:
```text
验证记录:
- PreflightN/N SVG 通过 root/role/geometry/path/image/bbox 检查。
- Dry-run已确认 create presentation + N 次 /slide。
- Readback实际页数 N / 预期 N未发现空白页、破图或缺失 closing slide。
- 版式:检查 safe area、文本重叠、越界和相邻页版式变化。
- 资产Preview 阶段优先丰富图片和 readback 可见性;若保留 http(s)/data URL 或 `preview_unverified` 来源,必须记录 warning。正式交付再替换为本地 @path 自动上传或 file token并补齐授权。
```
## 错误处理
任一页失败时,错误会包含:
- `xml_presentation_id`
- 失败页序号
- 已成功页数
- 已创建的 `slide_ids`
如果服务端 detail 带有 `SVGLIDE_ERROR_JSON:` markerCLI 会提取并在错误中展示 `svglide_error`,用于定位 `type``page_index``tag_name``element_id``role``hint`
失败后不要假设没有创建任何资源。先把恢复状态写回 plan 的 `recovery` 字段:
```json
{
"xml_presentation_id": "slides...",
"failed_page": 3,
"failed_svg_file": ".lark-slides/plan/<deck-id>/04-svg/prepared/page-003.svg",
"successful_slide_ids": ["abc", "def"],
"svglide_error": {"type": "svg_validation_error", "tag_name": "foreignObject"},
"next_action": "fix source SVG and rerun preflight before retry"
}
```
恢复顺序:
1. 本地 preflight 已失败:修对应 SVG 文件,不要调用 live API。
2. live 添加页失败且带 `svglide_error`:按 `type` / `tag_name` / `hint` 收敛 SVG 子集,例如降级复杂 filter、path、CSS 或文本结构。
3. plain XML 在同一路由成功但 SVG 失败:优先确认目标 server lane 是否部署了 SVGlide parser不要盲目重写整套 deck。
4. SVG 通过本地 preflight 且失败在第 1 页,服务端只返回 generic `nodeServer invalid param`:优先检查 `lark-cli` 环境、代理和 PPE/BOE lane 是否命中目标 slide server。不要先把已通过协议校验的 deck 改回低质量 SVG。
5. 已创建 presentation 或部分页面时,默认保留现场并回读确认;是否删除空 presentation 必须单独由用户确认。
### 编辑已创建的 SVG deck
SVG deck 后续编辑走双轨,不承诺 source SVG id 能稳定映射到 readback XML block id
| 修改类型 | 推荐路径 | 说明 |
|----------|----------|------|
| 小改标题、文本、图片或坐标 | `xml_presentation.slide.get` 读回 XML -> 找当前 block_id -> `slides +replace-slide` | 使用转换后的 XML 做块级编辑,页序和 slide_id 不变 |
| 大幅换版式、重画图表、调整视觉系统 | 修改 source SVG -> 重新 preflight -> 重新创建或替换目标页 | 保持 SVG 的视觉表达优势,避免在转换后 XML 上手搓复杂 SVG 结构 |
| 无法定位 block_id 或映射不可信 | 回 source SVG 修改 | 不生成 `edit-map.json`,除非服务端或转换结果能证明 source id 可稳定保留 |
小改前必须重新 `slide.get` 拿最新 block id 和 revision大改后必须更新同一个 `.lark-slides/plan/<deck-or-task-id>/02-plan/slide_plan.json`,保持 plan、SVG 文件、创建结果和验证记录一致。

View File

@@ -0,0 +1,105 @@
{
"generated_at": "2026-06-22",
"policy": {
"copied_or_adapted_code": "Keep upstream copyright and license notice with the SVGlide artifact that contains the copied or substantially adapted portion.",
"reference_absorption": "Record provenance and keep implementation SVGlide-owned; do not embed upstream HTML, CSS, JS, screenshots, or renderer source unless separately recorded.",
"satori": "Satori is an MPL-2.0 runtime dependency and must remain external to the artboard renderer bundle."
},
"sources": [
{
"head": "e5e204fb1f3b06290846e7dcd7aceddabeceec8c",
"license": "MIT",
"license_notice": "Copyright (c) 2026 Zara Zhang",
"local_path": "/Users/bytedance/bd-projects/beautiful-html-templates",
"name": "beautiful-html-templates",
"repo_url": "https://github.com/zarazhangrui/beautiful-html-templates.git",
"risk_level": "low",
"svglide_artifacts": [
"skills/lark-slides/references/absorptions/beautiful-html-templates/",
"skills/lark-slides/references/beautiful-html-template-families.json",
"skills/lark-slides/references/beautiful-html-template-cleanup-map.json"
],
"usage": "Template family, layout, component, visual DNA, and planner selection signal extraction.",
"usage_type": "reference_absorption"
},
{
"head": "45d9a79874d8700583feb60ddfbca46df437864b",
"license": "MIT",
"license_notice": "Copyright (c) 2025-2026 Hugo He",
"local_path": "/Users/bytedance/bd-projects/ppt-master",
"name": "ppt-master",
"repo_url": "https://github.com/hugohe3/ppt-master.git",
"risk_level": "low",
"svglide_artifacts": [
"skills/lark-slides/references/svglide-ppt-master-migration.matrix.md",
"skills/lark-slides/references/svglide-reference-source-inventory.json",
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"usage": "Reference for slide generation workflow, planning stages, visual QA, and artifact discipline.",
"usage_type": "reference_absorption"
},
{
"head": "8a54325f871ee10fa6545de3b3a9b771aa12620c",
"license": "MIT",
"license_notice": "Copyright (c) 2025 Y-Research @SBU",
"local_path": "/Users/bytedance/bd-projects/workspaces/SVGlide/PosterGen",
"name": "PosterGen",
"repo_url": "https://github.com/Y-Research-SBU/PosterGen.git",
"risk_level": "low",
"svglide_artifacts": [
"skills/lark-slides/references/svglide-reference-source-inventory.json",
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"usage": "Reference for poster-like composition, asset roles, and visual hierarchy patterns.",
"usage_type": "reference_absorption"
},
{
"head": "c5f465d40b5cedabbdf902b0b0c86bcc6bfa1943",
"license": "ISC",
"license_notice": "Copyright (c) 2024 Julian Cataldo - https://www.juliancataldo.com",
"local_path": "/Users/bytedance/bd-projects/og-images-generator",
"name": "og-images-generator",
"repo_url": "https://github.com/gracile-web/og-images-generator.git",
"risk_level": "low",
"svglide_artifacts": [
"skills/lark-slides/references/svglide-reference-source-inventory.json",
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"usage": "Reference for HTML/CSS-to-image generation pipeline shape and renderer operation boundaries.",
"usage_type": "reference_absorption"
},
{
"head": "2aadac07c93bc31eb3ce303e361461e944f25c6d",
"license": "Apache-2.0",
"license_notice": "Apache License Version 2.0",
"local_path": "/Users/bytedance/bd-projects/open-design",
"name": "open-design",
"repo_url": "https://github.com/nexu-io/open-design.git",
"risk_level": "low",
"svglide_artifacts": [
"skills/lark-slides/references/svglide-reference-source-inventory.json",
"skills/lark-slides/references/svglide-reference-absorption-report.md"
],
"usage": "Reference for design-generation concepts, abstraction vocabulary, and planning structure.",
"usage_type": "reference_absorption"
},
{
"head": "ab49fafbdfa04bd59e70db8988c139af09a59c6f",
"license": "MPL-2.0",
"license_notice": "Mozilla Public License Version 2.0; package author Shu Ding <g@shud.in>",
"local_path": "/Users/bytedance/bd-projects/workspaces/SVGlide/satori",
"name": "satori",
"repo_url": "https://github.com/vercel/satori.git",
"risk_level": "medium",
"svglide_artifacts": [
"skills/lark-slides/scripts/artboard_renderer/package.json",
"skills/lark-slides/scripts/artboard_renderer/pnpm-lock.yaml",
"skills/lark-slides/scripts/artboard_renderer/dist/render.mjs",
"skills/lark-slides/scripts/svglide_artboard_package_check.py"
],
"usage": "Runtime HTML/CSS-like tree to SVG renderer used by the SVGlide artboard renderer.",
"usage_type": "external_runtime_dependency"
}
],
"version": "svglide-oss-source-manifest/v1"
}

View File

@@ -0,0 +1,80 @@
{
"claim_boundary": "source page-family implementation smoke deck; not production/default_selectable evidence",
"family_id": "8-bit-orbit",
"pages": [
{
"page": 1,
"page_role": "cover",
"page_variant_id": "slide-1",
"title": "8-BIT ORBIT"
},
{
"page": 2,
"page_role": "agenda",
"page_variant_id": "slide-2",
"title": "Rewiring How We Share Ideas"
},
{
"page": 3,
"page_role": "content",
"page_variant_id": "slide-3",
"title": "Four Engines Running"
},
{
"page": 4,
"page_role": "data",
"page_variant_id": "slide-4",
"title": "Quarterly Growth Metrics"
},
{
"page": 5,
"page_role": "detail",
"page_variant_id": "slide-5",
"title": "Resource Allocation"
},
{
"page": 6,
"page_role": "process",
"page_variant_id": "slide-6",
"title": "Development Roadmap"
},
{
"page": 7,
"page_role": "data",
"page_variant_id": "slide-7",
"title": "Platform Vitals"
},
{
"page": 8,
"page_role": "quote",
"page_variant_id": "slide-8",
"title": "Immersive Quote"
},
{
"page": 9,
"page_role": "comparison",
"page_variant_id": "slide-9",
"title": "Choose Your Loadout"
},
{
"page": 10,
"page_role": "closing",
"page_variant_id": "slide-10",
"title": "Ready Player One?"
}
],
"production_minimum_roles": [
"cover",
"agenda",
"content",
"data",
"comparison",
"quote",
"process",
"detail",
"closing"
],
"schema_version": "svglide-page-family-smoke-deck/v1",
"template_id": "pixel-orbit-console",
"theme_id": "8-bit-orbit"
}

View File

@@ -0,0 +1,77 @@
{
"schema_version": "svglide-page-family-smoke-deck/v1",
"claim_boundary": "source page-family implementation smoke deck; not production/default_selectable evidence",
"family_id": "biennale-yellow",
"theme_id": "biennale-yellow",
"template_id": "biennale-programme-poster",
"pages": [
{
"page": 1,
"page_role": "cover",
"page_variant_id": "cover",
"title": "Aurora Programme"
},
{
"page": 2,
"page_role": "content",
"page_variant_id": "manifesto",
"title": "A room is a slow argument with the sun"
},
{
"page": 3,
"page_role": "agenda",
"page_variant_id": "programme",
"title": "Programme strands"
},
{
"page": 4,
"page_role": "process",
"page_variant_id": "chapter",
"title": "Slow Atmospheres"
},
{
"page": 5,
"page_role": "data",
"page_variant_id": "data",
"title": "Public attendance"
},
{
"page": 6,
"page_role": "quote",
"page_variant_id": "quote",
"title": "A note from the curator"
},
{
"page": 7,
"page_role": "comparison",
"page_variant_id": "cal",
"title": "Public calendar"
},
{
"page": 8,
"page_role": "detail",
"page_variant_id": "cal",
"title": "Calendar detail ledger"
},
{
"page": 9,
"page_role": "closing",
"page_variant_id": "colophon",
"title": "With thanks to the slow readers"
}
],
"production_minimum_roles": [
"cover",
"agenda",
"content",
"data",
"comparison",
"quote",
"process",
"detail",
"closing"
],
"variant_reuse_rationale": {
"cal": "Source template has eight pages; calendar ledger is reused once as a detail/table variant for smoke coverage only."
}
}

View File

@@ -0,0 +1,80 @@
{
"schema_version": "svglide-page-family-smoke-deck/v1",
"claim_boundary": "source page-family implementation smoke deck; not production/default_selectable evidence",
"family_id": "block-frame",
"theme_id": "block-frame",
"template_id": "block-frame-grid",
"pages": [
{
"page": 1,
"page_role": "cover",
"page_variant_id": "cover",
"title": "Neo-Brutalism Style"
},
{
"page": 2,
"page_role": "agenda",
"page_variant_id": "agenda",
"title": "What We Deliver"
},
{
"page": 3,
"page_role": "content",
"page_variant_id": "data_dashboard",
"title": "Core Features"
},
{
"page": 4,
"page_role": "data",
"page_variant_id": "data_dashboard-4",
"title": "Quarterly Growth Metrics"
},
{
"page": 5,
"page_role": "quote",
"page_variant_id": "quote_or_emphasis",
"title": "Design Principle"
},
{
"page": 6,
"page_role": "detail",
"page_variant_id": "process_or_timeline",
"title": "Methodology"
},
{
"page": 7,
"page_role": "process",
"page_variant_id": "process_or_timeline-7",
"title": "Project Timeline"
},
{
"page": 8,
"page_role": "data",
"page_variant_id": "data_dashboard-8",
"title": "Impact at a Glance"
},
{
"page": 9,
"page_role": "comparison",
"page_variant_id": "process_or_timeline-9",
"title": "Meet the Crew"
},
{
"page": 10,
"page_role": "closing",
"page_variant_id": "closing",
"title": "Build Something Bold"
}
],
"production_minimum_roles": [
"cover",
"agenda",
"content",
"data",
"comparison",
"quote",
"process",
"detail",
"closing"
]
}

View File

@@ -0,0 +1,29 @@
{
"schema_version": "svglide-page-family-smoke-deck/v1",
"family_id": "blue-professional",
"template_id": "executive-dashboard",
"theme_id": "blue-professional",
"production_minimum_roles": [
"cover",
"agenda",
"content",
"data",
"comparison",
"quote",
"process",
"detail",
"closing"
],
"pages": [
{"page": 1, "page_role": "cover", "page_variant_id": "cover", "title": "Executive Summary"},
{"page": 2, "page_role": "agenda", "page_variant_id": "agenda", "title": "Discussion Map"},
{"page": 3, "page_role": "content", "page_variant_id": "dashboard", "title": "Operating Context"},
{"page": 4, "page_role": "data", "page_variant_id": "metrics", "title": "Metric Dashboard"},
{"page": 5, "page_role": "data", "page_variant_id": "bars", "title": "Performance Bars"},
{"page": 6, "page_role": "comparison", "page_variant_id": "split", "title": "Option Comparison"},
{"page": 7, "page_role": "quote", "page_variant_id": "quote", "title": "Leadership Signal"},
{"page": 8, "page_role": "process", "page_variant_id": "timeline", "title": "Execution Timeline"},
{"page": 9, "page_role": "detail", "page_variant_id": "detail", "title": "Deep Dive"},
{"page": 10, "page_role": "closing", "page_variant_id": "closing", "title": "Decision Close"}
]
}

View File

@@ -0,0 +1,80 @@
{
"claim_boundary": "source page-family implementation smoke deck; not production/default_selectable evidence",
"family_id": "bold-poster",
"pages": [
{
"page": 1,
"page_role": "cover",
"page_variant_id": "hero",
"title": "Hero"
},
{
"page": 2,
"page_role": "quote",
"page_variant_id": "red",
"title": "Red"
},
{
"page": 3,
"page_role": "agenda",
"page_variant_id": "summary",
"title": "Summary"
},
{
"page": 4,
"page_role": "content",
"page_variant_id": "financial",
"title": "Financial"
},
{
"page": 5,
"page_role": "data",
"page_variant_id": "stat",
"title": "Stat"
},
{
"page": 6,
"page_role": "process",
"page_variant_id": "services",
"title": "Services"
},
{
"page": 7,
"page_role": "process",
"page_variant_id": "roadmap",
"title": "Roadmap"
},
{
"page": 8,
"page_role": "comparison",
"page_variant_id": "pillars",
"title": "Pillars"
},
{
"page": 9,
"page_role": "detail",
"page_variant_id": "global",
"title": "Global"
},
{
"page": 10,
"page_role": "closing",
"page_variant_id": "close",
"title": "Close"
}
],
"production_minimum_roles": [
"cover",
"agenda",
"content",
"data",
"comparison",
"quote",
"process",
"detail",
"closing"
],
"schema_version": "svglide-page-family-smoke-deck/v1",
"template_id": "poster-stat-punch",
"theme_id": "bold-poster"
}

View File

@@ -0,0 +1,92 @@
{
"claim_boundary": "Explicit current-run smoke deck for broadside page-family coverage; not production/default selectable evidence.",
"family_id": "broadside",
"pages": [
{
"page": 1,
"page_role": "cover",
"page_variant_id": "cover"
},
{
"page": 2,
"page_role": "agenda",
"page_variant_id": "chapter"
},
{
"page": 3,
"page_role": "content",
"page_variant_id": "statement"
},
{
"page": 4,
"page_role": "detail",
"page_variant_id": "split"
},
{
"page": 5,
"page_role": "data",
"page_variant_id": "stats"
},
{
"page": 6,
"page_role": "content",
"page_variant_id": "fadelist"
},
{
"page": 7,
"page_role": "content",
"page_variant_id": "list"
},
{
"page": 8,
"page_role": "quote",
"page_variant_id": "quote"
},
{
"page": 9,
"page_role": "comparison",
"page_variant_id": "compare"
},
{
"page": 10,
"page_role": "data",
"page_variant_id": "chart"
},
{
"page": 11,
"page_role": "process",
"page_variant_id": "diagram"
},
{
"page": 12,
"page_role": "data",
"page_variant_id": "pie"
},
{
"page": 13,
"page_role": "detail",
"page_variant_id": "pyramid"
},
{
"page": 14,
"page_role": "process",
"page_variant_id": "vtimeline"
},
{
"page": 15,
"page_role": "process",
"page_variant_id": "cycle"
},
{
"page": 16,
"page_role": "closing",
"page_variant_id": "end"
}
],
"production_selectable": false,
"promotion_status": "needs_review",
"runtime_template_id": "editorial-quote-chart",
"schema_version": "svglide-page-family-smoke-deck/v1",
"selected_theme_id": "broadside",
"variant_reuse_rationale": "No reuse; all 16 source-backed Broadside variants appear once."
}

View File

@@ -0,0 +1,62 @@
{
"claim_boundary": "Explicit current-run smoke deck for capsule page-family coverage; not production/default selectable evidence.",
"family_id": "capsule",
"pages": [
{
"page": 1,
"page_role": "cover",
"page_variant_id": "cover"
},
{
"page": 2,
"page_role": "agenda",
"page_variant_id": "agenda"
},
{
"page": 3,
"page_role": "content",
"page_variant_id": "data_dashboard"
},
{
"page": 4,
"page_role": "data",
"page_variant_id": "data_dashboard-4"
},
{
"page": 5,
"page_role": "quote",
"page_variant_id": "quote_or_emphasis"
},
{
"page": 6,
"page_role": "process",
"page_variant_id": "process_or_timeline"
},
{
"page": 7,
"page_role": "data",
"page_variant_id": "data_dashboard-7"
},
{
"page": 8,
"page_role": "detail",
"page_variant_id": "slide-8"
},
{
"page": 9,
"page_role": "comparison",
"page_variant_id": "slide-9"
},
{
"page": 10,
"page_role": "closing",
"page_variant_id": "closing"
}
],
"production_selectable": false,
"promotion_status": "needs_review",
"runtime_template_id": "capsule-card-system",
"schema_version": "svglide-page-family-smoke-deck/v1",
"selected_theme_id": "capsule",
"variant_reuse_rationale": "No reuse; all 10 source-backed Capsule variants appear once."
}

Some files were not shown because too many files have changed in this diff Show More