zer0-image-generator 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e7f4e6c4643293895075982356663b1f49b405838640ccb8455db1b93060ce5a
4
+ data.tar.gz: 8708e0c991d7dd350c18ce958e3a4628393dc923c4594c42eded85b04d3839d9
5
+ SHA512:
6
+ metadata.gz: 1fffd493ddbf20127ef605568aa4ce7b04ad8861a6c4e579c257dd3c253df924f7cba9272a866c0885e49e662dec568e0d1bc9e92eb5c780bd6c9a686de76db6
7
+ data.tar.gz: 77a9cc97328ac34db10e7285eac3aa6a535ae3fdcbd0b8e481063da402575335c9c260009b1f81fb0adbaeb50177e9d8dc750c1435987b77df7563f864352a6a
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-07-13
9
+
10
+ ### Added
11
+
12
+ - Initial extraction of the AI preview-image engine from the
13
+ [zer0-mistakes](https://github.com/bamr87/zer0-mistakes) theme
14
+ (`scripts/lib/preview_generator.py`, PR bamr87/zer0-mistakes#296) as a
15
+ standalone, portable project.
16
+ - `zer0-image-generator` Ruby gem: a `jekyll preview-images` command
17
+ (`Jekyll::Command`) that launches the vendored single-file Python engine —
18
+ the Ruby layer is a thin argv translator so gem and standalone usage can
19
+ never drift.
20
+ - Universality knobs (all new, all with zer0-compatible defaults):
21
+ - `collections_dir` — honored from Jekyll's own top-level key, the
22
+ `preview_images:` block, or `--collections-dir` (the theme hardcoded
23
+ `pages/`).
24
+ - `front_matter_key` — read/write any key (`preview` default; `image` for
25
+ jekyll-seo-tag sites) via config or `--front-matter-key`.
26
+ - `authors_file` — per-author style overrides from any YAML path; empty
27
+ disables (was hardcoded `_data/authors.yml`).
28
+ - `collections: auto` — discover collections from Jekyll's `collections:`
29
+ map instead of listing them.
30
+ - Vendored Playwright SVG→PNG rasterizer helper next to the engine (gem
31
+ layout), keeping the rsvg → inkscape → magick → playwright chain intact.
32
+ - Unit suite (98 tests, zero network) plus a standard-layout fixture site
33
+ exercised end-to-end in CI with the free `local` provider.
34
+
35
+ ### Changed
36
+
37
+ - Default `collections` is now `[posts]` (universal) instead of the theme's
38
+ `[posts, quickstart, docs]` — zer0-mistakes lists its collections
39
+ explicitly, so theme behavior is unchanged when it adopts this engine.
40
+ - `resolve_settings(args, config)` now takes the FULL `_config.yml` dict
41
+ (it extracts the `preview_images:` block itself) so top-level Jekyll keys
42
+ (`collections_dir`, `collections`) can participate in resolution.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Amr Abdel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # zer0-image-generator
2
+
3
+ [![Test](https://github.com/bamr87/zer0-image-generator/actions/workflows/test.yml/badge.svg)](https://github.com/bamr87/zer0-image-generator/actions/workflows/test.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+
6
+ AI preview/social images for **any Jekyll site** — Claude directs and reviews,
7
+ an image model renders, and your front matter gets wired up automatically.
8
+
9
+ Extracted from the [zer0-mistakes](https://github.com/bamr87/zer0-mistakes)
10
+ theme's consolidated engine and generalized: every theme-specific assumption
11
+ is now a config knob with zer0-compatible defaults, so the theme and this
12
+ plugin stay **separate but portable**.
13
+
14
+ ## How it works
15
+
16
+ Each post/page without a preview goes through a three-stage pipeline:
17
+
18
+ | stage | role | engine / credential |
19
+ | ------- | ----------------------------------------------------------- | -------------------------------------- |
20
+ | analyze | Claude reads the article and writes a vivid art brief | Claude credential chain (see below) |
21
+ | produce | an image model renders the brief | the selected provider |
22
+ | review | Claude inspects the render (vision); one refined regen max | same Claude credential chain |
23
+
24
+ | provider | renderer | credential |
25
+ | ----------- | ------------------------------------------ | ------------------- |
26
+ | **openai** (default) | gpt-image-2 / dall-e-3 (+ `--enhance`) | `OPENAI_API_KEY` |
27
+ | xai | grok-2-image | `XAI_API_KEY` |
28
+ | stability | Stable Diffusion XL | `STABILITY_API_KEY` |
29
+ | gemini | gemini-2.5-flash-image | `GEMINI_API_KEY` |
30
+ | local | deterministic template SVG → PNG | none (CI-safe) |
31
+
32
+ Claude never renders pixels (the Anthropic API has no image endpoint); without
33
+ a Claude credential the analyze/review stages degrade gracefully to a template
34
+ prompt and the renderer still runs.
35
+
36
+ ## Install
37
+
38
+ ### As a Jekyll plugin (recommended)
39
+
40
+ ```ruby
41
+ # Gemfile
42
+ group :jekyll_plugins do
43
+ gem "zer0-image-generator"
44
+ end
45
+ ```
46
+
47
+ Requires `python3` (3.9+) with PyYAML — the command checks and tells you if
48
+ either is missing:
49
+
50
+ ```bash
51
+ python3 -m pip install pyyaml
52
+ ```
53
+
54
+ Then, from your site root:
55
+
56
+ ```bash
57
+ bundle exec jekyll preview-images --list-missing # what needs a banner?
58
+ bundle exec jekyll preview-images --dry-run # what would be generated?
59
+ bundle exec jekyll preview-images # generate them
60
+ ```
61
+
62
+ ### Standalone (no Ruby required)
63
+
64
+ The engine is one dependency-light Python file. Copy it anywhere and run it
65
+ from your site root:
66
+
67
+ ```bash
68
+ curl -o preview_generator.py \
69
+ https://raw.githubusercontent.com/bamr87/zer0-image-generator/main/lib/zer0_image_generator/preview_generator.py
70
+ python3 preview_generator.py --dry-run
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ Everything lives under `preview_images:` in your `_config.yml`. Shown with
76
+ defaults:
77
+
78
+ ```yaml
79
+ preview_images:
80
+ enabled: true
81
+ provider: openai # openai | xai | stability | gemini | local
82
+ model: '' # empty = provider default (gpt-image-2, ...)
83
+ size: 1536x1024
84
+ quality: auto # auto (GPT Image) | standard/hd (DALL-E 3)
85
+ style: "retro pixel art, 8-bit video game aesthetic, vibrant colors"
86
+ style_modifiers: "pixelated, retro gaming style, CRT screen glow effect"
87
+ output_dir: assets/images/previews
88
+ prompt_engine: claude # claude analyzes each article | template
89
+ review_engine: claude # claude vision-reviews each render | none
90
+ # --- portability knobs (the zer0-isms, now configurable) ---
91
+ collections: [posts] # list of collections, or 'auto' to discover
92
+ # them from Jekyll's own `collections:` map
93
+ collections_dir: '' # falls back to Jekyll's top-level collections_dir
94
+ front_matter_key: preview # key to read/write (jekyll-seo-tag sites: image)
95
+ authors_file: _data/authors.yml # per-author style overrides ('' disables)
96
+ assets_prefix: /assets # written paths omit this prefix ...
97
+ auto_prefix: true # ... and it is re-added for existence checks
98
+ ```
99
+
100
+ Priority per file: author overrides → CLI flags → environment variables →
101
+ `_config.yml` → built-in defaults.
102
+
103
+ ### Recipe: standard Jekyll site with jekyll-seo-tag
104
+
105
+ ```yaml
106
+ preview_images:
107
+ provider: openai
108
+ collections: auto
109
+ front_matter_key: image # jekyll-seo-tag reads page.image
110
+ assets_prefix: '' # write full site-absolute paths
111
+ authors_file: ''
112
+ ```
113
+
114
+ Generated front matter: `image: /assets/images/previews/my-post.png` — exactly
115
+ what `jekyll-seo-tag` turns into `og:image`.
116
+
117
+ ### Recipe: zer0-mistakes theme
118
+
119
+ ```yaml
120
+ preview_images:
121
+ provider: openai
122
+ collections: [posts, docs, quickstart]
123
+ # collections_dir comes from the theme's own `collections_dir: pages`
124
+ # front_matter_key/preview + /assets prefixing are already the defaults
125
+ ```
126
+
127
+ ### Per-author style overrides
128
+
129
+ If `authors_file` points at a YAML map, an author's `preview:` block overrides
130
+ `style`, `style_modifiers`, `size`, `quality`, and `model` for their pages:
131
+
132
+ ```yaml
133
+ # _data/authors.yml
134
+ cassandra:
135
+ preview:
136
+ style: >-
137
+ noir graphic-novel, high-contrast ink
138
+ ```
139
+
140
+ ## Credentials
141
+
142
+ The renderer needs its own key (default: `OPENAI_API_KEY`). Claude
143
+ orchestration accepts any ONE of, in order — it is optional:
144
+
145
+ ```bash
146
+ export CLAUDE_CODE_OAUTH_TOKEN="..." # 1. from `claude setup-token` (Pro/Max)
147
+ export ANTHROPIC_AUTH_TOKEN="..." # 2. short-lived bearer token
148
+ export ANTHROPIC_API_KEY="..." # 3. console.anthropic.com
149
+ # 4. nothing — a logged-in `claude` CLI is used automatically
150
+ ```
151
+
152
+ Keys are read from the environment or a git-ignored `.env` at the site root.
153
+ On a Claude Pro/Max subscription the orchestration costs nothing extra; only
154
+ the renderer bills per image.
155
+
156
+ ## CLI reference
157
+
158
+ `jekyll preview-images` exposes the engine's flags (Jekyll claims `-s/-d/-p`
159
+ globally, so use long forms there; the Python CLI keeps all short flags):
160
+
161
+ ```text
162
+ --dry-run --verbose --force --list-missing
163
+ -f/--file FILE --collection NAME|all --provider NAME
164
+ --model NAME --output-dir DIR -j/--parallel N
165
+ --style TEXT --prompt-engine claude|template
166
+ --review claude|none --rasterizer auto|rsvg|inkscape|magick|playwright|none
167
+ -e/--enhance --enhance-prompt/-model/-quality/-fidelity/-format
168
+ --collections-dir DIR --front-matter-key KEY --authors-file FILE
169
+ --assets-prefix PREFIX --no-auto-prefix --batch N --log-file FILE
170
+ ```
171
+
172
+ The `local` provider rasterizes its SVG via the first available of
173
+ `rsvg-convert` (`brew install librsvg` / `apt install librsvg2-bin`),
174
+ `inkscape`, ImageMagick, or a vendored Playwright helper.
175
+
176
+ ## CI usage
177
+
178
+ ```yaml
179
+ - name: Generate preview images
180
+ env:
181
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
182
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
183
+ run: bundle exec jekyll preview-images
184
+ ```
185
+
186
+ Generation is script-driven on demand — it is never part of `jekyll build`
187
+ (builds stay fast, deterministic, and secret-free; GitHub Pages safe mode is
188
+ irrelevant to it).
189
+
190
+ ## Relationship to zer0-mistakes
191
+
192
+ The [zer0-mistakes](https://github.com/bamr87/zer0-mistakes) theme renders
193
+ `preview:` values with a pure-Liquid include (GitHub-Pages-safe, no plugin
194
+ needed) and currently vendors this engine at `scripts/lib/preview_generator.py`.
195
+ This repo is the portable home: the theme is expected to consume the gem (or
196
+ curl the engine from here) in a follow-up, so there is exactly ONE engine.
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ python3 test/test_preview_generator.py # 98 unit tests, zero network
202
+ python3 -m py_compile lib/zer0_image_generator/preview_generator.py
203
+ gem build zer0-image-generator.gemspec
204
+ cd test/fixture-site && bundle install
205
+ bundle exec jekyll preview-images --dry-run # integration fixture
206
+ ```
207
+
208
+ Conventional Commits drive releases via release-please. One concern per PR.
209
+
210
+ ## License
211
+
212
+ [MIT](LICENSE) © Amr Abdel
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point required by Bundler's :jekyll_plugins group. Registers the
4
+ # `jekyll preview-images` command when Jekyll is present; outside Jekyll the
5
+ # gem is inert (the Python engine can always be run directly:
6
+ # python3 lib/zer0_image_generator/preview_generator.py).
7
+ require_relative "zer0_image_generator/version"
8
+
9
+ begin
10
+ require "jekyll"
11
+ rescue LoadError
12
+ # Standalone/non-Jekyll context — nothing to register.
13
+ end
14
+
15
+ require_relative "zer0_image_generator/command" if defined?(Jekyll::Command)
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zer0ImageGenerator
4
+ # `jekyll preview-images` — a thin launcher for the vendored single-file
5
+ # Python engine (preview_generator.py, same directory). The Ruby layer only
6
+ # translates Mercenary options back into the engine's argv and execs
7
+ # python3: all behavior (config resolution, credentials, providers, front
8
+ # matter) lives in the engine so the gem and the standalone file can never
9
+ # drift.
10
+ #
11
+ # Jekyll's global switches already claim -s/-d/-p/-v at the program level,
12
+ # so conflicting engine shorts are exposed long-form here; the engine's own
13
+ # CLI keeps the full short surface for direct `python3` use.
14
+ class Command < Jekyll::Command
15
+ ENGINE = File.expand_path("preview_generator.py", __dir__)
16
+
17
+ # [engine flag, mercenary args] — booleans pass bare, values pass through.
18
+ SWITCHES = [
19
+ ["--dry-run", [:dry_run, "--dry-run", "Preview what would be generated (no changes)"]],
20
+ ["--verbose", [:verbose, "--verbose", "Enable verbose output"]],
21
+ ["--file", [:file, "-f", "--file FILE", "Process a specific file only"]],
22
+ ["--collection", [:collection, "--collection NAME", "Process one collection, or 'all'"]],
23
+ ["--provider", [:provider, "--provider NAME", "Renderer: openai, xai, stability, gemini, local"]],
24
+ ["--model", [:model, "--model NAME", "Override the provider's image model"]],
25
+ ["--output-dir", [:output_dir, "--output-dir DIR", "Image output directory"]],
26
+ ["--force", [:force, "--force", "Regenerate even when a preview exists"]],
27
+ ["--list-missing", [:list_missing, "--list-missing", "Only list files with missing previews"]],
28
+ ["--parallel", [:parallel, "-j", "--parallel N", "Concurrent workers (default 4)"]],
29
+ ["--enhance", [:enhance, "-e", "--enhance", "Enhance existing images (OpenAI images/edits)"]],
30
+ ["--enhance-prompt", [:enhance_prompt, "--enhance-prompt TEXT", "Custom enhancement prompt (implies --enhance)"]],
31
+ ["--enhance-model", [:enhance_model, "--enhance-model NAME", "Enhancement model (default: gpt-image-2)"]],
32
+ ["--enhance-quality", [:enhance_quality, "--enhance-quality Q", "low|medium|high|auto"]],
33
+ ["--enhance-fidelity", [:enhance_fidelity, "--enhance-fidelity F", "high|low (implies --enhance)"]],
34
+ ["--enhance-format", [:enhance_format, "--enhance-format FMT", "png|jpeg|webp (implies --enhance)"]],
35
+ ["--prompt-engine", [:prompt_engine, "--prompt-engine ENGINE", "claude (analyzes the article) or template"]],
36
+ ["--review", [:review, "--review ENGINE", "claude (vision-reviews the render) or none"]],
37
+ ["--rasterizer", [:rasterizer, "--rasterizer TOOL", "auto|rsvg|inkscape|magick|playwright|none"]],
38
+ ["--style", [:style, "--style TEXT", "Override the image style prompt"]],
39
+ ["--assets-prefix", [:assets_prefix, "--assets-prefix PREFIX", "Assets prefix for path normalization"]],
40
+ ["--no-auto-prefix", [:no_auto_prefix, "--no-auto-prefix", "Disable automatic assets-prefix prepending"]],
41
+ ["--batch", [:batch, "--batch N", "Limit number of files processed"]],
42
+ ["--log-file", [:log_file, "--log-file FILE", "Also write log output to a file"]],
43
+ ["--collections-dir", [:collections_dir, "--collections-dir DIR", "Directory holding _<collection> dirs"]],
44
+ ["--front-matter-key", [:front_matter_key, "--front-matter-key KEY", "Front-matter key (default preview; seo-tag sites: image)"]],
45
+ ["--authors-file", [:authors_file, "--authors-file FILE", "Author-overrides YAML ('' disables)"]],
46
+ ].freeze
47
+
48
+ BOOLEANS = %w[--dry-run --verbose --force --list-missing --enhance --no-auto-prefix].freeze
49
+
50
+ class << self
51
+ def init_with_program(prog)
52
+ prog.command(:"preview-images") do |c|
53
+ c.syntax "preview-images [options]"
54
+ c.description "Generate AI preview/social images (Claude directs & reviews; " \
55
+ "OpenAI/xAI/Stability/Gemini/local renders)"
56
+ SWITCHES.each { |_flag, merc| c.option(*merc) }
57
+
58
+ c.action do |_args, options|
59
+ new.run(options)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def run(options)
66
+ python = ENV["PYTHON"] || "python3"
67
+ ensure_python!(python)
68
+ argv = build_argv(options)
69
+ Jekyll.logger.info "preview-images:", "#{python} #{ENGINE} #{argv.join(' ')}"
70
+ exec(python, ENGINE, *argv)
71
+ end
72
+
73
+ def build_argv(options)
74
+ SWITCHES.each_with_object([]) do |(flag, merc), argv|
75
+ value = options[merc.first.to_s]
76
+ next if value.nil? || value == false
77
+
78
+ if BOOLEANS.include?(flag)
79
+ argv << flag
80
+ else
81
+ argv << flag << value.to_s
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def ensure_python!(python)
89
+ unless command_available?(python)
90
+ Jekyll.logger.abort_with "preview-images:",
91
+ "#{python} not found — install Python 3.9+ " \
92
+ "(or set PYTHON=/path/to/python3)."
93
+ end
94
+ return if system(python, "-c", "import yaml", out: File::NULL, err: File::NULL)
95
+
96
+ Jekyll.logger.abort_with "preview-images:",
97
+ "PyYAML is missing — run: #{python} -m pip install pyyaml"
98
+ end
99
+
100
+ def command_available?(cmd)
101
+ system(cmd, "--version", out: File::NULL, err: File::NULL)
102
+ rescue SystemCallError
103
+ false
104
+ end
105
+ end
106
+ end