Blog image gen
Useful Skills from Bogdan Baciu
npx -y skills add bogdanbaciu21/skills --skill blog-image-genAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Generate hero images for blog posts using the blog repo's OpenAI image-generation scripts after verifying current official API docs for model names, tool syntax, size/quality options, and pricing. Use when the user asks to create, generate, regenerate, or batch produce hero/header images for blog posts in priv/content/posts. Also use to ingest manually-downloaded images from the art/ folder into the served location and wire them to the blog.
SKILL.md
7.9 KB, as published. Nobody here has run it
Blog Image Generation
Generate editorial hero images for posts on bogdanbaciu.com via the blog repo's
image-generation scripts. Images land at
priv/static/images/posts/<slug>/hero.png and are auto-rendered on the post
page by Bogdan.Content.Post.detect_hero/1 (no frontmatter changes needed).
When to use
- "Generate the image for
<slug>." - "Generate images for every post that's missing one."
- "I dumped some PNGs into
art/— wire them up." - "Show me what's done and what's still pending."
Required setup (verify once per session)
- Work from the blog repo root. On this machine the expected repo is:
/Users/danb/Desktop/bogdanbaciu-dot-com. - Confirm the project scripts exist before invoking anything:
test -f scripts/gen_blog_image.py && test -f scripts/ingest_art.py
If either script is missing, stop and locate it with:
rg --files /Users/danb/Desktop | rg '(^|/)gen_blog_image\.py$|(^|/)ingest_art\.py$'
OPENAI_API_KEYis set in the shell that will run the script.pip show openaireturns a version (install withpip install --upgrade openaiif not).- Verify current OpenAI image-generation model names, tool syntax, pricing, size limits, and quality options from official OpenAI docs before quoting or relying on them. Do not treat model names or per-image costs in this skill as evergreen.
- Confirm estimated batch cost with the user before any
--allrun. ChatGPT subscriptions do not imply API credits.
Workflow
Step 1 — Check what's pending
python3 scripts/gen_blog_image.py --list
This prints one line per post showing whether it's already done (and where) or PENDING. Posts are considered done if EITHER of these exists:
priv/static/images/posts/<slug>/hero.png(script output, served)art/<slug-no-hyphens>.png(manual download — needs ingest)
Step 2 — If the user has manual downloads in art/, ingest first
python3 scripts/ingest_art.py --dry-run # preview
python3 scripts/ingest_art.py # do it
This copies art/<compact>.png → priv/static/images/posts/<slug>/hero.png
by matching the compact (no-hyphen) filename to a real post slug. Orphans
(filenames that don't map to a slug) are reported but left alone.
Step 3 — Smoke-test on one post (dry run, then real)
python3 scripts/gen_blog_image.py <slug> --dry-run # prints final prompt
python3 scripts/gen_blog_image.py <slug> # one real call (~$0.30)
Open the generated priv/static/images/posts/<slug>/hero.png and confirm
quality before any batch run.
Step 4 — Batch the rest
python3 scripts/gen_blog_image.py --all
Sequential, with SLEEP_BETWEEN_CALLS_SEC = 3 between calls. Already-done
slugs are skipped automatically. Failures are logged but don't halt the loop —
re-run to retry.
Step 5 — Verify rendering
Boot the server (mix phx.server) and visit /thoughts/<slug>. The hero
should appear between the title and the lede. If it doesn't:
- Check
Bogdan.Content.Post.detect_hero/1— it reads via:code.priv_dir(:bogdan), which requires a recompile after adding files (touchlib/bogdan/content.exor restart the server). - Confirm the file is at the exact path
priv/static/images/posts/<slug>/hero.png.
Step 6 — Prompt-quality and thumbnail review
Before accepting a generated image:
- The image matches the post's concrete subject, not just its mood.
- It is editorial and specific, not generic stock imagery.
- It has no fake screenshots, fake UI, fake charts, unreadable text, distorted hands/faces, or accidental brand/logo claims.
- It works as a crop at article-card size and as a full hero.
- It is visually distinct from nearby posts in the image inventory.
- The prompt mentions the one or two actual concepts the article is about and avoids stuffing in every paragraph of the post.
Output a compact review table:
| Slug | Thumbnail path | Pass/Revise | Reason | Follow-up |
|---|---|---|---|---|
<slug> | priv/static/images/posts/<slug>/hero.png | Pass | Specific to the article; clean crop | None |
If a thumbnail needs revision, keep the prior image until the replacement is generated and reviewed.
For batch review, build a local contact sheet from the blog repo root:
python3 /Users/danb/src/skills/blog-image-gen/scripts/thumbnail_contact_sheet.py --repo .
Open priv/static/images/posts/contact-sheet.html and scan for duplicate visual
ideas, awkward crops, distorted text, and subject drift before accepting a batch.
Step 7 — Commit
git add priv/static/images/posts/
git commit -m "blog: add hero images for <N> posts"
The PNGs are committed to the repo (Fly.io builds the release from this tree;
priv/static/ ships in the artifact).
Skill maintenance evals
When editing prompt parsing or review workflow text in this skill, run:
PYTHONDONTWRITEBYTECODE=1 python3 /Users/danb/src/skills/blog-image-gen/evals/prompt_fixture_eval.py
The fixture locks the contract that --dry-run output must expose the final
prompt as a fenced FINAL PROMPT block before any real API call is made.
Prompt sourcing
scripts/gen_blog_image.py resolves the prompt for each slug in this order:
- Pre-written: if
blog-image-prompts.mdhas a section## N. <slug> — ...with a fenced code block, that exact prompt is used. - Auto-generated: otherwise the script prepends the house-style preamble
to a one-liner derived from the post title + description (or first ~300
words of body if no description). The auto path is fine for batch but
pre-written prompts produce noticeably better results — encourage the user
to draft one in
blog-image-prompts.mdfor any post they care strongly about before running.
Tunable knobs (top of scripts/gen_blog_image.py)
Verify current allowed values in official OpenAI docs before changing these.
| Constant | Default | Notes |
|---|---|---|
MODEL | script-defined | Verify current image-generation model name |
SIZE | script-defined | Verify current allowed sizes and aspect limits |
QUALITY | script-defined | Verify current quality options |
OUTPUT_FORMAT | "png" | png / jpeg / webp (jpeg is faster) |
MODERATION | "auto" | auto / low |
SLEEP_BETWEEN_CALLS_SEC | 3 | Polite throttle for batch runs |
On "thinking" / reasoning mode
The script may use the Responses API with an image-generation tool and a separate planning model. Verify the current OpenAI API surface before editing this path; model names, tool payloads, and reasoning options are time-sensitive.
client.responses.create(
model="<current-planning-model>",
input=prompt,
tools=[{"type": "image_generation", "model": "<current-image-model>",
"size": "<verified-size>", "quality": "<verified-quality>",
"output_format": "webp", "moderation": "auto"}],
reasoning={"effort": "<verified-effort>"},
)
Image bytes come back as a base64 string on the image-generation output item. Do not downgrade to another API path without checking the project script and the current official docs.
What this skill does NOT do
- Does not edit blog post frontmatter — hero detection is automatic via filename.
- Does not push images to a CDN — they ship in the Phoenix release artifact.
- Does not retry failed API calls inline — re-run the command to retry skipped slugs.
- Does not write prompts for you when a pre-written one is desired; draft those in
blog-image-prompts.mdfirst.