Compare commits
45 Commits
feat/batch
...
feat/svgli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
455ff07527 | ||
|
|
1ba2a36ea3 | ||
|
|
f1e08bb920 | ||
|
|
8cfbced316 | ||
|
|
02559cc0d4 | ||
|
|
5a3a0049dc | ||
|
|
55d1166d31 | ||
|
|
2a3783794f | ||
|
|
96ea87a6db | ||
|
|
e8c1d53b86 | ||
|
|
b6dd77264d | ||
|
|
08687d1fa2 | ||
|
|
8e23a21d68 | ||
|
|
099b432336 | ||
|
|
8007c4e51a | ||
|
|
809d7d1a5b | ||
|
|
6318eaba64 | ||
|
|
a56ef7f7c4 | ||
|
|
8eb4358586 | ||
|
|
872cfd4281 | ||
|
|
480d4e2fbb | ||
|
|
985089291f | ||
|
|
52439e6f96 | ||
|
|
78b130c1bc | ||
|
|
551f333563 | ||
|
|
a9b2a416b4 | ||
|
|
d6c060279d | ||
|
|
c4a272d28d | ||
|
|
8502e8c433 | ||
|
|
4a37a2fe7b | ||
|
|
842e2d44de | ||
|
|
94c5eb5b25 | ||
|
|
2af5dd41b9 | ||
|
|
c98b7719bf | ||
|
|
071ec01247 | ||
|
|
b99975bd5d | ||
|
|
4aa6c1f56d | ||
|
|
07e57d7857 | ||
|
|
566f6cfd47 | ||
|
|
cd0854e931 | ||
|
|
be8a67b894 | ||
|
|
f5502416c8 | ||
|
|
182c541ea6 | ||
|
|
d980e54ab8 | ||
|
|
85965e41e4 |
31
.github/workflows/ci.yml
vendored
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common"
|
||||
func Shortcuts() []common.Shortcut {
|
||||
return []common.Shortcut{
|
||||
SlidesCreate,
|
||||
SlidesCreateSVG,
|
||||
SlidesMediaUpload,
|
||||
SlidesReplaceSlide,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
380
shortcuts/slides/slides_create_svg.go
Normal 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
|
||||
}
|
||||
1101
shortcuts/slides/slides_create_svg_test.go
Normal file
1782
shortcuts/slides/svg_helpers.go
Normal file
599
shortcuts/slides/svg_helpers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
366
shortcuts/slides/svg_rasterize.go
Normal 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()
|
||||
}
|
||||
288
shortcuts/slides/svg_rasterize_test.go
Normal 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)
|
||||
}
|
||||
@@ -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 routing:recipe -> 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 决策。若使用 reviewer,reviewer 只审查证据,不生成 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 gate,HTML 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
|
||||
|------|----------|
|
||||
| 简单 XML(1-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`
|
||||
- 新建 / 大幅改写必须先创建目录并写入 plan;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`
|
||||
- plan 字段、路径命名、模板边界和 `asset_need` 结构按 planning-layer.md / asset-planning.md 执行
|
||||
|
||||
Step 3: 按 slide_plan.json 生成 XML → 创建
|
||||
Step 3: 按 slide_plan.json 生成 XML 或 SVGlide SVG → 创建
|
||||
- 逐页消费 plan:key_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. **创建流程**:简单短 XML(1-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. **创建流程**:简单短 XML(1-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 私有清单中的策略正文。
|
||||
|
||||
30
skills/lark-slides/prompts/svglide/canvas-planner.prompt.md
Normal 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.
|
||||
25
skills/lark-slides/prompts/svglide/deck-planner.prompt.md
Normal 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`.
|
||||
24
skills/lark-slides/prompts/svglide/repair-planner.prompt.md
Normal 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`.
|
||||
20
skills/lark-slides/prompts/svglide/slide-planner.prompt.md
Normal 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.
|
||||
31
skills/lark-slides/references/THIRD_PARTY_NOTICES.md
Normal 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.
|
||||
10
skills/lark-slides/references/absorptions/README.md
Normal 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`.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
skills/lark-slides/references/asset-strategy-registry.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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}}
|
||||
}
|
||||
}
|
||||
44505
skills/lark-slides/references/beautiful-html-template-families.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30133
skills/lark-slides/references/beautiful-template-executable-matrix.json
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
skills/lark-slides/references/component-registry.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
20
skills/lark-slides/references/component-registry.schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}}
|
||||
]
|
||||
}
|
||||
@@ -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}}
|
||||
]
|
||||
}
|
||||
12
skills/lark-slides/references/font-fallback-policy.json
Normal 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"]
|
||||
}
|
||||
18
skills/lark-slides/references/font-fallback-policy.md
Normal 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
|
||||
697
skills/lark-slides/references/lark-slides-create-svg.md
Normal 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 primitive,preflight 必须失败。
|
||||
- 禁止零尺寸元素;文本框、图片、卡片和圆/椭圆必须有正向宽高,不能生成 `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
|
||||
验证记录:
|
||||
- Preflight:N/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:` marker,CLI 会提取并在错误中展示 `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 文件、创建结果和验证记录一致。
|
||||
105
skills/lark-slides/references/oss-source-manifest.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||