jekyll-theme-zer0 0.7.2 → 0.8.1
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 +72 -0
- data/README.md +33 -4
- data/_plugins/preview_image_generator.rb +258 -0
- data/_plugins/theme_version.rb +88 -0
- data/assets/images/previews/git-workflow-best-practices-for-modern-teams.png +0 -0
- data/scripts/README.md +443 -0
- data/scripts/analyze-commits.sh +313 -0
- data/scripts/build +115 -0
- data/scripts/build.sh +33 -0
- data/scripts/build.sh.legacy +174 -0
- data/scripts/example-usage.sh +102 -0
- data/scripts/fix-markdown-format.sh +265 -0
- data/scripts/gem-publish.sh +42 -0
- data/scripts/gem-publish.sh.legacy +700 -0
- data/scripts/generate-preview-images.sh +846 -0
- data/scripts/install-preview-generator.sh +531 -0
- data/scripts/lib/README.md +263 -0
- data/scripts/lib/changelog.sh +313 -0
- data/scripts/lib/common.sh +154 -0
- data/scripts/lib/gem.sh +226 -0
- data/scripts/lib/git.sh +205 -0
- data/scripts/lib/preview_generator.py +646 -0
- data/scripts/lib/test/run_tests.sh +140 -0
- data/scripts/lib/test/test_changelog.sh +87 -0
- data/scripts/lib/test/test_gem.sh +68 -0
- data/scripts/lib/test/test_git.sh +82 -0
- data/scripts/lib/test/test_validation.sh +72 -0
- data/scripts/lib/test/test_version.sh +96 -0
- data/scripts/lib/validation.sh +139 -0
- data/scripts/lib/version.sh +178 -0
- data/scripts/release +240 -0
- data/scripts/release.sh +33 -0
- data/scripts/release.sh.legacy +342 -0
- data/scripts/setup.sh +155 -0
- data/scripts/test-auto-version.sh +260 -0
- data/scripts/test-mermaid.sh +251 -0
- data/scripts/test.sh +156 -0
- data/scripts/version.sh +152 -0
- metadata +37 -1
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Script Name: generate-preview-images.sh
|
|
4
|
+
# Description: AI-powered preview image generator for Jekyll posts/articles/quests
|
|
5
|
+
# Scans content files, detects missing preview images, and generates
|
|
6
|
+
# images using AI (OpenAI DALL-E, Stable Diffusion, or other providers)
|
|
7
|
+
#
|
|
8
|
+
# Usage: ./scripts/generate-preview-images.sh [options]
|
|
9
|
+
#
|
|
10
|
+
# Options:
|
|
11
|
+
# -h, --help Show this help message
|
|
12
|
+
# -d, --dry-run Preview what would be generated (no actual changes)
|
|
13
|
+
# -v, --verbose Enable verbose output
|
|
14
|
+
# -f, --file FILE Process a specific file only
|
|
15
|
+
# -c, --collection NAME Process specific collection (posts, quickstart, docs)
|
|
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
|
|
20
|
+
#
|
|
21
|
+
# Dependencies:
|
|
22
|
+
# - bash 4.0+
|
|
23
|
+
# - curl (for API calls)
|
|
24
|
+
# - jq (for JSON processing)
|
|
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
|
|
38
|
+
#
|
|
39
|
+
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
|
|
42
|
+
# Get script directory and source common utilities
|
|
43
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
45
|
+
|
|
46
|
+
# Load environment variables from .env file if it exists
|
|
47
|
+
if [[ -f "$PROJECT_ROOT/.env" ]]; then
|
|
48
|
+
# Export variables from .env file, overriding any existing values
|
|
49
|
+
while IFS='=' read -r key value; do
|
|
50
|
+
# Skip comments and empty lines
|
|
51
|
+
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
|
52
|
+
# Remove surrounding quotes from value if present
|
|
53
|
+
value="${value%\"}"
|
|
54
|
+
value="${value#\"}"
|
|
55
|
+
value="${value%\'}"
|
|
56
|
+
value="${value#\'}"
|
|
57
|
+
# Export the variable
|
|
58
|
+
export "$key=$value"
|
|
59
|
+
done < "$PROJECT_ROOT/.env"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Source common library if available
|
|
63
|
+
if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
|
|
64
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
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
|
+
}
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# =============================================================================
|
|
92
|
+
# Configuration Loading
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# Priority: CLI args > Environment variables > _config.yml > Defaults
|
|
95
|
+
|
|
96
|
+
CONFIG_FILE="$PROJECT_ROOT/_config.yml"
|
|
97
|
+
|
|
98
|
+
# Function to read config value from _config.yml using grep (handles YAML anchors)
|
|
99
|
+
read_config() {
|
|
100
|
+
local key="$1"
|
|
101
|
+
local default="$2"
|
|
102
|
+
|
|
103
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
104
|
+
# Use grep to find the key under preview_images section
|
|
105
|
+
# This is more robust than yq for files with anchors
|
|
106
|
+
local in_section=false
|
|
107
|
+
local value=""
|
|
108
|
+
|
|
109
|
+
while IFS= read -r line; do
|
|
110
|
+
# Check if we're entering the preview_images section
|
|
111
|
+
if [[ "$line" =~ ^preview_images: ]]; then
|
|
112
|
+
in_section=true
|
|
113
|
+
continue
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Check if we're leaving the section (new top-level key)
|
|
117
|
+
if [[ "$in_section" == true && "$line" =~ ^[a-zA-Z_]+: && ! "$line" =~ ^[[:space:]] ]]; then
|
|
118
|
+
break
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Look for the key within the section
|
|
122
|
+
if [[ "$in_section" == true && "$line" =~ ^[[:space:]]+${key}[[:space:]]*:[[:space:]]*(.*) ]]; then
|
|
123
|
+
value="${BASH_REMATCH[1]}"
|
|
124
|
+
# First, remove inline comments (only if not inside quotes)
|
|
125
|
+
# Simple approach: if value starts with quote, find closing quote
|
|
126
|
+
if [[ "$value" =~ ^\'([^\']*)\' ]]; then
|
|
127
|
+
value="${BASH_REMATCH[1]}"
|
|
128
|
+
elif [[ "$value" =~ ^\"([^\"]*)\" ]]; then
|
|
129
|
+
value="${BASH_REMATCH[1]}"
|
|
130
|
+
else
|
|
131
|
+
# No quotes, just trim and remove comment
|
|
132
|
+
value="${value%%#*}"
|
|
133
|
+
# Trim whitespace
|
|
134
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
135
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
136
|
+
fi
|
|
137
|
+
if [[ -n "$value" ]]; then
|
|
138
|
+
echo "$value"
|
|
139
|
+
return
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
done < "$CONFIG_FILE"
|
|
143
|
+
fi
|
|
144
|
+
echo "$default"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Load defaults from _config.yml, with fallbacks
|
|
148
|
+
CONFIG_PROVIDER=$(read_config "provider" "openai")
|
|
149
|
+
CONFIG_MODEL=$(read_config "model" "dall-e-3")
|
|
150
|
+
CONFIG_SIZE=$(read_config "size" "1792x1024")
|
|
151
|
+
CONFIG_QUALITY=$(read_config "quality" "standard")
|
|
152
|
+
CONFIG_STYLE=$(read_config "style" "retro pixel art, 8-bit video game aesthetic, vibrant colors, nostalgic, clean pixel graphics")
|
|
153
|
+
CONFIG_STYLE_MODIFIERS=$(read_config "style_modifiers" "pixelated, retro gaming style, CRT screen glow effect, limited color palette")
|
|
154
|
+
CONFIG_OUTPUT_DIR=$(read_config "output_dir" "assets/images/previews")
|
|
155
|
+
|
|
156
|
+
# Default configuration (env vars override config file)
|
|
157
|
+
DRY_RUN="${DRY_RUN:-false}"
|
|
158
|
+
VERBOSE="${VERBOSE:-false}"
|
|
159
|
+
FORCE="${FORCE:-false}"
|
|
160
|
+
LIST_ONLY="${LIST_ONLY:-false}"
|
|
161
|
+
SPECIFIC_FILE=""
|
|
162
|
+
COLLECTION=""
|
|
163
|
+
AI_PROVIDER="${AI_PROVIDER:-$CONFIG_PROVIDER}"
|
|
164
|
+
OUTPUT_DIR="${OUTPUT_DIR:-$CONFIG_OUTPUT_DIR}"
|
|
165
|
+
IMAGE_STYLE="${IMAGE_STYLE:-$CONFIG_STYLE}"
|
|
166
|
+
IMAGE_STYLE_MODIFIERS="${IMAGE_STYLE_MODIFIERS:-$CONFIG_STYLE_MODIFIERS}"
|
|
167
|
+
IMAGE_SIZE="${IMAGE_SIZE:-$CONFIG_SIZE}"
|
|
168
|
+
IMAGE_QUALITY="${IMAGE_QUALITY:-$CONFIG_QUALITY}"
|
|
169
|
+
IMAGE_MODEL="${IMAGE_MODEL:-$CONFIG_MODEL}"
|
|
170
|
+
|
|
171
|
+
# Counters
|
|
172
|
+
PROCESSED=0
|
|
173
|
+
GENERATED=0
|
|
174
|
+
SKIPPED=0
|
|
175
|
+
ERRORS=0
|
|
176
|
+
|
|
177
|
+
# Print usage
|
|
178
|
+
show_help() {
|
|
179
|
+
cat << 'EOF'
|
|
180
|
+
Usage: generate-preview-images.sh [OPTIONS]
|
|
181
|
+
|
|
182
|
+
AI-powered preview image generator for Jekyll posts and content.
|
|
183
|
+
|
|
184
|
+
OPTIONS:
|
|
185
|
+
-h, --help Show this help message
|
|
186
|
+
-d, --dry-run Preview what would be generated (no changes)
|
|
187
|
+
-v, --verbose Enable verbose output
|
|
188
|
+
-f, --file FILE Process a specific file only
|
|
189
|
+
-c, --collection NAME Process specific collection (posts, quickstart, docs)
|
|
190
|
+
-p, --provider PROVIDER AI provider: openai, stability, local (default: openai)
|
|
191
|
+
--output-dir DIR Output directory for images (default: assets/images/previews)
|
|
192
|
+
--force Regenerate images even if preview exists
|
|
193
|
+
--list-missing Only list files with missing previews
|
|
194
|
+
|
|
195
|
+
ENVIRONMENT VARIABLES:
|
|
196
|
+
OPENAI_API_KEY Required for OpenAI DALL-E provider
|
|
197
|
+
STABILITY_API_KEY Required for Stability AI provider
|
|
198
|
+
IMAGE_STYLE Override style from _config.yml
|
|
199
|
+
IMAGE_SIZE Override size (default: 1792x1024 landscape)
|
|
200
|
+
IMAGE_MODEL OpenAI model (default: dall-e-3)
|
|
201
|
+
|
|
202
|
+
CONFIGURATION:
|
|
203
|
+
Default settings are loaded from _config.yml under 'preview_images' section.
|
|
204
|
+
Environment variables override config file settings.
|
|
205
|
+
|
|
206
|
+
EXAMPLES:
|
|
207
|
+
# List all files missing preview images
|
|
208
|
+
./scripts/generate-preview-images.sh --list-missing
|
|
209
|
+
|
|
210
|
+
# Dry run to see what would be generated
|
|
211
|
+
./scripts/generate-preview-images.sh --dry-run --verbose
|
|
212
|
+
|
|
213
|
+
# Generate images for posts collection
|
|
214
|
+
./scripts/generate-preview-images.sh --collection posts
|
|
215
|
+
|
|
216
|
+
# Generate image for a specific file
|
|
217
|
+
./scripts/generate-preview-images.sh -f pages/_posts/my-article.md
|
|
218
|
+
|
|
219
|
+
# Force regenerate all images
|
|
220
|
+
./scripts/generate-preview-images.sh --force
|
|
221
|
+
|
|
222
|
+
EOF
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Parse command line arguments
|
|
226
|
+
parse_args() {
|
|
227
|
+
while [[ $# -gt 0 ]]; do
|
|
228
|
+
case $1 in
|
|
229
|
+
-h|--help)
|
|
230
|
+
show_help
|
|
231
|
+
exit 0
|
|
232
|
+
;;
|
|
233
|
+
-d|--dry-run)
|
|
234
|
+
DRY_RUN="true"
|
|
235
|
+
;;
|
|
236
|
+
-v|--verbose)
|
|
237
|
+
VERBOSE="true"
|
|
238
|
+
;;
|
|
239
|
+
-f|--file)
|
|
240
|
+
SPECIFIC_FILE="$2"
|
|
241
|
+
shift
|
|
242
|
+
;;
|
|
243
|
+
-c|--collection)
|
|
244
|
+
COLLECTION="$2"
|
|
245
|
+
shift
|
|
246
|
+
;;
|
|
247
|
+
-p|--provider)
|
|
248
|
+
AI_PROVIDER="$2"
|
|
249
|
+
shift
|
|
250
|
+
;;
|
|
251
|
+
--output-dir)
|
|
252
|
+
OUTPUT_DIR="$2"
|
|
253
|
+
shift
|
|
254
|
+
;;
|
|
255
|
+
--force)
|
|
256
|
+
FORCE="true"
|
|
257
|
+
;;
|
|
258
|
+
--list-missing)
|
|
259
|
+
LIST_ONLY="true"
|
|
260
|
+
;;
|
|
261
|
+
*)
|
|
262
|
+
error "Unknown option: $1. Use --help for usage."
|
|
263
|
+
;;
|
|
264
|
+
esac
|
|
265
|
+
shift
|
|
266
|
+
done
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Validate environment and dependencies
|
|
270
|
+
validate_environment() {
|
|
271
|
+
step "Validating environment..."
|
|
272
|
+
|
|
273
|
+
# Check for required commands
|
|
274
|
+
local required_cmds=("curl" "jq")
|
|
275
|
+
for cmd in "${required_cmds[@]}"; do
|
|
276
|
+
if ! command -v "$cmd" &> /dev/null; then
|
|
277
|
+
error "Required command not found: $cmd"
|
|
278
|
+
fi
|
|
279
|
+
done
|
|
280
|
+
|
|
281
|
+
# Check for YAML parser (prefer yq, fallback to python)
|
|
282
|
+
if command -v yq &> /dev/null; then
|
|
283
|
+
YAML_PARSER="yq"
|
|
284
|
+
debug "Using yq for YAML parsing"
|
|
285
|
+
elif command -v python3 &> /dev/null; then
|
|
286
|
+
YAML_PARSER="python"
|
|
287
|
+
debug "Using python for YAML parsing"
|
|
288
|
+
else
|
|
289
|
+
error "No YAML parser found. Install yq or python3."
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# Validate AI provider credentials (unless list-only or dry-run)
|
|
293
|
+
if [[ "$LIST_ONLY" != "true" && "$DRY_RUN" != "true" ]]; then
|
|
294
|
+
case "$AI_PROVIDER" in
|
|
295
|
+
openai)
|
|
296
|
+
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
|
297
|
+
error "OPENAI_API_KEY environment variable is required for OpenAI provider"
|
|
298
|
+
fi
|
|
299
|
+
;;
|
|
300
|
+
stability)
|
|
301
|
+
if [[ -z "${STABILITY_API_KEY:-}" ]]; then
|
|
302
|
+
error "STABILITY_API_KEY environment variable is required for Stability AI provider"
|
|
303
|
+
fi
|
|
304
|
+
;;
|
|
305
|
+
local)
|
|
306
|
+
info "Using local provider - no API key required"
|
|
307
|
+
;;
|
|
308
|
+
*)
|
|
309
|
+
error "Unknown AI provider: $AI_PROVIDER"
|
|
310
|
+
;;
|
|
311
|
+
esac
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Ensure output directory exists
|
|
315
|
+
local full_output_dir="$PROJECT_ROOT/$OUTPUT_DIR"
|
|
316
|
+
if [[ ! -d "$full_output_dir" ]]; then
|
|
317
|
+
if [[ "$DRY_RUN" != "true" ]]; then
|
|
318
|
+
mkdir -p "$full_output_dir"
|
|
319
|
+
debug "Created output directory: $full_output_dir"
|
|
320
|
+
else
|
|
321
|
+
debug "Would create output directory: $full_output_dir"
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
success "Environment validation passed"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# Extract front matter from a markdown file
|
|
329
|
+
extract_front_matter() {
|
|
330
|
+
local file="$1"
|
|
331
|
+
|
|
332
|
+
# Extract content between --- markers
|
|
333
|
+
sed -n '/^---$/,/^---$/p' "$file" | sed '1d;$d'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Get YAML value using available parser
|
|
337
|
+
get_yaml_value() {
|
|
338
|
+
local yaml="$1"
|
|
339
|
+
local key="$2"
|
|
340
|
+
local result=""
|
|
341
|
+
|
|
342
|
+
if [[ "$YAML_PARSER" == "yq" ]]; then
|
|
343
|
+
# yq v4 syntax - read from stdin and get specific key
|
|
344
|
+
result=$(echo "$yaml" | yq eval ".$key" - 2>/dev/null | head -1)
|
|
345
|
+
# Filter out null values
|
|
346
|
+
if [[ "$result" == "null" || -z "$result" ]]; then
|
|
347
|
+
result=""
|
|
348
|
+
fi
|
|
349
|
+
else
|
|
350
|
+
# Python fallback
|
|
351
|
+
result=$(python3 -c "
|
|
352
|
+
import yaml
|
|
353
|
+
import sys
|
|
354
|
+
try:
|
|
355
|
+
data = yaml.safe_load('''$yaml''')
|
|
356
|
+
if data and '$key' in data:
|
|
357
|
+
val = data['$key']
|
|
358
|
+
if val is not None:
|
|
359
|
+
print(val)
|
|
360
|
+
except:
|
|
361
|
+
pass
|
|
362
|
+
" 2>/dev/null || echo "")
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
echo "$result"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# Extract post content (without front matter)
|
|
369
|
+
extract_content() {
|
|
370
|
+
local file="$1"
|
|
371
|
+
|
|
372
|
+
# Skip front matter and get content
|
|
373
|
+
awk '/^---$/ { if (++count == 2) found=1; next } found { print }' "$file"
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Check if preview image exists
|
|
377
|
+
check_preview_exists() {
|
|
378
|
+
local preview_path="$1"
|
|
379
|
+
|
|
380
|
+
if [[ -z "$preview_path" ]]; then
|
|
381
|
+
return 1
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# Handle paths starting with /
|
|
385
|
+
local clean_path="${preview_path#/}"
|
|
386
|
+
local full_path="$PROJECT_ROOT/$clean_path"
|
|
387
|
+
|
|
388
|
+
# Also check in assets/images
|
|
389
|
+
if [[ ! -f "$full_path" ]]; then
|
|
390
|
+
full_path="$PROJECT_ROOT/assets/$clean_path"
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
[[ -f "$full_path" ]]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# Generate image prompt from content
|
|
397
|
+
generate_prompt() {
|
|
398
|
+
local title="$1"
|
|
399
|
+
local description="$2"
|
|
400
|
+
local categories="$3"
|
|
401
|
+
local content="$4"
|
|
402
|
+
|
|
403
|
+
# Build a meaningful prompt
|
|
404
|
+
local prompt="Create a blog preview banner image for an article titled '$title'."
|
|
405
|
+
|
|
406
|
+
if [[ -n "$description" ]]; then
|
|
407
|
+
prompt="$prompt The article is about: $description."
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
if [[ -n "$categories" ]]; then
|
|
411
|
+
prompt="$prompt Categories: $categories."
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
# Extract key themes from content (first 500 chars)
|
|
415
|
+
local content_excerpt="${content:0:500}"
|
|
416
|
+
if [[ -n "$content_excerpt" ]]; then
|
|
417
|
+
prompt="$prompt Key themes from content: $content_excerpt"
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
# Add style instructions and modifiers
|
|
421
|
+
prompt="$prompt Art style: $IMAGE_STYLE."
|
|
422
|
+
if [[ -n "$IMAGE_STYLE_MODIFIERS" ]]; then
|
|
423
|
+
prompt="$prompt Additional style: $IMAGE_STYLE_MODIFIERS."
|
|
424
|
+
fi
|
|
425
|
+
prompt="$prompt The image should be suitable as a wide blog header/banner image with clean composition. No text or words in the image."
|
|
426
|
+
|
|
427
|
+
echo "$prompt"
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Generate image using OpenAI DALL-E
|
|
431
|
+
generate_image_openai() {
|
|
432
|
+
local prompt="$1"
|
|
433
|
+
local output_file="$2"
|
|
434
|
+
|
|
435
|
+
debug "Generating image with OpenAI DALL-E..."
|
|
436
|
+
debug "Prompt: ${prompt:0:200}..."
|
|
437
|
+
|
|
438
|
+
local response
|
|
439
|
+
response=$(curl -s -X POST "https://api.openai.com/v1/images/generations" \
|
|
440
|
+
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
441
|
+
-H "Content-Type: application/json" \
|
|
442
|
+
-d "{
|
|
443
|
+
\"model\": \"$IMAGE_MODEL\",
|
|
444
|
+
\"prompt\": $(echo "$prompt" | jq -Rs .),
|
|
445
|
+
\"n\": 1,
|
|
446
|
+
\"size\": \"$IMAGE_SIZE\",
|
|
447
|
+
\"quality\": \"$IMAGE_QUALITY\"
|
|
448
|
+
}")
|
|
449
|
+
|
|
450
|
+
# Check for errors
|
|
451
|
+
local error_msg
|
|
452
|
+
error_msg=$(echo "$response" | jq -r '.error.message // empty')
|
|
453
|
+
if [[ -n "$error_msg" ]]; then
|
|
454
|
+
warn "OpenAI API error: $error_msg"
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
# Extract image URL
|
|
459
|
+
local image_url
|
|
460
|
+
image_url=$(echo "$response" | jq -r '.data[0].url // empty')
|
|
461
|
+
|
|
462
|
+
if [[ -z "$image_url" ]]; then
|
|
463
|
+
warn "No image URL in response"
|
|
464
|
+
debug "Response: $response"
|
|
465
|
+
return 1
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
# Download image
|
|
469
|
+
debug "Downloading image from: $image_url"
|
|
470
|
+
curl -s -o "$output_file" "$image_url"
|
|
471
|
+
|
|
472
|
+
if [[ -f "$output_file" ]]; then
|
|
473
|
+
success "Image saved to: $output_file"
|
|
474
|
+
return 0
|
|
475
|
+
else
|
|
476
|
+
warn "Failed to save image"
|
|
477
|
+
return 1
|
|
478
|
+
fi
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
# Generate image using Stability AI
|
|
482
|
+
generate_image_stability() {
|
|
483
|
+
local prompt="$1"
|
|
484
|
+
local output_file="$2"
|
|
485
|
+
|
|
486
|
+
debug "Generating image with Stability AI..."
|
|
487
|
+
|
|
488
|
+
local response
|
|
489
|
+
response=$(curl -s -X POST "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image" \
|
|
490
|
+
-H "Authorization: Bearer $STABILITY_API_KEY" \
|
|
491
|
+
-H "Content-Type: application/json" \
|
|
492
|
+
-d "{
|
|
493
|
+
\"text_prompts\": [{\"text\": $(echo "$prompt" | jq -Rs .)}],
|
|
494
|
+
\"cfg_scale\": 7,
|
|
495
|
+
\"height\": 1024,
|
|
496
|
+
\"width\": 1024,
|
|
497
|
+
\"samples\": 1,
|
|
498
|
+
\"steps\": 30
|
|
499
|
+
}")
|
|
500
|
+
|
|
501
|
+
# Check for errors
|
|
502
|
+
local error_msg
|
|
503
|
+
error_msg=$(echo "$response" | jq -r '.message // empty')
|
|
504
|
+
if [[ -n "$error_msg" ]]; then
|
|
505
|
+
warn "Stability API error: $error_msg"
|
|
506
|
+
return 1
|
|
507
|
+
fi
|
|
508
|
+
|
|
509
|
+
# Extract and decode base64 image
|
|
510
|
+
local base64_image
|
|
511
|
+
base64_image=$(echo "$response" | jq -r '.artifacts[0].base64 // empty')
|
|
512
|
+
|
|
513
|
+
if [[ -z "$base64_image" ]]; then
|
|
514
|
+
warn "No image data in response"
|
|
515
|
+
return 1
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
echo "$base64_image" | base64 -d > "$output_file"
|
|
519
|
+
|
|
520
|
+
if [[ -f "$output_file" ]]; then
|
|
521
|
+
success "Image saved to: $output_file"
|
|
522
|
+
return 0
|
|
523
|
+
else
|
|
524
|
+
warn "Failed to save image"
|
|
525
|
+
return 1
|
|
526
|
+
fi
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Generate placeholder for local provider (for testing)
|
|
530
|
+
generate_image_local() {
|
|
531
|
+
local prompt="$1"
|
|
532
|
+
local output_file="$2"
|
|
533
|
+
|
|
534
|
+
warn "Local provider: No actual image generation. Creating placeholder..."
|
|
535
|
+
debug "Would generate image with prompt: ${prompt:0:200}..."
|
|
536
|
+
|
|
537
|
+
# Create a simple placeholder file
|
|
538
|
+
echo "PLACEHOLDER: $prompt" > "$output_file.txt"
|
|
539
|
+
|
|
540
|
+
info "Placeholder created: $output_file.txt"
|
|
541
|
+
return 0
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
# Generate image using selected provider
|
|
545
|
+
generate_image() {
|
|
546
|
+
local prompt="$1"
|
|
547
|
+
local output_file="$2"
|
|
548
|
+
|
|
549
|
+
case "$AI_PROVIDER" in
|
|
550
|
+
openai)
|
|
551
|
+
generate_image_openai "$prompt" "$output_file"
|
|
552
|
+
;;
|
|
553
|
+
stability)
|
|
554
|
+
generate_image_stability "$prompt" "$output_file"
|
|
555
|
+
;;
|
|
556
|
+
local)
|
|
557
|
+
generate_image_local "$prompt" "$output_file"
|
|
558
|
+
;;
|
|
559
|
+
*)
|
|
560
|
+
error "Unknown provider: $AI_PROVIDER"
|
|
561
|
+
;;
|
|
562
|
+
esac
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
# Update front matter with new preview path
|
|
566
|
+
update_front_matter() {
|
|
567
|
+
local file="$1"
|
|
568
|
+
local preview_path="$2"
|
|
569
|
+
|
|
570
|
+
debug "Updating front matter in: $file"
|
|
571
|
+
|
|
572
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
573
|
+
info "[DRY RUN] Would update preview in $file to: $preview_path"
|
|
574
|
+
return 0
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
# Create backup
|
|
578
|
+
cp "$file" "$file.bak"
|
|
579
|
+
|
|
580
|
+
# Always use sed for reliability (yq can fail on complex YAML)
|
|
581
|
+
# Check if preview field exists
|
|
582
|
+
if grep -q "^preview:" "$file"; then
|
|
583
|
+
# Update existing preview field using sed
|
|
584
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
585
|
+
# macOS sed requires empty string for -i
|
|
586
|
+
sed -i '' "s|^preview:.*|preview: $preview_path|" "$file"
|
|
587
|
+
else
|
|
588
|
+
sed -i "s|^preview:.*|preview: $preview_path|" "$file"
|
|
589
|
+
fi
|
|
590
|
+
else
|
|
591
|
+
# Add preview field after description or title
|
|
592
|
+
if grep -q "^description:" "$file"; then
|
|
593
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
594
|
+
sed -i '' "/^description:/a\\
|
|
595
|
+
preview: $preview_path" "$file"
|
|
596
|
+
else
|
|
597
|
+
sed -i "/^description:/a\\
|
|
598
|
+
preview: $preview_path" "$file"
|
|
599
|
+
fi
|
|
600
|
+
else
|
|
601
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
602
|
+
sed -i '' "/^title:/a\\
|
|
603
|
+
preview: $preview_path" "$file"
|
|
604
|
+
else
|
|
605
|
+
sed -i "/^title:/a\\
|
|
606
|
+
preview: $preview_path" "$file"
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
609
|
+
fi
|
|
610
|
+
|
|
611
|
+
# Remove backup on success
|
|
612
|
+
rm -f "$file.bak"
|
|
613
|
+
|
|
614
|
+
success "Updated front matter with preview: $preview_path"
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# Process a single file
|
|
618
|
+
process_file() {
|
|
619
|
+
local file="$1"
|
|
620
|
+
|
|
621
|
+
PROCESSED=$((PROCESSED + 1))
|
|
622
|
+
|
|
623
|
+
debug "Processing file: $file"
|
|
624
|
+
|
|
625
|
+
# Extract front matter
|
|
626
|
+
local front_matter
|
|
627
|
+
front_matter=$(extract_front_matter "$file")
|
|
628
|
+
|
|
629
|
+
if [[ -z "$front_matter" ]]; then
|
|
630
|
+
debug "No front matter found in: $file"
|
|
631
|
+
SKIPPED=$((SKIPPED + 1))
|
|
632
|
+
return 0
|
|
633
|
+
fi
|
|
634
|
+
|
|
635
|
+
# Get metadata
|
|
636
|
+
local title description categories preview
|
|
637
|
+
title=$(get_yaml_value "$front_matter" "title")
|
|
638
|
+
description=$(get_yaml_value "$front_matter" "description")
|
|
639
|
+
categories=$(get_yaml_value "$front_matter" "categories")
|
|
640
|
+
preview=$(get_yaml_value "$front_matter" "preview")
|
|
641
|
+
|
|
642
|
+
debug "Title: $title"
|
|
643
|
+
debug "Preview: $preview"
|
|
644
|
+
|
|
645
|
+
# Check if preview exists and is valid
|
|
646
|
+
if [[ -n "$preview" ]] && check_preview_exists "$preview"; then
|
|
647
|
+
if [[ "$FORCE" != "true" ]]; then
|
|
648
|
+
debug "Preview already exists and is valid: $preview"
|
|
649
|
+
SKIPPED=$((SKIPPED + 1))
|
|
650
|
+
return 0
|
|
651
|
+
else
|
|
652
|
+
info "Force mode: regenerating preview for $title"
|
|
653
|
+
fi
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
# Report missing preview
|
|
657
|
+
if [[ "$LIST_ONLY" == "true" ]]; then
|
|
658
|
+
echo -e "${YELLOW}Missing preview:${NC} $file"
|
|
659
|
+
echo -e " Title: $title"
|
|
660
|
+
if [[ -n "$preview" ]]; then
|
|
661
|
+
echo -e " Current preview (not found): $preview"
|
|
662
|
+
fi
|
|
663
|
+
echo ""
|
|
664
|
+
return 0
|
|
665
|
+
fi
|
|
666
|
+
|
|
667
|
+
info "Generating preview for: $title"
|
|
668
|
+
|
|
669
|
+
# Generate filename from title
|
|
670
|
+
local safe_filename
|
|
671
|
+
safe_filename=$(echo "$title" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
|
|
672
|
+
safe_filename="${safe_filename:0:50}" # Limit length
|
|
673
|
+
|
|
674
|
+
local output_file="$PROJECT_ROOT/$OUTPUT_DIR/${safe_filename}.png"
|
|
675
|
+
# Preview path should NOT include /assets/ prefix since the template adds it
|
|
676
|
+
local preview_path="/images/previews/${safe_filename}.png"
|
|
677
|
+
|
|
678
|
+
# Extract content for prompt generation
|
|
679
|
+
local content
|
|
680
|
+
content=$(extract_content "$file")
|
|
681
|
+
|
|
682
|
+
# Generate prompt
|
|
683
|
+
local prompt
|
|
684
|
+
prompt=$(generate_prompt "$title" "$description" "$categories" "$content")
|
|
685
|
+
|
|
686
|
+
debug "Generated prompt: ${prompt:0:500}..."
|
|
687
|
+
|
|
688
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
689
|
+
info "[DRY RUN] Would generate image:"
|
|
690
|
+
echo " Output: $output_file"
|
|
691
|
+
echo " Preview path: $preview_path"
|
|
692
|
+
echo " Prompt: ${prompt:0:400}..."
|
|
693
|
+
echo ""
|
|
694
|
+
GENERATED=$((GENERATED + 1))
|
|
695
|
+
return 0
|
|
696
|
+
fi
|
|
697
|
+
|
|
698
|
+
# Generate image
|
|
699
|
+
if generate_image "$prompt" "$output_file"; then
|
|
700
|
+
# Update front matter with new preview path
|
|
701
|
+
update_front_matter "$file" "$preview_path"
|
|
702
|
+
GENERATED=$((GENERATED + 1))
|
|
703
|
+
else
|
|
704
|
+
warn "Failed to generate image for: $title"
|
|
705
|
+
ERRORS=$((ERRORS + 1))
|
|
706
|
+
fi
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
# Find and process content files
|
|
710
|
+
process_collection() {
|
|
711
|
+
local collection_path="$1"
|
|
712
|
+
local pattern="${2:-*.md}"
|
|
713
|
+
|
|
714
|
+
debug "Processing collection: $collection_path with pattern: $pattern"
|
|
715
|
+
|
|
716
|
+
if [[ ! -d "$collection_path" ]]; then
|
|
717
|
+
warn "Collection directory not found: $collection_path"
|
|
718
|
+
return 1
|
|
719
|
+
fi
|
|
720
|
+
|
|
721
|
+
while IFS= read -r -d '' file; do
|
|
722
|
+
process_file "$file"
|
|
723
|
+
done < <(find "$collection_path" -name "$pattern" -type f -print0)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
# Main function
|
|
727
|
+
main() {
|
|
728
|
+
print_header "🎨 Preview Image Generator"
|
|
729
|
+
|
|
730
|
+
# Validate environment
|
|
731
|
+
validate_environment
|
|
732
|
+
|
|
733
|
+
# Display configuration
|
|
734
|
+
info "Configuration:"
|
|
735
|
+
echo " AI Provider: $AI_PROVIDER"
|
|
736
|
+
echo " Output Dir: $OUTPUT_DIR"
|
|
737
|
+
echo " Image Size: $IMAGE_SIZE"
|
|
738
|
+
echo " Dry Run: $DRY_RUN"
|
|
739
|
+
echo " Force: $FORCE"
|
|
740
|
+
echo " List Only: $LIST_ONLY"
|
|
741
|
+
echo ""
|
|
742
|
+
|
|
743
|
+
# Get configured collections from _config.yml
|
|
744
|
+
get_configured_collections() {
|
|
745
|
+
local collections=()
|
|
746
|
+
local in_preview_images=false
|
|
747
|
+
local in_collections=false
|
|
748
|
+
|
|
749
|
+
while IFS= read -r line; do
|
|
750
|
+
if [[ "$line" =~ ^preview_images: ]]; then
|
|
751
|
+
in_preview_images=true
|
|
752
|
+
continue
|
|
753
|
+
fi
|
|
754
|
+
if [[ "$in_preview_images" == true && "$line" =~ ^[[:space:]]+collections: ]]; then
|
|
755
|
+
in_collections=true
|
|
756
|
+
continue
|
|
757
|
+
fi
|
|
758
|
+
if [[ "$in_collections" == true && "$line" =~ ^[[:space:]]+- ]]; then
|
|
759
|
+
local collection="${line#*- }"
|
|
760
|
+
collection="${collection%%#*}"
|
|
761
|
+
collection="${collection%"${collection##*[![:space:]]}"}"
|
|
762
|
+
collections+=("$collection")
|
|
763
|
+
elif [[ "$in_collections" == true && ! "$line" =~ ^[[:space:]]+- && ! "$line" =~ ^[[:space:]]*$ ]]; then
|
|
764
|
+
break
|
|
765
|
+
fi
|
|
766
|
+
if [[ "$in_preview_images" == true && "$line" =~ ^[a-zA-Z_]+: && ! "$line" =~ ^[[:space:]] ]]; then
|
|
767
|
+
break
|
|
768
|
+
fi
|
|
769
|
+
done < "$CONFIG_FILE"
|
|
770
|
+
|
|
771
|
+
if [[ ${#collections[@]} -eq 0 ]]; then
|
|
772
|
+
collections=("posts" "quickstart" "docs")
|
|
773
|
+
fi
|
|
774
|
+
|
|
775
|
+
echo "${collections[@]}"
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
# Process a collection by name
|
|
779
|
+
process_collection_by_name() {
|
|
780
|
+
local name="$1"
|
|
781
|
+
local path="$PROJECT_ROOT/pages/_${name}"
|
|
782
|
+
|
|
783
|
+
if [[ -d "$path" ]]; then
|
|
784
|
+
step "Processing ${name} collection..."
|
|
785
|
+
process_collection "$path"
|
|
786
|
+
else
|
|
787
|
+
warn "Collection directory not found: $path"
|
|
788
|
+
fi
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
# Process files
|
|
792
|
+
if [[ -n "$SPECIFIC_FILE" ]]; then
|
|
793
|
+
# Process single file
|
|
794
|
+
if [[ ! -f "$PROJECT_ROOT/$SPECIFIC_FILE" ]]; then
|
|
795
|
+
error "File not found: $SPECIFIC_FILE"
|
|
796
|
+
fi
|
|
797
|
+
process_file "$PROJECT_ROOT/$SPECIFIC_FILE"
|
|
798
|
+
elif [[ -n "$COLLECTION" ]]; then
|
|
799
|
+
# Process specific collection
|
|
800
|
+
if [[ "$COLLECTION" == "all" ]]; then
|
|
801
|
+
step "Processing all configured collections..."
|
|
802
|
+
for col in $(get_configured_collections); do
|
|
803
|
+
process_collection_by_name "$col"
|
|
804
|
+
done
|
|
805
|
+
else
|
|
806
|
+
# Check if collection directory exists
|
|
807
|
+
local collection_path="$PROJECT_ROOT/pages/_${COLLECTION}"
|
|
808
|
+
if [[ -d "$collection_path" ]]; then
|
|
809
|
+
process_collection_by_name "$COLLECTION"
|
|
810
|
+
else
|
|
811
|
+
local available=$(get_configured_collections | tr ' ' ', ')
|
|
812
|
+
error "Unknown collection: $COLLECTION. Available: $available, all"
|
|
813
|
+
fi
|
|
814
|
+
fi
|
|
815
|
+
else
|
|
816
|
+
# Process all configured collections by default
|
|
817
|
+
step "Processing all configured collections..."
|
|
818
|
+
for col in $(get_configured_collections); do
|
|
819
|
+
process_collection_by_name "$col"
|
|
820
|
+
done
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
# Print summary
|
|
824
|
+
echo ""
|
|
825
|
+
print_header "📊 Summary"
|
|
826
|
+
echo " Files processed: $PROCESSED"
|
|
827
|
+
echo " Images generated: $GENERATED"
|
|
828
|
+
echo " Files skipped: $SKIPPED"
|
|
829
|
+
echo " Errors: $ERRORS"
|
|
830
|
+
echo ""
|
|
831
|
+
|
|
832
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
833
|
+
info "This was a dry run. No actual changes were made."
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
if [[ "$ERRORS" -gt 0 ]]; then
|
|
837
|
+
warn "Some files had errors. Check the output above."
|
|
838
|
+
exit 1
|
|
839
|
+
fi
|
|
840
|
+
|
|
841
|
+
success "Preview image generation complete!"
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
# Parse arguments and run
|
|
845
|
+
parse_args "$@"
|
|
846
|
+
main
|