@aksp/opencrew 1.4.1 → 1.5.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/CHANGELOG.md +116 -0
- package/README.md +38 -17
- package/bin/opencrew.js +4 -4
- package/package.json +4 -3
- package/src/cli.js +93 -43
- package/src/commands/init.js +79 -53
- package/src/commands/update.js +44 -17
- package/src/lib/errors.js +12 -0
- package/src/lib/fsx.js +18 -9
- package/src/lib/ides.js +9 -34
- package/templates/AGENTS.md +42 -63
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/best-practices/copywriting.md +4 -1
- package/templates/_opencrew/core/best-practices/image-design.md +5 -5
- package/templates/_opencrew/core/best-practices/instagram-feed.md +4 -4
- package/templates/_opencrew/core/best-practices/instagram-reels.md +1 -1
- package/templates/_opencrew/core/best-practices/review.md +7 -0
- package/templates/_opencrew/core/best-practices/social-networks-publishing.md +8 -8
- package/templates/_opencrew/core/prompts/build.prompt.md +25 -0
- package/templates/_opencrew/core/prompts/design.prompt.md +9 -5
- package/templates/_opencrew/core/runner.pipeline.md +56 -7
- package/templates/_opencrew/core/scripts/verificar/leitura.mjs +99 -0
- package/templates/_opencrew/core/scripts/verificar/regras.mjs +127 -0
- package/templates/_opencrew/core/scripts/verificar.mjs +118 -0
- package/templates/gitignore +3 -1
- package/templates/skills/image-ai-generator/SKILL.md +9 -5
- package/templates/skills/image-creator/SKILL.md +5 -3
- package/templates/skills/image-fetcher/SKILL.md +1 -1
- package/templates/skills/instagram-publisher/SKILL.md +34 -16
- package/templates/skills/instagram-publisher/scripts/publish.js +58 -27
- package/templates/skills/template-designer/SKILL.md +3 -3
- package/templates/skills/template-designer/base-templates/model-a.html +1 -1
- package/templates/skills/template-designer/base-templates/model-b.html +1 -1
- package/templates/skills/template-designer/base-templates/model-c.html +1 -1
|
@@ -14,7 +14,7 @@ type: script
|
|
|
14
14
|
version: "1.0.0"
|
|
15
15
|
script:
|
|
16
16
|
path: scripts/generate.py
|
|
17
|
-
runtime:
|
|
17
|
+
runtime: python
|
|
18
18
|
invoke: "python3 {skill_path}/scripts/generate.py --prompt \"{prompt}\" --output \"{output}\" --mode \"{mode}\""
|
|
19
19
|
env:
|
|
20
20
|
- OPENROUTER_API_KEY
|
|
@@ -52,10 +52,14 @@ Use the Image Generator when you need to create visual assets from text prompts.
|
|
|
52
52
|
|
|
53
53
|
## Instructions
|
|
54
54
|
|
|
55
|
+
`{skill_path}` is this skill's folder (normally `skills/image-ai-generator`). The commands below use
|
|
56
|
+
`python3` (macOS/Linux). **On Windows** use `py -3` instead (or `python` if the `py` launcher is
|
|
57
|
+
not installed).
|
|
58
|
+
|
|
55
59
|
### Single image generation
|
|
56
60
|
|
|
57
61
|
```bash
|
|
58
|
-
python3
|
|
62
|
+
python3 {skill_path}/scripts/generate.py \
|
|
59
63
|
--prompt "A detailed description of the image to generate" \
|
|
60
64
|
--output "crews/{crew}/output/{run_id}/assets/image-name.jpg" \
|
|
61
65
|
--mode test
|
|
@@ -66,7 +70,7 @@ python3 skills/image-generator/scripts/generate.py \
|
|
|
66
70
|
Use `--reference` to send a local image to the model as visual context. The model will incorporate the referenced image (e.g., a logo or mascot) into the generated output.
|
|
67
71
|
|
|
68
72
|
```bash
|
|
69
|
-
python3
|
|
73
|
+
python3 {skill_path}/scripts/generate.py \
|
|
70
74
|
--prompt "A social media banner featuring the company logo prominently in the center" \
|
|
71
75
|
--output "crews/{crew}/output/{run_id}/assets/banner.jpg" \
|
|
72
76
|
--reference "crews/{crew}/assets/logo.png" \
|
|
@@ -78,7 +82,7 @@ Supported reference formats: PNG, JPEG, WEBP, GIF.
|
|
|
78
82
|
### Batch generation
|
|
79
83
|
|
|
80
84
|
```bash
|
|
81
|
-
python3
|
|
85
|
+
python3 {skill_path}/scripts/generate.py \
|
|
82
86
|
--batch "crews/{crew}/output/{run_id}/assets/batch.json" \
|
|
83
87
|
--mode production
|
|
84
88
|
```
|
|
@@ -96,7 +100,7 @@ Each item can optionally include a `"reference": "path/to/ref.png"` field.
|
|
|
96
100
|
### Prompt guidelines
|
|
97
101
|
|
|
98
102
|
- Be specific about composition, lighting, style, and mood
|
|
99
|
-
- Specify aspect ratio or orientation when relevant (e.g., "portrait
|
|
103
|
+
- Specify aspect ratio or orientation when relevant (e.g., "portrait 4:5", "landscape 16:9")
|
|
100
104
|
- Include "hyper realistic, 4K quality" for photographic styles
|
|
101
105
|
- Include "clean composition" to avoid cluttered outputs
|
|
102
106
|
- Avoid requesting text in images — AI models struggle with text rendering
|
|
@@ -46,7 +46,8 @@ Use the Visual Renderer when you need to generate production-ready images from H
|
|
|
46
46
|
4. **Render** -- Use Playwright to:
|
|
47
47
|
- `browser_navigate` to `http://localhost:8765/slide-01.html` (filename only, not full path)
|
|
48
48
|
- `browser_resize` to target viewport dimensions
|
|
49
|
-
- `browser_take_screenshot` to save as PNG
|
|
49
|
+
- `browser_take_screenshot` to save as PNG — **except when the images will be published to
|
|
50
|
+
Instagram**: Instagram accepts JPEG only, so save with `type: "jpeg"` (`slide-01.jpg`)
|
|
50
51
|
|
|
51
52
|
5. **Verify** -- Read the screenshot to confirm quality. Re-render if needed.
|
|
52
53
|
|
|
@@ -59,7 +60,7 @@ Use the Visual Renderer when you need to generate production-ready images from H
|
|
|
59
60
|
|
|
60
61
|
Use these standard dimensions:
|
|
61
62
|
- Instagram Post: 1080 x 1080
|
|
62
|
-
- Instagram Carousel: 1080 x
|
|
63
|
+
- Instagram Carousel: 1080 x 1350
|
|
63
64
|
- Instagram Story/Reel: 1080 x 1920
|
|
64
65
|
- Facebook Post: 1200 x 630
|
|
65
66
|
- Twitter/X Post: 1200 x 675
|
|
@@ -85,7 +86,7 @@ Example minimal structure:
|
|
|
85
86
|
<meta charset="UTF-8">
|
|
86
87
|
<style>
|
|
87
88
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
88
|
-
body { width: 1080px; height:
|
|
89
|
+
body { width: 1080px; height: 1350px; overflow: hidden; }
|
|
89
90
|
/* ... your design ... */
|
|
90
91
|
</style>
|
|
91
92
|
</head>
|
|
@@ -103,6 +104,7 @@ For multi-image outputs like carousels:
|
|
|
103
104
|
3. Render each slide sequentially (step 4 repeated per slide)
|
|
104
105
|
4. Stop the HTTP server **once** after all slides are done (step 6 of Core Workflow)
|
|
105
106
|
5. Name output files with zero-padded numbers: slide-01.png, slide-02.png, slide-03.png
|
|
107
|
+
(`.jpg` when the destination is Instagram — see step 4)
|
|
106
108
|
6. Keep all slides at the same viewport dimensions
|
|
107
109
|
|
|
108
110
|
### Best Practices
|
|
@@ -47,7 +47,7 @@ When taking a screenshot:
|
|
|
47
47
|
1. Navigate to the URL with `browser_navigate`
|
|
48
48
|
2. Set viewport: `browser_resize` with width/height for target format
|
|
49
49
|
- Instagram post: 1080x1080
|
|
50
|
-
- Instagram carousel:
|
|
50
|
+
- Instagram carousel: 1080x1350
|
|
51
51
|
- Story/Reel: 1080x1920
|
|
52
52
|
- Generic: 1280x720
|
|
53
53
|
3. Wait for page load (`browser_wait_for` if needed)
|
|
@@ -20,7 +20,8 @@ version: "1.0.0"
|
|
|
20
20
|
script:
|
|
21
21
|
path: scripts/publish.js
|
|
22
22
|
runtime: node
|
|
23
|
-
invoke: "node --env-file=.env {skill_path}/scripts/publish.js --images \"{images}\" --caption \"{
|
|
23
|
+
invoke: "node --env-file=.env {skill_path}/scripts/publish.js --images \"{images}\" --caption-file \"{caption_file}\""
|
|
24
|
+
side_effects: irreversible
|
|
24
25
|
env:
|
|
25
26
|
- INSTAGRAM_ACCESS_TOKEN
|
|
26
27
|
- INSTAGRAM_USER_ID
|
|
@@ -39,36 +40,53 @@ Use the Instagram Publisher when you need to publish carousel posts directly to
|
|
|
39
40
|
|
|
40
41
|
### Workflow
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
Publishing is **irreversible**: a post cannot be taken back once it is live. This step
|
|
44
|
+
runs only at the end of the pipeline, after the Review and the Final Approval checkpoint,
|
|
45
|
+
and is **never** retried automatically.
|
|
46
|
+
|
|
47
|
+
1. Find the images produced by the rendering step **of this run** in
|
|
48
|
+
`crews/{crew}/output/{run_id}/` (use the latest `vN/` folder if there are versions),
|
|
49
|
+
JPEG only, sorted by name. If there are no `.jpg`/`.jpeg` files: stop and ask the user
|
|
50
|
+
(PNG renders must be re-rendered as JPEG — Instagram accepts JPEG only).
|
|
51
|
+
2. Extract the caption from the approved content draft (hook slide text + CTA slide text,
|
|
52
|
+
max 2200 characters) and **write it to a file**:
|
|
53
|
+
`crews/{crew}/output/{run_id}/caption.txt`. Never put the caption inside a shell command.
|
|
54
|
+
3. **Preview**: show the user the ordered image list and the full caption, and let them
|
|
55
|
+
confirm the order (use your IDE's native interactive-choice mechanism if it has one;
|
|
56
|
+
otherwise a numbered list).
|
|
57
|
+
4. **Dry run** — validates images, credentials and containers without posting:
|
|
49
58
|
```
|
|
50
|
-
node --env-file=.env
|
|
59
|
+
node --env-file=.env {skill_path}/scripts/publish.js \
|
|
51
60
|
--images "<comma-separated-ordered-paths>" \
|
|
52
|
-
--caption "
|
|
61
|
+
--caption-file "crews/{crew}/output/{run_id}/caption.txt" \
|
|
62
|
+
--dry-run
|
|
53
63
|
```
|
|
54
|
-
|
|
55
|
-
5.
|
|
56
|
-
|
|
64
|
+
If it fails: show the error and stop.
|
|
65
|
+
5. **Ask for explicit confirmation** before going live: the user must answer with the
|
|
66
|
+
word **publish** (or **publicar**). Any other answer — including silence, "ok" or an
|
|
67
|
+
ambiguous reply — means do not publish.
|
|
68
|
+
6. **Live publish** — the same command without `--dry-run`. Run it **once**.
|
|
69
|
+
7. On success: save the post URL and post ID to the step output file immediately.
|
|
70
|
+
8. On failure or missing output: do NOT run the command again. Tell the user the post may
|
|
71
|
+
already be live, ask them to check the Instagram profile, and let them decide.
|
|
57
72
|
|
|
58
73
|
### Constraints
|
|
59
74
|
|
|
60
|
-
- Images: JPEG only, 2-10 per carousel
|
|
75
|
+
- Images: JPEG only (`.jpg`/`.jpeg`), 2-10 per carousel, inside `crews/*/output/` — the
|
|
76
|
+
script refuses anything else before uploading
|
|
77
|
+
- Images are hosted on imgBB for 24h only (enough for Instagram to fetch them)
|
|
61
78
|
- Caption: max 2200 characters
|
|
62
79
|
- Requires Instagram Business account (not Personal or Creator)
|
|
63
80
|
- Rate limit: 25 API-published posts per 24 hours
|
|
64
81
|
|
|
65
82
|
### Setup (first-time)
|
|
66
83
|
|
|
67
|
-
Copy `.env.example` to `.env` and fill in the
|
|
84
|
+
Copy `.env.example` to `.env` and fill in the three required variables:
|
|
68
85
|
|
|
69
86
|
```
|
|
70
87
|
INSTAGRAM_ACCESS_TOKEN=
|
|
71
88
|
INSTAGRAM_USER_ID=
|
|
89
|
+
IMGBB_API_KEY=
|
|
72
90
|
```
|
|
73
91
|
|
|
74
92
|
#### INSTAGRAM_ACCESS_TOKEN
|
|
@@ -115,5 +133,5 @@ Pré-requisito: conta Instagram Business conectada a uma Página do Facebook, e
|
|
|
115
133
|
|
|
116
134
|
- **Publish Carousel** -- Upload images and publish a carousel post to Instagram
|
|
117
135
|
- **Dry Run** -- Test the full publishing flow without actually posting (use `--dry-run` flag)
|
|
118
|
-
- **Image Upload** -- Upload
|
|
136
|
+
- **Image Upload** -- Upload the crew's JPEG renders to imgBB for 24h (requires API key)
|
|
119
137
|
- **Status Check** -- Monitor media container processing status before publishing
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Instagram Carousel Publisher
|
|
3
|
-
// Usage: node --env-file=.env publish.js --images "
|
|
3
|
+
// Usage: node --env-file=.env publish.js --images "a.jpg,b.jpg" --caption-file caption.txt [--dry-run]
|
|
4
|
+
//
|
|
5
|
+
// Safety rules (specs/fase-1-hotfix.md F1-10):
|
|
6
|
+
// - the caption comes from a FILE, never from a shell-interpolated argument;
|
|
7
|
+
// - only .jpg/.jpeg files inside crews/*/output/ can be uploaded (public hosting);
|
|
8
|
+
// - imgBB uploads expire; the Graph API token goes in POST bodies, not URLs.
|
|
4
9
|
|
|
5
10
|
import { readFileSync } from 'node:fs';
|
|
6
|
-
import {
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { resolve, relative, isAbsolute, sep, extname } from 'node:path';
|
|
7
13
|
import { fileURLToPath } from 'node:url';
|
|
8
14
|
|
|
15
|
+
const IMGBB_EXPIRATION_SECONDS = 86_400; // Instagram fetches the images right away; 1 day is plenty.
|
|
16
|
+
|
|
9
17
|
// ── Argument parsing ──────────────────────────────────────────
|
|
10
18
|
|
|
11
19
|
export function parseArgs(argv) {
|
|
12
|
-
const args = { images: [], caption: '', dryRun: false };
|
|
20
|
+
const args = { images: [], caption: '', captionFile: '', dryRun: false };
|
|
13
21
|
for (let i = 2; i < argv.length; i++) {
|
|
14
22
|
if (argv[i] === '--images') {
|
|
15
|
-
if (i + 1 < argv.length) args.images = argv[++i].split(',').map(s => s.trim());
|
|
23
|
+
if (i + 1 < argv.length) args.images = argv[++i].split(',').map(s => s.trim()).filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
else if (argv[i] === '--caption-file') {
|
|
26
|
+
if (i + 1 < argv.length) args.captionFile = argv[++i];
|
|
16
27
|
}
|
|
17
28
|
else if (argv[i] === '--caption') {
|
|
29
|
+
// Legacy: prefer --caption-file (a caption in a shell command can execute code).
|
|
18
30
|
if (i + 1 < argv.length) args.caption = argv[++i];
|
|
19
31
|
}
|
|
20
32
|
else if (argv[i] === '--dry-run') args.dryRun = true;
|
|
@@ -22,15 +34,32 @@ export function parseArgs(argv) {
|
|
|
22
34
|
return args;
|
|
23
35
|
}
|
|
24
36
|
|
|
37
|
+
export async function readCaption(args) {
|
|
38
|
+
return args.captionFile ? readFile(args.captionFile, 'utf8') : args.caption;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Throws unless every path is a .jpg/.jpeg inside <cwd>/crews/<crew>/output/. */
|
|
42
|
+
export function validateImagePaths(images, cwd = process.cwd()) {
|
|
43
|
+
const crewsDir = resolve(cwd, 'crews');
|
|
44
|
+
for (const img of images) {
|
|
45
|
+
const rel = relative(crewsDir, resolve(cwd, img));
|
|
46
|
+
const parts = rel.split(sep);
|
|
47
|
+
const inside = rel && !rel.startsWith('..') && !isAbsolute(rel) && parts.length >= 3 && parts[1] === 'output';
|
|
48
|
+
const jpeg = ['.jpg', '.jpeg'].includes(extname(img).toLowerCase());
|
|
49
|
+
if (!inside || !jpeg) {
|
|
50
|
+
throw new Error(`Refusing to upload ${img}: only .jpg/.jpeg files inside crews/*/output/ are allowed.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
// ── Image upload (imgBB) ──────────────────────────────────────
|
|
26
56
|
|
|
27
57
|
export async function uploadToImgBB(imagePath, apiKey) {
|
|
28
|
-
const
|
|
29
|
-
const fileBuffer = readFileSync(absolutePath);
|
|
30
|
-
const base64Image = fileBuffer.toString('base64');
|
|
58
|
+
const base64Image = readFileSync(resolve(imagePath)).toString('base64');
|
|
31
59
|
const form = new FormData();
|
|
32
60
|
form.append('key', apiKey);
|
|
33
61
|
form.append('image', base64Image);
|
|
62
|
+
form.append('expiration', String(IMGBB_EXPIRATION_SECONDS));
|
|
34
63
|
const res = await fetch('https://api.imgbb.com/1/upload', {
|
|
35
64
|
method: 'POST',
|
|
36
65
|
body: form,
|
|
@@ -45,15 +74,19 @@ export async function uploadToImgBB(imagePath, apiKey) {
|
|
|
45
74
|
|
|
46
75
|
const IG_BASE = 'https://graph.facebook.com/v21.0';
|
|
47
76
|
|
|
77
|
+
// POST with form-encoded body: keeps the access token out of URLs (and out of server/proxy logs).
|
|
78
|
+
async function igPost(path, params, label) {
|
|
79
|
+
const res = await fetch(`${IG_BASE}/${path}`, { method: 'POST', body: new URLSearchParams(params) });
|
|
80
|
+
if (!res.ok) throw new Error(`${label} failed [${res.status}]: ${await res.text()}`);
|
|
81
|
+
return (await res.json()).id;
|
|
82
|
+
}
|
|
83
|
+
|
|
48
84
|
export async function createChildContainer(userId, imageUrl, accessToken) {
|
|
49
|
-
|
|
85
|
+
return igPost(`${userId}/media`, {
|
|
50
86
|
image_url: imageUrl,
|
|
51
87
|
is_carousel_item: 'true',
|
|
52
88
|
access_token: accessToken,
|
|
53
|
-
});
|
|
54
|
-
const res = await fetch(`${IG_BASE}/${userId}/media?${params}`, { method: 'POST' });
|
|
55
|
-
if (!res.ok) throw new Error(`createChildContainer failed [${res.status}]: ${await res.text()}`);
|
|
56
|
-
return (await res.json()).id;
|
|
89
|
+
}, 'createChildContainer');
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
export async function getContainerStatus(containerId, accessToken) {
|
|
@@ -75,22 +108,16 @@ export async function pollUntilFinished(containerId, accessToken, timeoutMs = 60
|
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
export async function createCarouselContainer(userId, childIds, caption, accessToken) {
|
|
78
|
-
|
|
111
|
+
return igPost(`${userId}/media`, {
|
|
79
112
|
media_type: 'CAROUSEL',
|
|
80
113
|
children: childIds.join(','),
|
|
81
114
|
caption,
|
|
82
115
|
access_token: accessToken,
|
|
83
|
-
});
|
|
84
|
-
const res = await fetch(`${IG_BASE}/${userId}/media?${params}`, { method: 'POST' });
|
|
85
|
-
if (!res.ok) throw new Error(`createCarouselContainer failed [${res.status}]: ${await res.text()}`);
|
|
86
|
-
return (await res.json()).id;
|
|
116
|
+
}, 'createCarouselContainer');
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
export async function publishMedia(userId, containerId, accessToken) {
|
|
90
|
-
|
|
91
|
-
const res = await fetch(`${IG_BASE}/${userId}/media_publish?${params}`, { method: 'POST' });
|
|
92
|
-
if (!res.ok) throw new Error(`publishMedia failed [${res.status}]: ${await res.text()}`);
|
|
93
|
-
return (await res.json()).id;
|
|
120
|
+
return igPost(`${userId}/media_publish`, { creation_id: containerId, access_token: accessToken }, 'publishMedia');
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
export async function getPermalink(mediaId, accessToken) {
|
|
@@ -103,25 +130,29 @@ export async function getPermalink(mediaId, accessToken) {
|
|
|
103
130
|
|
|
104
131
|
// ── Main ──────────────────────────────────────────────────────
|
|
105
132
|
|
|
106
|
-
async function main() {
|
|
107
|
-
const
|
|
133
|
+
export async function main(argv = process.argv, { env = process.env, cwd = process.cwd() } = {}) {
|
|
134
|
+
const args = parseArgs(argv);
|
|
135
|
+
const { images, dryRun } = args;
|
|
108
136
|
|
|
109
137
|
if (!images.length) throw new Error('--images is required (e.g. --images "slide1.jpg,slide2.jpg")');
|
|
110
|
-
if (!caption) throw new Error('--caption is required');
|
|
111
138
|
if (images.length < 2 || images.length > 10) {
|
|
112
139
|
throw new Error(`Instagram carousels require 2–10 images (got ${images.length})`);
|
|
113
140
|
}
|
|
141
|
+
validateImagePaths(images, cwd);
|
|
142
|
+
|
|
143
|
+
const caption = await readCaption(args);
|
|
144
|
+
if (!caption) throw new Error('--caption-file is required');
|
|
114
145
|
if (caption.length > 2200) {
|
|
115
146
|
throw new Error(`Caption exceeds Instagram's 2200-character limit (got ${caption.length})`);
|
|
116
147
|
}
|
|
117
148
|
|
|
118
|
-
const { INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_USER_ID, IMGBB_API_KEY } =
|
|
149
|
+
const { INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_USER_ID, IMGBB_API_KEY } = env;
|
|
119
150
|
if (!INSTAGRAM_ACCESS_TOKEN) throw new Error('INSTAGRAM_ACCESS_TOKEN is not set in environment');
|
|
120
151
|
if (!INSTAGRAM_USER_ID) throw new Error('INSTAGRAM_USER_ID is not set in environment');
|
|
121
152
|
if (!IMGBB_API_KEY) throw new Error('IMGBB_API_KEY is not set in environment. Get one at https://api.imgbb.com/');
|
|
122
153
|
|
|
123
|
-
console.log(`📸 Uploading ${images.length} image(s) to imgBB...`);
|
|
124
|
-
const imageUrls = await Promise.all(images.map(p => uploadToImgBB(p, IMGBB_API_KEY)));
|
|
154
|
+
console.log(`📸 Uploading ${images.length} image(s) to imgBB (expire in 24h)...`);
|
|
155
|
+
const imageUrls = await Promise.all(images.map(p => uploadToImgBB(resolve(cwd, p), IMGBB_API_KEY)));
|
|
125
156
|
imageUrls.forEach((url, i) => console.log(` [${i + 1}] ${url}`));
|
|
126
157
|
|
|
127
158
|
console.log('\n📦 Creating Instagram media containers...');
|
|
@@ -46,7 +46,7 @@ Before generating any template, read and internalize the design best practices:
|
|
|
46
46
|
- `_opencrew/core/best-practices/image-design.md` — **REQUIRED reading**. Contains platform-specific minimum font sizes, typography rules, spacing guidelines, color palette constraints, contrast requirements, and layout methodology. Every template you generate MUST comply with these rules.
|
|
47
47
|
|
|
48
48
|
Key rules to always follow:
|
|
49
|
-
- **Font sizes**: Hero 58px, Heading 43px, Body 34px, Caption 24px minimum for Instagram carousel (
|
|
49
|
+
- **Font sizes**: Hero 58px, Heading 43px, Body 34px, Caption 24px minimum for Instagram carousel (1080x1350). Absolute minimum 20px for any readable text on any platform.
|
|
50
50
|
- **Font weight**: 500 or higher for body text and above.
|
|
51
51
|
- **Colors**: Maximum 5 colors per design system (primary, secondary, accent, background, text).
|
|
52
52
|
- **Contrast**: WCAG AA minimum 4.5:1 for all text against background.
|
|
@@ -61,7 +61,7 @@ You should also apply general web design best practices: proper white space, vis
|
|
|
61
61
|
These rules are NON-NEGOTIABLE. Every template must comply:
|
|
62
62
|
|
|
63
63
|
**Fixed Dimensions (never use height: auto or flexible height):**
|
|
64
|
-
- Instagram Carousel: `width: 1080px; height:
|
|
64
|
+
- Instagram Carousel: `width: 1080px; height: 1350px` (4:5 portrait)
|
|
65
65
|
- Instagram Story/Reel: `width: 1080px; height: 1920px` (9:16 portrait)
|
|
66
66
|
- Instagram Post: `width: 1080px; height: 1080px` (1:1 square)
|
|
67
67
|
- LinkedIn Post: `width: 1200px; height: 627px` (1.91:1 horizontal)
|
|
@@ -155,7 +155,7 @@ When the user approves, create two files:
|
|
|
155
155
|
|
|
156
156
|
Save to: `crews/{code}/pipeline/data/template-reference.html`
|
|
157
157
|
|
|
158
|
-
The complete, self-contained HTML/CSS of the approved template at full resolution (e.g.,
|
|
158
|
+
The complete, self-contained HTML/CSS of the approved template at full resolution (e.g., 1080x1350). This is the literal example the design agent will use.
|
|
159
159
|
|
|
160
160
|
### 2. Visual Identity Rules
|
|
161
161
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- Style: Black background, tweet-style layout with avatar, verified badge, editorial text, contextual image -->
|
|
3
3
|
<!-- Fonts: Inter (body), weight 400-700 -->
|
|
4
4
|
<!-- Colors: #000 bg, #fff text, #1D9BF0 verified, #71767B muted, #2a2a2a borders/avatar, #1a1a2e image gradient -->
|
|
5
|
-
<div style="width:1080px;height:
|
|
5
|
+
<div style="width:1080px;height:1350px;background:#000;color:#fff;font-family:'Inter',sans-serif;display:flex;flex-direction:column;padding:72px;gap:0;position:relative;overflow:hidden;">
|
|
6
6
|
<!-- Tweet header -->
|
|
7
7
|
<div style="display:flex;gap:24px;align-items:center;">
|
|
8
8
|
<div style="width:80px;height:80px;border-radius:50%;background:#2a2a2a;flex-shrink:0;"></div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- Style: Warm cream background, bold uppercase title, numbered cards with warm gradient progression -->
|
|
3
3
|
<!-- Fonts: Montserrat 900 (title), Inter 500-700 (body) -->
|
|
4
4
|
<!-- Colors: #FDF6EE bg, #E85D2A primary orange, #D4742A/#C05A1A/#A04010 gradient steps, #FFF5E6-#D07030 card backgrounds -->
|
|
5
|
-
<div style="width:1080px;height:
|
|
5
|
+
<div style="width:1080px;height:1350px;background:#FDF6EE;color:#111;font-family:'Inter',sans-serif;display:flex;flex-direction:column;padding:72px;justify-content:center;gap:48px;position:relative;overflow:hidden;">
|
|
6
6
|
<!-- Title -->
|
|
7
7
|
<div style="text-align:center;">
|
|
8
8
|
<h1 style="font-size:58px;font-weight:900;line-height:1.15;margin:0;text-transform:uppercase;font-family:'Montserrat',sans-serif;">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- Style: Purple-dark gradient, data metric cards, decorative glow effects -->
|
|
3
3
|
<!-- Fonts: Montserrat 900 (title, metrics), Inter 500 (body, labels) -->
|
|
4
4
|
<!-- Colors: gradient #0f0326-#1a0a3e-#0d0d0d bg, #F59E0B yellow, #EC4899 pink, #7C3AED purple, #4ADE80 green accents -->
|
|
5
|
-
<div style="width:1080px;height:
|
|
5
|
+
<div style="width:1080px;height:1350px;background:linear-gradient(160deg,#0f0326,#1a0a3e 40%,#0d0d0d);color:#fff;font-family:'Inter',sans-serif;display:flex;flex-direction:column;padding:72px;position:relative;overflow:hidden;">
|
|
6
6
|
<!-- Glow effects -->
|
|
7
7
|
<div style="position:absolute;top:-120px;right:-120px;width:480px;height:480px;background:radial-gradient(circle,#7C3AED33,transparent 70%);pointer-events:none;"></div>
|
|
8
8
|
<div style="position:absolute;bottom:-100px;left:-100px;width:400px;height:400px;background:radial-gradient(circle,#F59E0B22,transparent 70%);pointer-events:none;"></div>
|