Carousel Skill
SkillDocs & knowledgeAgent-authored workflow task: build slides for a carousel project — pick aspect-aware images via generate_image, position image and overlay elements on each slide, set base colors, and render to PNGs. Load this when you hit montaj/carousel in a workflow.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Carousel Skill skill
What this skill tells your AI
The instructions your AI receives, as published by thesampadilla/montaj in skills/carousel/SKILL.md and read by ahel’s review.
Use this skill whenever you are working on a carousel workflow project in montaj.
1 — What a carousel is
A carousel is N still slides at one fixed aspect ratio, rendered to slide_01.png … slide_NN.png in <project>/render/. There is no time axis, no audio, no video export.
| Aspect | Constant | Resolution | Platform |
|---|---|---|---|
square | CAROUSEL_ASPECTS[0] | 1080 × 1080 | Instagram square |
portrait | CAROUSEL_ASPECTS[1] | 1080 × 1350 | Instagram portrait (dominant in 2026) |
vertical | CAROUSEL_ASPECTS[2] | 1080 × 1920 | TikTok photo mode, Stories |
The aspect is locked at project creation — Instagram and TikTok enforce uniformity across all slides in a carousel. Read it from project.carousel.aspect; read pixel dimensions from project.settings.resolution ([width, height]). Never attempt to change the aspect after init.
Reference: /Users/Sam/Work/ByCrux/dev/montaj/lib/types/carousel.py — CAROUSEL_ASPECTS, CAROUSEL_RESOLUTIONS.
Sub-skills
| Name | Path | When to load |
|---|---|---|
editable-text | skills/editable-text/SKILL.md | Before authoring any text overlay — defines the 9-prop contract the editor toolbar relies on. |
2 — Inputs you have
project.editingPrompt— the user's natural-language instruction for the whole carousel (e.g. "5-slide skincare routine, clean aesthetic, soft pastels").project.assets— list of reference images the user attached at intake or in the editor:[{ id, src, type, name }].srcis an absolute path (the file lives inside the project workspace directory). Pass these as--ref-imagetogenerate_imagewhen style consistency matters, or use them directly as image-elementsrcvalues on slides.
3 — Slide schema
// project.slides: Slide[]
{
"id": "uuid", // crypto.randomUUID() — never positional IDs
"base_color": "#ffffff", // background fill shown wherever no image covers
"elements": [ // z-order: bottom → top
// Image element
{
"id": "uuid",
"type": "image",
"src": "assets/<mediaId>.png", // workspace-relative path; required
"mediaId": "<uuid>", // optional metadata, references org Media row
"x": 0, "y": 0,
"w": 1080, "h": 1080,
"rotation": 0
},
// Overlay element
{
"id": "uuid",
"type": "overlay",
"overlay": {
"template": "<overlay-id>", // path or name returned by GET /api/overlays
"props": {
// overlay-specific knobs (text, color, animation timing, etc.)
// DO NOT set offsetX / offsetY / scale here — they are forced to
// identity at render time. Slide-side x/y/w/h/rotation own position.
}
},
"frame": 59, // static frame to render; default = overlay's
// staticFrame export, or duration - 1
"x": 140, "y": 200,
"w": 800, "h": 300,
"rotation": 0
}
]
}
src is the workspace-relative path the renderer reads from disk. mediaId is optional metadata — when present, it lets Hub's editor surface the filename, link back to the Media library row, and reason about provenance. The renderer ignores mediaId.
When working from Hub, never write a presigned URL into src. The materialization machinery puts a workspace path there; Hub returns the workspace path from hub.attach_media.
Key rules:
- Use
crypto.randomUUID()for everyidon slides and elements. - Element ordering in
elements[]is z-order, bottom → top. Put full-bleed images at index 0. - A "background" image is just an image element sized to the slide at index 0. No special background type exists.
- Text and shapes are overlays, not dedicated element types.
frameselects which frame of an overlay's animation gets statically rendered. Default: the overlay's declaredstaticFrame, orduration - 1(end of entrance animation) whenstaticFrameis absent.
Reference: /Users/Sam/Work/ByCrux/dev/montaj/montaj_assets/render/templates/slide.jsx — the React component that receives and renders this shape.
4 — Generating images
Use the existing generate_image step (/Users/Sam/Work/ByCrux/dev/montaj/steps/generate/generate_image.json). Always generate at the project's native resolution so the image fills the slide without scaling artifacts.
montaj generate-image \
--prompt "soft pastel background, skincare product flat lay, minimalist" \
--out <project_workspace>/assets/slide1_bg.png \
--provider gemini \
--aspect-ratio "4:5" # match the project aspect: square → 1:1, portrait → 4:5, vertical → 9:16
CLI signature summary:
| Param | Required | Notes |
|---|---|---|
--prompt | yes | Describe the image |
--out | yes | Absolute path, .png |
--provider | no | gemini (default) or openai |
--ref-image | no, repeatable | Reference images from project.assets for style consistency |
--aspect-ratio | no | Gemini only. 1:1, 4:5, 9:16 — match the carousel aspect |
--model | no | Override the connector's default model |
After generation, store the file under the project workspace (e.g. <workspace>/assets/) and set the element's src to the relative path from the workspace root (e.g. "assets/slide1_bg.png").
5 — Overlays
Carousel overlays are custom JSX written per project — there is no built-in template library. Author each overlay you need (headline, subhead, callout, etc.) per the rules below.
- For text overlays that the operator should be able to restyle from the editor toolbar (font / size / color / weight / case / alignment), follow
skills/editable-text/SKILL.md. The canonical reference ismontaj_assets/render/templates/overlays/static-text/static-text.jsx. - For charts (
bar-chart,line-chart,pie-chart) — single-series bar, multi-series line, pie/donut. SVG-rendered via Recharts. Sized from the overlay element's w/h. - For non-text overlays (logos, decorative shapes, image-based overlays, stylized brand marks), follow
skills/write-overlay/SKILL.mdas before.
Save the JSX under the project workspace (e.g. <workspace>/overlays/headline.jsx) and reference it by absolute path in overlay.template.
Carousel-specific rules layered on top of write-overlay:
- Position belongs to the slide, not the overlay. Do not set
offsetX,offsetY, orscaleinoverlay.props— the slide-sidex/y/w/h/rotationalways win, and the renderer forces those three fields to identity at render time (seeslide.jsx). - Pick the static frame explicitly. Carousels render a single still frame from the overlay's animation. If the JSX module exports
staticFrame, use that value. Otherwise default toduration - 1(the settled pose after any entrance animation). Always store the resolved value explicitly inproject.json— do not rely on defaults.
Reference: /Users/Sam/Work/ByCrux/dev/montaj/docs/plans/2026-05-04-image-carousel.md — Decisions section ("Overlay-coordinate precedence", "Static rendering of frame-driven overlays").
Typography for carousels
Carousels carry full sentences and paragraphs — the viewer is reading, not glancing past a video frame. Do NOT apply write-overlay's "Go large / 96px floor" rule. That rule is calibrated for short hooks (3–6 words) on 1080×1920 video with moving footage; it produces unreadable wall-of-text on a 1080×1080 carousel.
Use these defaults instead, sized to the carousel's resolution[0] (1080 for square/portrait, 1080 for vertical):
| Role | Short copy (≤ 40 chars) | Medium (40–80) | Long (80+) |
|---|---|---|---|
| Hook headline (slide 1) | 64–80px | 44–56px | 32–44px |
| Body / answer text (FAQ, captions in cards) | 28–34px | 24–28px | 22–26px |
| Eyebrows, kickers, all-caps labels | 18–26px | 18–22px | 18–22px |
| Footer / sign-off | 26–32px | 22–26px | 20–24px |
Rules of thumb:
- Measure your headline first. Long headlines (the typical Idea title — 60–90 chars) need small type at carousel sizes, not big type. A 78px headline wrapping to 6+ lines is broken, not bold.
- Card-bound body text (text inside a solid background card with padding) can run smaller — the card provides contrast, so 22–24px reads cleanly. Free-floating text on top of a base color needs more weight — 26–30px.
- Line height 1.1–1.25 for headlines, 1.35–1.5 for body. Tight lines compress headlines; loose lines breathe body.
- Letter-spacing stays near
normalfor body, slightly negative (-0.5pxto-1.5px) for large headlines, slightly positive (2–6px) for small all-caps labels. - One accent color max per slide. Inherit accent / primary / fg / cream from the brand palette via overlay props — don't hardcode hex values that drift from
hub.get_brand.
If a slide's text doesn't fit at these sizes, the copy is too long for that slide — split it, don't shrink past the lower bound. The viewer's eye still has to land.
HTTP / sidecar mode
In CLI / headless mode, you author overlays by writing JSX files directly to
<workspace>/overlays/<name>.jsx. In HTTP / sidecar mode, you don't have
filesystem access — use the dedicated write endpoint instead:
PUT /api/projects/{id}/overlays/{name}
Content-Type: text/plain
Body: <JSX source>
Names are slug-only (a-z, A-Z, 0-9, _, -, up to 64 chars); the server
appends .jsx. PUT is idempotent — re-PUTting the same name overwrites. The
written file lands at <workspace>/overlays/{name}.jsx and is referenceable
from slide elements exactly as in CLI mode.
6 — Persisting slide edits
PUT /api/projects/{id} with the entire project body. The server writes project.json and SSE-broadcasts the updated state.
# Example via curl (agent-side calls go through api.saveProject in the UI,
# or direct HTTP from a script)
curl -s -X PUT http://localhost:<port>/api/projects/<id> \
-H "Content-Type: application/json" \
-d @project.json
Every save is a whole-project PUT. If streaming live drag/resize updates, debounce at ~100 ms trailing to avoid one PUT per pointer-move frame.
7 — Authoring overlays alongside images
When a slide combines an image element with an overlay element, the overlay's transparent root div sits ON TOP of the image (z-order, bottom→top). The image is visible only where the overlay doesn't paint.
The default hook overlay's text container has no max-width, so a long headline can span the full slide and visually obscure an image element placed on the right. Two rules:
- Overlays designed to potentially share a slide with images should accept a
textMaxWidthprop (CSS value, default'100%'). The text container applies it asmaxWidth. When the agent composes a slide with an image, passtextMaxWidth: '60%'(or similar) on the overlay's props. - Position image elements on the side opposite the overlay's text alignment. For
align: 'top'overlays, place images bottom. For left-aligned overlays (the default for hook), place images on the right.
Brand-shipped overlays (hook, faq_card) follow this pattern. Author new overlays with textMaxWidth from day one.
8 — Render
Set project.status to "final" via PUT /api/projects/{id} before running render — the render command requires it.
montaj render <project_workspace>/project.json
Output: <project_workspace>/render/slide_01.png … slide_NN.png plus a manifest.json describing the run.
The renderer (render-carousel.js) launches Puppeteer, bundles each slide's JSX via esbuild, screenshots at the project resolution, and writes PNGs. No ffmpeg, no audio, no video encoding. Pass --out <dir> to write PNGs elsewhere; pass --clean to delete only prior slide_*.png + manifest.json without touching any coexisting video renders.
Reference: /Users/Sam/Work/ByCrux/dev/montaj/montaj_assets/render/render-carousel.js.
9 — Status lifecycle
pending → (build slides) → final → render
New carousel projects start at status: "pending" — same as video projects. The pending status signals that this carousel is waiting on you (the agent) to do the initial build. Build and populate all slides, then:
PUT /api/projects/{id}withstatus: "final"(and the completeslidesarray).montaj render <project_workspace>/project.json
The project is complete when render/slide_01.png … slide_NN.png exist.
Signals
- GitHub stars
- 25
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
carousel-thesampadilla- Source
- github.com/thesampadilla/montaj