From bd7278569eec2bf397d735fbc671ad61d18e603d Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Thu, 21 May 2026 14:41:42 -0400 Subject: [PATCH] chore: add .gitignore and docs/specs/2026-05-21-recipe-images-design.md --- .gitignore | 1 + docs/plans/2026-05-21-recipe-images-plan.md | 450 ++++++++++++++++++ docs/specs/2026-05-21-recipe-images-design.md | 71 +++ 3 files changed, 522 insertions(+) create mode 100644 .gitignore create mode 100644 docs/plans/2026-05-21-recipe-images-plan.md create mode 100644 docs/specs/2026-05-21-recipe-images-design.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e458ed5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.worktrees/ diff --git a/docs/plans/2026-05-21-recipe-images-plan.md b/docs/plans/2026-05-21-recipe-images-plan.md new file mode 100644 index 0000000..253f3e0 --- /dev/null +++ b/docs/plans/2026-05-21-recipe-images-plan.md @@ -0,0 +1,450 @@ +# Recipe Image Ingestion Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give every `.cook` recipe file an `image:` field in YAML frontmatter pointing to `https://roux.roo.lol/recipe-images/.webp`, and download each image locally. + +**Architecture:** Three parallel batches — (1) relink the 19 existing images, (2) discover + download + link 48 missing images (recipes with frontmatter), (3) create frontmatter + discover + link 9 more (recipes without frontmatter). Use subagent parallel dispatch for image discovery to keep wall-clock time manageable. + +**Tech Stack:** bash, curl, ImageMagick (convert), subagent researcher for web image search + +--- + +### Task 0: Setup — slug map, image dir, helper script + +**Files:** +- Create: `/home/jbrechtel/recipes/docs/slugs.csv` +- Modify: none yet + +- [ ] **Step 1: Generate slug mapping** + +Run a one-liner that prints every `.cook` file with its slugified stem: + +```bash +cd /home/jbrechtel/recipes +for f in *.cook; do + stem="${f%.cook}" + slug=$(echo "$stem" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//') + echo "$f|$slug" +done > docs/slugs.csv +head docs/slugs.csv +``` + +Expected: a pipe-delimited CSV like `Alain-Ducase-Bread.cook|alain-ducase-bread`. + +- [ ] **Step 2: Create image directory** + +```bash +mkdir -p /home/jbrechtel/recipes/recipe-images +``` + +- [ ] **Step 3: Classify each recipe** + +```bash +cd /home/jbrechtel/recipes +echo "=== Already has image ===" +while IFS='|' read -r f slug; do + if grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi +done < docs/slugs.csv > docs/has-image.csv + +echo "=== Has frontmatter, no image ===" +while IFS='|' read -r f slug; do + if head -1 "$f" 2>/dev/null | grep -q "^---"; then + if ! grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi + fi +done < docs/slugs.csv > docs/no-image-has-fm.csv + +echo "=== No frontmatter ===" +while IFS='|' read -r f slug; do + if ! head -1 "$f" 2>/dev/null | grep -q "^---"; then echo "$f|$slug"; fi +done < docs/slugs.csv > docs/no-fm.csv + +wc -l docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv +``` + +Expected: 19 / 48 / 9 lines respectively. + +- [ ] **Step 4: Commit setup** + +```bash +cd /home/jbrechtel/recipes +git add docs/slugs.csv docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv +git commit -m "chore: add recipe slug mapping and classification CSVs" +``` + +--- + +### Task 1: Download & relink existing images (19 recipes) + +**Files:** +- Modify: each of the 19 `.cook` files listed in `docs/has-image.csv` +- Create: `recipe-images/.webp` for each + +- [ ] **Step 1: Write batch relink script** + +```bash +cd /home/jbrechtel/recipes +cat > bin/batch-relink.sh << 'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +while IFS='|' read -r file slug; do + url=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//') + echo "[$slug] Downloading from $url" + curl -sL -o "recipe-images/$slug.webp" "$url" || echo "[$slug] DOWNLOAD FAILED" +done +SCRIPT +chmod +x bin/batch-relink.sh +``` + +- [ ] **Step 2: Run relink batch** + +```bash +cd /home/jbrechtel/recipes +bash bin/batch-relink.sh < docs/has-image.csv +ls recipe-images/ | wc -l +``` + +Expected: 19 `.webp` files in `recipe-images/`. + +- [ ] **Step 3: Verify all downloads succeeded** + +```bash +cd /home/jbrechtel/recipes +while IFS='|' read -r file slug; do + if [ ! -f "recipe-images/$slug.webp" ]; then + echo "MISSING: $slug.webp" + fi +done < docs/has-image.csv +``` + +Expected: no output (all present). + +- [ ] **Step 4: Update image fields in the 19 .cook files** + +Use a script to replace each existing `image:` line with the new URL: + +```bash +cd /home/jbrechtel/recipes +cat > bin/update-image-field.sh << 'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +while IFS='|' read -r file slug; do + new_url="https://roux.roo.lol/recipe-images/$slug.webp" + # Replace the image line in-place using sed + sed -i "s|^image:.*|image: $new_url|" "$file" + echo "[$slug] Updated image field" +done +SCRIPT +chmod +x bin/update-image-field.sh +bash bin/update-image-field.sh < docs/has-image.csv +``` + +- [ ] **Step 5: Verify the updates** + +```bash +cd /home/jbrechtel/recipes +while IFS='|' read -r file slug; do + expected="https://roux.roo.lol/recipe-images/$slug.webp" + actual=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//') + if [ "$actual" != "$expected" ]; then + echo "MISMATCH: $file — expected $expected, got $actual" + fi +done < docs/has-image.csv +``` + +Expected: no output (all match). + +- [ ] **Step 6: Commit** + +```bash +cd /home/jbrechtel/recipes +git add recipe-images/ bin/ +git commit -m "feat: download and relink existing recipe images to roux.roo.lol" +``` + +--- + +### Task 2: Find & download images for 48 recipes with frontmatter but no image + +**Files:** +- Modify: each of the 48 `.cook` files listed in `docs/no-image-has-fm.csv` +- Create: `recipe-images/.webp` for each + +Approach: Use parallel subagent dispatches, each subagent handling a small batch of recipes. The subagent will search the web for an appropriate image URL for each recipe, then the parent session downloads and links. + +- [ ] **Step 1: Split the 48 recipes into manageable batches** + +```bash +cd /home/jbrechtel/recipes +split -l 8 docs/no-image-has-fm.csv docs/batch-no-image- +ls docs/batch-no-image-* +``` + +Expected: 6 files (48 ÷ 8 = 6 batches). + +- [ ] **Step 2: Dispatch parallel subagents for image discovery** + +For each batch file, dispatch a researcher subagent with task: + +> "For each recipe filename and slug in `docs/`, search the web to find ONE publicly accessible image URL of the finished dish that I can download with curl. Return the result as pipe-delimited lines: `|` or `|NOT FOUND` if no good image is found." + +Each subagent outputs to `docs/batch-no-image--results.txt`. + +Run 6 subagents in parallel (or sequentially if parallelism is limited). + +- [ ] **Step 3: Download all discovered images** + +Collect all results and download: + +```bash +cd /home/jbrechtel/recipes +cat docs/batch-no-image-*-results.txt | while IFS='|' read -r slug url; do + if [ "$url" != "NOT FOUND" ] && [ -n "$url" ]; then + echo "[$slug] Downloading $url" + curl -sL -o "recipe-images/$slug.webp" "$url" || echo "[$slug] DOWNLOAD FAILED" + else + echo "[$slug] SKIPPED — no image found" + fi +done +``` + +- [ ] **Step 4: Convert any non-WebP images** + +```bash +cd /home/jbrechtel/recipes +for img in recipe-images/*; do + mime=$(file --mime-type -b "$img") + if [ "$mime" != "image/webp" ]; then + convert "$img" "$img" 2>/dev/null || true + fi +done +``` + +(ImageMagick's `convert` re-encodes to match extension. The `file` check is advisory; `convert` handles format detection.) + +- [ ] **Step 5: Add image field to each .cook file** + +For each line in `docs/no-image-has-fm.csv`: + +```bash +cd /home/jbrechtel/recipes +cat > bin/add-image-field.sh << 'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +while IFS='|' read -r file slug; do + # Only proceed if an image was actually downloaded + if [ -f "recipe-images/$slug.webp" ]; then + new_url="https://roux.roo.lol/recipe-images/$slug.webp" + # Insert image: line after the opening --- or after the last metadata field + # Find the line number of the closing --- and insert before it + awk -v url="$new_url" ' + /^---/ && !seen { seen=1; print; next } + seen && /^---/ { print "image: " url; print; exit } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" + echo "[$slug] Added image field" + else + echo "[$slug] SKIPPED — no downloaded image" + fi +done +SCRIPT +chmod +x bin/add-image-field.sh +bash bin/add-image-field.sh < docs/no-image-has-fm.csv +``` + +- [ ] **Step 6: Verify** + +```bash +cd /home/jbrechtel/recipes +while IFS='|' read -r file slug; do + expected="https://roux.roo.lol/recipe-images/$slug.webp" + actual=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//') + if [ "$actual" != "$expected" ]; then + echo "MISSING or MISMATCH: $file" + fi +done < docs/no-image-has-fm.csv +``` + +Expected: no output (all match). + +- [ ] **Step 7: Commit** + +```bash +cd /home/jbrechtel/recipes +git add recipe-images/ bin/ docs/batch-no-image-*-results.txt +git commit -m "feat: add images to 48 recipes with frontmatter" +``` + +--- + +### Task 3: Add frontmatter + images for 9 recipes without frontmatter + +**Files:** +- Modify: each of the 9 `.cook` files listed in `docs/no-fm.csv` +- Create: `recipe-images/.webp` for each + +- [ ] **Step 1: Inspect each no-frontmatter recipe to determine a title** + +```bash +cd /home/jbrechtel/recipes +while IFS='|' read -r file slug; do + echo "=== $file ===" + head -5 "$file" + echo "" +done < docs/no-fm.csv +``` + +For each, determine a reasonable title. Use the filename stem (de-slugified) as the default title, or the first substantive line of the recipe. + +- [ ] **Step 2: Dispatch subagent to find images for these 9 recipes** + +Similar to Task 2 Step 2, but for the smaller batch. Either one batch of 9 or use the same split approach. + +- [ ] **Step 3: Download images** + +Same as Task 2 Step 3. + +- [ ] **Step 4: Add YAML frontmatter + image field to each file** + +For each file, prepend: + +```yaml +--- +title: +description: +servings: 1 +category: uncategorized +source: Unknown +image: https://roux.roo.lol/recipe-images/.webp +--- +``` + +Using a script: + +```bash +cd /home/jbrechtel/recipes +cat > bin/add-frontmatter.sh << 'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +# Input: pipe-delimited file|slug|title +while IFS='|' read -r file slug title; do + if [ ! -f "recipe-images/$slug.webp" ]; then + echo "[$slug] SKIPPED — no image downloaded" + continue + fi + url="https://roux.roo.lol/recipe-images/$slug.webp" + cat > "${file}.tmp" << EOF +--- +title: $title +description: +servings: 1 +category: uncategorized +source: Unknown +image: $url +--- + +$(cat "$file") +EOF + mv "${file}.tmp" "$file" + echo "[$slug] Added frontmatter" +done +SCRIPT +chmod +x bin/add-frontmatter.sh +``` + +- [ ] **Step 5: Run the frontmatter addition** + +First create the input with detected titles. The title for each should be extracted from the recipe content or derived from the filename. For example: +- `Caramel Cake.cook` → `Caramel Cake` +- `example-fried-rice.cook` → `Fried Rice` +- `shrimp-feta.cook` → `Shrimp Feta` +etc. + +```bash +cd /home/jbrechtel/recipes +# Create a pipe-delimited file: filename|slug|title +while IFS='|' read -r file slug; do + # Extract a reasonable title from the recipe content + # Use the filename stem as a fallback + stem="${file%.cook}" + title="${stem//_/ }" + title="${title//-/ }" + echo "$file|$slug|$title" +done < docs/no-fm.csv > docs/no-fm-with-titles.csv + +bash bin/add-frontmatter.sh < docs/no-fm-with-titles.csv +``` + +- [ ] **Step 6: Verify** + +```bash +cd /home/jbrechtel/recipes +while IFS='|' read -r file slug _; do + if ! head -1 "$file" | grep -q "^---"; then + echo "NO FRONTMATTER: $file" + elif ! grep -q "^image:" "$file"; then + echo "NO IMAGE: $file" + fi +done < docs/no-fm.csv +``` + +Expected: no output (all have frontmatter and image). + +- [ ] **Step 7: Commit** + +```bash +cd /home/jbrechtel/recipes +git add recipe-images/ bin/ docs/no-fm-with-titles.csv +git commit -m "feat: add frontmatter and images to 9 recipes without frontmatter" +``` + +--- + +### Task 4: Verification & cleanup + +- [ ] **Step 1: Final full audit** + +```bash +cd /home/jbrechtel/recipes +all_ok=true +for f in *.cook; do + stem="${f%.cook}" + slug=$(echo "$stem" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//') + expected="https://roux.roo.lol/recipe-images/$slug.webp" + + if ! grep -q "^image: $expected" "$f"; then + echo "FAIL: $f — missing or wrong image field" + all_ok=false + fi + if [ ! -f "recipe-images/$slug.webp" ]; then + echo "FAIL: recipe-images/$slug.webp does not exist" + all_ok=false + fi +done +if $all_ok; then + echo "ALL 78 RECIPES VERIFIED OK" +fi +``` + +- [ ] **Step 2: Clean up temp files** + +```bash +cd /home/jbrechtel/recipes +rm -f docs/batch-no-image-* docs/slugs.csv docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv docs/no-fm-with-titles.csv +``` + +- [ ] **Step 3: Final commit** + +```bash +cd /home/jbrechtel/recipes +git add -A +git commit -m "chore: cleanup temp files after recipe image ingestion" +``` diff --git a/docs/specs/2026-05-21-recipe-images-design.md b/docs/specs/2026-05-21-recipe-images-design.md new file mode 100644 index 0000000..9d63123 --- /dev/null +++ b/docs/specs/2026-05-21-recipe-images-design.md @@ -0,0 +1,71 @@ +# Recipe Image Ingestion Spec + +**Goal:** Give every `.cook` recipe file in the repository a high-quality, relevant food image hosted at `https://roux.roo.lol/recipe-images/` and referenced in its YAML frontmatter. + +## Scope + +| Category | Count | Action | +|---|---|---| +| Has YAML frontmatter + already has `image:` | 19 | Download existing image, re-host, update URL | +| Has YAML frontmatter + no `image:` | 48 | Find + download image, add `image:` field | +| No YAML frontmatter | 9 | Add frontmatter with title, find + download image, add `image:` field | +| **Total** | **76** | | + +## Image Filename Convention + +Slugify the `.cook` filename stem: + +1. Strip `.cook` extension +2. Lowercase +3. Replace runs of non-alphanumeric characters (spaces, punctuation, `&`, etc.) with a single hyphen +4. Strip leading/trailing hyphens +5. Append `.webp` + +**Examples:** +- `Black Bean and Corn Salad.cook` → `black-bean-and-corn-salad.webp` +- `BUTTERNUT & TURKEY CHILI with sweet peppers, spiced pepitas & queso fresco.cook` → `butternut-turkey-chili-with-sweet-peppers-spiced-pepitas-queso-fresco.webp` +- `Chickpea_Butter_Curry.cook` → `chickpea-butter-curry.webp` + +## Image URL Format + +``` +https://roux.roo.lol/recipe-images/.webp +``` + +## Image Sourcing + +### For recipes that already have an `image:` field +- Download the image from its current URL (e.g., `https://kitchen.roo.lol/data/images/.webp`) +- Save locally as `.webp` +- Update the `image:` field to `https://roux.roo.lol/recipe-images/.webp` + +### For recipes without an `image:` field +- Use the recipe title (from frontmatter or content) to search for a relevant, free-to-use food image online +- Prefer reputable recipe sites, Unsplash/Pexels, or other permissively-licensed sources +- Download as `.webp` +- Add `image: https://roux.roo.lol/recipe-images/.webp` to the frontmatter + +### For recipes without frontmatter +- Parse the recipe content to determine a title (from context, first line, or filename) +- Construct minimal YAML frontmatter: `---`, `title: `, `---` +- Find and download image as above +- Insert frontmatter + image field above the recipe body + +## Implementation Approach + +1. **Batch by category** to minimize context switching: + - Batch 1: 19 existing-image recipes (download + relink) + - Batch 2: 48 no-image recipes with frontmatter (find + download + add field) + - Batch 3: 9 no-frontmatter recipes (create frontmatter + find + download + add field) + +2. **For image discovery**, use a web-search agent to find an appropriate image URL for each recipe, then download via `curl`. + +3. **Parallelize** image discovery across multiple subagent workers to keep total time reasonable. + +## Edge Cases + +- **Duplicate-ish recipes** (e.g., `butternut-turkey-chili.cook`, `Butternut Turkey Chili.cook`, `BUTTERNUT & TURKEY CHILI...`): Each gets its own image slug. They may share similar images, but each file gets its own entry. +- **Image download failure**: If an image URL is unreachable (for existing images) or no suitable image is found (for new ones), log the recipe slug and skip it. +- **Special characters in filenames**: Slugify handles `&`, `'`, `(`, `)`, etc. by collapsing them into hyphens. +- **Existing image URLs that are already in the target format**: Skip re-download (they're already correct). +- **Non-WebP source images**: Downloaded images should be saved as `.webp`. If the source is not WebP, use `ffmpeg` or `convert` to re-encode.