@cliangdev/conductor 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/claude/commands/conductor/creative.md +38 -46
- package/assets/claude/skills/conductor-creative/SKILL.md +111 -45
- package/assets/claude/skills/conductor-creative/references/brand-kit-setup.md +41 -0
- package/assets/cli-manifest.json +16 -3
- package/dist/creative/assertions.js +22 -11
- package/dist/creative/frame.js +3 -3
- package/dist/creative/job/file-transport.mjs +211 -0
- package/dist/creative/job/render.mjs +84 -2
- package/dist/creative/job/server.mjs +70 -13
- package/dist/lib/creative-render.d.ts +62 -0
- package/dist/lib/creative-render.d.ts.map +1 -1
- package/dist/lib/creative-render.js +51 -0
- package/dist/lib/creative-render.js.map +1 -1
- package/dist/lib/plugin-assets.d.ts.map +1 -1
- package/dist/lib/plugin-assets.js +1 -0
- package/dist/lib/plugin-assets.js.map +1 -1
- package/dist/mcp/index.d.ts +856 -261
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +164 -75
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools/creative-drafts.d.ts +134 -0
- package/dist/mcp/tools/creative-drafts.d.ts.map +1 -0
- package/dist/mcp/tools/creative-drafts.js +405 -0
- package/dist/mcp/tools/creative-drafts.js.map +1 -0
- package/dist/mcp/tools/creatives.d.ts +3 -0
- package/dist/mcp/tools/creatives.d.ts.map +1 -1
- package/dist/mcp/tools/creatives.js.map +1 -1
- package/package.json +1 -1
- package/scripts/build-creative.mjs +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor:creative
|
|
3
|
-
description: One entry point for Conductor creative work — checks the workspace brand, offers to set it up if it's never been configured, asks what to make (static ad, short video, an existing clip), gathers the few inputs needed, then hands off to the conductor-creative skill through
|
|
3
|
+
description: One entry point for Conductor creative work — checks the workspace brand, offers to set it up if it's never been configured, asks what to make (static ad, short video, an existing clip), gathers the few inputs needed, then hands off to the conductor-creative skill through draft, preview, approval, commit and attach.
|
|
4
4
|
allowed-tools: mcp__conductor__*, AskUserQuestion, Skill, WebFetch, Read, Glob, Grep
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -18,39 +18,10 @@ follow it.
|
|
|
18
18
|
|
|
19
19
|
## Step 1 — Check the brand
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```json
|
|
27
|
-
{
|
|
28
|
-
"questions": [{
|
|
29
|
-
"question": "This workspace's Brand Kit hasn't been set up yet (default colours, no font, no logo). Set it up now?",
|
|
30
|
-
"header": "Brand Kit",
|
|
31
|
-
"options": [
|
|
32
|
-
{"label": "From a website", "description": "Fetch a URL and infer colours, voice and name"},
|
|
33
|
-
{"label": "From a brand doc", "description": "Read a local file (guidelines, style doc)"},
|
|
34
|
-
{"label": "Answer a few questions", "description": "Name, colours, font, CTA line, approved lines"},
|
|
35
|
-
{"label": "Skip for now", "description": "Proceed with the default kit as-is"}
|
|
36
|
-
],
|
|
37
|
-
"multiSelect": false
|
|
38
|
-
}]
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
- **From a website**: ask for the URL, `WebFetch` it, infer a plausible accent colour, name, CTA
|
|
43
|
-
claim and a couple of approved lines from the copy on the page. Show what you inferred and confirm
|
|
44
|
-
before writing.
|
|
45
|
-
- **From a brand doc**: ask for the file path, `Read` it, extract the same fields, confirm before
|
|
46
|
-
writing.
|
|
47
|
-
- **Answer a few questions**: ask for name, primary/accent colour (hex or a description you convert),
|
|
48
|
-
font (if any), the CTA claim, and any lines that must run verbatim. Keep it to one AskUserQuestion
|
|
49
|
-
batch, not a long interview.
|
|
50
|
-
- Once confirmed, call `update_brand_kit` with whatever was gathered. If a logo file or URL is
|
|
51
|
-
available, `upload_brand_image` for the `mark` slot at least. Verify with `get_brand_kit` —
|
|
52
|
-
`configured` should now read `true`.
|
|
53
|
-
- **Skip for now**: proceed; the skill below will render against the default kit as given.
|
|
21
|
+
Read `references/brand-kit-setup.md` in the `conductor-creative` skill
|
|
22
|
+
(`.claude/skills/conductor-creative/references/brand-kit-setup.md`, in this project or under `~/.claude`)
|
|
23
|
+
and follow it: check the Brand Kit, and offer to set it up if it has never been configured. Then
|
|
24
|
+
continue with Step 2.
|
|
54
25
|
|
|
55
26
|
## Step 2 — What to make
|
|
56
27
|
|
|
@@ -77,22 +48,43 @@ Keep this light — one or two follow-up questions, not a form:
|
|
|
77
48
|
|
|
78
49
|
- **Static ad / short video from a photo**: the headline (or let the skill draft one from the
|
|
79
50
|
Knowledge page), and a photo. Call `list_creative_media({kind: "IMAGE"})` first and offer what's
|
|
80
|
-
already on hand before asking for a new
|
|
51
|
+
already on hand before asking for a new photo. A new photo is a local file path (a URL: download it
|
|
52
|
+
to a local file first) — it is only uploaded after the person approves the preview.
|
|
81
53
|
- **Video from a clip I have / Post an existing video as-is**: call
|
|
82
|
-
`list_creative_media({kind: "VIDEO"})` first; if nothing fits, ask for a local path
|
|
54
|
+
`list_creative_media({kind: "VIDEO"})` first; if nothing fits, ask for a local path (a URL:
|
|
55
|
+
download it first). A clip used as a MOTION background is previewed from the local file too.
|
|
83
56
|
- **Test a headline variant**: ask which existing Creative (id or display id like `12a`) and the new
|
|
84
57
|
headline.
|
|
85
58
|
|
|
86
59
|
## Step 4 — Hand off to the skill
|
|
87
60
|
|
|
88
61
|
Call `Skill(skill: "conductor-creative")` and follow it from wherever this command's gathering left
|
|
89
|
-
off — angle, copy rules, layout, `
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
62
|
+
off — angle, copy rules, layout, then the draft-first flow: `preview_creative_draft`, look at the
|
|
63
|
+
image, ask for approval, and only then `commit_creative_draft`. Don't re-derive what that skill
|
|
64
|
+
already covers.
|
|
65
|
+
|
|
66
|
+
## Step 5 — Preview, approve, commit, attach
|
|
67
|
+
|
|
68
|
+
Nothing is saved to Conductor until the person approves what they have seen:
|
|
69
|
+
|
|
70
|
+
1. `preview_creative_draft` with what was gathered (local files go in `photoPath`/`clipPath`/
|
|
71
|
+
`audioPath`; an edit to an existing Creative passes `baseCreativeId`). It renders on this machine
|
|
72
|
+
and returns the image — no Creative is created and nothing is uploaded.
|
|
73
|
+
2. Read `passed` and `checks` in the result first. The preview runs the full render's per-placement
|
|
74
|
+
checks (text spill, platform safe zones such as TikTok/Reels' overlay band, contrast, fonts,
|
|
75
|
+
images), so a design that would fail the real render shows up here. If `passed` is false, fix the
|
|
76
|
+
design (shorter copy, `layoutOverrides`, `typeOverrides`, a different layout) and preview again
|
|
77
|
+
**before** showing it or asking anything — never offer Approve & upload while a blocking check
|
|
78
|
+
fails, and tell the person what you changed. (Bigger or smaller text on one placement is
|
|
79
|
+
`typeOverrides`, e.g. `{"9x16": [96]}`, not a layout switch.)
|
|
80
|
+
3. Once `passed` is true, look at the image against the skill's checklist, then show it and ask with
|
|
81
|
+
`AskUserQuestion`: Approve & upload / Request changes / Stop. On changes, preview again; on
|
|
82
|
+
Stop, leave it — the draft stays local under `.conductor/drafts/`.
|
|
83
|
+
4. On approval, `commit_creative_draft` with the `draftDir`. It uploads the files, saves the
|
|
84
|
+
Creative and does the full render. (A headline variant is previewed with `baseCreativeId` plus the
|
|
85
|
+
new headline, but on approval is saved with `create_creative` and `variantOf` — never commit that
|
|
86
|
+
draft, which would update the original.)
|
|
87
|
+
5. Once the skill's readiness check passes, `attach_creative_to_post` if there's already a Post to
|
|
88
|
+
attach it to (ask for the Post's id if not obvious from context). If there is no Post yet, say so
|
|
89
|
+
plainly and name the next step — creating one via the `conductor-publisher` skill — rather than
|
|
90
|
+
leaving the artwork stranded with no stated destination.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-creative
|
|
3
|
-
description: Creates on-brand marketing creative through the Conductor MCP tools — static ads and short videos alike, rendered locally on this machine
|
|
3
|
+
description: Creates on-brand marketing creative through the Conductor MCP tools — static ads and short videos alike, rendered locally on this machine as a draft, checked by eye and approved before anything is saved to Conductor. Covers a photo-plus-headline ad at every enabled placement (e.g. TikTok/Reels 9:16, Instagram feed 4:5, Facebook 1:1); a short video made from a still photo or an existing clip (animated text, zoom/pan, background music, a TikTok/Reels/Shorts-ready cut); posting a finished video as-is; a story or carousel sequence; a lettered headline/hook variant for testing; and, when a workspace's Brand Kit has never been set up, setting one up first (colours, font, logo, copy rules) from a website, a brand doc, or a few direct answers. Use when asked to make, design, write or export a social ad, a promo image, a short-form video, an animated ad, or to configure/update a project's brand.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
allowed-tools: mcp__conductor__*, AskUserQuestion, Read, Glob, Grep, WebFetch
|
|
6
6
|
---
|
|
@@ -17,7 +17,13 @@ things that already exist for this workspace — a Brand Kit (data: tokens, copy
|
|
|
17
17
|
lines) and a Knowledge page (prose: positioning, voice, facts). Read them rather than inventing.
|
|
18
18
|
|
|
19
19
|
Rendering happens locally, through this MCP server, with a browser on this machine — there is no
|
|
20
|
-
separate render service to wait on.
|
|
20
|
+
separate render service to wait on. A preview typically finishes in well under a minute.
|
|
21
|
+
|
|
22
|
+
**Draft first, save after approval.** Every Creative here goes draft, look, approve, commit:
|
|
23
|
+
`preview_creative_draft` renders the artwork on this machine and writes **nothing** to Conductor — no
|
|
24
|
+
Creative, no upload — then the person looks at it, and only on their approval does
|
|
25
|
+
`commit_creative_draft` upload the media, save the Creative and do the full render. Never commit a
|
|
26
|
+
draft the person has not seen and approved.
|
|
21
27
|
|
|
22
28
|
## How to make a Creative
|
|
23
29
|
|
|
@@ -50,8 +56,8 @@ Prefer an approved line from the Knowledge page verbatim. Otherwise write one th
|
|
|
50
56
|
real and specific, not a generic claim.
|
|
51
57
|
|
|
52
58
|
If the Brand Kit's `accentPhraseRequired` is true, mark exactly one phrase per headline with
|
|
53
|
-
asterisks: `Every saved link, *finally usable*.` Never zero, never two — `
|
|
54
|
-
`update_creative` refuse a headline that gets this wrong, naming the rule.
|
|
59
|
+
asterisks: `Every saved link, *finally usable*.` Never zero, never two — `preview_creative_draft`,
|
|
60
|
+
`create_creative` and `update_creative` refuse a headline that gets this wrong, naming the rule.
|
|
55
61
|
|
|
56
62
|
Keep it short — checked at thumbnail size, not read up close.
|
|
57
63
|
|
|
@@ -69,10 +75,13 @@ uploading a new one — id, dimensions, checked/blocked state and provenance for
|
|
|
69
75
|
the project's media library. A photo needs `checked: true` and not `blocked` before a Creative can
|
|
70
76
|
go to `READY`.
|
|
71
77
|
|
|
72
|
-
A
|
|
73
|
-
|
|
74
|
-
(
|
|
75
|
-
|
|
78
|
+
A photo that is not in the library yet is previewed straight from its local file (`photoPath` on
|
|
79
|
+
`preview_creative_draft`; a URL is downloaded to a local file first) and is only uploaded when the
|
|
80
|
+
draft is committed — pass `source` (a URL or a short provenance note, e.g. "Generated with a named
|
|
81
|
+
model on a given date") and `licence` (e.g. "Own work", "Unsplash Licence", "Generated, house use")
|
|
82
|
+
to `commit_creative_draft`, which records them on every file it uploads. This closes a real legal
|
|
83
|
+
exposure: an unattributed or unlicensed photo running as paid or organic media. A photo already in
|
|
84
|
+
the library is used by its id (`photoId`) with no upload at all.
|
|
76
85
|
|
|
77
86
|
**Open the photo and look at it before using it.** A picture can break every copy rule with text,
|
|
78
87
|
a logo or a claim burned into its pixels, and no automated check can see that — this is the first
|
|
@@ -83,48 +92,98 @@ Match the crop to the subject: a layout that lays copy over the photo (e.g. `ble
|
|
|
83
92
|
the top third in collision with the headline — prefer a layout that keeps the photo and copy on
|
|
84
93
|
separate panels (e.g. `stacked`) for those.
|
|
85
94
|
|
|
86
|
-
### 6.
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
### 6. Preview the draft
|
|
96
|
+
|
|
97
|
+
`preview_creative_draft` with the Brand Kit (if not the default), layout, theme, photo (`photoPath`
|
|
98
|
+
for a local file, `photoId` for one already in the library), headline, body, caption and alt text.
|
|
99
|
+
It renders a contact sheet on this machine and returns it as an image, with the `draftDir` it wrote
|
|
100
|
+
to (`.conductor/drafts/<timestamp>-<slug>/`), the readiness gaps and any render warnings. **Nothing
|
|
101
|
+
has been saved to Conductor.** A copy-rule or accent-phrase violation comes back as an error naming
|
|
102
|
+
the rule and field, without rendering — fix the copy and preview again.
|
|
103
|
+
|
|
104
|
+
To change an existing Creative's copy, layout or photo, preview the edit first with `baseCreativeId`:
|
|
105
|
+
only the fields you pass change, and the commit then updates that Creative instead of creating one.
|
|
106
|
+
|
|
107
|
+
To test a headline variant of an existing concept, preview the new headline the same way
|
|
108
|
+
(`baseCreativeId` plus `headline`) so the person sees it on the artwork — but do **not** commit that
|
|
109
|
+
draft, since a commit would update the original. Once they approve, call `create_creative` with
|
|
110
|
+
`variantOf` and the headline instead: it inherits everything else and gets the next letter.
|
|
111
|
+
|
|
112
|
+
Pass `full: true` to render every placement at full size instead of the contact sheet (slower; the
|
|
113
|
+
image returned is the first placement).
|
|
114
|
+
|
|
115
|
+
**Read `checks` and `passed` before you show anything.** The contact sheet never runs the render's
|
|
116
|
+
safety checks, so the preview also runs them for every placement (and, for a video, its end card)
|
|
117
|
+
and returns them as `checks: [{placementKey, index?, rule, message, severity}]` plus `passed`. An
|
|
118
|
+
`error` check (`safeZone`, `spill`, `contrast`, `font`, `image`, ...) is exactly what would fail the
|
|
119
|
+
full render after the person approved: e.g. an App Store badge intruding into the band TikTok/Reels
|
|
120
|
+
reserve on 9:16. A `warning` (a soft, upscaled photo) does not block, but mention it.
|
|
121
|
+
|
|
122
|
+
When `passed` is false, **do not show the image or ask for approval yet.** Fix the design and
|
|
123
|
+
preview again, repeating until `passed` is true:
|
|
124
|
+
|
|
125
|
+
- the copy collides with a reserved zone: shorten the headline or drop the body;
|
|
126
|
+
- the photo takes too much of a placement: lower its height there with `layoutOverrides.band`
|
|
127
|
+
(e.g. `{"band": {"9x16": 1100}}`);
|
|
128
|
+
- the bottom row (CTA/badge) does not clear the safe zone: raise the clearance with
|
|
129
|
+
`layoutOverrides.padBottom`, or pick a layout that keeps it clear;
|
|
130
|
+
- the text is too large or too small for a placement: set it with `typeOverrides` (below).
|
|
131
|
+
|
|
132
|
+
Tell the person what you changed and why when you do show it. Never offer **Approve & upload**
|
|
133
|
+
while a blocking check fails.
|
|
134
|
+
|
|
135
|
+
**Bigger or smaller text on one placement: use `typeOverrides`, not a different layout.** It pins
|
|
136
|
+
the headline per placement as `{"<placementKey>": [fontSize, lineHeight?, letterSpacing?]}`, e.g.
|
|
137
|
+
`{"9x16": [96], "1x1": [88]}` for a bigger headline on 9:16 and 1:1. `fontSize` is in artboard px
|
|
138
|
+
(a 9:16 board is 1080 wide), `lineHeight` a multiplier (e.g. `1.0`) and `letterSpacing` in px
|
|
139
|
+
(usually negative); leave the last two out to take the engine's defaults for that size. A placement
|
|
140
|
+
you do not name stays auto-fitted. A pinned size is not shrunk to fit, so re-check `checks` after
|
|
141
|
+
setting one. It works on `preview_creative_draft` and `create_creative`/`update_creative`; changing
|
|
142
|
+
the headline or layout of a saved Creative clears pinned sizes unless you send them again.
|
|
96
143
|
|
|
97
144
|
### 7. Look at the result honestly
|
|
98
145
|
|
|
99
|
-
**
|
|
100
|
-
|
|
146
|
+
**Passing checks prove the frame is structurally correct, not that the ad is good.** The checks
|
|
147
|
+
(`passed: true`) catch text spilling out of a safe zone, a font that failed to load, a photo that
|
|
101
148
|
never decoded, or contrast below the accessible minimum — not whether the headline reads at a
|
|
102
149
|
glance, whether the accent phrase falls on the words that carry the meaning, or whether the copy is
|
|
103
|
-
sitting on the subject's face. Judge those yourself from the image `
|
|
104
|
-
the copy collides with the photo, switch layout (see step 5) and
|
|
105
|
-
text until it technically fits.
|
|
150
|
+
sitting on the subject's face. Judge those yourself from the image `preview_creative_draft` hands
|
|
151
|
+
back. If the copy collides with the photo, switch layout (see step 5) and preview again rather than
|
|
152
|
+
shrinking text until it technically fits.
|
|
153
|
+
|
|
154
|
+
Only once `passed` is true, show the image to the person and ask with `AskUserQuestion` —
|
|
155
|
+
**Approve & upload**, **Request changes**, **Stop**. If `passed` is false you are not at this step
|
|
156
|
+
yet: fix it and re-preview first (step 6). On changes, preview again with the edits (another draft; nothing was saved). On
|
|
157
|
+
Stop, leave it: the draft stays on this machine under `.conductor/drafts/` and is cleaned up
|
|
158
|
+
automatically once it is committed or two weeks old. Only on **Approve & upload**, call
|
|
159
|
+
`commit_creative_draft({draftDir})` with the `draftDir` the approved preview returned.
|
|
106
160
|
|
|
107
161
|
### 8. Check readiness before calling it done
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
The preview already lists the readiness gaps, so fix what you can (caption, alt text, a checked
|
|
164
|
+
photo) before asking for approval. After the commit, `get_creative` includes a `readiness` block: caption, alt text, the photo's checked/blocked state
|
|
110
165
|
and provenance, and the Creative's own state (not `DRAFT`) are blocking; an AI-generation disclosure
|
|
111
166
|
is informational only. Do not call a Creative done while `ready` is false.
|
|
112
167
|
|
|
113
|
-
### 9.
|
|
168
|
+
### 9. Commit, then hand off
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
full-size
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
170
|
+
`commit_creative_draft` uploads the draft's local files, creates the Creative (or updates
|
|
171
|
+
`baseCreativeId`'s), and runs the full-size render in one call, then returns the Creative id and
|
|
172
|
+
display id, the frames and the readiness. It can be run once per draft; if its render fails the
|
|
173
|
+
Creative is already saved — call `render_creative`, don't commit again. Verify with `get_creative`.
|
|
174
|
+
Then `attach_creative_to_post` with the Post's Work Item id — it maps each placement to the right
|
|
175
|
+
destinations (9:16 to TikTok/Reels, 4:5 to Instagram feed, 1:1 to Facebook, story to story targets)
|
|
176
|
+
for every destination that has not set its own custom media, and never touches one that has. Verify
|
|
177
|
+
with `get_post_status`. From here, use the `conductor-publisher` skill for scheduling, review and
|
|
178
|
+
publishing — this skill's job ends at attaching the artwork.
|
|
121
179
|
|
|
122
180
|
## Making a sequence instead of a single ad
|
|
123
181
|
|
|
124
182
|
For a platform surface that's tapped or swiped through, set `sequenceKind` to `story` (2 to 7
|
|
125
|
-
beats, aim for 3 to 5) or `carousel` (2 to 10 cards, 5 to 7 performs best) on
|
|
126
|
-
`
|
|
127
|
-
own `headline`, and optionally `body`, `
|
|
183
|
+
beats, aim for 3 to 5) or `carousel` (2 to 10 cards, 5 to 7 performs best) on
|
|
184
|
+
`preview_creative_draft` (then `commit_creative_draft`), with a `sequence` array. Each beat inherits
|
|
185
|
+
the Creative and overrides only its own `headline`, and optionally `body`, `photoPath` (a local
|
|
186
|
+
photo, uploaded on commit) or `photoId`, or `cta`. Structure a story hook, mechanism,
|
|
128
187
|
payoff — the first beat has to earn the next tap on its own, since that's where the drop-off is
|
|
129
188
|
worst. A logo lockup and CTA claim land on the last beat only by default; `cta: true` on an earlier
|
|
130
189
|
beat overrides that. A carousel renders every card at one shared aspect ratio (`carouselRatio`),
|
|
@@ -136,6 +195,9 @@ Not every video needs the brand layout above — a finished clip (a demo recordi
|
|
|
136
195
|
something a person or another tool already produced) can go up as-is. That's a CLIP Creative: no
|
|
137
196
|
photo, headline or layout, just the video.
|
|
138
197
|
|
|
198
|
+
A CLIP has no artwork to render, so it is the one flow without a draft preview: open the video
|
|
199
|
+
itself and check it with the person before uploading.
|
|
200
|
+
|
|
139
201
|
1. Check `list_creative_media({kind: "VIDEO"})` for a clip already in the project before uploading a
|
|
140
202
|
new one. A new one goes through `upload_creative_media` (local path or URL) — same provenance
|
|
141
203
|
fields as a photo (`source`, `licence`, `aiGenerated`). It detects video vs. audio vs. photo
|
|
@@ -162,10 +224,11 @@ of a static frame — no separate video tool needed. Use it when a still doesn't
|
|
|
162
224
|
destination rewards video over an image.
|
|
163
225
|
|
|
164
226
|
1. Pick a photo (step 5) or, for a moving background, a video — check `list_creative_media({kind:
|
|
165
|
-
"VIDEO"})` first
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
2. `
|
|
227
|
+
"VIDEO"})` first and use its media id if something fits; otherwise pass the local file as
|
|
228
|
+
`clipPath` (previewed from disk, uploaded on commit). For background music,
|
|
229
|
+
`list_creative_media({kind: "AUDIO"})` the same way, else `audioPath`.
|
|
230
|
+
2. `preview_creative_draft` with `kind: "MOTION"` (to change a saved MOTION Creative, `baseCreativeId`
|
|
231
|
+
instead) plus:
|
|
169
232
|
- `motion.preset` — how the copy enters: `fade-up` (default), `word-by-word`, `accent-pop`, or
|
|
170
233
|
`none` (everything visible from the first frame).
|
|
171
234
|
- `motion.durationSec` — 3 to 60, default 8.
|
|
@@ -177,13 +240,13 @@ destination rewards video over an image.
|
|
|
177
240
|
- `audio` — `source: "clip"` (the background clip's own recorded sound — the default when there
|
|
178
241
|
is one), `"track"` (a separate library track: `trackId` from `upload_creative_media`, plus
|
|
179
242
|
`volume` and `fadeOutSec`), or `"none"`.
|
|
180
|
-
3.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
243
|
+
3. The preview renders three key moments across the timeline into one sheet image (not the finished
|
|
244
|
+
video). Judge it the same honest way as step 7 above — does the copy's entrance finish before the
|
|
245
|
+
next beat, does the end card actually hold long enough to read — and get approval before
|
|
246
|
+
spending a full render on it.
|
|
247
|
+
4. `commit_creative_draft` then uploads the media, saves the Creative and runs the local,
|
|
248
|
+
ffmpeg-driven encode — roughly 20 seconds per placement for an 8-second video. No ffmpeg on this
|
|
249
|
+
machine fails with a clear message naming how to get one rather than silently producing nothing.
|
|
187
250
|
5. `attach_creative_to_post`, same as any Creative — one MP4 per placement.
|
|
188
251
|
|
|
189
252
|
A library music track's `licence` has to be recorded on upload (same as a photo's provenance) — the
|
|
@@ -203,6 +266,9 @@ Automated checks are real but incomplete. These are the ones worth remembering t
|
|
|
203
266
|
- **A Brand Kit with no copy rules, or no accent-phrase requirement, never fails a save on that
|
|
204
267
|
account.** That is permissive by design, not a bug — but it means the model, not the platform, is
|
|
205
268
|
the only check on brand voice for that workspace until someone configures rules.
|
|
269
|
+
- **A draft is not a saved Creative.** `preview_creative_draft` writes nothing to Conductor, so a
|
|
270
|
+
Creative or media id does not exist until `commit_creative_draft` runs — don't hand a draft's
|
|
271
|
+
folder to `attach_creative_to_post` or a Post.
|
|
206
272
|
- **A stale write is refused, not silently overwritten.** `update_creative` requires the Creative's
|
|
207
273
|
current `version`; a mismatch comes back as a 409 naming the conflict rather than clobbering a
|
|
208
274
|
concurrent edit — reread with `get_creative` and reapply, don't retry blind.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Brand Kit check and setup
|
|
2
|
+
|
|
3
|
+
Shared by `/conductor:creative` and the `conductor-creative` skill: check whether this workspace's
|
|
4
|
+
Brand Kit has been set up, and offer to set it up if not. The text below is the command's Step 1,
|
|
5
|
+
unchanged.
|
|
6
|
+
|
|
7
|
+
## Step 1 — Check the brand
|
|
8
|
+
|
|
9
|
+
Call `get_brand_kit` (pass `kitId` only if the person already named a specific brand).
|
|
10
|
+
|
|
11
|
+
**If `configured` is `false`**, this workspace has no brand set up yet — offer to fix that before
|
|
12
|
+
making anything:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"questions": [{
|
|
17
|
+
"question": "This workspace's Brand Kit hasn't been set up yet (default colours, no font, no logo). Set it up now?",
|
|
18
|
+
"header": "Brand Kit",
|
|
19
|
+
"options": [
|
|
20
|
+
{"label": "From a website", "description": "Fetch a URL and infer colours, voice and name"},
|
|
21
|
+
{"label": "From a brand doc", "description": "Read a local file (guidelines, style doc)"},
|
|
22
|
+
{"label": "Answer a few questions", "description": "Name, colours, font, CTA line, approved lines"},
|
|
23
|
+
{"label": "Skip for now", "description": "Proceed with the default kit as-is"}
|
|
24
|
+
],
|
|
25
|
+
"multiSelect": false
|
|
26
|
+
}]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **From a website**: ask for the URL, `WebFetch` it, infer a plausible accent colour, name, CTA
|
|
31
|
+
claim and a couple of approved lines from the copy on the page. Show what you inferred and confirm
|
|
32
|
+
before writing.
|
|
33
|
+
- **From a brand doc**: ask for the file path, `Read` it, extract the same fields, confirm before
|
|
34
|
+
writing.
|
|
35
|
+
- **Answer a few questions**: ask for name, primary/accent colour (hex or a description you convert),
|
|
36
|
+
font (if any), the CTA claim, and any lines that must run verbatim. Keep it to one AskUserQuestion
|
|
37
|
+
batch, not a long interview.
|
|
38
|
+
- Once confirmed, call `update_brand_kit` with whatever was gathered. If a logo file or URL is
|
|
39
|
+
available, `upload_brand_image` for the `mark` slot at least. Verify with `get_brand_kit` —
|
|
40
|
+
`configured` should now read `true`.
|
|
41
|
+
- **Skip for now**: proceed; the skill below will render against the default kit as given.
|
package/assets/cli-manifest.json
CHANGED
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"name": "conductor:creative",
|
|
174
|
-
"description": "One entry point for creative work \u2014 checks the workspace brand (offers to set it up if unconfigured), asks what to make (static ad, short video, an existing clip), then hands off to the conductor-creative skill through
|
|
174
|
+
"description": "One entry point for creative work \u2014 checks the workspace brand (offers to set it up if unconfigured), asks what to make (static ad, short video, an existing clip), then hands off to the conductor-creative skill through draft, preview, approval, commit and attach."
|
|
175
175
|
}
|
|
176
176
|
],
|
|
177
177
|
"agents": [
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
"name": "conductor-creative",
|
|
198
|
-
"description": "Makes a Creative end to end, static or short video: sets up the Brand Kit if unconfigured, reads it and its Knowledge page, picks media and an angle,
|
|
198
|
+
"description": "Makes a Creative end to end, static or short video: sets up the Brand Kit if unconfigured, reads it and its Knowledge page, picks media and an angle, previews a draft locally and shows it for approval before anything is saved, commits it, fixes what's wrong, then hands it to conductor-publisher."
|
|
199
199
|
}
|
|
200
200
|
]
|
|
201
201
|
},
|
|
@@ -781,7 +781,7 @@
|
|
|
781
781
|
},
|
|
782
782
|
{
|
|
783
783
|
"name": "upload_creative_photo",
|
|
784
|
-
"description": "Deprecated alias for upload_creative_media
|
|
784
|
+
"description": "Deprecated alias for upload_creative_media \u2014 same behavior, including for video/audio.",
|
|
785
785
|
"category": "marketing"
|
|
786
786
|
},
|
|
787
787
|
{
|
|
@@ -800,6 +800,19 @@
|
|
|
800
800
|
"creativeId"
|
|
801
801
|
]
|
|
802
802
|
},
|
|
803
|
+
{
|
|
804
|
+
"name": "preview_creative_draft",
|
|
805
|
+
"description": "Look at a Creative before anything is saved: renders a draft locally from the create_creative fields plus local files (photoPath, clipPath, audioPath) and returns the contact sheet as an image. Never writes to Conductor; commit only after the person approves.",
|
|
806
|
+
"category": "marketing"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"name": "commit_creative_draft",
|
|
810
|
+
"description": "Save an approved draft: uploads its local files, creates (or updates) the Creative, and renders the real frames. The first write to Conductor; a draft commits once. Call get_creative to verify.",
|
|
811
|
+
"category": "marketing",
|
|
812
|
+
"requiredParams": [
|
|
813
|
+
"draftDir"
|
|
814
|
+
]
|
|
815
|
+
},
|
|
803
816
|
{
|
|
804
817
|
"name": "attach_creative_to_post",
|
|
805
818
|
"description": "Attach a Creative's rendered frames to a Post as its media, placement to platform, for every destination without its own custom media. Call get_post_status (postId) to verify.",
|
|
@@ -121,12 +121,23 @@ function sameRgb(a, b) {
|
|
|
121
121
|
export async function runAssertions(board, placement, brand, { outputScale = 2 } = {}) {
|
|
122
122
|
const errors = [];
|
|
123
123
|
const warnings = [];
|
|
124
|
+
// The same findings with a stable `rule` id each, for callers (the draft preview) that report them
|
|
125
|
+
// structured. `errors` / `warnings` stay plain message arrays, exactly as before.
|
|
126
|
+
const checks = [];
|
|
127
|
+
const fail = (rule, message) => {
|
|
128
|
+
errors.push(message);
|
|
129
|
+
checks.push({ rule, severity: 'error', message });
|
|
130
|
+
};
|
|
131
|
+
const warn = (rule, message) => {
|
|
132
|
+
warnings.push(message);
|
|
133
|
+
checks.push({ rule, severity: 'warning', message });
|
|
134
|
+
};
|
|
124
135
|
const br = rectOf(board);
|
|
125
136
|
|
|
126
137
|
const nodes = [...board.querySelectorAll(CHECKED_SELECTOR)].map((n) => ({ name: n.className, rect: rectOf(n) }));
|
|
127
138
|
|
|
128
139
|
const spill = spillDetect(br, nodes);
|
|
129
|
-
if (spill.length)
|
|
140
|
+
if (spill.length) fail('spill', `text spill: ${spill.join('; ')}`);
|
|
130
141
|
|
|
131
142
|
const safe = {
|
|
132
143
|
top: Number(board.dataset.safeTop) || 0,
|
|
@@ -134,19 +145,19 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
|
|
|
134
145
|
right: Number(board.dataset.safeRight) || 0,
|
|
135
146
|
};
|
|
136
147
|
const intrusion = safeZoneIntrusion(br, nodes, safe);
|
|
137
|
-
if (intrusion.length)
|
|
148
|
+
if (intrusion.length) fail('safeZone', `safe-zone intrusion: ${intrusion.join('; ')}`);
|
|
138
149
|
|
|
139
150
|
if (brand && brand.fontFamily) {
|
|
140
151
|
const headlineEl = board.querySelector('.cc-headline');
|
|
141
152
|
const size = Math.round(parseFloat(getComputedStyle(headlineEl).fontSize)) || 16;
|
|
142
153
|
if (!document.fonts.check(`800 ${size}px "${brand.fontFamily}"`)) {
|
|
143
|
-
|
|
154
|
+
fail('font', `font "${brand.fontFamily}" did not load (would export in a fallback face)`);
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
const imgs = [...board.querySelectorAll('img')];
|
|
148
159
|
const broken = imgs.filter((i) => !i.complete || i.naturalWidth === 0).map((i) => i.getAttribute('src'));
|
|
149
|
-
if (broken.length)
|
|
160
|
+
if (broken.length) fail('image', `image(s) failed to load: ${broken.join(', ')}`);
|
|
150
161
|
|
|
151
162
|
// A bleed/card layout paints its photo as a CSS background, invisible to
|
|
152
163
|
// the <img> check above — probe it directly, the same way a typo'd path
|
|
@@ -161,11 +172,11 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
|
|
|
161
172
|
img.src = bgUrlMatch[1];
|
|
162
173
|
});
|
|
163
174
|
if (!probe.ok) {
|
|
164
|
-
|
|
175
|
+
fail('image', `background photo failed to load: ${bgUrlMatch[1]}`);
|
|
165
176
|
} else if (placement && probe.w < placement.w * outputScale && probe.h < placement.h * outputScale) {
|
|
166
177
|
// Soft photography check: a source smaller than the output frame (2x for images, 1x for video) is
|
|
167
178
|
// upscaled, so the photo goes soft while the text stays crisp.
|
|
168
|
-
|
|
179
|
+
warn('photoResolution', `photo is ${probe.w}x${probe.h}, upscaled to fit ${placement.w * outputScale}x${placement.h * outputScale}`);
|
|
169
180
|
}
|
|
170
181
|
}
|
|
171
182
|
|
|
@@ -176,9 +187,9 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
|
|
|
176
187
|
const expectedRaw = getComputedStyle(board).getPropertyValue('--cc-accent').trim();
|
|
177
188
|
const expectedRgb = expectedRaw ? resolveCssColor(expectedRaw) : null;
|
|
178
189
|
if (!emRgb) {
|
|
179
|
-
|
|
190
|
+
fail('accent', 'accent colour did not resolve on the headline');
|
|
180
191
|
} else if (expectedRgb && !sameRgb(emRgb, expectedRgb)) {
|
|
181
|
-
|
|
192
|
+
fail('accent', `accent colour unresolved: headline em is ${getComputedStyle(em).color}, brand accent resolves to ${expectedRaw}`);
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
195
|
|
|
@@ -207,15 +218,15 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
|
|
|
207
218
|
if (fgRgb && bgRgb) contrast = contrastRatio(fgRgb, bgRgb);
|
|
208
219
|
}
|
|
209
220
|
if (contrast !== null && contrast < 3) {
|
|
210
|
-
|
|
221
|
+
fail('contrast', `headline contrast is ${contrast}:1 against its background, below the 3:1 floor for large text`);
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
const boxRect = board.getBoundingClientRect();
|
|
214
225
|
const width = Math.round(boxRect.width);
|
|
215
226
|
const height = Math.round(boxRect.height);
|
|
216
227
|
if (placement && (width !== placement.w || height !== placement.h)) {
|
|
217
|
-
|
|
228
|
+
fail('size', `artboard is ${width}x${height}, expected ${placement.w}x${placement.h}`);
|
|
218
229
|
}
|
|
219
230
|
|
|
220
|
-
return { errors, warnings, contrast, width, height };
|
|
231
|
+
return { errors, warnings, checks, contrast, width, height };
|
|
221
232
|
}
|
package/dist/creative/frame.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* }
|
|
16
16
|
*
|
|
17
17
|
* Result contract:
|
|
18
|
-
* window.__RENDER_RESULT = { ok, errors?, warnings?, width, height } | { ok: false, error }
|
|
18
|
+
* window.__RENDER_RESULT = { ok, errors?, warnings?, checks?, width, height } | { ok: false, error }
|
|
19
19
|
* window.__ready = true // set exactly once, success or failure, when the
|
|
20
20
|
* // job's page.waitForFunction should stop waiting
|
|
21
21
|
*
|
|
@@ -108,8 +108,8 @@ async function main() {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const { errors, warnings, width, height } = await runAssertions(board, placement, spec.brand || {});
|
|
112
|
-
finish({ ok: errors.length === 0, errors, warnings, width, height });
|
|
111
|
+
const { errors, warnings, checks, width, height } = await runAssertions(board, placement, spec.brand || {});
|
|
112
|
+
finish({ ok: errors.length === 0, errors, warnings, checks, width, height });
|
|
113
113
|
} catch (err) {
|
|
114
114
|
finish({ ok: false, error: String((err && err.message) || err) });
|
|
115
115
|
}
|