jekyll-theme-zer0 1.26.0 → 1.27.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 +4 -4
- data/CHANGELOG.md +172 -1
- data/README.md +10 -27
- data/_data/authors.yml +4 -3
- data/_data/backlog.yml +28 -0
- data/_data/features.yml +65 -18
- data/_data/i18n/languages.yml +36 -0
- data/_data/theme-manifest.yml +0 -2
- data/_data/ui-text.yml +36 -246
- data/_includes/README.md +4 -0
- data/_includes/components/author-avatar-url.html +4 -2
- data/_includes/components/env-switcher.html +3 -1
- data/_includes/components/language-toggle.html +81 -0
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +1 -1
- data/_includes/components/translation-notice.html +27 -0
- data/_includes/content/intro.html +16 -15
- data/_includes/core/footer.html +9 -4
- data/_includes/core/head.html +9 -0
- data/_includes/core/header.html +9 -4
- data/_includes/core/hreflang.html +33 -0
- data/_includes/core/i18n.html +36 -0
- data/_includes/navigation/breadcrumbs.html +1 -1
- data/_includes/navigation/navbar.html +5 -4
- data/_includes/navigation/sidebar-right.html +3 -2
- data/_includes/navigation/unified-drawer.html +1 -1
- data/_layouts/article.html +7 -1
- data/_layouts/default.html +5 -3
- data/_layouts/news.html +4 -2
- data/_layouts/root.html +14 -8
- data/_layouts/section.html +4 -2
- data/_sass/core/_obsidian.scss +9 -1
- data/_sass/layouts/_navbar-extras.scss +6 -1
- data/assets/js/obsidian-graph.js +5 -1
- data/scripts/README.md +20 -26
- data/scripts/bin/audit-consumer +1 -1
- data/scripts/bin/manifest +0 -1
- data/scripts/bin/sync-plugins +0 -1
- data/scripts/dev/rasterize-svg.js +65 -0
- data/scripts/features/generate-preview-images +49 -1390
- data/scripts/features/install-preview-generator +55 -33
- data/scripts/install/README.md +9 -20
- data/scripts/install/ai/prompts/wizard.system.md +8 -17
- data/scripts/lib/README.md +1 -5
- data/scripts/lib/install/deploy/README.md +3 -9
- data/scripts/lib/preview_generator.py +2261 -1341
- data/scripts/translate.rb +1114 -0
- metadata +9 -3
- data/_plugins/preview_image_generator.rb +0 -351
|
@@ -1,1404 +1,63 @@
|
|
|
1
|
-
#!/bin/bash
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Features: ZER0-004, ZER0-028
|
|
2
3
|
#
|
|
3
4
|
# Script Name: generate-preview-images
|
|
4
|
-
# Description: AI-powered preview image generator for Jekyll posts/articles
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Description: AI-powered preview image generator for Jekyll posts/articles.
|
|
6
|
+
# Thin wrapper — ALL logic lives in the consolidated Python engine
|
|
7
|
+
# at scripts/lib/preview_generator.py. Claude ORCHESTRATES
|
|
8
|
+
# (analyzes the article into an art brief, reviews the render);
|
|
9
|
+
# a raster model RENDERS (openai [default], xai, stability,
|
|
10
|
+
# gemini, or the offline local template).
|
|
7
11
|
#
|
|
8
12
|
# Usage: ./scripts/features/generate-preview-images [options]
|
|
13
|
+
# Run with --help for the full option list (rendered by the engine).
|
|
9
14
|
#
|
|
10
|
-
#
|
|
11
|
-
# -
|
|
12
|
-
# -
|
|
13
|
-
# -
|
|
14
|
-
# -f
|
|
15
|
-
# -
|
|
16
|
-
# -p, --provider PROVIDER AI provider (openai, stability, local)
|
|
17
|
-
# --output-dir DIR Output directory for images (default: assets/images/previews)
|
|
18
|
-
# --force Regenerate images even if preview exists
|
|
19
|
-
# --list-missing Only list files with missing previews
|
|
15
|
+
# Common examples:
|
|
16
|
+
# ./scripts/generate-preview-images.sh --list-missing
|
|
17
|
+
# ./scripts/generate-preview-images.sh --dry-run --verbose
|
|
18
|
+
# ./scripts/generate-preview-images.sh --collection posts
|
|
19
|
+
# ./scripts/generate-preview-images.sh -f pages/_posts/my-post.md --force
|
|
20
|
+
# ./scripts/generate-preview-images.sh --provider openai --enhance -f <file>
|
|
20
21
|
#
|
|
21
22
|
# Dependencies:
|
|
22
|
-
# -
|
|
23
|
-
# -
|
|
24
|
-
#
|
|
25
|
-
# - yq or python (for YAML parsing)
|
|
26
|
-
#
|
|
27
|
-
# Environment Variables:
|
|
28
|
-
# OPENAI_API_KEY OpenAI API key for DALL-E image generation
|
|
29
|
-
# STABILITY_API_KEY Stability AI API key for Stable Diffusion
|
|
30
|
-
# IMAGE_STYLE Default image style (default: "digital art, professional")
|
|
31
|
-
# IMAGE_SIZE Image dimensions (default: "1024x1024")
|
|
32
|
-
#
|
|
33
|
-
# Examples:
|
|
34
|
-
# ./scripts/generate-preview-images.sh --dry-run
|
|
35
|
-
# ./scripts/generate-preview-images.sh --collection posts
|
|
36
|
-
# ./scripts/generate-preview-images.sh --file pages/_posts/my-post.md
|
|
37
|
-
# ./scripts/generate-preview-images.sh --provider openai --verbose
|
|
23
|
+
# - python3 (3.9+) with PyYAML
|
|
24
|
+
# - Optional SVG rasterizers for the local template provider:
|
|
25
|
+
# rsvg-convert | inkscape | magick | Playwright (scripts/dev/rasterize-svg.js)
|
|
38
26
|
#
|
|
27
|
+
# Environment — renderer key (default openai): OPENAI_API_KEY (or XAI_API_KEY /
|
|
28
|
+
# STABILITY_API_KEY / GEMINI_API_KEY for the matching --provider). Claude
|
|
29
|
+
# orchestration additionally uses any ONE of (optional; degrades to template):
|
|
30
|
+
# CLAUDE_CODE_OAUTH_TOKEN `claude setup-token` (Claude Pro/Max)
|
|
31
|
+
# ANTHROPIC_AUTH_TOKEN short-lived Bearer token
|
|
32
|
+
# ANTHROPIC_API_KEY console.anthropic.com API key
|
|
33
|
+
# (or a logged-in `claude` CLI — used automatically)
|
|
34
|
+
# .env in the project root is loaded by the engine (exported vars win).
|
|
39
35
|
|
|
40
36
|
set -euo pipefail
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export "$key=$value"
|
|
59
|
-
done < "$PROJECT_ROOT/.env"
|
|
37
|
+
IFS=$'\n\t'
|
|
38
|
+
|
|
39
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
|
40
|
+
|
|
41
|
+
# Engine location — theme layout (scripts/features/ → ../lib/) first, then the
|
|
42
|
+
# consumer-install layout (wrapper at scripts/ → ./lib/).
|
|
43
|
+
ENGINE=""
|
|
44
|
+
for candidate in "$SCRIPT_DIR/../lib/preview_generator.py" "$SCRIPT_DIR/lib/preview_generator.py"; do
|
|
45
|
+
if [[ -f "$candidate" ]]; then
|
|
46
|
+
ENGINE="$candidate"
|
|
47
|
+
break
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
if [[ -z "$ENGINE" ]]; then
|
|
51
|
+
echo "[ERROR] preview_generator.py not found next to $SCRIPT_DIR" >&2
|
|
52
|
+
echo " Expected at scripts/lib/preview_generator.py" >&2
|
|
53
|
+
exit 1
|
|
60
54
|
fi
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
else
|
|
66
|
-
# Fallback logging functions
|
|
67
|
-
RED='\033[0;31m'
|
|
68
|
-
GREEN='\033[0;32m'
|
|
69
|
-
YELLOW='\033[1;33m'
|
|
70
|
-
BLUE='\033[0;34m'
|
|
71
|
-
CYAN='\033[0;36m'
|
|
72
|
-
PURPLE='\033[0;35m'
|
|
73
|
-
NC='\033[0m'
|
|
74
|
-
|
|
75
|
-
log() { echo -e "${GREEN}[LOG]${NC} $1"; }
|
|
76
|
-
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
77
|
-
step() { echo -e "${CYAN}[STEP]${NC} $1"; }
|
|
78
|
-
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
79
|
-
warn() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
|
80
|
-
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
|
|
81
|
-
debug() { [[ "${VERBOSE:-false}" == "true" ]] && echo -e "${PURPLE}[DEBUG]${NC} $1" >&2 || true; }
|
|
82
|
-
print_header() {
|
|
83
|
-
echo ""
|
|
84
|
-
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
|
85
|
-
echo -e " ${GREEN}$1${NC}"
|
|
86
|
-
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
|
87
|
-
echo ""
|
|
88
|
-
}
|
|
56
|
+
if ! command -v python3 &>/dev/null; then
|
|
57
|
+
echo "[ERROR] python3 is required. Install it (macOS: brew install python3; Debian/Ubuntu: apt-get install python3)." >&2
|
|
58
|
+
exit 1
|
|
89
59
|
fi
|
|
90
60
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
# Priority (per file): author preview overrides (_data/authors.yml) >
|
|
95
|
-
# CLI args > Environment variables > _config.yml > Defaults
|
|
96
|
-
# A post's `author:` may point at an entry in _data/authors.yml that carries a
|
|
97
|
-
# `preview:` block (style / style_modifiers / size / quality / model). Those win
|
|
98
|
-
# over the site-wide preview_images config for that post's banner, giving each
|
|
99
|
-
# AI author persona a distinct, recognisable art style.
|
|
100
|
-
|
|
101
|
-
CONFIG_FILE="$PROJECT_ROOT/_config.yml"
|
|
102
|
-
AUTHORS_FILE="$PROJECT_ROOT/_data/authors.yml"
|
|
103
|
-
|
|
104
|
-
# Function to read config value from _config.yml using grep (handles YAML anchors)
|
|
105
|
-
read_config() {
|
|
106
|
-
local key="$1"
|
|
107
|
-
local default="$2"
|
|
108
|
-
|
|
109
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
110
|
-
# Use grep to find the key under preview_images section
|
|
111
|
-
# This is more robust than yq for files with anchors
|
|
112
|
-
local in_section=false
|
|
113
|
-
local value=""
|
|
114
|
-
|
|
115
|
-
while IFS= read -r line; do
|
|
116
|
-
# Check if we're entering the preview_images section
|
|
117
|
-
if [[ "$line" =~ ^preview_images: ]]; then
|
|
118
|
-
in_section=true
|
|
119
|
-
continue
|
|
120
|
-
fi
|
|
121
|
-
|
|
122
|
-
# Check if we're leaving the section (new top-level key)
|
|
123
|
-
if [[ "$in_section" == true && "$line" =~ ^[a-zA-Z_]+: && ! "$line" =~ ^[[:space:]] ]]; then
|
|
124
|
-
break
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
# Look for the key within the section
|
|
128
|
-
if [[ "$in_section" == true && "$line" =~ ^[[:space:]]+${key}[[:space:]]*:[[:space:]]*(.*) ]]; then
|
|
129
|
-
value="${BASH_REMATCH[1]}"
|
|
130
|
-
# First, remove inline comments (only if not inside quotes)
|
|
131
|
-
# Simple approach: if value starts with quote, find closing quote
|
|
132
|
-
if [[ "$value" =~ ^\'([^\']*)\' ]]; then
|
|
133
|
-
value="${BASH_REMATCH[1]}"
|
|
134
|
-
elif [[ "$value" =~ ^\"([^\"]*)\" ]]; then
|
|
135
|
-
value="${BASH_REMATCH[1]}"
|
|
136
|
-
else
|
|
137
|
-
# No quotes, just trim and remove comment
|
|
138
|
-
value="${value%%#*}"
|
|
139
|
-
# Trim whitespace
|
|
140
|
-
value="${value%"${value##*[![:space:]]}"}"
|
|
141
|
-
value="${value#"${value%%[![:space:]]*}"}"
|
|
142
|
-
fi
|
|
143
|
-
if [[ -n "$value" ]]; then
|
|
144
|
-
echo "$value"
|
|
145
|
-
return
|
|
146
|
-
fi
|
|
147
|
-
fi
|
|
148
|
-
done < "$CONFIG_FILE"
|
|
149
|
-
fi
|
|
150
|
-
echo "$default"
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
# Load defaults from _config.yml, with fallbacks
|
|
154
|
-
CONFIG_PROVIDER=$(read_config "provider" "openai")
|
|
155
|
-
CONFIG_MODEL=$(read_config "model" "gpt-image-2")
|
|
156
|
-
CONFIG_SIZE=$(read_config "size" "1536x1024")
|
|
157
|
-
CONFIG_QUALITY=$(read_config "quality" "auto")
|
|
158
|
-
CONFIG_STYLE=$(read_config "style" "retro pixel art, 8-bit video game aesthetic, vibrant colors, nostalgic, clean pixel graphics")
|
|
159
|
-
CONFIG_STYLE_MODIFIERS=$(read_config "style_modifiers" "pixelated, retro gaming style, CRT screen glow effect, limited color palette")
|
|
160
|
-
CONFIG_OUTPUT_DIR=$(read_config "output_dir" "assets/images/previews")
|
|
161
|
-
|
|
162
|
-
# Default configuration (env vars override config file)
|
|
163
|
-
DRY_RUN="${DRY_RUN:-false}"
|
|
164
|
-
VERBOSE="${VERBOSE:-false}"
|
|
165
|
-
FORCE="${FORCE:-false}"
|
|
166
|
-
LIST_ONLY="${LIST_ONLY:-false}"
|
|
167
|
-
SPECIFIC_FILE=""
|
|
168
|
-
COLLECTION=""
|
|
169
|
-
AI_PROVIDER="${AI_PROVIDER:-$CONFIG_PROVIDER}"
|
|
170
|
-
OUTPUT_DIR="${OUTPUT_DIR:-$CONFIG_OUTPUT_DIR}"
|
|
171
|
-
IMAGE_STYLE="${IMAGE_STYLE:-$CONFIG_STYLE}"
|
|
172
|
-
IMAGE_STYLE_MODIFIERS="${IMAGE_STYLE_MODIFIERS:-$CONFIG_STYLE_MODIFIERS}"
|
|
173
|
-
IMAGE_SIZE="${IMAGE_SIZE:-$CONFIG_SIZE}"
|
|
174
|
-
IMAGE_QUALITY="${IMAGE_QUALITY:-$CONFIG_QUALITY}"
|
|
175
|
-
IMAGE_MODEL="${IMAGE_MODEL:-$CONFIG_MODEL}"
|
|
176
|
-
|
|
177
|
-
# Enhancement configuration
|
|
178
|
-
ENHANCE="${ENHANCE:-false}"
|
|
179
|
-
ENHANCE_PROMPT=""
|
|
180
|
-
ENHANCE_MODEL="${ENHANCE_MODEL:-gpt-image-2}"
|
|
181
|
-
ENHANCE_QUALITY="${ENHANCE_QUALITY:-auto}"
|
|
182
|
-
ENHANCE_FIDELITY="${ENHANCE_FIDELITY:-high}"
|
|
183
|
-
ENHANCE_FORMAT="${ENHANCE_FORMAT:-png}"
|
|
184
|
-
DEFAULT_ENHANCE_PROMPT="Improve this preview banner image: fix any misspelled, garbled, or incorrect text so it reads clearly and accurately. Sharpen visual details and improve composition while preserving the original art style, color palette, and theme. Ensure the image is clean and professional."
|
|
185
|
-
|
|
186
|
-
# Counters (used in serial mode; replaced by file-based counters in parallel mode)
|
|
187
|
-
PROCESSED=0
|
|
188
|
-
GENERATED=0
|
|
189
|
-
ENHANCED=0
|
|
190
|
-
SKIPPED=0
|
|
191
|
-
ERRORS=0
|
|
192
|
-
|
|
193
|
-
# Parallel processing — number of concurrent API workers (1 = serial)
|
|
194
|
-
MAX_PARALLEL="${MAX_PARALLEL:-4}"
|
|
195
|
-
COUNTER_DIR="" # Initialised in main() for parallel generation
|
|
196
|
-
|
|
197
|
-
# Temporary files may contain request payloads or auth headers. Keep a registry so
|
|
198
|
-
# interrupted runs clean them up before exiting.
|
|
199
|
-
TEMP_FILES=()
|
|
200
|
-
|
|
201
|
-
cleanup_temp_files() {
|
|
202
|
-
if [[ ${#TEMP_FILES[@]} -gt 0 ]]; then
|
|
203
|
-
rm -f "${TEMP_FILES[@]}" 2>/dev/null || true
|
|
204
|
-
fi
|
|
205
|
-
# Clean up counter directory if we own it
|
|
206
|
-
if [[ -n "$COUNTER_DIR" && -d "$COUNTER_DIR" ]]; then
|
|
207
|
-
rm -rf "$COUNTER_DIR" 2>/dev/null || true
|
|
208
|
-
fi
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
trap cleanup_temp_files EXIT HUP INT TERM
|
|
212
|
-
|
|
213
|
-
# ---------------------------------------------------------------------------
|
|
214
|
-
# Parallel-safe counter helpers
|
|
215
|
-
# Each background worker subshell writes a unique sentinel file; the parent
|
|
216
|
-
# aggregates the counts with ls | wc -l (no shared variable races).
|
|
217
|
-
# ---------------------------------------------------------------------------
|
|
218
|
-
init_counters() {
|
|
219
|
-
COUNTER_DIR=$(mktemp -d)
|
|
220
|
-
mkdir -p "$COUNTER_DIR"/{processed,generated,enhanced,skipped,errors}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
inc_counter() {
|
|
224
|
-
local name="$1"
|
|
225
|
-
if [[ -n "$COUNTER_DIR" && -d "$COUNTER_DIR/$name" ]]; then
|
|
226
|
-
mktemp "$COUNTER_DIR/$name/XXXXXX" >/dev/null 2>&1 || true
|
|
227
|
-
else
|
|
228
|
-
# Serial fallback — update parent variables directly
|
|
229
|
-
case "$name" in
|
|
230
|
-
processed) PROCESSED=$((PROCESSED + 1)) ;;
|
|
231
|
-
generated) GENERATED=$((GENERATED + 1)) ;;
|
|
232
|
-
enhanced) ENHANCED=$((ENHANCED + 1)) ;;
|
|
233
|
-
skipped) SKIPPED=$((SKIPPED + 1)) ;;
|
|
234
|
-
errors) ERRORS=$((ERRORS + 1)) ;;
|
|
235
|
-
esac
|
|
236
|
-
fi
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
get_counter() {
|
|
240
|
-
local name="$1"
|
|
241
|
-
if [[ -n "$COUNTER_DIR" && -d "$COUNTER_DIR/$name" ]]; then
|
|
242
|
-
ls "$COUNTER_DIR/$name/" 2>/dev/null | wc -l | tr -d ' '
|
|
243
|
-
else
|
|
244
|
-
case "$name" in
|
|
245
|
-
processed) echo "$PROCESSED" ;;
|
|
246
|
-
generated) echo "$GENERATED" ;;
|
|
247
|
-
enhanced) echo "$ENHANCED" ;;
|
|
248
|
-
skipped) echo "$SKIPPED" ;;
|
|
249
|
-
errors) echo "$ERRORS" ;;
|
|
250
|
-
esac
|
|
251
|
-
fi
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
# Print usage
|
|
255
|
-
show_help() {
|
|
256
|
-
cat << 'EOF'
|
|
257
|
-
Usage: generate-preview-images.sh [OPTIONS]
|
|
258
|
-
|
|
259
|
-
AI-powered preview image generator for Jekyll posts and content.
|
|
260
|
-
|
|
261
|
-
OPTIONS:
|
|
262
|
-
-h, --help Show this help message
|
|
263
|
-
-d, --dry-run Preview what would be generated (no changes)
|
|
264
|
-
-v, --verbose Enable verbose output
|
|
265
|
-
-f, --file FILE Process a specific file only
|
|
266
|
-
-c, --collection NAME Process specific collection (posts, quickstart, docs)
|
|
267
|
-
-p, --provider PROVIDER AI provider: openai, stability, local (default: openai)
|
|
268
|
-
-e, --enhance Enhance/improve existing preview images using AI
|
|
269
|
-
--enhance-prompt TEXT Custom enhancement prompt (implies --enhance)
|
|
270
|
-
--enhance-model MODEL Model for enhancement (default: gpt-image-2)
|
|
271
|
-
--enhance-fidelity VAL Input fidelity: high or low (default: high)
|
|
272
|
-
--enhance-format FMT Output format: png, jpeg, webp (default: png)
|
|
273
|
-
--output-dir DIR Output directory for images (default: assets/images/previews)
|
|
274
|
-
--force Regenerate images even if preview exists
|
|
275
|
-
--list-missing Only list files with missing previews
|
|
276
|
-
|
|
277
|
-
ENVIRONMENT VARIABLES:
|
|
278
|
-
OPENAI_API_KEY Required for OpenAI DALL-E provider
|
|
279
|
-
STABILITY_API_KEY Required for Stability AI provider
|
|
280
|
-
IMAGE_STYLE Override style from _config.yml
|
|
281
|
-
IMAGE_SIZE Override size (default: 1536x1024 landscape)
|
|
282
|
-
IMAGE_MODEL OpenAI model (default: gpt-image-2)
|
|
283
|
-
ENHANCE_MODEL Model for enhancement (default: gpt-image-2)
|
|
284
|
-
ENHANCE_QUALITY Quality: low, medium, high, auto (default: auto)
|
|
285
|
-
ENHANCE_FIDELITY Input fidelity: high or low (default: high)
|
|
286
|
-
ENHANCE_FORMAT Output format: png, jpeg, webp (default: png)
|
|
287
|
-
|
|
288
|
-
CONFIGURATION:
|
|
289
|
-
Default settings are loaded from _config.yml under 'preview_images' section.
|
|
290
|
-
Environment variables override config file settings.
|
|
291
|
-
|
|
292
|
-
PER-AUTHOR ART STYLE:
|
|
293
|
-
A post's `author:` may reference an entry in _data/authors.yml that defines a
|
|
294
|
-
`preview:` block (style, style_modifiers, size, quality, model). When present,
|
|
295
|
-
those values override the site-wide preview_images settings for that post's
|
|
296
|
-
banner only — giving each AI author persona (e.g. cassandra, vega) a distinct
|
|
297
|
-
look. Other posts keep the default style.
|
|
298
|
-
|
|
299
|
-
EXAMPLES:
|
|
300
|
-
# List all files missing preview images
|
|
301
|
-
./scripts/generate-preview-images.sh --list-missing
|
|
302
|
-
|
|
303
|
-
# Dry run to see what would be generated
|
|
304
|
-
./scripts/generate-preview-images.sh --dry-run --verbose
|
|
305
|
-
|
|
306
|
-
# Generate images for posts collection
|
|
307
|
-
./scripts/generate-preview-images.sh --collection posts
|
|
308
|
-
|
|
309
|
-
# Generate image for a specific file
|
|
310
|
-
./scripts/generate-preview-images.sh -f pages/_posts/my-article.md
|
|
311
|
-
|
|
312
|
-
# Force regenerate all images
|
|
313
|
-
./scripts/generate-preview-images.sh --force
|
|
314
|
-
|
|
315
|
-
# Enhance an existing preview image with default improvements
|
|
316
|
-
./scripts/generate-preview-images.sh --enhance -f pages/_posts/my-post.md
|
|
317
|
-
|
|
318
|
-
# Enhance with a custom prompt
|
|
319
|
-
./scripts/generate-preview-images.sh --enhance-prompt "Fix the text to read 'CI/CD Pipeline' and sharpen the icons" -f my-post.md
|
|
320
|
-
|
|
321
|
-
# Enhance with high fidelity and JPEG output
|
|
322
|
-
./scripts/generate-preview-images.sh --enhance --enhance-fidelity high --enhance-format jpeg -f my-post.md
|
|
323
|
-
|
|
324
|
-
# Enhance all images in a collection
|
|
325
|
-
./scripts/generate-preview-images.sh --enhance --collection posts
|
|
326
|
-
|
|
327
|
-
EOF
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
# Parse command line arguments
|
|
331
|
-
parse_args() {
|
|
332
|
-
while [[ $# -gt 0 ]]; do
|
|
333
|
-
case $1 in
|
|
334
|
-
-h|--help)
|
|
335
|
-
show_help
|
|
336
|
-
exit 0
|
|
337
|
-
;;
|
|
338
|
-
-d|--dry-run)
|
|
339
|
-
DRY_RUN="true"
|
|
340
|
-
;;
|
|
341
|
-
-v|--verbose)
|
|
342
|
-
VERBOSE="true"
|
|
343
|
-
;;
|
|
344
|
-
-f|--file)
|
|
345
|
-
SPECIFIC_FILE="$2"
|
|
346
|
-
shift
|
|
347
|
-
;;
|
|
348
|
-
-c|--collection)
|
|
349
|
-
COLLECTION="$2"
|
|
350
|
-
shift
|
|
351
|
-
;;
|
|
352
|
-
-p|--provider)
|
|
353
|
-
AI_PROVIDER="$2"
|
|
354
|
-
shift
|
|
355
|
-
;;
|
|
356
|
-
--output-dir)
|
|
357
|
-
OUTPUT_DIR="$2"
|
|
358
|
-
shift
|
|
359
|
-
;;
|
|
360
|
-
--force)
|
|
361
|
-
FORCE="true"
|
|
362
|
-
;;
|
|
363
|
-
--list-missing)
|
|
364
|
-
LIST_ONLY="true"
|
|
365
|
-
;;
|
|
366
|
-
-j|--parallel)
|
|
367
|
-
MAX_PARALLEL="$2"
|
|
368
|
-
shift
|
|
369
|
-
;;
|
|
370
|
-
-e|--enhance)
|
|
371
|
-
ENHANCE="true"
|
|
372
|
-
;;
|
|
373
|
-
--enhance-prompt)
|
|
374
|
-
ENHANCE_PROMPT="$2"
|
|
375
|
-
ENHANCE="true"
|
|
376
|
-
shift
|
|
377
|
-
;;
|
|
378
|
-
--enhance-model)
|
|
379
|
-
ENHANCE_MODEL="$2"
|
|
380
|
-
shift
|
|
381
|
-
;;
|
|
382
|
-
--enhance-fidelity)
|
|
383
|
-
ENHANCE_FIDELITY="$2"
|
|
384
|
-
ENHANCE="true"
|
|
385
|
-
shift
|
|
386
|
-
;;
|
|
387
|
-
--enhance-format)
|
|
388
|
-
ENHANCE_FORMAT="$2"
|
|
389
|
-
ENHANCE="true"
|
|
390
|
-
shift
|
|
391
|
-
;;
|
|
392
|
-
*)
|
|
393
|
-
error "Unknown option: $1. Use --help for usage."
|
|
394
|
-
;;
|
|
395
|
-
esac
|
|
396
|
-
shift
|
|
397
|
-
done
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
# Validate environment and dependencies
|
|
401
|
-
validate_environment() {
|
|
402
|
-
step "Validating environment..."
|
|
403
|
-
|
|
404
|
-
# Check for required commands
|
|
405
|
-
local required_cmds=("curl" "jq")
|
|
406
|
-
for cmd in "${required_cmds[@]}"; do
|
|
407
|
-
if ! command -v "$cmd" &> /dev/null; then
|
|
408
|
-
error "Required command not found: $cmd"
|
|
409
|
-
fi
|
|
410
|
-
done
|
|
411
|
-
|
|
412
|
-
# Check for YAML parser (prefer yq, fallback to python)
|
|
413
|
-
if command -v yq &> /dev/null; then
|
|
414
|
-
YAML_PARSER="yq"
|
|
415
|
-
debug "Using yq for YAML parsing"
|
|
416
|
-
elif command -v python3 &> /dev/null; then
|
|
417
|
-
YAML_PARSER="python"
|
|
418
|
-
debug "Using python for YAML parsing"
|
|
419
|
-
else
|
|
420
|
-
error "No YAML parser found. Install yq or python3."
|
|
421
|
-
fi
|
|
422
|
-
|
|
423
|
-
# Validate AI provider credentials (unless list-only or dry-run)
|
|
424
|
-
if [[ "$LIST_ONLY" != "true" && "$DRY_RUN" != "true" ]]; then
|
|
425
|
-
case "$AI_PROVIDER" in
|
|
426
|
-
openai)
|
|
427
|
-
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
|
428
|
-
error "OPENAI_API_KEY environment variable is required for OpenAI provider"
|
|
429
|
-
fi
|
|
430
|
-
;;
|
|
431
|
-
stability)
|
|
432
|
-
if [[ -z "${STABILITY_API_KEY:-}" ]]; then
|
|
433
|
-
error "STABILITY_API_KEY environment variable is required for Stability AI provider"
|
|
434
|
-
fi
|
|
435
|
-
;;
|
|
436
|
-
local)
|
|
437
|
-
info "Using local provider - no API key required"
|
|
438
|
-
;;
|
|
439
|
-
*)
|
|
440
|
-
error "Unknown AI provider: $AI_PROVIDER"
|
|
441
|
-
;;
|
|
442
|
-
esac
|
|
443
|
-
fi
|
|
444
|
-
|
|
445
|
-
# Ensure output directory exists
|
|
446
|
-
local full_output_dir="$PROJECT_ROOT/$OUTPUT_DIR"
|
|
447
|
-
if [[ ! -d "$full_output_dir" ]]; then
|
|
448
|
-
if [[ "$DRY_RUN" != "true" ]]; then
|
|
449
|
-
mkdir -p "$full_output_dir"
|
|
450
|
-
debug "Created output directory: $full_output_dir"
|
|
451
|
-
else
|
|
452
|
-
debug "Would create output directory: $full_output_dir"
|
|
453
|
-
fi
|
|
454
|
-
fi
|
|
455
|
-
|
|
456
|
-
success "Environment validation passed"
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
# Extract front matter from a markdown file
|
|
460
|
-
extract_front_matter() {
|
|
461
|
-
local file="$1"
|
|
462
|
-
|
|
463
|
-
# Extract content between first pair of --- markers only
|
|
464
|
-
awk 'BEGIN{found=0} /^---$/{found++; next} found==1{print} found==2{exit}' "$file"
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
# Get YAML value using available parser
|
|
468
|
-
get_yaml_value() {
|
|
469
|
-
local yaml="$1"
|
|
470
|
-
local key="$2"
|
|
471
|
-
local result=""
|
|
472
|
-
|
|
473
|
-
if [[ "$YAML_PARSER" == "yq" ]]; then
|
|
474
|
-
# yq v4 syntax - read from stdin and get specific key
|
|
475
|
-
result=$(echo "$yaml" | yq eval ".$key" - 2>/dev/null | head -1)
|
|
476
|
-
# Filter out null values
|
|
477
|
-
if [[ "$result" == "null" || -z "$result" ]]; then
|
|
478
|
-
result=""
|
|
479
|
-
fi
|
|
480
|
-
else
|
|
481
|
-
# Python fallback - use stdin to avoid quote-escaping issues
|
|
482
|
-
result=$(echo "$yaml" | python3 -c "
|
|
483
|
-
import yaml, sys
|
|
484
|
-
try:
|
|
485
|
-
data = yaml.safe_load(sys.stdin.read())
|
|
486
|
-
if data and '$key' in data:
|
|
487
|
-
val = data['$key']
|
|
488
|
-
if val is not None:
|
|
489
|
-
print(val)
|
|
490
|
-
except:
|
|
491
|
-
pass
|
|
492
|
-
" 2>/dev/null || echo "")
|
|
493
|
-
fi
|
|
494
|
-
|
|
495
|
-
echo "$result"
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
# Read a per-author preview override from _data/authors.yml
|
|
499
|
-
# Usage: get_author_preview_value <author_key> <setting>
|
|
500
|
-
# e.g. get_author_preview_value cassandra style
|
|
501
|
-
# Returns the value (or empty string when the author / setting is absent).
|
|
502
|
-
get_author_preview_value() {
|
|
503
|
-
local author_key="$1"
|
|
504
|
-
local setting="$2"
|
|
505
|
-
|
|
506
|
-
[[ -z "$author_key" || ! -f "$AUTHORS_FILE" ]] && return 0
|
|
507
|
-
|
|
508
|
-
local result=""
|
|
509
|
-
if [[ "${YAML_PARSER:-python}" == "yq" ]]; then
|
|
510
|
-
# Capture the whole value (do NOT `head -1`) so a multi-line literal/folded
|
|
511
|
-
# scalar survives intact, matching the python3 branch below; trim a single
|
|
512
|
-
# trailing newline that yq appends.
|
|
513
|
-
result=$(yq eval ".\"${author_key}\".preview.${setting} // \"\"" "$AUTHORS_FILE" 2>/dev/null)
|
|
514
|
-
result="${result%$'\n'}"
|
|
515
|
-
[[ "$result" == "null" ]] && result=""
|
|
516
|
-
else
|
|
517
|
-
result=$(AUTHOR_KEY="$author_key" SETTING="$setting" AUTHORS_FILE="$AUTHORS_FILE" python3 -c '
|
|
518
|
-
import os, yaml
|
|
519
|
-
try:
|
|
520
|
-
data = yaml.safe_load(open(os.environ["AUTHORS_FILE"])) or {}
|
|
521
|
-
author = data.get(os.environ["AUTHOR_KEY"]) or {}
|
|
522
|
-
preview = author.get("preview") or {}
|
|
523
|
-
val = preview.get(os.environ["SETTING"])
|
|
524
|
-
if val is not None:
|
|
525
|
-
print(str(val).strip())
|
|
526
|
-
except Exception:
|
|
527
|
-
pass
|
|
528
|
-
' 2>/dev/null)
|
|
529
|
-
fi
|
|
530
|
-
|
|
531
|
-
echo "$result"
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
# Apply a post author's per-author preview overrides (from _data/authors.yml) on
|
|
535
|
-
# top of the current image settings. MUST be called from a function that has
|
|
536
|
-
# already declared `local IMAGE_STYLE` (+ IMAGE_STYLE_MODIFIERS / IMAGE_SIZE /
|
|
537
|
-
# IMAGE_QUALITY / IMAGE_MODEL) — bash dynamic scoping lets this helper reassign
|
|
538
|
-
# those caller locals, scoping the override to the current file. Call it only at
|
|
539
|
-
# the point an image/prompt is actually built, so --list-missing stays clean and
|
|
540
|
-
# already-satisfied files don't pay for the lookup.
|
|
541
|
-
apply_author_preview_overrides() {
|
|
542
|
-
local author="$1"
|
|
543
|
-
[[ -z "$author" ]] && return 0
|
|
544
|
-
|
|
545
|
-
local ov_style ov_mods ov_size ov_quality ov_model
|
|
546
|
-
ov_style=$(get_author_preview_value "$author" "style")
|
|
547
|
-
ov_mods=$(get_author_preview_value "$author" "style_modifiers")
|
|
548
|
-
ov_size=$(get_author_preview_value "$author" "size")
|
|
549
|
-
ov_quality=$(get_author_preview_value "$author" "quality")
|
|
550
|
-
ov_model=$(get_author_preview_value "$author" "model")
|
|
551
|
-
|
|
552
|
-
[[ -n "$ov_style" ]] && IMAGE_STYLE="$ov_style"
|
|
553
|
-
[[ -n "$ov_mods" ]] && IMAGE_STYLE_MODIFIERS="$ov_mods"
|
|
554
|
-
[[ -n "$ov_size" ]] && IMAGE_SIZE="$ov_size"
|
|
555
|
-
[[ -n "$ov_quality" ]] && IMAGE_QUALITY="$ov_quality"
|
|
556
|
-
[[ -n "$ov_model" ]] && IMAGE_MODEL="$ov_model"
|
|
557
|
-
|
|
558
|
-
if [[ -n "$ov_style" || -n "$ov_mods" || -n "$ov_size" || -n "$ov_quality" || -n "$ov_model" ]]; then
|
|
559
|
-
info " ↳ Author '$author' preview overrides applied (style from _data/authors.yml)"
|
|
560
|
-
debug "Author override style: ${IMAGE_STYLE:0:120}..."
|
|
561
|
-
fi
|
|
562
|
-
return 0
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
# Extract post content (without front matter)
|
|
566
|
-
extract_content() {
|
|
567
|
-
local file="$1"
|
|
568
|
-
|
|
569
|
-
# Skip front matter and get content
|
|
570
|
-
awk '/^---$/ { if (++count == 2) found=1; next } found { print }' "$file"
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
# Check if preview image exists
|
|
574
|
-
check_preview_exists() {
|
|
575
|
-
local preview_path="$1"
|
|
576
|
-
|
|
577
|
-
if [[ -z "$preview_path" ]]; then
|
|
578
|
-
return 1
|
|
579
|
-
fi
|
|
580
|
-
|
|
581
|
-
# Handle paths starting with /
|
|
582
|
-
local clean_path="${preview_path#/}"
|
|
583
|
-
local full_path="$PROJECT_ROOT/$clean_path"
|
|
584
|
-
|
|
585
|
-
# Also check in assets/images
|
|
586
|
-
if [[ ! -f "$full_path" ]]; then
|
|
587
|
-
full_path="$PROJECT_ROOT/assets/$clean_path"
|
|
588
|
-
fi
|
|
589
|
-
|
|
590
|
-
[[ -f "$full_path" ]]
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
# Generate image prompt from content
|
|
594
|
-
generate_prompt() {
|
|
595
|
-
local title="$1"
|
|
596
|
-
local description="$2"
|
|
597
|
-
local categories="$3"
|
|
598
|
-
local content="$4"
|
|
599
|
-
|
|
600
|
-
# Build a meaningful prompt
|
|
601
|
-
local prompt="Create a blog preview banner image for an article titled '$title'."
|
|
602
|
-
|
|
603
|
-
if [[ -n "$description" ]]; then
|
|
604
|
-
prompt="$prompt The article is about: $description."
|
|
605
|
-
fi
|
|
606
|
-
|
|
607
|
-
if [[ -n "$categories" ]]; then
|
|
608
|
-
prompt="$prompt Categories: $categories."
|
|
609
|
-
fi
|
|
610
|
-
|
|
611
|
-
# Extract key themes from content (first 500 chars)
|
|
612
|
-
local content_excerpt="${content:0:500}"
|
|
613
|
-
if [[ -n "$content_excerpt" ]]; then
|
|
614
|
-
prompt="$prompt Key themes from content: $content_excerpt"
|
|
615
|
-
fi
|
|
616
|
-
|
|
617
|
-
# Add style instructions and modifiers
|
|
618
|
-
prompt="$prompt Art style: $IMAGE_STYLE."
|
|
619
|
-
if [[ -n "$IMAGE_STYLE_MODIFIERS" ]]; then
|
|
620
|
-
prompt="$prompt Additional style: $IMAGE_STYLE_MODIFIERS."
|
|
621
|
-
fi
|
|
622
|
-
prompt="$prompt The image should be suitable as a wide blog header/banner image with clean composition. No text or words in the image."
|
|
623
|
-
|
|
624
|
-
echo "$prompt"
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
# Generate image using OpenAI DALL-E
|
|
628
|
-
generate_image_openai() {
|
|
629
|
-
local prompt="$1"
|
|
630
|
-
local output_file="$2"
|
|
631
|
-
|
|
632
|
-
debug "Generating image with OpenAI model: $IMAGE_MODEL"
|
|
633
|
-
debug "Prompt: ${prompt:0:200}..."
|
|
634
|
-
|
|
635
|
-
local request_size="$IMAGE_SIZE"
|
|
636
|
-
local request_quality="$IMAGE_QUALITY"
|
|
637
|
-
if [[ "$IMAGE_MODEL" == gpt-image-* && "$request_size" == "1792x1024" ]]; then
|
|
638
|
-
request_size="1536x1024"
|
|
639
|
-
debug "Using GPT Image landscape size: $request_size"
|
|
640
|
-
elif [[ "$IMAGE_MODEL" == dall-e-* && "$request_quality" == "auto" ]]; then
|
|
641
|
-
request_quality="standard"
|
|
642
|
-
debug "Using DALL-E quality: $request_quality"
|
|
643
|
-
fi
|
|
644
|
-
|
|
645
|
-
local payload
|
|
646
|
-
payload=$(jq -n \
|
|
647
|
-
--arg model "$IMAGE_MODEL" \
|
|
648
|
-
--arg prompt "$prompt" \
|
|
649
|
-
--arg size "$request_size" \
|
|
650
|
-
--arg quality "$request_quality" \
|
|
651
|
-
'{model: $model, prompt: $prompt, n: 1, size: $size, quality: $quality}')
|
|
652
|
-
local auth_config payload_file
|
|
653
|
-
auth_config=$(mktemp)
|
|
654
|
-
payload_file=$(mktemp)
|
|
655
|
-
TEMP_FILES+=("$auth_config" "$payload_file")
|
|
656
|
-
chmod 600 "$auth_config" "$payload_file"
|
|
657
|
-
printf 'header = "Authorization: Bearer %s"\n' "$OPENAI_API_KEY" > "$auth_config"
|
|
658
|
-
printf '%s' "$payload" > "$payload_file"
|
|
659
|
-
|
|
660
|
-
local response
|
|
661
|
-
local max_retries=3
|
|
662
|
-
local attempt=0
|
|
663
|
-
local api_success=false
|
|
664
|
-
while [[ $attempt -lt $max_retries ]]; do
|
|
665
|
-
attempt=$((attempt + 1))
|
|
666
|
-
if [[ $attempt -gt 1 ]]; then
|
|
667
|
-
local backoff=$(( (attempt - 1) * 15 ))
|
|
668
|
-
warn "Retrying API call (attempt $attempt/$max_retries) after ${backoff}s..."
|
|
669
|
-
sleep "$backoff"
|
|
670
|
-
fi
|
|
671
|
-
if response=$(curl -s --max-time 120 -X POST "https://api.openai.com/v1/images/generations" \
|
|
672
|
-
--config "$auth_config" \
|
|
673
|
-
-H "Content-Type: application/json" \
|
|
674
|
-
--data-binary "@$payload_file"); then
|
|
675
|
-
api_success=true
|
|
676
|
-
break
|
|
677
|
-
fi
|
|
678
|
-
warn "OpenAI API request failed (attempt $attempt/$max_retries)"
|
|
679
|
-
done
|
|
680
|
-
rm -f "$auth_config" "$payload_file"
|
|
681
|
-
if [[ "$api_success" != "true" ]]; then
|
|
682
|
-
warn "OpenAI API request failed after $max_retries attempts"
|
|
683
|
-
return 1
|
|
684
|
-
fi
|
|
685
|
-
|
|
686
|
-
# Check for errors
|
|
687
|
-
local error_msg
|
|
688
|
-
error_msg=$(echo "$response" | jq -r '.error.message // empty')
|
|
689
|
-
if [[ -n "$error_msg" ]]; then
|
|
690
|
-
warn "OpenAI API error: $error_msg"
|
|
691
|
-
debug "Full response: $response"
|
|
692
|
-
return 1
|
|
693
|
-
fi
|
|
694
|
-
|
|
695
|
-
# GPT Image models may return b64_json; DALL-E models usually return a URL.
|
|
696
|
-
local b64_data
|
|
697
|
-
b64_data=$(echo "$response" | jq -r '.data[0].b64_json // empty')
|
|
698
|
-
if [[ -n "$b64_data" ]]; then
|
|
699
|
-
debug "Decoding base64 image data..."
|
|
700
|
-
echo "$b64_data" | base64 -d > "$output_file"
|
|
701
|
-
else
|
|
702
|
-
# Extract image URL
|
|
703
|
-
local image_url
|
|
704
|
-
image_url=$(echo "$response" | jq -r '.data[0].url // empty')
|
|
705
|
-
|
|
706
|
-
if [[ -z "$image_url" ]]; then
|
|
707
|
-
warn "No image URL or base64 data in response"
|
|
708
|
-
debug "Response: $response"
|
|
709
|
-
return 1
|
|
710
|
-
fi
|
|
711
|
-
|
|
712
|
-
# Download image
|
|
713
|
-
debug "Downloading image from: $image_url"
|
|
714
|
-
curl -s -o "$output_file" "$image_url"
|
|
715
|
-
fi
|
|
716
|
-
|
|
717
|
-
if [[ -f "$output_file" && -s "$output_file" ]]; then
|
|
718
|
-
local file_size
|
|
719
|
-
file_size=$(du -h "$output_file" | cut -f1)
|
|
720
|
-
success "Image saved to: $output_file ($file_size)"
|
|
721
|
-
return 0
|
|
722
|
-
else
|
|
723
|
-
warn "Failed to save image"
|
|
724
|
-
return 1
|
|
725
|
-
fi
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
# Generate image using Stability AI
|
|
729
|
-
generate_image_stability() {
|
|
730
|
-
local prompt="$1"
|
|
731
|
-
local output_file="$2"
|
|
732
|
-
|
|
733
|
-
debug "Generating image with Stability AI..."
|
|
734
|
-
|
|
735
|
-
local response
|
|
736
|
-
response=$(curl -s -X POST "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image" \
|
|
737
|
-
-H "Authorization: Bearer $STABILITY_API_KEY" \
|
|
738
|
-
-H "Content-Type: application/json" \
|
|
739
|
-
-d "{
|
|
740
|
-
\"text_prompts\": [{\"text\": $(echo "$prompt" | jq -Rs .)}],
|
|
741
|
-
\"cfg_scale\": 7,
|
|
742
|
-
\"height\": 1024,
|
|
743
|
-
\"width\": 1024,
|
|
744
|
-
\"samples\": 1,
|
|
745
|
-
\"steps\": 30
|
|
746
|
-
}")
|
|
747
|
-
|
|
748
|
-
# Check for errors
|
|
749
|
-
local error_msg
|
|
750
|
-
error_msg=$(echo "$response" | jq -r '.message // empty')
|
|
751
|
-
if [[ -n "$error_msg" ]]; then
|
|
752
|
-
warn "Stability API error: $error_msg"
|
|
753
|
-
return 1
|
|
754
|
-
fi
|
|
755
|
-
|
|
756
|
-
# Extract and decode base64 image
|
|
757
|
-
local base64_image
|
|
758
|
-
base64_image=$(echo "$response" | jq -r '.artifacts[0].base64 // empty')
|
|
759
|
-
|
|
760
|
-
if [[ -z "$base64_image" ]]; then
|
|
761
|
-
warn "No image data in response"
|
|
762
|
-
return 1
|
|
763
|
-
fi
|
|
764
|
-
|
|
765
|
-
echo "$base64_image" | base64 -d > "$output_file"
|
|
766
|
-
|
|
767
|
-
if [[ -f "$output_file" ]]; then
|
|
768
|
-
success "Image saved to: $output_file"
|
|
769
|
-
return 0
|
|
770
|
-
else
|
|
771
|
-
warn "Failed to save image"
|
|
772
|
-
return 1
|
|
773
|
-
fi
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
# Generate placeholder for local provider (for testing)
|
|
777
|
-
generate_image_local() {
|
|
778
|
-
local prompt="$1"
|
|
779
|
-
local output_file="$2"
|
|
780
|
-
|
|
781
|
-
warn "Local provider: No actual image generation. Creating placeholder..."
|
|
782
|
-
debug "Would generate image with prompt: ${prompt:0:200}..."
|
|
783
|
-
|
|
784
|
-
# Create a simple placeholder file
|
|
785
|
-
echo "PLACEHOLDER: $prompt" > "$output_file.txt"
|
|
786
|
-
|
|
787
|
-
info "Placeholder created: $output_file.txt"
|
|
788
|
-
return 0
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
# Generate image using selected provider
|
|
792
|
-
generate_image() {
|
|
793
|
-
local prompt="$1"
|
|
794
|
-
local output_file="$2"
|
|
795
|
-
|
|
796
|
-
case "$AI_PROVIDER" in
|
|
797
|
-
openai)
|
|
798
|
-
generate_image_openai "$prompt" "$output_file"
|
|
799
|
-
;;
|
|
800
|
-
stability)
|
|
801
|
-
generate_image_stability "$prompt" "$output_file"
|
|
802
|
-
;;
|
|
803
|
-
local)
|
|
804
|
-
generate_image_local "$prompt" "$output_file"
|
|
805
|
-
;;
|
|
806
|
-
*)
|
|
807
|
-
error "Unknown provider: $AI_PROVIDER"
|
|
808
|
-
;;
|
|
809
|
-
esac
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
# =============================================================================
|
|
813
|
-
# Enhancement Functions (improve existing preview images)
|
|
814
|
-
# =============================================================================
|
|
815
|
-
|
|
816
|
-
# Build the enhancement prompt from title, description, and optional custom prompt
|
|
817
|
-
build_enhance_prompt() {
|
|
818
|
-
local title="$1"
|
|
819
|
-
local description="$2"
|
|
820
|
-
local custom_prompt="$3"
|
|
821
|
-
|
|
822
|
-
local prompt=""
|
|
823
|
-
|
|
824
|
-
if [[ -n "$custom_prompt" ]]; then
|
|
825
|
-
prompt="$custom_prompt"
|
|
826
|
-
else
|
|
827
|
-
prompt="$DEFAULT_ENHANCE_PROMPT"
|
|
828
|
-
fi
|
|
829
|
-
|
|
830
|
-
# Add article context
|
|
831
|
-
if [[ -n "$title" ]]; then
|
|
832
|
-
prompt="$prompt Context: This is a preview banner for an article titled '$title'."
|
|
833
|
-
fi
|
|
834
|
-
|
|
835
|
-
if [[ -n "$description" ]]; then
|
|
836
|
-
prompt="$prompt Article topic: $description."
|
|
837
|
-
fi
|
|
838
|
-
|
|
839
|
-
# Add style consistency instruction
|
|
840
|
-
prompt="$prompt Maintain the $IMAGE_STYLE artistic style."
|
|
841
|
-
|
|
842
|
-
echo "$prompt"
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
# Enhance an existing image using OpenAI's /v1/images/edits endpoint
|
|
846
|
-
# API Reference: https://developers.openai.com/api/reference/resources/images/methods/edit
|
|
847
|
-
enhance_image_openai() {
|
|
848
|
-
local image_file="$1"
|
|
849
|
-
local prompt="$2"
|
|
850
|
-
local output_file="$3"
|
|
851
|
-
local model="${ENHANCE_MODEL}"
|
|
852
|
-
|
|
853
|
-
debug "Enhancing image with OpenAI ($model)..."
|
|
854
|
-
debug "Source image: $image_file"
|
|
855
|
-
debug "Enhancement prompt: ${prompt:0:200}..."
|
|
856
|
-
debug "Input fidelity: $ENHANCE_FIDELITY"
|
|
857
|
-
debug "Output format: $ENHANCE_FORMAT"
|
|
858
|
-
debug "Quality: $ENHANCE_QUALITY"
|
|
859
|
-
|
|
860
|
-
local auth_config
|
|
861
|
-
auth_config=$(mktemp)
|
|
862
|
-
TEMP_FILES+=("$auth_config")
|
|
863
|
-
chmod 600 "$auth_config"
|
|
864
|
-
printf 'header = "Authorization: Bearer %s"\n' "$OPENAI_API_KEY" > "$auth_config"
|
|
865
|
-
local curl_args=(
|
|
866
|
-
-s --max-time 900 -X POST "https://api.openai.com/v1/images/edits"
|
|
867
|
-
--config "$auth_config"
|
|
868
|
-
-F "image[]=@$image_file"
|
|
869
|
-
-F "prompt=$prompt"
|
|
870
|
-
-F "model=$model"
|
|
871
|
-
-F "n=1"
|
|
872
|
-
-F "size=auto"
|
|
873
|
-
-F "quality=$ENHANCE_QUALITY"
|
|
874
|
-
-F "output_format=$ENHANCE_FORMAT"
|
|
875
|
-
)
|
|
876
|
-
if [[ "$model" != "gpt-image-2" ]]; then
|
|
877
|
-
curl_args+=(-F "input_fidelity=$ENHANCE_FIDELITY")
|
|
878
|
-
else
|
|
879
|
-
debug "Skipping input_fidelity: $model does not support it"
|
|
880
|
-
fi
|
|
881
|
-
|
|
882
|
-
local response
|
|
883
|
-
if ! response=$(curl "${curl_args[@]}"); then
|
|
884
|
-
rm -f "$auth_config"
|
|
885
|
-
warn "OpenAI enhance API request failed"
|
|
886
|
-
return 1
|
|
887
|
-
fi
|
|
888
|
-
rm -f "$auth_config"
|
|
889
|
-
|
|
890
|
-
# Check for errors
|
|
891
|
-
local error_msg
|
|
892
|
-
error_msg=$(echo "$response" | jq -r '.error.message // empty')
|
|
893
|
-
if [[ -n "$error_msg" ]]; then
|
|
894
|
-
warn "OpenAI enhance API error: $error_msg"
|
|
895
|
-
debug "Full response: $response"
|
|
896
|
-
return 1
|
|
897
|
-
fi
|
|
898
|
-
|
|
899
|
-
# Log token usage if available
|
|
900
|
-
local total_tokens
|
|
901
|
-
total_tokens=$(echo "$response" | jq -r '.usage.total_tokens // empty')
|
|
902
|
-
if [[ -n "$total_tokens" ]]; then
|
|
903
|
-
debug "Token usage: $total_tokens total (input: $(echo "$response" | jq -r '.usage.input_tokens // "?"'), output: $(echo "$response" | jq -r '.usage.output_tokens // "?"'))"
|
|
904
|
-
fi
|
|
905
|
-
|
|
906
|
-
# Response returns b64_json by default for GPT Image models
|
|
907
|
-
local b64_data
|
|
908
|
-
b64_data=$(echo "$response" | jq -r '.data[0].b64_json // empty')
|
|
909
|
-
|
|
910
|
-
if [[ -n "$b64_data" ]]; then
|
|
911
|
-
debug "Decoding base64 enhanced image..."
|
|
912
|
-
echo "$b64_data" | base64 -d > "$output_file"
|
|
913
|
-
else
|
|
914
|
-
# Fallback: try URL (returned by DALL-E 2)
|
|
915
|
-
local image_url
|
|
916
|
-
image_url=$(echo "$response" | jq -r '.data[0].url // empty')
|
|
917
|
-
if [[ -n "$image_url" ]]; then
|
|
918
|
-
debug "Downloading enhanced image from URL..."
|
|
919
|
-
curl -s -o "$output_file" "$image_url"
|
|
920
|
-
else
|
|
921
|
-
warn "No image data in enhance response"
|
|
922
|
-
debug "Response: $(echo "$response" | jq -c '.' 2>/dev/null || echo "$response")"
|
|
923
|
-
return 1
|
|
924
|
-
fi
|
|
925
|
-
fi
|
|
926
|
-
|
|
927
|
-
# Log revised prompt if available
|
|
928
|
-
local revised_prompt
|
|
929
|
-
revised_prompt=$(echo "$response" | jq -r '.data[0].revised_prompt // empty')
|
|
930
|
-
if [[ -n "$revised_prompt" ]]; then
|
|
931
|
-
debug "Revised prompt: ${revised_prompt:0:200}..."
|
|
932
|
-
fi
|
|
933
|
-
|
|
934
|
-
if [[ -f "$output_file" && -s "$output_file" ]]; then
|
|
935
|
-
local file_size
|
|
936
|
-
file_size=$(du -h "$output_file" | cut -f1)
|
|
937
|
-
success "Enhanced image saved to: $output_file ($file_size)"
|
|
938
|
-
return 0
|
|
939
|
-
else
|
|
940
|
-
warn "Failed to save enhanced image"
|
|
941
|
-
return 1
|
|
942
|
-
fi
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
# Find the existing preview image file on disk
|
|
946
|
-
find_preview_image() {
|
|
947
|
-
local preview_path="$1"
|
|
948
|
-
|
|
949
|
-
if [[ -z "$preview_path" ]]; then
|
|
950
|
-
return 1
|
|
951
|
-
fi
|
|
952
|
-
|
|
953
|
-
# Try various path resolutions
|
|
954
|
-
local clean_path="${preview_path#/}"
|
|
955
|
-
local candidates=(
|
|
956
|
-
"$PROJECT_ROOT/$clean_path"
|
|
957
|
-
"$PROJECT_ROOT/assets/$clean_path"
|
|
958
|
-
"$PROJECT_ROOT/$OUTPUT_DIR/$(basename "$clean_path")"
|
|
959
|
-
)
|
|
960
|
-
|
|
961
|
-
for candidate in "${candidates[@]}"; do
|
|
962
|
-
if [[ -f "$candidate" ]]; then
|
|
963
|
-
echo "$candidate"
|
|
964
|
-
return 0
|
|
965
|
-
fi
|
|
966
|
-
done
|
|
967
|
-
|
|
968
|
-
return 1
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
# Enhance image using selected provider
|
|
972
|
-
enhance_image() {
|
|
973
|
-
local image_file="$1"
|
|
974
|
-
local prompt="$2"
|
|
975
|
-
local output_file="$3"
|
|
976
|
-
|
|
977
|
-
case "$AI_PROVIDER" in
|
|
978
|
-
openai)
|
|
979
|
-
enhance_image_openai "$image_file" "$prompt" "$output_file"
|
|
980
|
-
;;
|
|
981
|
-
local)
|
|
982
|
-
warn "Local provider: No actual enhancement. Logging prompt..."
|
|
983
|
-
debug "Enhancement prompt: ${prompt:0:400}..."
|
|
984
|
-
info "Placeholder: would enhance $image_file"
|
|
985
|
-
cp "$image_file" "$output_file" 2>/dev/null || true
|
|
986
|
-
return 0
|
|
987
|
-
;;
|
|
988
|
-
*)
|
|
989
|
-
warn "Enhancement not supported for provider: $AI_PROVIDER (falling back to OpenAI)"
|
|
990
|
-
enhance_image_openai "$image_file" "$prompt" "$output_file"
|
|
991
|
-
;;
|
|
992
|
-
esac
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
# Update front matter with new preview path
|
|
996
|
-
update_front_matter() {
|
|
997
|
-
local file="$1"
|
|
998
|
-
local preview_path="$2"
|
|
999
|
-
|
|
1000
|
-
debug "Updating front matter in: $file"
|
|
1001
|
-
|
|
1002
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
1003
|
-
info "[DRY RUN] Would update preview in $file to: $preview_path"
|
|
1004
|
-
return 0
|
|
1005
|
-
fi
|
|
1006
|
-
|
|
1007
|
-
# Create backup
|
|
1008
|
-
cp "$file" "$file.bak"
|
|
1009
|
-
|
|
1010
|
-
# Always use sed for reliability (yq can fail on complex YAML)
|
|
1011
|
-
# Check if preview field exists
|
|
1012
|
-
if grep -q "^preview:" "$file"; then
|
|
1013
|
-
# Update existing preview field using sed
|
|
1014
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1015
|
-
# macOS sed requires empty string for -i
|
|
1016
|
-
sed -i '' "s|^preview:.*|preview: $preview_path|" "$file"
|
|
1017
|
-
else
|
|
1018
|
-
sed -i "s|^preview:.*|preview: $preview_path|" "$file"
|
|
1019
|
-
fi
|
|
1020
|
-
else
|
|
1021
|
-
# Add preview field after description or title
|
|
1022
|
-
if grep -q "^description:" "$file"; then
|
|
1023
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1024
|
-
sed -i '' "/^description:/a\\
|
|
1025
|
-
preview: $preview_path" "$file"
|
|
1026
|
-
else
|
|
1027
|
-
sed -i "/^description:/a\\
|
|
1028
|
-
preview: $preview_path" "$file"
|
|
1029
|
-
fi
|
|
1030
|
-
else
|
|
1031
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1032
|
-
sed -i '' "/^title:/a\\
|
|
1033
|
-
preview: $preview_path" "$file"
|
|
1034
|
-
else
|
|
1035
|
-
sed -i "/^title:/a\\
|
|
1036
|
-
preview: $preview_path" "$file"
|
|
1037
|
-
fi
|
|
1038
|
-
fi
|
|
1039
|
-
fi
|
|
1040
|
-
|
|
1041
|
-
# Remove backup on success
|
|
1042
|
-
rm -f "$file.bak"
|
|
1043
|
-
|
|
1044
|
-
success "Updated front matter with preview: $preview_path"
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
# Process a single file
|
|
1048
|
-
process_file() {
|
|
1049
|
-
local file="$1"
|
|
1050
|
-
|
|
1051
|
-
inc_counter processed
|
|
1052
|
-
|
|
1053
|
-
debug "Processing file: $file"
|
|
1054
|
-
|
|
1055
|
-
# Extract front matter
|
|
1056
|
-
local front_matter
|
|
1057
|
-
front_matter=$(extract_front_matter "$file")
|
|
1058
|
-
|
|
1059
|
-
if [[ -z "$front_matter" ]]; then
|
|
1060
|
-
debug "No front matter found in: $file"
|
|
1061
|
-
inc_counter skipped
|
|
1062
|
-
return 0
|
|
1063
|
-
fi
|
|
1064
|
-
|
|
1065
|
-
# Get metadata
|
|
1066
|
-
local title description categories preview author
|
|
1067
|
-
title=$(get_yaml_value "$front_matter" "title")
|
|
1068
|
-
description=$(get_yaml_value "$front_matter" "description")
|
|
1069
|
-
categories=$(get_yaml_value "$front_matter" "categories")
|
|
1070
|
-
preview=$(get_yaml_value "$front_matter" "preview")
|
|
1071
|
-
author=$(get_yaml_value "$front_matter" "author")
|
|
1072
|
-
|
|
1073
|
-
debug "Title: $title"
|
|
1074
|
-
debug "Preview: $preview"
|
|
1075
|
-
|
|
1076
|
-
# -------------------------------------------------------------------------
|
|
1077
|
-
# Per-author art-style override (e.g. AI personas Cassandra / Vega).
|
|
1078
|
-
# These `local`s shadow the global image settings for the duration of this
|
|
1079
|
-
# function and everything it calls (generate_prompt, generate_image,
|
|
1080
|
-
# build_enhance_prompt) via bash dynamic scoping — so each worker/file gets
|
|
1081
|
-
# its author's style without leaking to other files, in serial or parallel.
|
|
1082
|
-
# The actual lookup runs later, via apply_author_preview_overrides, only
|
|
1083
|
-
# once an image/prompt is really built — never in --list-missing mode and
|
|
1084
|
-
# not for files whose preview already exists (keeps listing output clean and
|
|
1085
|
-
# skips wasted YAML-parser subprocesses).
|
|
1086
|
-
# -------------------------------------------------------------------------
|
|
1087
|
-
local IMAGE_STYLE="$IMAGE_STYLE"
|
|
1088
|
-
local IMAGE_STYLE_MODIFIERS="$IMAGE_STYLE_MODIFIERS"
|
|
1089
|
-
local IMAGE_SIZE="$IMAGE_SIZE"
|
|
1090
|
-
local IMAGE_QUALITY="$IMAGE_QUALITY"
|
|
1091
|
-
local IMAGE_MODEL="$IMAGE_MODEL"
|
|
1092
|
-
|
|
1093
|
-
# =========================================================================
|
|
1094
|
-
# ENHANCE MODE: improve an existing preview image
|
|
1095
|
-
# =========================================================================
|
|
1096
|
-
if [[ "$ENHANCE" == "true" ]]; then
|
|
1097
|
-
# Find the existing preview image on disk
|
|
1098
|
-
local existing_image
|
|
1099
|
-
existing_image=$(find_preview_image "$preview" 2>/dev/null || echo "")
|
|
1100
|
-
|
|
1101
|
-
if [[ -z "$existing_image" || ! -f "$existing_image" ]]; then
|
|
1102
|
-
warn "No existing preview image found for: $title"
|
|
1103
|
-
warn " Expected at: $preview"
|
|
1104
|
-
warn " Use without --enhance to generate a new image first."
|
|
1105
|
-
inc_counter skipped
|
|
1106
|
-
return 0
|
|
1107
|
-
fi
|
|
1108
|
-
|
|
1109
|
-
info "Enhancing preview for: $title"
|
|
1110
|
-
debug "Source image: $existing_image ($(du -h "$existing_image" | cut -f1))"
|
|
1111
|
-
|
|
1112
|
-
# Apply the author's art style (if any) before building the prompt
|
|
1113
|
-
apply_author_preview_overrides "$author"
|
|
1114
|
-
|
|
1115
|
-
# Build enhancement prompt
|
|
1116
|
-
local enhance_prompt
|
|
1117
|
-
enhance_prompt=$(build_enhance_prompt "$title" "$description" "$ENHANCE_PROMPT")
|
|
1118
|
-
|
|
1119
|
-
debug "Enhancement prompt: ${enhance_prompt:0:400}..."
|
|
1120
|
-
|
|
1121
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
1122
|
-
info "[DRY RUN] Would enhance image:"
|
|
1123
|
-
echo " Source: $existing_image"
|
|
1124
|
-
echo " Model: $ENHANCE_MODEL"
|
|
1125
|
-
echo " Prompt: ${enhance_prompt:0:400}..."
|
|
1126
|
-
echo ""
|
|
1127
|
-
ENHANCED=$((ENHANCED + 1))
|
|
1128
|
-
return 0
|
|
1129
|
-
fi
|
|
1130
|
-
|
|
1131
|
-
# Backup original before overwriting
|
|
1132
|
-
local backup_file="${existing_image%.png}_pre-enhance.png"
|
|
1133
|
-
if [[ ! -f "$backup_file" ]]; then
|
|
1134
|
-
cp "$existing_image" "$backup_file"
|
|
1135
|
-
info "Original backed up to: $(basename "$backup_file")"
|
|
1136
|
-
else
|
|
1137
|
-
debug "Backup already exists: $backup_file"
|
|
1138
|
-
fi
|
|
1139
|
-
|
|
1140
|
-
# Enhance the image (output overwrites original)
|
|
1141
|
-
if enhance_image "$existing_image" "$enhance_prompt" "$existing_image"; then
|
|
1142
|
-
inc_counter enhanced
|
|
1143
|
-
else
|
|
1144
|
-
warn "Failed to enhance image for: $title"
|
|
1145
|
-
info "Original preserved at: $backup_file"
|
|
1146
|
-
inc_counter errors
|
|
1147
|
-
fi
|
|
1148
|
-
|
|
1149
|
-
return 0
|
|
1150
|
-
fi
|
|
1151
|
-
|
|
1152
|
-
# =========================================================================
|
|
1153
|
-
# GENERATE MODE: create new preview image (default)
|
|
1154
|
-
# =========================================================================
|
|
1155
|
-
|
|
1156
|
-
# Check if preview exists and is valid
|
|
1157
|
-
if [[ -n "$preview" ]] && check_preview_exists "$preview"; then
|
|
1158
|
-
if [[ "$FORCE" != "true" ]]; then
|
|
1159
|
-
debug "Preview already exists and is valid: $preview"
|
|
1160
|
-
inc_counter skipped
|
|
1161
|
-
return 0
|
|
1162
|
-
else
|
|
1163
|
-
info "Force mode: regenerating preview for $title"
|
|
1164
|
-
fi
|
|
1165
|
-
fi
|
|
1166
|
-
|
|
1167
|
-
# Report missing preview
|
|
1168
|
-
if [[ "$LIST_ONLY" == "true" ]]; then
|
|
1169
|
-
echo -e "${YELLOW}Missing preview:${NC} $file"
|
|
1170
|
-
echo -e " Title: $title"
|
|
1171
|
-
if [[ -n "$preview" ]]; then
|
|
1172
|
-
echo -e " Current preview (not found): $preview"
|
|
1173
|
-
fi
|
|
1174
|
-
echo ""
|
|
1175
|
-
return 0
|
|
1176
|
-
fi
|
|
1177
|
-
|
|
1178
|
-
info "Generating preview for: $title"
|
|
1179
|
-
|
|
1180
|
-
# Generate filename from title
|
|
1181
|
-
local safe_filename
|
|
1182
|
-
safe_filename=$(echo "$title" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
|
|
1183
|
-
safe_filename="${safe_filename:0:50}" # Limit length
|
|
1184
|
-
|
|
1185
|
-
local output_file="$PROJECT_ROOT/$OUTPUT_DIR/${safe_filename}.png"
|
|
1186
|
-
# Preview path should NOT include /assets/ prefix since the template adds it
|
|
1187
|
-
local preview_path="/images/previews/${safe_filename}.png"
|
|
1188
|
-
|
|
1189
|
-
# Apply the author's art style (if any) before building the prompt
|
|
1190
|
-
apply_author_preview_overrides "$author"
|
|
1191
|
-
|
|
1192
|
-
# Extract content for prompt generation
|
|
1193
|
-
local content
|
|
1194
|
-
content=$(extract_content "$file")
|
|
1195
|
-
|
|
1196
|
-
# Generate prompt
|
|
1197
|
-
local prompt
|
|
1198
|
-
prompt=$(generate_prompt "$title" "$description" "$categories" "$content")
|
|
1199
|
-
|
|
1200
|
-
debug "Generated prompt: ${prompt:0:500}..."
|
|
1201
|
-
|
|
1202
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
1203
|
-
info "[DRY RUN] Would generate image:"
|
|
1204
|
-
echo " Output: $output_file"
|
|
1205
|
-
echo " Preview path: $preview_path"
|
|
1206
|
-
echo " Prompt: ${prompt:0:400}..."
|
|
1207
|
-
echo ""
|
|
1208
|
-
inc_counter generated
|
|
1209
|
-
return 0
|
|
1210
|
-
fi
|
|
1211
|
-
|
|
1212
|
-
# Generate image
|
|
1213
|
-
if generate_image "$prompt" "$output_file"; then
|
|
1214
|
-
# Update front matter with new preview path
|
|
1215
|
-
update_front_matter "$file" "$preview_path"
|
|
1216
|
-
inc_counter generated
|
|
1217
|
-
# Brief pause between successful generations to stay within API rate limits
|
|
1218
|
-
sleep 2
|
|
1219
|
-
else
|
|
1220
|
-
warn "Failed to generate image for: $title"
|
|
1221
|
-
inc_counter errors
|
|
1222
|
-
fi
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
# Find and process content files
|
|
1226
|
-
process_collection() {
|
|
1227
|
-
local collection_path="$1"
|
|
1228
|
-
local pattern="${2:-*.md}"
|
|
1229
|
-
|
|
1230
|
-
debug "Processing collection: $collection_path with pattern: $pattern"
|
|
1231
|
-
|
|
1232
|
-
if [[ ! -d "$collection_path" ]]; then
|
|
1233
|
-
warn "Collection directory not found: $collection_path"
|
|
1234
|
-
return 1
|
|
1235
|
-
fi
|
|
1236
|
-
|
|
1237
|
-
# Serial mode for dry-run / list-only (clean, ordered output) or when parallelism=1
|
|
1238
|
-
if [[ "$LIST_ONLY" == "true" || "$DRY_RUN" == "true" || "$MAX_PARALLEL" -le 1 ]]; then
|
|
1239
|
-
while IFS= read -r -d '' file; do
|
|
1240
|
-
process_file "$file"
|
|
1241
|
-
done < <(find "$collection_path" -name "$pattern" -type f -print0 | sort -z)
|
|
1242
|
-
return 0
|
|
1243
|
-
fi
|
|
1244
|
-
|
|
1245
|
-
# Parallel mode: dispatch up to MAX_PARALLEL concurrent workers
|
|
1246
|
-
# Uses a busy-wait throttle compatible with bash 3.2+
|
|
1247
|
-
while IFS= read -r -d '' file; do
|
|
1248
|
-
# Block until a worker slot opens
|
|
1249
|
-
while true; do
|
|
1250
|
-
local active
|
|
1251
|
-
active=$(jobs -rp 2>/dev/null | wc -l | tr -d ' ')
|
|
1252
|
-
[[ "$active" -lt "$MAX_PARALLEL" ]] && break
|
|
1253
|
-
sleep 0.2
|
|
1254
|
-
done
|
|
1255
|
-
process_file "$file" &
|
|
1256
|
-
done < <(find "$collection_path" -name "$pattern" -type f -print0 | sort -z)
|
|
1257
|
-
|
|
1258
|
-
# Drain all remaining workers before returning
|
|
1259
|
-
wait
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
# Main function
|
|
1263
|
-
main() {
|
|
1264
|
-
print_header "🎨 Preview Image Generator"
|
|
1265
|
-
|
|
1266
|
-
# Validate environment
|
|
1267
|
-
validate_environment
|
|
1268
|
-
|
|
1269
|
-
# Initialise file-based counters for parallel-safe counting
|
|
1270
|
-
if [[ "$LIST_ONLY" != "true" && "$DRY_RUN" != "true" ]]; then
|
|
1271
|
-
init_counters
|
|
1272
|
-
fi
|
|
1273
|
-
|
|
1274
|
-
# Display configuration
|
|
1275
|
-
info "Configuration:"
|
|
1276
|
-
echo " AI Provider: $AI_PROVIDER"
|
|
1277
|
-
echo " Image Model: $IMAGE_MODEL"
|
|
1278
|
-
echo " Output Dir: $OUTPUT_DIR"
|
|
1279
|
-
echo " Image Size: $IMAGE_SIZE"
|
|
1280
|
-
echo " Parallel Workers: $MAX_PARALLEL"
|
|
1281
|
-
echo " Dry Run: $DRY_RUN"
|
|
1282
|
-
echo " Force: $FORCE"
|
|
1283
|
-
echo " List Only: $LIST_ONLY"
|
|
1284
|
-
if [[ "$ENHANCE" == "true" ]]; then
|
|
1285
|
-
echo " Mode: ENHANCE (improve existing images)"
|
|
1286
|
-
echo " Enhance Model: $ENHANCE_MODEL"
|
|
1287
|
-
echo " Enhance Quality: $ENHANCE_QUALITY"
|
|
1288
|
-
echo " Input Fidelity: $ENHANCE_FIDELITY"
|
|
1289
|
-
echo " Output Format: $ENHANCE_FORMAT"
|
|
1290
|
-
if [[ -n "$ENHANCE_PROMPT" ]]; then
|
|
1291
|
-
echo " Custom Prompt: ${ENHANCE_PROMPT:0:80}..."
|
|
1292
|
-
else
|
|
1293
|
-
echo " Prompt: (default improvement prompt)"
|
|
1294
|
-
fi
|
|
1295
|
-
fi
|
|
1296
|
-
echo ""
|
|
1297
|
-
|
|
1298
|
-
# Get configured collections from _config.yml
|
|
1299
|
-
get_configured_collections() {
|
|
1300
|
-
local collections=()
|
|
1301
|
-
local in_preview_images=false
|
|
1302
|
-
local in_collections=false
|
|
1303
|
-
|
|
1304
|
-
while IFS= read -r line; do
|
|
1305
|
-
if [[ "$line" =~ ^preview_images: ]]; then
|
|
1306
|
-
in_preview_images=true
|
|
1307
|
-
continue
|
|
1308
|
-
fi
|
|
1309
|
-
if [[ "$in_preview_images" == true && "$line" =~ ^[[:space:]]+collections: ]]; then
|
|
1310
|
-
in_collections=true
|
|
1311
|
-
continue
|
|
1312
|
-
fi
|
|
1313
|
-
if [[ "$in_collections" == true && "$line" =~ ^[[:space:]]+- ]]; then
|
|
1314
|
-
local collection="${line#*- }"
|
|
1315
|
-
collection="${collection%%#*}"
|
|
1316
|
-
collection="${collection%"${collection##*[![:space:]]}"}"
|
|
1317
|
-
collections+=("$collection")
|
|
1318
|
-
elif [[ "$in_collections" == true && ! "$line" =~ ^[[:space:]]+- && ! "$line" =~ ^[[:space:]]*$ ]]; then
|
|
1319
|
-
break
|
|
1320
|
-
fi
|
|
1321
|
-
if [[ "$in_preview_images" == true && "$line" =~ ^[a-zA-Z_]+: && ! "$line" =~ ^[[:space:]] ]]; then
|
|
1322
|
-
break
|
|
1323
|
-
fi
|
|
1324
|
-
done < "$CONFIG_FILE"
|
|
1325
|
-
|
|
1326
|
-
if [[ ${#collections[@]} -eq 0 ]]; then
|
|
1327
|
-
collections=("posts" "quickstart" "docs")
|
|
1328
|
-
fi
|
|
1329
|
-
|
|
1330
|
-
echo "${collections[@]}"
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
# Process a collection by name
|
|
1334
|
-
process_collection_by_name() {
|
|
1335
|
-
local name="$1"
|
|
1336
|
-
local path="$PROJECT_ROOT/pages/_${name}"
|
|
1337
|
-
|
|
1338
|
-
if [[ -d "$path" ]]; then
|
|
1339
|
-
step "Processing ${name} collection..."
|
|
1340
|
-
process_collection "$path"
|
|
1341
|
-
else
|
|
1342
|
-
warn "Collection directory not found: $path"
|
|
1343
|
-
fi
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
# Process files
|
|
1347
|
-
if [[ -n "$SPECIFIC_FILE" ]]; then
|
|
1348
|
-
# Process single file
|
|
1349
|
-
if [[ ! -f "$PROJECT_ROOT/$SPECIFIC_FILE" ]]; then
|
|
1350
|
-
error "File not found: $SPECIFIC_FILE"
|
|
1351
|
-
fi
|
|
1352
|
-
process_file "$PROJECT_ROOT/$SPECIFIC_FILE"
|
|
1353
|
-
elif [[ -n "$COLLECTION" ]]; then
|
|
1354
|
-
# Process specific collection
|
|
1355
|
-
if [[ "$COLLECTION" == "all" ]]; then
|
|
1356
|
-
step "Processing all configured collections..."
|
|
1357
|
-
for col in $(get_configured_collections); do
|
|
1358
|
-
process_collection_by_name "$col"
|
|
1359
|
-
done
|
|
1360
|
-
else
|
|
1361
|
-
# Check if collection directory exists
|
|
1362
|
-
local collection_path="$PROJECT_ROOT/pages/_${COLLECTION}"
|
|
1363
|
-
if [[ -d "$collection_path" ]]; then
|
|
1364
|
-
process_collection_by_name "$COLLECTION"
|
|
1365
|
-
else
|
|
1366
|
-
local available=$(get_configured_collections | tr ' ' ', ')
|
|
1367
|
-
error "Unknown collection: $COLLECTION. Available: $available, all"
|
|
1368
|
-
fi
|
|
1369
|
-
fi
|
|
1370
|
-
else
|
|
1371
|
-
# Process all configured collections by default
|
|
1372
|
-
step "Processing all configured collections..."
|
|
1373
|
-
for col in $(get_configured_collections); do
|
|
1374
|
-
process_collection_by_name "$col"
|
|
1375
|
-
done
|
|
1376
|
-
fi
|
|
1377
|
-
|
|
1378
|
-
# Print summary
|
|
1379
|
-
echo ""
|
|
1380
|
-
print_header "📊 Summary"
|
|
1381
|
-
echo " Files processed: $(get_counter processed)"
|
|
1382
|
-
echo " Images generated: $(get_counter generated)"
|
|
1383
|
-
echo " Images enhanced: $(get_counter enhanced)"
|
|
1384
|
-
echo " Files skipped: $(get_counter skipped)"
|
|
1385
|
-
echo " Errors: $(get_counter errors)"
|
|
1386
|
-
echo ""
|
|
1387
|
-
|
|
1388
|
-
if [[ "$DRY_RUN" == "true" ]]; then
|
|
1389
|
-
info "This was a dry run. No actual changes were made."
|
|
1390
|
-
fi
|
|
1391
|
-
|
|
1392
|
-
local err_count
|
|
1393
|
-
err_count=$(get_counter errors)
|
|
1394
|
-
if [[ "$err_count" -gt 0 ]]; then
|
|
1395
|
-
warn "Some files had errors. Check the output above."
|
|
1396
|
-
exit 1
|
|
1397
|
-
fi
|
|
1398
|
-
|
|
1399
|
-
success "Preview image generation complete!"
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
# Parse arguments and run
|
|
1403
|
-
parse_args "$@"
|
|
1404
|
-
main
|
|
61
|
+
# PyYAML availability is checked by the engine itself (ensure_yaml) with an
|
|
62
|
+
# actionable message — no duplicate probe here.
|
|
63
|
+
exec python3 "$ENGINE" "$@"
|