groups-compose
The groups-compose skill organizes semantic clusters on a filmmaking canvas by creating visual frames around related nodes. Use it when three or more nodes share a clear conceptual tie (such as the same scene, character, or story beat) and would be easier to read if grouped together with a labeled frame. Groups are strictly visual layout tools stored in canvas_positions.json and do not affect workflow content. Common patterns include scene groupings that wrap a script note with its images and videos, and character-reference sets combining a character card with related images.
git clone --depth 1 https://github.com/Utopai-Research/pai-pro /tmp/groups-compose && cp -r /tmp/groups-compose/skills/groups-compose ~/.claude/skills/groups-composeSKILL.md
## When to propose a group
A good grouping earns its frame. Rule of thumb:
- **3+ nodes** share a clear semantic tie (same scene, same character, same beat)
- The relationship would be obvious to a reader within 2 seconds of scanning the canvas
- You can write a ≤ 30-character title that names the tie
If fewer than 3 members resolve, or the tie is just "these happened to be generated in a row", skip the group. A too-eager grouping is worse than none — it adds a frame the reader has to parse without carrying real meaning.
## Contract recap (enforced)
A group is a **visual frame** that wraps member nodes on the canvas. The frame and the member node positions are view state in `canvas_positions.json`; they are not workflow content.
- Use `canvas_layout.js` to write member `positions` and `groupFrames` in one atomic sidecar update.
- Never write or edit `workflow.json`; never put `x` / `y` into node data.
- A layout change may move member nodes when that makes the canvas clearer.
- The frame's geometry (`x`, `y`, `width`, `height`) is a bounding box computed from the final member positions.
- A node may appear in at most one frame. If a proposed member is already in an existing frame, evict it in the same layout update (upsert the old frame with reduced `memberIds`, or delete the old frame if fewer than 2 members would remain).
- No nested frames.
- `frameId` format: `frame_<unix_ms>` (e.g. `frame_1716579123456`). Matches the frontend's convention. Titles are free-form (≤ 30 chars recommended).
## Patterns
Pick the one that fits. Grouping is current canvas state; read `workflow.json` per the project `PROJECT_AGENT.md` § "Choosing context" to verify ids.
### 1. Scene grouping
Most common. Group the prompt, generated imagery, and any notes that all belong to the same scripted scene.
- Triggers: the user framed a sequence of generations around a single scene (location, beat, or plot point). Look for 3+ nodes that share a scene tag in prompts / labels.
- Title format: `Scene <N> — <location or beat>`. Examples:
- `Scene 1 — Causeway`
- `Scene 3 — Kitchen, 2 AM`
- `Scene 5 — Rooftop chase`
- Typical size: 3–8 members.
- Members: the scene's prompt / shot / image_result / video_result cards, plus any notes that scope to that scene.
- Layout: place the script/shot note on the left, then images/videos in reading order to the right. Put attached voice/audio below the source card.
### 2. Character-reference set
A character card + its reference images.
- Triggers: ≥ 2 images of the same person / character.
- Title format: `<Character name> — references`. Examples:
- `Morris — references`
- `Riya — references`
- Typical size: 2–6 images.
- Members: any `image_result` nodes depicting the same character.
- Layout: hero/reference card first, variations in a compact grid, attached voice node below.
### 3. Act / beat grouping
Coarser than scene — groups a whole Act or story beat.
- Triggers: the user framed the session at act/beat granularity ("everything for act 2", "the whole chase sequence", "opening titles").
- Title format: `Act <N>` or a beat name. Examples:
- `Act 1`
- `Opening titles`
- `Chase sequence`
- Typical size: 8–15 members. If larger, prefer splitting into scene subgroups instead.
- Members: all nodes that belong to that act/beat, spanning multiple scenes.
- Layout: arrange scene clusters left-to-right in story order, with enough gutter that frames do not overlap.
### 4. Production-state grouping (opt-in)
Less common; use only when the user explicitly sorts by quality / status.
- Triggers: "approved shots", "draft", "rejected", "WIP", "final".
- Title format: a single status word. Examples: `Approved`, `In progress`, `Rejected`.
- Typical size: open-ended.
## Recipe
Frames and positions go through the layout CLI. The workflow mutator has no group ops; grouping is visible canvas layout.
1. **Read `./workflow.json` + `./canvas_positions.json`.** workflow.json gives you node ids + labels + subtypes; canvas_positions.json gives you each node's `x` / `y` AND the existing `groupFrames` map. Reads are unrestricted; writes go through `canvas_layout.js`.
2. **Pick members.** Identify which nodes belong in the proposed frame by looking at their ids, labels, prompts, and subtypes. Keep only ids that actually exist in `nodes`.
3. **Plan positions.** Preserve existing positions when they already read well. Otherwise move the selected nodes into a compact layout for the chosen pattern. Use these default gaps:
- horizontal card gap: 40 px
- vertical row gap: 36 px
- frame padding: 24 px
4. **Evict any member already in another frame.** For each id in your proposed `memberIds`, scan existing `groupFrames`. If you find a frame that contains it, include that old frame in the same layout JSON:
- If the old frame would still have ≥ 2 members after eviction: include it under `groupFrames.upsert` with `memberIds` minus the evictee.
- If the old frame would have < 2 members: include its id under `groupFrames.delete`.
5. **Compute frame bboxes** from final member positions. Use 24px padding (matches the frontend's `FRAME_BBOX_PADDING`). Member widths/heights are determined by `pickSize` in `web/src/pages/CanvasPage/placement.ts`. Use these fallbacks per type:
- `note`: **280 × 420** (width hardcoded; height = `NOTE_CARD_FALLBACK_HEIGHT` for first paint)
- `image_result`: **290 × 220** (16:9 default; if `data.metadata.aspect_ratio` is present, scale accordingly)
- `video_result`: **290 × 220** (same caveat; check `data.aspect` or `data.metadata.aspect_ratio`)
- `audio_result`: **240 × 64**
- `pending` / `pending_generation` / `pending_attachment`: **260 × 200**
*Heads-up on dynamic heights*: React Flow measures each card's real rendered height after first paint and stores it in `measuredHeights` (see `useCanvasPositions.ts`). The 420 px fallback for `note` is the **maximum** initial height; short notes will measure smaller and the frame may>-
>-
>-
Generates and prompts video clips on the filmmaking canvas. Use when the user asks to generate, render, animate, continue, restyle, edit, shoot, or compose a video clip; render script or shot notes as video; animate a storyboard, starting frame, image, character, location, or reference; use image, video, audio, storyboard, starting-frame, or voice refs; compose an ad, brand film, product promo, music-video shot, or video sequence; or before calling generate_video.js. Owns video CLI flags, refs, prompt construction, audio-ref handling, and video-specific failure hints.
Designs and attaches voice samples or final narration/line audio on the filmmaking canvas via the local generate_voice.js CLI. Use before calling generate_voice.js; when the user asks to give a character a voice, preview how a character sounds, create a reusable timbre anchor for video dialogue, or create exact narration/VO/final line audio.