@cliangdev/conductor 0.27.0 → 0.28.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/agents/conductor-content-art-director.md +16 -4
- package/assets/claude/agents/conductor-content-scriptwriter.md +4 -0
- package/assets/claude/commands/conductor/creative.md +2 -1
- package/assets/claude/skills/conductor-content-studio/SKILL.md +39 -17
- package/assets/claude/skills/conductor-content-studio/references/capabilities.md +11 -2
- package/assets/claude/skills/conductor-content-studio/references/compliance-checklist.md +1 -0
- package/assets/claude/skills/conductor-content-studio/references/idea-template.md +6 -1
- package/assets/claude/skills/conductor-content-studio/references/publish-pack-template.md +3 -1
- package/assets/claude/skills/conductor-content-studio/references/script-template.md +1 -1
- package/assets/claude/skills/conductor-creative/SKILL.md +37 -8
- package/assets/cli-manifest.json +8 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +20 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/creative/assertions.js +57 -7
- package/dist/creative/font.js +29 -0
- package/dist/creative/frame.css +2 -0
- package/dist/creative/frame.js +7 -4
- package/dist/creative/layouts/bleed/layout.css +14 -7
- package/dist/creative/motion.js +61 -19
- package/dist/creative/render.js +21 -2
- package/dist/creative/sheet.js +7 -2
- package/dist/creative/styles.css +16 -7
- package/dist/lib/codex-image.d.ts +51 -0
- package/dist/lib/codex-image.d.ts.map +1 -0
- package/dist/lib/codex-image.js +329 -0
- package/dist/lib/codex-image.js.map +1 -0
- package/dist/lib/media-probe.d.ts +4 -0
- package/dist/lib/media-probe.d.ts.map +1 -1
- package/dist/lib/media-probe.js +5 -2
- package/dist/lib/media-probe.js.map +1 -1
- package/dist/lib/provenance-sidecar.d.ts +19 -0
- package/dist/lib/provenance-sidecar.d.ts.map +1 -0
- package/dist/lib/provenance-sidecar.js +39 -0
- package/dist/lib/provenance-sidecar.js.map +1 -0
- package/dist/mcp/index.d.ts +531 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +34 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools/creative-drafts.d.ts +12 -0
- package/dist/mcp/tools/creative-drafts.d.ts.map +1 -1
- package/dist/mcp/tools/creative-drafts.js +36 -7
- package/dist/mcp/tools/creative-drafts.js.map +1 -1
- package/dist/mcp/tools/creative-images.d.ts +39 -0
- package/dist/mcp/tools/creative-images.d.ts.map +1 -0
- package/dist/mcp/tools/creative-images.js +89 -0
- package/dist/mcp/tools/creative-images.js.map +1 -0
- package/dist/mcp/tools/creatives.d.ts.map +1 -1
- package/dist/mcp/tools/creatives.js +7 -0
- package/dist/mcp/tools/creatives.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ send to the draft renderer, so it has to be valid on the first try.
|
|
|
15
15
|
- Read only the files you are given: `script.md`, `context/media.json`, `context/brand-kit.json`,
|
|
16
16
|
`references/shotlist-template.md`, `references/platform-specs.md`, `references/capabilities.md` and
|
|
17
17
|
`references/publish-pack-template.md` when given.
|
|
18
|
-
- Write only your output files: `direction.md` and, for `RENDER`, `creative-spec.json`.
|
|
18
|
+
- Write only your output files: `direction.md` and, for `RENDER`, `creative-spec.json`, plus `image-prompts.json` when `media.json` has `generate: true`.
|
|
19
19
|
- Never call Conductor or any MCP tool.
|
|
20
20
|
|
|
21
21
|
## direction.md
|
|
@@ -45,7 +45,9 @@ A single JSON object whose keys are exactly `preview_creative_draft` inputs, and
|
|
|
45
45
|
- `photoId`: only an id that appears in `media.json`; or `photoPath` only for a local file the person
|
|
46
46
|
supplied, given in the request or listed as `local` in `media.json`. Never invent an id or a path.
|
|
47
47
|
Every STILL, carousel and story needs a photo, and a MOTION needs a photo or clip (see
|
|
48
|
-
`references/capabilities.md`). If
|
|
48
|
+
`references/capabilities.md`). If `media.json` has `generate: true` and no library photo fits,
|
|
49
|
+
write `image-prompts.json` instead (below) and omit `photoPath`/`photoId` for those slots; the
|
|
50
|
+
orchestrator fills them. Otherwise, if none is available, stop and say so in `direction.md` rather
|
|
49
51
|
than writing a spec the preview will refuse.
|
|
50
52
|
- `typeOverrides` when text must read bigger or smaller on a placement, and `layoutOverrides` for
|
|
51
53
|
photo height or focal point. Don't change layout just to resize text.
|
|
@@ -57,11 +59,21 @@ A single JSON object whose keys are exactly `preview_creative_draft` inputs, and
|
|
|
57
59
|
- A story or carousel: `sequenceKind` (`story` 2 to 7 beats, `carousel` 2 to 10 cards, plus
|
|
58
60
|
`carouselRatio` such as `4x5` or `1x1`) and a `sequence` array of beats, each with `headline`
|
|
59
61
|
and optional `body`, `photoId` or `photoPath`, `cta`.
|
|
60
|
-
- MOTION: `motion` (`
|
|
61
|
-
|
|
62
|
+
- MOTION: `motion.preset` (`fade-up`, `word-by-word`, `accent-pop` or `none`), `motion.durationSec`
|
|
63
|
+
(3 to 60), `motion.background` (`{source: "photo", motion: zoom-in|zoom-out|pan-left|pan-right|none}`
|
|
64
|
+
or `{source: "clip", clipMediaId}`), `motion.endCard`, and optionally top-level `audio`. A MOTION
|
|
65
|
+
renders one headline and one optional body plus an end card holding the final frame: no timed
|
|
66
|
+
multi-beat on-screen text (extra beats go in a story or carousel sequence, or the caption). A clip background uses `background.clipMediaId` from `media.json`, or `clipPath` for a
|
|
62
67
|
user-supplied local file.
|
|
63
68
|
- Omit any field you do not need. Do not add comments or fields the tool does not take.
|
|
64
69
|
|
|
70
|
+
## image-prompts.json (only when `generate: true`)
|
|
71
|
+
|
|
72
|
+
`{"photo": "<scene prompt>", "beats": {"2": "<scene prompt>"}}` next to the spec: `photo` for the
|
|
73
|
+
main photo, `beats` keyed by sequence position for cards or beats that need their own. Each prompt
|
|
74
|
+
describes the scene, subject, light, framing and where the negative space for the headline sits. No
|
|
75
|
+
text in the image, and no product depiction unless a reference image is listed in `media.json`.
|
|
76
|
+
|
|
65
77
|
## Finish
|
|
66
78
|
|
|
67
79
|
Return at most about 150 words: the layout and look in one sentence, any risk you see, and the
|
|
@@ -22,6 +22,10 @@ anything.
|
|
|
22
22
|
1. Follow the script template exactly: hook, beats table, payoff, CTA, publish copy, copy-rule check.
|
|
23
23
|
2. The hook lands within 3 seconds, spoken and on screen, and is what the winning idea promised.
|
|
24
24
|
3. A new beat or pattern interrupt every 3 to 5 seconds. Everything works with the sound off.
|
|
25
|
+
Exception, a `MOTION` idea: Conductor renders exactly one headline and one optional body
|
|
26
|
+
(animated by preset) plus an end card that holds the final frame. Write one on-screen headline
|
|
27
|
+
and body, not timed beats of on-screen text; extra beats belong in a story or carousel sequence
|
|
28
|
+
or the caption.
|
|
25
29
|
4. Respect the brief's must-say and must-avoid lists. Use approved lines verbatim. If the brand kit
|
|
26
30
|
requires an accent phrase, mark exactly one `*phrase*` in on-screen headlines. Check every line
|
|
27
31
|
against the copy rules and write the result in the final section.
|
|
@@ -48,7 +48,8 @@ Keep this light — one or two follow-up questions, not a form:
|
|
|
48
48
|
|
|
49
49
|
- **Static ad / short video from a photo**: the headline (or let the skill draft one from the
|
|
50
50
|
Knowledge page), and a photo. Call `list_creative_media({kind: "IMAGE"})` first and offer what's
|
|
51
|
-
already on hand before asking for a new photo
|
|
51
|
+
already on hand before asking for a new photo; no photo fits and none is offered? Offer to generate one with
|
|
52
|
+
`generate_creative_image` (needs Codex installed and `codex login`). A new photo is a local file path (a URL: download it
|
|
52
53
|
to a local file first) — it is only uploaded after the person approves the preview.
|
|
53
54
|
- **Video from a clip I have / Post an existing video as-is**: call
|
|
54
55
|
`list_creative_media({kind: "VIDEO"})` first; if nothing fits, ask for a local path (a URL:
|
|
@@ -44,8 +44,9 @@ folder; give subagents absolute paths.
|
|
|
44
44
|
|
|
45
45
|
```
|
|
46
46
|
request.md run.json brief.md script.md direction.md creative-spec.json decision.md
|
|
47
|
+
image-prompts.json (S5, only when generation is enabled)
|
|
47
48
|
context/ brand-kit.json knowledge.md performance.md learning.md media.json
|
|
48
|
-
ideas/ angle-1.md angle-2.md angle-3.md pool.md
|
|
49
|
+
ideas/ angle-1.md angle-2.md angle-3.md pool.md winner.md
|
|
49
50
|
judging/ scores.md
|
|
50
51
|
drafts/ finalist-<id>/ (contact sheets from the finalist renders)
|
|
51
52
|
publish-pack.md publish-pack.json (S5b; see references/publish-pack-template.md)
|
|
@@ -124,8 +125,16 @@ Write `request.md` (the request verbatim, `--quick` if given, the platform if st
|
|
|
124
125
|
6. Every renderable idea needs a photo (or, for MOTION, a clip); see `references/capabilities.md`. If
|
|
125
126
|
`media.json` has no unblocked image and no clip, ask once with `AskUserQuestion` before ideating:
|
|
126
127
|
**Give a photo or two** (local paths; record them in `context/media.json` as `local` entries the
|
|
127
|
-
art director may use via `photoPath`)
|
|
128
|
-
|
|
128
|
+
art director may use via `photoPath`), **Generate with Codex** (record `"generate": true` in
|
|
129
|
+
`context/media.json`; ideas can then be `RENDER` with no library photo, each giving an
|
|
130
|
+
`imagePrompt`, and photos are made with `generate_creative_image` from the person's local Codex CLI)
|
|
131
|
+
or **Plan for filming** (ideas will be `FILM`). Never let the team design a type-only still; the
|
|
132
|
+
preview refuses it.
|
|
133
|
+
When the library does have usable images, or the request itself asks for generated imagery, set
|
|
134
|
+
`"generate": true` without asking: ideas use a library photo when one fits and give an
|
|
135
|
+
`imagePrompt` only when none does. If `generate_creative_image` later fails (Codex missing, logged
|
|
136
|
+
out, or a plan without image generation), treat that finalist as unrenderable in S3b; at S6, say
|
|
137
|
+
why and ask for a photo instead of offering Approve & upload.
|
|
129
138
|
|
|
130
139
|
### S1 — Strategist -> `brief.md`
|
|
131
140
|
|
|
@@ -165,7 +174,9 @@ before each score, and names the top 3. Store the finalists in `run.json`.
|
|
|
165
174
|
|
|
166
175
|
For each `RENDER` finalist, call `preview_creative_draft` with that idea's draft spec (map the
|
|
167
176
|
draft-spec block to the tool's fields) and `draftDir` set to `<run>/drafts/finalist-<id>`. Use
|
|
168
|
-
`full: false`. Record each draft dir in `run.json`.
|
|
177
|
+
`full: false`. Record each draft dir in `run.json`. For a finalist whose draft spec carries an
|
|
178
|
+
`imagePrompt`, call `generate_creative_image({prompt})` first, eye-check the result, and use its
|
|
179
|
+
path as `photoPath` (generate only for finalists, not the whole pool).
|
|
169
180
|
|
|
170
181
|
- If the validator refuses the copy (accent phrase or a copy rule), fix the copy once yourself and
|
|
171
182
|
retry. If it still fails, mark that finalist unrenderable in `run.json`, keep its text score, and
|
|
@@ -176,7 +187,7 @@ draft-spec block to the tool's fields) and `draftDir` set to `<run>/drafts/final
|
|
|
176
187
|
shorter body) and re-preview; otherwise keep it and record the failure for the judge.
|
|
177
188
|
- `FILM` finalists have no render.
|
|
178
189
|
|
|
179
|
-
### S3c — Creative director, visual round -> `decision.md`
|
|
190
|
+
### S3c — Creative director, visual round (text-only with `--quick`) -> `decision.md`
|
|
180
191
|
|
|
181
192
|
Same agent, mode `visual-round`. Inputs: `brief.md`, `judging/scores.md`, `ideas/pool.md`, the
|
|
182
193
|
finalist image paths (`drafts/finalist-<id>/sheet.jpg`; the director views them with Read), the
|
|
@@ -194,11 +205,13 @@ which idea to take; show the decision at the gate.
|
|
|
194
205
|
|
|
195
206
|
### S4 — Scriptwriter -> `script.md`
|
|
196
207
|
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
First copy the winning idea's block from `ideas/pool.md` to `ideas/winner.md`. Then
|
|
209
|
+
`Agent(subagent_type: "conductor-content-scriptwriter")` with `brief.md`, `decision.md`, `ideas/winner.md`, `references/script-template.md`, `references/platform-specs.md` and
|
|
199
210
|
`references/publish-pack-template.md`. Besides the script it writes the Publish copy section: a
|
|
200
211
|
caption per platform, hashtags, a first comment and 2 to 3 alternate hooks with a hookType each.
|
|
201
|
-
|
|
212
|
+
For a `MOTION` winner tell it that the render shows one headline and one optional body, animated
|
|
213
|
+
by preset, then an end card that holds the final frame: no timed multi-beat on-screen text. Extra
|
|
214
|
+
beats belong in a story or carousel sequence, or in the caption. When merging into `publish-pack.json`, append each platform's hashtags to its `captionOverride`
|
|
202
215
|
(caption, blank line, hashtags): that field is posted verbatim.
|
|
203
216
|
|
|
204
217
|
### S5 — Art director (`RENDER` only) -> `direction.md` + `creative-spec.json`
|
|
@@ -212,8 +225,12 @@ Tell it the supported options: the kit's enabled placements, and the layouts and
|
|
|
212
225
|
the creative registry supports (at the time of writing `stacked`, `bleed`, `split` dark only, `card` dark or
|
|
213
226
|
light; a refusal from the validator names the valid ones). The spec's keys must be exactly
|
|
214
227
|
`preview_creative_draft` inputs. Give it `references/capabilities.md` too. The spec's `name` is the
|
|
215
|
-
idea's title only: never include the internal `I<n>` ID, which means nothing outside this run.
|
|
216
|
-
`
|
|
228
|
+
idea's title only: never include the internal `I<n>` ID, which means nothing outside this run. When
|
|
229
|
+
`media.json` has `generate: true`, the art director may also write `image-prompts.json`
|
|
230
|
+
(`{"photo": "...", "beats": {"2": "..."}}`) next to the spec. Before S6, call
|
|
231
|
+
`generate_creative_image` for each prompt, eye-check each image, and set `photoPath` (or
|
|
232
|
+
`sequence[i].photoPath`) in `creative-spec.json`. A `FILM` winner skips S5 and goes to the shoot
|
|
233
|
+
script, below.
|
|
217
234
|
|
|
218
235
|
### S5b — Publish pack and pre-publish checks (you; RENDER after S5, FILM after the shoot script)
|
|
219
236
|
|
|
@@ -253,18 +270,23 @@ does not apply is answered "not applicable".
|
|
|
253
270
|
re-preview **before** showing anything, and never offer Approve & upload while one still fails.
|
|
254
271
|
Revise at most twice beyond that; if it is still wrong, show it honestly and say what is off.
|
|
255
272
|
3. Show the person: the draft image, the finalist sheets of the runners-up (view them from
|
|
256
|
-
`drafts/finalist-<id>/sheet.jpg`; say when a runner-up could not be rendered
|
|
273
|
+
`drafts/finalist-<id>/sheet.jpg`; say when a runner-up could not be rendered; with `--quick` no
|
|
274
|
+
sheets exist, so show runners-up by title and tag only), and a short decision
|
|
257
275
|
summary (the winner and why, the runners-up, what was cut and why), and a short pack summary: the
|
|
258
276
|
disclosure setting (which toggle, or none), the AI-label setting, the sound and its licence, and
|
|
259
277
|
the alternate hooks with their types.
|
|
260
278
|
4. Ask with `AskUserQuestion`, header "Draft", with exactly these four options. `AskUserQuestion`
|
|
261
279
|
allows at most four, so never add one per runner-up, and **Stop** must always be among them:
|
|
262
|
-
- **Approve & upload**:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
`
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
- **Approve & upload**: the question says approving also confirms the photo carries no burned-in
|
|
281
|
+
text, logo or claim. Ask for provenance (`source`, `licence`, and whether it is AI-generated)
|
|
282
|
+
only for local files the preview lists in `provenance.needed` (a file from
|
|
283
|
+
`generate_creative_image` already carries it), then
|
|
284
|
+
`commit_creative_draft({draftDir, photoChecked: true, source, licence, aiGenerated})` (provenance
|
|
285
|
+
fields only when needed). Then `get_creative` and read its `readiness`. If `state` is the only
|
|
286
|
+
blocking item, `update_creative({creativeId, state: "READY"})` and verify with `get_creative`;
|
|
287
|
+
otherwise say exactly what is missing and ask for it. Never call a Creative done while `ready`
|
|
288
|
+
is false. After the commit, write the Creative's id into `publish-pack.json` as `creativeId` and
|
|
289
|
+
its state into `publish-pack.json` as `creativeState` (and both into `run.json`).
|
|
268
290
|
- **Use a runner-up**: ask a second `AskUserQuestion` listing the runners-up by title (with
|
|
269
291
|
their tag; label one that could not be rendered as such and say why, rather than offering it as
|
|
270
292
|
if it would work). Then set the chosen idea as `winner` and re-run S4 onward for it (S5, S5b and S6,
|
|
@@ -26,7 +26,8 @@ needs changing until it fits.
|
|
|
26
26
|
|
|
27
27
|
- **A photo.** Every STILL needs one, and so does a carousel or story, whose beats inherit the main
|
|
28
28
|
photo. A MOTION needs a photo or a clip as its background. There are no type-only stills. If
|
|
29
|
-
`media.json` has no usable (unblocked) image, a `RENDER` idea needs the person to supply a photo
|
|
29
|
+
`media.json` has no usable (unblocked) image, a `RENDER` idea needs the person to supply a photo,
|
|
30
|
+
or `media.json` must have `generate: true` so the photo is made with `generate_creative_image`.
|
|
30
31
|
- **A headline.** If the Brand Kit requires an accent phrase, the headline needs exactly one
|
|
31
32
|
`*accent phrase*`.
|
|
32
33
|
- **Copy rules and approved lines.** Every Brand Kit copy rule applies to the headline, body,
|
|
@@ -51,11 +52,19 @@ needs changing until it fits.
|
|
|
51
52
|
text or a badge under the platform's buttons, is fixed before anyone is asked to approve.
|
|
52
53
|
- **The caption is never drawn on the artwork.** It goes with the post.
|
|
53
54
|
|
|
55
|
+
## Generated photos
|
|
56
|
+
|
|
57
|
+
When `media.json` has `generate: true`, one generated photo per card or beat (or as a MOTION
|
|
58
|
+
background) is renderable. It comes from `generate_creative_image` (the person's local Codex CLI):
|
|
59
|
+
photographic, no text, logos or screens in the picture, and no depiction of the product unless a real
|
|
60
|
+
reference image is supplied. It is `aiGenerated: true`. Collages, charts and text-in-image stay out.
|
|
61
|
+
Ask for a mid-tone scene: under `bleed`, a dark or night scene can render almost black. For a dark scene, use `card` or `stacked`.
|
|
62
|
+
|
|
54
63
|
## What it cannot do (so the idea needs filming or a rethink)
|
|
55
64
|
|
|
56
65
|
- Footage of people, places, cooking or the product in use. Screen recordings of an app.
|
|
57
66
|
- Voiceover or spoken dialogue: MOTION has music or clip sound, not speech.
|
|
58
|
-
- Grids, charts, collages of several photos on one card, illustrations, or
|
|
67
|
+
- Grids, charts, collages of several photos on one card, illustrations, or text inside an image.
|
|
59
68
|
Each card is one photo plus text.
|
|
60
69
|
- Anything that relies on a product claim the brief can't confirm. That's a copy problem, not a
|
|
61
70
|
rendering one: soften the claim or drop the idea.
|
|
@@ -41,6 +41,7 @@ disclose paid, gifted, employee and affiliate relationships; in video, disclose
|
|
|
41
41
|
**Is any realistic imagery, video or voice AI-generated or AI-altered?** Check the Creative's and
|
|
42
42
|
each media file's provenance (`aiGenerated` in `context/media.json`) and the script's visuals.
|
|
43
43
|
|
|
44
|
+
- A photo from `generate_creative_image` is `aiGenerated: true`, so this is a **Yes**: the AI label / `isAigc` applies.
|
|
44
45
|
- **Yes**: set TikTok `isAigc: true` on a video post. Conductor does not send `isAigc` on a TikTok
|
|
45
46
|
photo post, so for a photo post note "turn on TikTok's AI label by hand" as a manual step. Set
|
|
46
47
|
YouTube `containsSyntheticMedia: true`. Meta has no option: note that Instagram's and Facebook's
|
|
@@ -28,10 +28,15 @@ Draft spec (RENDER only):
|
|
|
28
28
|
- body: <max 20 words, or none>
|
|
29
29
|
- caption: <the post caption, within the platform's limit>
|
|
30
30
|
- photoId: <id from media.json, or none>
|
|
31
|
+
- imagePrompt: <only when media.json has `generate: true` and no library photo fits: the scene in one line, no text in the image, no product depiction>
|
|
31
32
|
- sequence: <for a story or carousel only: one line per beat, "headline | body">
|
|
32
33
|
- motion: <MOTION only: preset, durationSec, background>
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
The idea's format maps to the publish pack's `tags.format` as: still -> `still`; carousel ->
|
|
37
|
+
`carousel`; story sequence -> `story`; motion text over photo or clip -> `motion`; talking head,
|
|
38
|
+
demo, skit or other -> `film`.
|
|
39
|
+
|
|
35
40
|
## Sound
|
|
36
41
|
|
|
37
42
|
An idea may not rely on a trending song. A business account cannot license most trending tracks, so
|
|
@@ -40,7 +45,7 @@ name original audio or a Commercial Music Library track, or leave sound out of t
|
|
|
40
45
|
## Tagging honestly
|
|
41
46
|
|
|
42
47
|
- `RENDER` means Conductor can make it end to end: a still, a carousel, a story sequence, or motion
|
|
43
|
-
text over photos or clips that exist in `media.json` (or
|
|
48
|
+
text over photos or clips that exist in `media.json` (or a generated photo, via `imagePrompt`, when `media.json` has `generate: true`; there are no type-only renders). Nothing in it
|
|
44
49
|
requires a person to be filmed or footage that is not on hand.
|
|
45
50
|
- `FILM` means it needs real filming: a person speaking to camera, a demonstration, a skit, a
|
|
46
51
|
product in use. A `FILM` idea has no draft spec; it gets a shoot-ready script instead.
|
|
@@ -60,6 +60,7 @@ Run: <runId> Creative: <display id, or "not yet committed"> Tags: hook <type
|
|
|
60
60
|
```json
|
|
61
61
|
{
|
|
62
62
|
"creativeId": null,
|
|
63
|
+
"creativeState": null,
|
|
63
64
|
"platforms": [
|
|
64
65
|
{
|
|
65
66
|
"platform": "tiktok",
|
|
@@ -84,6 +85,7 @@ Rules:
|
|
|
84
85
|
|
|
85
86
|
- `creativeId` is null until the person approves the commit (S6, On Approve), then it is the
|
|
86
87
|
Creative's id. A `FILM` run leaves it null: the publisher is pointed at the pack by the person.
|
|
88
|
+
- `creativeState` is the Creative's state after S6 (`READY`, or `DRAFT` if something still blocks it).
|
|
87
89
|
- `platforms[]` items are shaped to drop straight into `create_post` targets or
|
|
88
90
|
`set_publish_targets` targets: `platform`, `format` (`feed`, `reel` or `story`, and only one the destination's `formats` offers: at the time
|
|
89
91
|
of writing TikTok and YouTube are feed only, Facebook and Instagram offer all three),
|
|
@@ -117,5 +119,5 @@ Rules:
|
|
|
117
119
|
- `alternateHooks` and `tags` are for learning and experiments, not for publishing. `tags` is also
|
|
118
120
|
copied into `run.json`.
|
|
119
121
|
- `tags.angle` is the ideator's angle: `story-emotion`, `utility-education` or `trend-humor`.
|
|
120
|
-
`tags.format` is the idea's format (`still`, `carousel`, `story`, `motion`, `clip` or `film`).
|
|
122
|
+
`tags.format` is the idea's format (`still`, `carousel`, `story`, `motion`, `clip` or `film`; map the idea template's formats: story sequence -> `story`, motion text over photo or clip -> `motion`, talking head, demo, skit or other -> `film`; `clip` is for a finished video posted as-is).
|
|
121
123
|
- Valid JSON, no comments.
|
|
@@ -48,6 +48,6 @@ curiosity-gap, story, social-proof.
|
|
|
48
48
|
<each must-say / must-avoid item from brief.md, and ok or the fix>
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Rules: a beat every 3 to 5 seconds; no beat longer than 5 seconds without a visual or text change;
|
|
51
|
+
Rules: a beat every 3 to 5 seconds (a `MOTION` idea instead has one headline, one optional body and an end card, with no timed on-screen beats); no beat longer than 5 seconds without a visual or text change;
|
|
52
52
|
caption limits are in `platform-specs.md`; alternates differ from the main hook in hook type, not just wording (check current platform guidance); approved lines run
|
|
53
53
|
verbatim; leave out platforms the brief did not ask for.
|
|
@@ -83,7 +83,20 @@ to `commit_creative_draft`, which records them on every file it uploads. This cl
|
|
|
83
83
|
exposure: an unattributed or unlicensed photo running as paid or organic media. A photo already in
|
|
84
84
|
the library is used by its id (`photoId`) with no upload at all.
|
|
85
85
|
|
|
86
|
-
**
|
|
86
|
+
**Nothing in the library fits, or the person asks for a generated one?** Call
|
|
87
|
+
`generate_creative_image` with a scene prompt drawn from the brand's Knowledge page and the angle:
|
|
88
|
+
the scene, subject, light, framing and where the headline's negative space sits. The tool adds its own
|
|
89
|
+
fixed rules (photographic, no text, letters, logos, UI or phone screens) and runs the person's local
|
|
90
|
+
Codex CLI, so it needs `codex login` on a ChatGPT Plus (or higher) plan (`conductor doctor` reports the login), takes about 30-90 s and uses
|
|
91
|
+
their Codex quota. It returns the first image plus `files[].path`; pass a path as `photoPath` (or
|
|
92
|
+
`sequence[].photoPath`). When the real product must appear, list a real shot in
|
|
93
|
+
`referenceImagePaths` — never describe an invented product. Each file gets a provenance sidecar
|
|
94
|
+
(`aiGenerated: true`) that the preview and commit use automatically, so don't ask about it, and the
|
|
95
|
+
AI label applies. A 1024x1536 image cropped to 9:16 may draw the "soft, upscaled photo" warning:
|
|
96
|
+
mention it, don't fight it.
|
|
97
|
+
|
|
98
|
+
**Open the photo and look at it before using it** — a generated one most of all (pseudo-text, warped
|
|
99
|
+
hands or faces, a fake product or app screen). A picture can break every copy rule with text,
|
|
87
100
|
a logo or a claim burned into its pixels, and no automated check can see that — this is the first
|
|
88
101
|
of this skill's silent failure modes (the full list is below). A human or a prior pass marks a bad
|
|
89
102
|
photo `blocked`; if you notice one that should be, say so rather than using it anyway.
|
|
@@ -155,15 +168,25 @@ Only once `passed` is true, show the image to the person and ask with `AskUserQu
|
|
|
155
168
|
**Approve & upload**, **Request changes**, **Stop**. If `passed` is false you are not at this step
|
|
156
169
|
yet: fix it and re-preview first (step 6). On changes, preview again with the edits (another draft; nothing was saved). On
|
|
157
170
|
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.
|
|
159
|
-
|
|
171
|
+
automatically once it is committed or two weeks old. Say in the question that approving
|
|
172
|
+
also confirms the photo carries no burned-in text, logo or claim. Only on **Approve & upload**, call
|
|
173
|
+
`commit_creative_draft({draftDir, photoChecked: true})` with the `draftDir` the approved preview
|
|
174
|
+
returned, plus provenance (`source`, `licence`, `aiGenerated`) only for local files the preview
|
|
175
|
+
lists in `provenance.needed`. Pass `photoChecked` only after that approval, never on your own
|
|
176
|
+
judgement.
|
|
160
177
|
|
|
161
178
|
### 8. Check readiness before calling it done
|
|
162
179
|
|
|
163
|
-
The preview already lists the readiness gaps
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
The preview already lists the readiness gaps (including a local file's sidecar provenance), so fix
|
|
181
|
+
what you can (caption, alt text) before asking for approval; the photo check is cleared by the
|
|
182
|
+
approval itself (`photoChecked: true` on the commit, for files that commit uploads; a library photo
|
|
183
|
+
keeps its own verdict). After the commit, `get_creative` includes a `readiness` block: caption, alt
|
|
184
|
+
text, the photo's checked/blocked state and provenance, and the Creative's own state (not `DRAFT`)
|
|
185
|
+
are blocking; an AI-generation disclosure is informational only (when publishing, turn on the
|
|
186
|
+
platform's AI-generated label). If the only blocking item left is `state`, call
|
|
187
|
+
`update_creative({creativeId, state: "READY"})` and verify with `get_creative`; the backend refuses
|
|
188
|
+
READY while anything else blocking fails. Otherwise name exactly what is missing and ask. Do not
|
|
189
|
+
call a Creative done while `ready` is false.
|
|
167
190
|
|
|
168
191
|
### 9. Commit, then hand off
|
|
169
192
|
|
|
@@ -223,7 +246,7 @@ A MOTION Creative animates the same brand layout from steps 1-5 above into a sho
|
|
|
223
246
|
of a static frame — no separate video tool needed. Use it when a still doesn't hold attention, or a
|
|
224
247
|
destination rewards video over an image.
|
|
225
248
|
|
|
226
|
-
1. Pick a photo (step 5) or, for a moving background, a video — check `list_creative_media({kind:
|
|
249
|
+
1. Pick a photo (step 5; a generated photo works here too) or, for a moving background, a video — check `list_creative_media({kind:
|
|
227
250
|
"VIDEO"})` first and use its media id if something fits; otherwise pass the local file as
|
|
228
251
|
`clipPath` (previewed from disk, uploaded on commit). For background music,
|
|
229
252
|
`list_creative_media({kind: "AUDIO"})` the same way, else `audioPath`.
|
|
@@ -240,6 +263,10 @@ destination rewards video over an image.
|
|
|
240
263
|
- `audio` — `source: "clip"` (the background clip's own recorded sound — the default when there
|
|
241
264
|
is one), `"track"` (a separate library track: `trackId` from `upload_creative_media`, plus
|
|
242
265
|
`volume` and `fadeOutSec`), or `"none"`.
|
|
266
|
+
|
|
267
|
+
A MOTION Creative is one animated frame per placement: it cannot also be a story or a carousel
|
|
268
|
+
(`sequenceKind`/`sequence`) — that is refused with a 422 (`motionNoSequence`). For several beats, make
|
|
269
|
+
one MOTION Creative per beat, or a STILL story/carousel.
|
|
243
270
|
3. The preview renders three key moments across the timeline into one sheet image (not the finished
|
|
244
271
|
video). Judge it the same honest way as step 7 above — does the copy's entrance finish before the
|
|
245
272
|
next beat, does the end card actually hold long enough to read — and get approval before
|
|
@@ -260,6 +287,8 @@ Automated checks are real but incomplete. These are the ones worth remembering t
|
|
|
260
287
|
- **A photo with text, a logo or a claim burned into its pixels** passes every copy-rule and
|
|
261
288
|
render check, because none of them look at pixels for meaning — only step 5's "open and look at
|
|
262
289
|
it" catches this.
|
|
290
|
+
- **A generated image depicting a product, result or claim that isn't real** (a made-up app screen,
|
|
291
|
+
a before/after, an invented package) passes every check — only looking at it catches it.
|
|
263
292
|
- **A structurally valid render is not necessarily a good one.** The render's own assertions check
|
|
264
293
|
spill, safe zones, fonts and contrast — never whether the headline reads at a glance or the copy
|
|
265
294
|
sits on the subject's face. Step 7 exists because of this gap.
|
package/assets/cli-manifest.json
CHANGED
|
@@ -832,6 +832,14 @@
|
|
|
832
832
|
"creativeId"
|
|
833
833
|
]
|
|
834
834
|
},
|
|
835
|
+
{
|
|
836
|
+
"name": "generate_creative_image",
|
|
837
|
+
"description": "Generate a photo locally with the Codex CLI (needs codex login): no text in the image, returns it with local paths and a provenance sidecar. Pass a path as photoPath to preview_creative_draft.",
|
|
838
|
+
"category": "marketing",
|
|
839
|
+
"requiredParams": [
|
|
840
|
+
"prompt"
|
|
841
|
+
]
|
|
842
|
+
},
|
|
835
843
|
{
|
|
836
844
|
"name": "preview_creative_draft",
|
|
837
845
|
"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.",
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { type CodexStatus } from '../lib/codex-image.js';
|
|
2
3
|
export declare function checkApiHealth(apiUrl: string): Promise<boolean>;
|
|
3
4
|
export declare function checkMcpJson(workingDir: string): boolean;
|
|
4
5
|
export declare function checkConfigFile(): boolean;
|
|
6
|
+
export declare function describeCodexStatus(status: CodexStatus): {
|
|
7
|
+
status: 'pass' | 'warn';
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
5
10
|
export declare function registerDoctor(program: Command): void;
|
|
6
11
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAInC,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAGvE,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOrE;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAID,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAMrG;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0HrD"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from 'os';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { readConfig, CONFIG_PATH } from '../lib/config.js';
|
|
6
6
|
import { getAssetSrcDir, getPluginInstallStatus } from '../lib/plugin-assets.js';
|
|
7
|
+
import { checkCodexCli } from '../lib/codex-image.js';
|
|
7
8
|
import { printNextSteps } from '../lib/next-steps.js';
|
|
8
9
|
export async function checkApiHealth(apiUrl) {
|
|
9
10
|
try {
|
|
@@ -20,6 +21,15 @@ export function checkMcpJson(workingDir) {
|
|
|
20
21
|
export function checkConfigFile() {
|
|
21
22
|
return fs.existsSync(CONFIG_PATH);
|
|
22
23
|
}
|
|
24
|
+
const CODEX_CHECK_NAME = 'Codex CLI (image generation)';
|
|
25
|
+
export function describeCodexStatus(status) {
|
|
26
|
+
if (status.state === 'ready')
|
|
27
|
+
return { status: 'pass', message: `${CODEX_CHECK_NAME}: ${status.version}, logged in` };
|
|
28
|
+
if (status.state === 'logged-out') {
|
|
29
|
+
return { status: 'warn', message: `${CODEX_CHECK_NAME}: ${status.version} installed but not logged in — run \`codex login\`` };
|
|
30
|
+
}
|
|
31
|
+
return { status: 'warn', message: `${CODEX_CHECK_NAME}: not found — npm i -g @openai/codex, then \`codex login\` (optional; only needed to generate photos)` };
|
|
32
|
+
}
|
|
23
33
|
export function registerDoctor(program) {
|
|
24
34
|
program
|
|
25
35
|
.command('doctor')
|
|
@@ -72,6 +82,8 @@ Examples:
|
|
|
72
82
|
if (location !== 'none' && outdated) {
|
|
73
83
|
checks.push({ name: 'plugin-version', status: 'warn', message: 'Claude plugin outdated — re-run conductor init to update' });
|
|
74
84
|
}
|
|
85
|
+
const codex = describeCodexStatus(await checkCodexCli());
|
|
86
|
+
checks.push({ name: 'codex', status: codex.status, message: codex.message });
|
|
75
87
|
const allPass = checks.every(c => c.status !== 'fail');
|
|
76
88
|
process.stdout.write(JSON.stringify({ checks }, null, 2) + '\n');
|
|
77
89
|
process.exit(allPass ? 0 : 1);
|
|
@@ -118,6 +130,14 @@ Examples:
|
|
|
118
130
|
console.log(`${chalk.yellow('⚠')} Claude plugin outdated — re-run conductor init to update`);
|
|
119
131
|
warnings++;
|
|
120
132
|
}
|
|
133
|
+
const codex = describeCodexStatus(await checkCodexCli());
|
|
134
|
+
if (codex.status === 'pass') {
|
|
135
|
+
console.log(`${chalk.green('✓')} ${codex.message}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(`${chalk.yellow('⚠')} ${codex.message}`);
|
|
139
|
+
warnings++;
|
|
140
|
+
}
|
|
121
141
|
if (failures === 0 && warnings === 0) {
|
|
122
142
|
printNextSteps();
|
|
123
143
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
* loaded (including a bleed layout's CSS background photo, invisible to a
|
|
23
23
|
* plain <img> check), headline contrast, the accent colour, and the
|
|
24
24
|
* rendered box size.
|
|
25
|
+
*
|
|
26
|
+
* Presence checks (`missingPhoto`, `missingLockup`, `missingHeadline`): every check above judges what
|
|
27
|
+
* IS on the board, so a board that silently lost its photo or logo used to pass them all. The caller
|
|
28
|
+
* says what the spec requires (`expect`, see expectedElements) and a required element that is absent,
|
|
29
|
+
* collapsed or hidden fails. `bodyDropped` is the one warning in this family: a layout that has no room
|
|
30
|
+
* for the body line at a placement drops it by design, but the author supplied that copy.
|
|
25
31
|
*/
|
|
26
32
|
|
|
27
33
|
/* ── pure math ─────────────────────────────────────────────────────────── */
|
|
@@ -86,6 +92,19 @@ export function safeZoneIntrusion(boardRect, nodes, safe) {
|
|
|
86
92
|
return issues;
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/** Pure: what a render requires on the board, from the resolved ad and the brand kit it renders with —
|
|
96
|
+
* `{ photo, lockup, headline }`. A photo is required when the ad has a photo URL (a clip-only background
|
|
97
|
+
* does not need one); a lockup when the kit has a mark or a wordmark (a kit without logos renders none,
|
|
98
|
+
* by design); a headline when the ad has one. Pass the result as `runAssertions`'s `expect`. */
|
|
99
|
+
export function expectedElements(ad, brand) {
|
|
100
|
+
const logos = (brand && brand.logos) || {};
|
|
101
|
+
return {
|
|
102
|
+
photo: Boolean(ad && ad.photoUrl),
|
|
103
|
+
lockup: Boolean(logos.mark || logos.wordmarkLight || logos.wordmarkDark),
|
|
104
|
+
headline: Boolean(ad && ad.headline && String(ad.headline).trim()),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
/* ── DOM-dependent (real browser only) ────────────────────────────────────── */
|
|
90
109
|
|
|
91
110
|
const CHECKED_SELECTOR = '.cc-headline, .cc-body, .cc-cta, .cc-lockup';
|
|
@@ -116,9 +135,10 @@ function sameRgb(a, b) {
|
|
|
116
135
|
/* Runs every check against a live, fitted `.cc-board` and returns
|
|
117
136
|
* `{ errors, warnings, width, height }`. `placement` is this placement's own
|
|
118
137
|
* registry entry (pixel size, safe zone); `brand` is the same object handed
|
|
119
|
-
* to renderBoard
|
|
120
|
-
* `
|
|
121
|
-
|
|
138
|
+
* to renderBoard; `expect` (optional, see expectedElements) names the elements the spec requires, so a
|
|
139
|
+
* missing one fails. An empty `errors` array means the frame is safe to ship; `warnings` are non-fatal
|
|
140
|
+
* (e.g. a soft, upscaled photo, or a body line the layout drops at this placement). */
|
|
141
|
+
export async function runAssertions(board, placement, brand, { outputScale = 2, expect = null } = {}) {
|
|
122
142
|
const errors = [];
|
|
123
143
|
const warnings = [];
|
|
124
144
|
// The same findings with a stable `rule` id each, for callers (the draft preview) that report them
|
|
@@ -128,9 +148,9 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
|
|
|
128
148
|
errors.push(message);
|
|
129
149
|
checks.push({ rule, severity: 'error', message });
|
|
130
150
|
};
|
|
131
|
-
const warn = (rule, message) => {
|
|
151
|
+
const warn = (rule, message, extra) => {
|
|
132
152
|
warnings.push(message);
|
|
133
|
-
checks.push({ rule, severity: 'warning', message });
|
|
153
|
+
checks.push({ rule, severity: 'warning', message, ...extra });
|
|
134
154
|
};
|
|
135
155
|
const br = rectOf(board);
|
|
136
156
|
|
|
@@ -155,14 +175,44 @@ export async function runAssertions(board, placement, brand, { outputScale = 2 }
|
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
177
|
|
|
178
|
+
// ── required elements: present, sized and visible (a spec that names them must get them) ──
|
|
179
|
+
if (expect) {
|
|
180
|
+
const shown = (n) => {
|
|
181
|
+
if (!n) return false;
|
|
182
|
+
const r = n.getBoundingClientRect();
|
|
183
|
+
const cs = getComputedStyle(n);
|
|
184
|
+
return r.width > 0 && r.height > 0 && cs.display !== 'none' && cs.visibility !== 'hidden';
|
|
185
|
+
};
|
|
186
|
+
if (expect.headline) {
|
|
187
|
+
const h = board.querySelector('.cc-headline');
|
|
188
|
+
if (!h || !h.textContent.trim() || !shown(h)) fail('missingHeadline', 'the headline is missing from the rendered frame');
|
|
189
|
+
}
|
|
190
|
+
if (expect.lockup && !shown(board.querySelector('.cc-lockup:not(.cc-lockup--spacer)'))) {
|
|
191
|
+
fail('missingLockup', 'the brand logo lockup is missing from the rendered frame (the brand kit has a logo)');
|
|
192
|
+
}
|
|
193
|
+
if (expect.photo) {
|
|
194
|
+
const layer = board.querySelector('.cc-bg-photo') || board.querySelector('.cc-board__band img, .cc-board__card img');
|
|
195
|
+
const hasImage = layer && (layer.tagName === 'IMG'
|
|
196
|
+
? Boolean(layer.getAttribute('src')) && layer.complete && layer.naturalWidth > 0
|
|
197
|
+
: /url\(/.test(getComputedStyle(layer).backgroundImage || ''));
|
|
198
|
+
if (!shown(layer) || !hasImage) fail('missingPhoto', 'the photo is missing from the rendered frame (the spec has a photo URL)');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (board.dataset.bodyDropped === 'true') {
|
|
202
|
+
const layout = board.dataset.layout || 'this layout';
|
|
203
|
+
const key = board.dataset.ratio;
|
|
204
|
+
warn('bodyDropped', `the body line is not shown: the ${layout} layout drops the body at ${key}. Put the words in the headline, or pick another layout or placement.`, { placementKey: key });
|
|
205
|
+
}
|
|
206
|
+
|
|
158
207
|
const imgs = [...board.querySelectorAll('img')];
|
|
159
208
|
const broken = imgs.filter((i) => !i.complete || i.naturalWidth === 0).map((i) => i.getAttribute('src'));
|
|
160
209
|
if (broken.length) fail('image', `image(s) failed to load: ${broken.join(', ')}`);
|
|
161
210
|
|
|
162
|
-
// A bleed
|
|
211
|
+
// A bleed layout paints its photo as a CSS background (the `.cc-bg-photo` layer), invisible to
|
|
163
212
|
// the <img> check above — probe it directly, the same way a typo'd path
|
|
164
213
|
// would otherwise export a flat, photo-less board and pass everything else.
|
|
165
|
-
const
|
|
214
|
+
const photoLayer = board.querySelector('.cc-bg-photo');
|
|
215
|
+
const bgImage = getComputedStyle(photoLayer || board).backgroundImage;
|
|
166
216
|
const bgUrlMatch = /url\("?(.+?)"?\)/.exec(bgImage || '');
|
|
167
217
|
if (bgUrlMatch && bgUrlMatch[1]) {
|
|
168
218
|
const probe = await new Promise((done) => {
|
package/dist/creative/font.js
CHANGED
|
@@ -37,3 +37,32 @@ export async function ensureBrandFont(brand) {
|
|
|
37
37
|
await Promise.all([400, 700].map((w) => document.fonts.load(`${w} 32px "${family}"`).catch(() => [])));
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/* Waits until every picture under `root` has loaded: each <img> (the logo lockup, the badge, a band/card
|
|
42
|
+
* photo) AND each bleed photo layer, which is a CSS background and so is not in `document.images` — the
|
|
43
|
+
* page's `load` event does not wait for it either. Without this a screenshot (the contact sheet most of
|
|
44
|
+
* all, which never ran this wait) could be taken before the photo or logos had arrived: a board with no
|
|
45
|
+
* photo and no logo that nothing flagged. A picture that fails to load resolves anyway; it is
|
|
46
|
+
* assertions.js's job (frame.html) to report it. Browser-only; a no-op outside a document. */
|
|
47
|
+
export async function settleImages(root) {
|
|
48
|
+
if (typeof document === 'undefined') return;
|
|
49
|
+
const scope = root || document;
|
|
50
|
+
const loads = [...scope.querySelectorAll('img')].map((img) => (
|
|
51
|
+
img.complete ? Promise.resolve() : new Promise((done) => {
|
|
52
|
+
img.addEventListener('load', done, { once: true });
|
|
53
|
+
img.addEventListener('error', done, { once: true });
|
|
54
|
+
})
|
|
55
|
+
));
|
|
56
|
+
for (const layer of scope.querySelectorAll('.cc-bg-photo')) {
|
|
57
|
+
const m = /url\("?(.+?)"?\)/.exec(getComputedStyle(layer).backgroundImage || '');
|
|
58
|
+
if (!m) continue;
|
|
59
|
+
loads.push(new Promise((done) => {
|
|
60
|
+
const probe = new Image();
|
|
61
|
+
probe.onload = done;
|
|
62
|
+
probe.onerror = done;
|
|
63
|
+
probe.src = m[1];
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
await Promise.all(loads);
|
|
67
|
+
await Promise.all([...scope.querySelectorAll('img')].map((img) => (img.decode ? img.decode().catch(() => {}) : null)));
|
|
68
|
+
}
|