{"id":30717,"plugin_id":"plugin_asdk_app_6a18d4c190a0819186e6b129a09e931e","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-09T18:02:36.707Z","digest":"62e01a84bf5250ae95439314eb034304d3e8acc286901ea03f3ff77e756c59e2","against":null,"payload":{"description":"Stage 1 of the motion pipeline — bring a design in as LAYERS, not flat frames. Pulls each Figma frame's layer tree (elements + transforms), captures each layer in its NATIVE format (live text, vector, or raster — PNG is a fallback, not the default), tags elements that recur across frames as matched, gets every asset onto a fetchable Picsart-hosted URL, and sets the video format (including output scale/sizes up front). Falls back to flat-frame ingest only when layers cannot be had. Called by the `motion` skill; not a standalone entry point.","included_files":[],"name":"motion-import","skill_md_contents":"---\nname: motion-import\ndescription: Stage 1 of the motion pipeline — bring a design in as LAYERS, not flat frames. Pulls each Figma frame's layer tree (elements + transforms), captures each layer in its NATIVE format (live text, vector, or raster — PNG is a fallback, not the default), tags elements that recur across frames as matched, gets every asset onto a fetchable Picsart-hosted URL, and sets the video format (including output scale/sizes up front). Falls back to flat-frame ingest only when layers cannot be had. Called by the `motion` skill; not a standalone entry point.\n---\n\n# ChatGPT and Codex integration\n\nApply these host conventions throughout this skill and its references.\n\n- Discover the connected Picsart tools and use their actual names and schemas.\n  The `picsart_*` names in the workflow identify tools, not a fixed host namespace.\n  If the connection is missing, explain that Picsart must be connected; continue\n  script and shot planning when possible. Never invent tool results or renders.\n- Read board decisions from the current user message and attached widget context.\n  Use `read_widget_context` only if the host actually exposes that tool. Do not\n  assume ChatGPT has Claude's context reader. Treat a board's initial tool result\n  and unsent widget state as previews, never as user approval. If a submitted\n  decision has no accessible payload, explain briefly and use the text fallback\n  to collect the missing decision; do not guess or repeat charged operations.\n- Use the host's available question interface or one concise conversational\n  question for preset choices.\n- UI rendering and delivery of feedback are the remote MCP server's responsibility.\n  If a board is unavailable, present its relevant options in text and record the\n  user's selection using the same documented feedback fields. Do not claim that\n  a plugin manifest makes Claude-specific widgets compatible with ChatGPT.\n- Save `motion.json` and artifacts in the user's writable project workspace, not\n  the installed plugin directory. If persistent file tools are unavailable,\n  keep a structured project checkpoint in the conversation and provide it for\n  download when the host supports files. Never claim a file was saved without\n  confirmation from a file tool.\n- Honor the user's current instructions and already authorized scope. Preserve\n  quoted spend limits and production locks; do not infer authorization from a\n  preview, a timeout, or a connection being installed. A timed-out charged call\n  needs a status check before considering a retry.\n- This package holds six film skills (`film`, `film-development`, `film-assets`, `film-scenes`, `film-edit`, `film-finishing`) and three motion skills (`motion`, `motion-import`, `motion-design`). Route between them directly.\n- `motion-import` reads a design's layers through the Figma app's tools. If Figma is not connected in this host, use its flat-frame fallback and say so once.\n\n\n### Motion server compatibility\n\nThese host conventions qualify tool and widget instructions below. Discover\nlive tool names and schemas before invoking the pipeline. A ZIP update changes\ninstructions; it does not deploy new server tools.\n\nWhen `picsart_scene_editor` is available, it accepts `scene`, `title` and\n`notes` and emits `scene_editor_feedback` v1. Accept without edits or comments\nreturns `verdict: \"approved\"`; Submit after edits or comments returns\n`verdict: \"needs_changes\"`. Read the returned `document` when `edited` is true:\nframe comments carry `instruction`, and timeline comments carry `text`, `start`\nand `end`. Opening the editor is a preview, never approval. If the tool is\nabsent, use the preview and text-review fallback already described in this skill.\n\nDedicated import/gradient/tracking tools in the new workflow may require a\nnewer MCP deployment. Call them only when exposed. Use a documented live\nrecipe or the source's explicit fallback when it supports the same operation;\notherwise explain the missing capability before proceeding with that step.\nKeep the original editable assets and the current scene intact.\n\n# Stage 1 — Import: the design in as layers\n\nMotion design animates *elements*, so the import's whole job is to bring the design in as\n**layers, not flat frames**. A frame delivered as one sealed PNG can only ever\ncross-dissolve; a frame delivered as its layers — panels, headline, buttons, the image,\nthe list — is a thing whose elements can slide, scroll, type on, parallax and\nsmart-animate. **Everything downstream is gated on this**: if the layers are lost here,\nno amount of tuning later can recover the ad; it stays a slideshow.\n\n## Pull the layer tree, per frame\n\n**With the Figma connector present**, read structure before pixels: `get_metadata` /\n`get_design_context` to walk each frame's node tree, then export each animatable node as\nits own asset. For every layer capture:\n\n> **AUDIT EVERY NESTED LAYER — never compose from Figma's simplified, frame-level output.** The\n> single biggest way items go missing from the ad: Figma's frame-level / summarized read (a shallow\n> `get_design_context`, or a `get_metadata` that stops at the top level) **collapses nested children**\n> — groups, component instances, auto-layout children, masked contents — into a summary. Take that at\n> face value and **every nested element is silently dropped**, so the composed frame is missing pieces\n> that were plainly in the design. **Recursively expand each frame's FULL tree** (`get_metadata` on the\n> frame node with its children expanded, or `use_figma` to walk *all* descendants) and audit it\n> **node by node** — do not trust a frame-level layer list to be complete. **Completeness check:** the\n> layers you capture must match the count of leaf/animatable nodes in the *fully-expanded* tree, not\n> the handful the frame-level view showed. If a group or instance shows as one node, open it — its\n> children are the layers. (Downstream this is the Stage-2 \"layer count ≈ node count\" gate; catch it\n> here, at the read, not after a frame composes short.)\n\n\n- **id** and **name/role** (what it is — `chrome`, `headline`, `cta`, `image`, `list`, `fx`)\n- **type** — `text`, `vector` (icon/logo/shape), or `raster` (photo/complex image); plus `group`\n- **transform** — `x, y, w, h, rotation, opacity`, and **z-order** (front-to-back)\n- **parent/group** — so a group can move as one\n- **for a text layer, also the type properties** — the **string**, resolved **font URL** + size,\n  **horizontal alignment** (read Figma's `textAlignHorizontal` — `left`/`center`/`right`; *never*\n  guess it from position or from \"it's a big headline\"), and the **box sizing/wrap**: a Figma\n  **fixed-width** text box that runs past the frame **clips, it does not wrap** — record the box\n  width and that it clips, or the renderer soft-wraps the line into two downstream.\n\n### Export each layer *as what it is* — PNG is the fallback, not the default\n\nThe layer's **source type** decides the export format. Deciding this per layer is the job;\nreaching for PNG on every element is the freelance-default that quietly wrecks quality —\n**stop and ask \"is this layer actually raster?\" before you export a PNG.**\n\n- **Text** → keep as **live text** (the string + resolved font URL), *never* an image. This is\n  what makes type-on, per-line and per-character animation possible; baking text to PNG kills\n  all of it and can't reflow.\n- **Vector** — icons, logos, line art, flat shapes → keep **vector**: export **SVG**, or keep a\n  flat `#rrggbb` fill as a fill (no asset at all). **Default: turn the SVG's paths into a live\n  shape layer** with `picsart_media_import_svg_path` (one call per `<path>` `d`, carrying its\n  fill/stroke/opacity). A live shape needs no hosting, is never CSP-blocked, and can recolor and\n  draw on. Keep the SVG itself as an uploaded asset only when it holds what paths can't express\n  (filters, masks, embedded images/text). Vector stays crisp when it scales/pops and is lighter to\n  move. Rasterize to PNG **only** if the node genuinely can't be had as vector.\n- **Raster / photographic** — the hero image, a photo, a gradient/blur that won't survive as\n  vector → **PNG with transparency** so the layer floats over what's behind it.\n\nIf you're about to export a PNG of an icon, logo, or shape, that's the tell you *defaulted*\ninstead of decided — go back and take it as vector.\n\n**Settle output scale in the first five minutes, not the fiftieth.** Whether the ad ships at\none size or several (multiple ratios/resolutions, or any later up-scale) is what makes vector\nthe *normal* case, not an edge case — a PNG baked at one scale is soft or jagged at any other,\nwhile vector is resolution-free. Decide this up front (see *Set the format* below); don't\ndiscover the constraint at export time and re-import. And when you do capture\na vector layer, **declare it `type: \"vector\"`** — silently recording an SVG as `type: \"image\"`\nis the same defaulting mistake wearing a different hat.\n\n**Format doesn't excuse hosting:** a vector converted to a live shape needs no asset at all. But\nwhatever you keep as a file, the asset still has to be lifted onto a\n**Picsart-hosted `https://` URL** (below). A `figma.com` SVG URL is CSP-blocked in the preview\nexactly like any other figma.com asset — \"keep it vector\" does **not** mean \"author the\nfigma.com link.\" Export vector → upload → author the returned Picsart URL.\n\n### Granularity — split to what moves\n\nNot every node, and not one blob. **A layer is anything that either moves independently or\npersists across frames.** Group the rest.\n\n- Split out: the headline, each CTA, the hero image, a scrollable list, a tap target,\n  anything that will animate on its own.\n- Keep grouped: static decoration, a card's fixed internals, backgrounds.\n- Over-splitting (hundreds of nodes) is noise the designer must wrangle; under-splitting\n  (the whole screen as one image) is the slideshow trap in disguise. When unsure, split\n  by what the reference ad would animate.\n\n### Import every element the design shows — visibility is the design's call, not yours\n\nImport **every element the design renders**. A layer is dropped for exactly one reason: the\n**design itself hides it** (Figma `visible:false` / a `hidden` node) — that is the designer's\nsignal, and even then a *prominent* hidden layer is worth a note, since it may be meant to appear\nmid-animation. **Never drop a layer because *you* judged it \"won't be seen\":**\n\n- **Partially on-frame** (clipped by the frame edge) is **on screen** — it shows in the very first\n  hold, so omitting it is a visible hole against the design. This is the frame-6 tell: the search\n  pill and the tag row are *clipped, not absent*, and dropping them leaves the composed frame\n  missing content the mockup plainly shows.\n- **Fully off-frame is not a licence to drop it either.** In motion a wide row or an off-edge\n  element is usually there to **scroll or pan into view** — import it and let choreography decide,\n  don't pre-empt the motion by deleting it (that is how a design's intended scroll becomes a\n  static gap).\n\nA judgment call to leave something out is **surfaced to the designer, never silent** — same rule\nas a missing font. The Stage-2 `get_screenshot` compare is what catches a dropped element, as a\nhole where the design has content.\n\n## Tag matched elements across frames\n\nThe thing that makes an interface *move* between screens instead of dissolving is a\n**matched element** — a layer present in consecutive frames that should animate from its\nstate in A to its state in B. So as you import, give any recurring layer a stable\n**`matchKey`** (the same key on the header in every frame it appears in). Match by\nname/role first, geometry second. `motion.json.matched[]` is built from these keys, and\nStage 2 keeps each matched element as **one continuous layer**, not a fresh copy per\nframe. Miss this and the compose step can only cross-dissolve — the biggest \"slideshow\"\ntell there is.\n\n**A recurring element keeps its `matchKey` even when it is transformed or its content changed** —\na title that moves and shrinks, a headline whose wording grows (`AI` → `AI Effects`), a button that\nrecolours. Match by **semantic role** (\"the title\", \"the CTA\"), *never* by exact string or position:\n`AI` and `AI Effects` are the same title. The fact that the same element is **present in both frames\nbut different** is the *strongest* match signal there is — it means the element should **animate from\nA to B**, and it is precisely the case where matching must not fail. A same-role text or object that\nis merely repositioned/resized/reworded must **never** be tagged as one element leaving and a\ndifferent one arriving; that misread is what makes it disappear-and-reappear (or get papered over\nwith a transition) downstream instead of moving.\n\n**Matching is at the layer level — so an assembling sentence needs the layers split to match.**\nThe rule \"a header in frames 1–4 is one layer with four transform states\" is right for chrome, but\nit breaks for text that *grows*: frames whose text reads `AI` → `AI Effects` → `AI Effects made\neasy.` are usually **one whole-string text node each**, i.e. three differently-named layers — so\nnothing marks `AI` as persisting; the thing that actually persists is a **substring**. Capture it\nthat way: split the shared words into their **own layer** carried by one `matchKey` across the\nframes, and each added clause into its own layer that `enter`s. A single whole-string node per\nframe gives the diff three different strings and no persisting layer, and the assembling effect\ncollapses to a cut. (This is the import half of `MOTION-PATTERNS.md` *\"Progressive text build.\"*)\n\nThe A→B **delta** itself (how far a matched layer moved, what actually changed) is not judged\nby eye at motion time — it is **read with `picsart_media_diff_layouts`** (which resolves both\nframes through `query_layout` and returns the classified delta; `query_layout`-on-both is the\nfallback when the tool is unavailable). The `matchKey` you set here is exactly what that diff\ntakes as its `matchHints` to pair the layers up — a dropped key becomes a missed match there.\nThe full frame-diff procedure is in\n[`../motion/references/MOTION-PRINCIPLES.md`](../motion/references/MOTION-PRINCIPLES.md) §1.\n\n## Get every asset onto a real URL\n\nA Figma export and a `file://` path are both **unfetchable by the renderer**. So each\nlayer asset that is not already an `https://` URL must be lifted onto one: `picsart_media_upload`\n(the drag-and-drop panel) or `picsart_drive`, then **read back the resulting `https://` URL and\nauthor *that*.** Text and fills carry no asset and need no lift.\n\n**A scene asset's `uri` must be a Picsart-hosted `https://` URL (`pastatic`/`gcdn`, from\n`picsart_media_upload` / `picsart_drive`) — never a base64 `data:` URI, and never a `figma.com`\nURL.** The interactive **preview engine runs in a browser with a Content-Security-Policy** whose\n`connect-src` allowlist excludes both — they get **blocked → blank** in the editor preview\n(`csp: blocked connect-src … figma.com/….svg`), even though the scene *validates* and the\nserver-side `export`/`contact_sheet` render them fine. That server/browser split is the trap: a\n`figma.com` URL or a `data:` URI is only ever an **intermediate** — hand it to `upload`/`drive` and\nauthor the **Picsart-hosted URL it returns**. **`validate_scene` and a clean `contact_sheet` do NOT\nprove it renders in the preview** (both are lenient server-side paths) — the only reliable proof is\nthe editor preview itself, so default to Picsart-hosted URLs and it's fine everywhere.\n\n## When layers cannot be had — the honest fallback\n\nTwo cases force flat-frame ingest, and both are a **degrade, announced**:\n\n- **No Figma connector.** Suggest it once, and offer the fallback in the same turn —\n  never block, never re-ask, and you cannot connect it for them:\n  > Connecting the Figma connector (Settings → Connectors → Figma) lets me pull your\n  > design's layers, which is what makes real motion design possible. Without it I can\n  > only animate whole flat frames — a slideshow. Share the Figma file, or paste flat\n  > frame images to do the simpler version.\n- **Figma export quota'd out.** Figma export is metered **per Figma seat** and can quota\n  out mid-batch — that is **status, not an error**: continue on the upload path for the\n  remaining assets, and if only flats are reachable, say the result will be a slideshow\n  and offer to resume layered when the seat's quota returns.\n\nA flat frame is imported as a single `image` layer filling the composition — the montage\nfallback in the `motion` skill. Say plainly what is lost: *\"without the layers this is a\nslideshow, not motion design.\"*\n\n## Order — take Figma's, never ask for it\n\nThe running order is **Figma's own** — the frame/artboard sequence in the file (or, for\npasted images, the order they were given in). **Take it as authoritative, record it, and\nmove on — do not ask the designer to confirm it.** A question the source already answers is\nnoise — the house rule: *\"don't turn something the pipeline\nalready knows into a user choice.\"*\n\nReordering is **optional and designer-initiated**: it happens on the storyboard board\n(shown for a glance and a drag, never a mandatory confirm) or when the designer explicitly\nsays \"swap 2 and 3.\" Absent that, Figma order stands, silently. Never open an\nthe host question interface to confirm the running order.\n\n## Set the format — inferred directly (no setup widget)\n\nDecide the video-wide format yourself from the material — **infer it, state it, and proceed**;\nthis workflow does not require the `picsart_motion_setup` console, which production may expose. Choose:\n\n- **ratio / resolution** — from the frames' own aspect (1080×1920 → 9:16 at 1080×1920). Take what\n  the design already is.\n- **fps** — 30 by default.\n- **energy** — read from the design (see below); pick the register the ad calls for.\n- **output scale/sizes** — default to a single size, but **surface the question when it's genuinely\n  open**: will this ship at one size or several (multiple ratios/resolutions, or a later up-scale)?\n  The answer decides raster-vs-vector for every layer above, and \"multiple sizes\" makes vector the\n  norm — cheap to settle now, expensive to discover at export. When it isn't obvious, a plain\n  the host question interface is the right surface; otherwise state the single-size default and move on.\n\nState the chosen ratio/fps/energy to the designer in one line and let them adjust in plain\nconversation (\"make it square\", \"60fps\", \"more energetic\") — a normal message, not a widget.\n\n**`energy` is a productive↔expressive dial**, not just \"fast vs slow\": *productive* =\nsubtle, efficient motion for routine beats; *expressive* = vibrant, highly-visible motion for\nsignificant moments (an opening, a primary CTA, a hero reveal). It scales the motion's duration\nand easing register downstream — see\n[`../motion/references/MOTION-PRINCIPLES.md`](../motion/references/MOTION-PRINCIPLES.md) §6.\n\nTwo things are never asked: **order** (taken from Figma silently, above) and **total duration**\n(derived from the motion, never a target; a designer who wants it tighter says \"punchier\").\n\n## Output → `motion.json`\n\nWrite frames-of-layers and the target (schema in the `motion` skill). Each frame carries\na `layers[]` with `id`, `role`, `type`, `transform`, `asset`/`content`, and `matchKey`\nwhere it recurs; `matched[]` is assembled from the keys.\n\n## Gate to pass Stage 1\n\nPrint it with ✓/✗ before compose:\n\n- Every frame imported as **layers** (or consciously flagged flat-fallback with the\n  reason recorded).\n- Every animatable layer has an **asset at a real `https://` URL** (or live text/fill)\n  **and a transform** with z-order.\n- **Every element the design shows is imported** — nothing dropped on a self-made \"won't be\n  visible\" call; a partially-clipped or off-frame layer is still imported. Only designer-hidden\n  layers (`visible:false`) are omitted, and a prominent one is noted.\n- **Matched elements tagged** with `matchKey` across the frames they appear in.\n- Each layer captured **in its native format** — text is live text, icons/logos/shapes are\n  `type: \"vector\"` (SVG/fill), only genuinely raster content is PNG. No vector or text baked to\n  PNG by default.\n- **Order** recorded, **target format** set, and **output scale/sizes** settled.\n\nA frame that came in flat while its layers were available is a **gate failure, not a\nshortcut** — the whole pipeline's value is here.\n\n## Mistakes\n\n- Exporting flat frames when the Figma layers were right there — the slideshow trap at\n  its source.\n- **Silently dropping a design element because you judged it \"won't be visible.\"** A\n  partially-clipped element is *on screen now*; a fully off-frame one usually *scrolls in*. Only a\n  designer-hidden layer is a legitimate skip — and even that is surfaced if prominent, never a\n  silent judgment call.\n- Losing transforms/z-order, so composed layers stack or land wrong.\n- Not tagging matched elements, forcing cross-dissolves later.\n- Over- or under-splitting: hundreds of nodes to wrangle, or one blob that cannot move.\n- **Flattening a vector or text layer to PNG by default** — text should stay live, icons/logos/\n  shapes stay vector; PNG is only for genuinely raster content. Reaching for PNG per element is\n  the freelance-default that softens the ad at any other scale.\n- **Declaring an SVG layer `type: \"image\"`** — recording vector as raster is the same default\n  wearing a different hat; it loses the resolution-free path.\n- **Dropping a text layer's alignment or box behaviour** — capturing only `x/y/w/h` and then\n  guessing `center`, or losing the fixed-width-clip box so the line soft-wraps to two downstream.\n  Read `textAlignHorizontal` and the box sizing from Figma; don't infer them from position.\n- **Not settling output scale up front** — discovering \"it ships at three sizes\" after baking\n  single-scale PNGs means a re-import; ask it on the setup panel in the first five minutes.\n- Binding a Figma link or local path directly — unfetchable; lift to a Picsart-hosted URL first\n  (a `figma.com` SVG is CSP-blocked in preview just like any figma.com asset).\n- Nagging about the Figma connector after the designer already chose the flat path.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}