Skip to content

Commit 662cea6

Browse files
Merge pull request #188 from CodeForPhilly/TOOL/zbl-studioimagepipeline
Tool/zbl studioimagepipeline
2 parents 883f9f1 + 95a3788 commit 662cea6

16 files changed

+1532
-89
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#google api key for nano banana
2+
GOOG_API_KEY=
3+
14
# =============================================================================
25
# DEVELOPMENT MODE CONFIGURATION
36
# =============================================================================

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,16 @@ mongodb://[username]:[password]@localhost:27017/pa-wildflower-selector?authSourc
465465
- `npm run check-mongodb` - Verify MongoDB connection (via scripts/check-mongodb.js)
466466
- `npm run predev:local` - Run development environment checks before starting local dev
467467

468+
### Studio plant images (dev-only)
469+
470+
This repo includes a small **dev-only** pipeline to generate “studio” plant images (single plant, side view, white background) from photos in `images/`.
471+
472+
- Script: `scripts/studio_images/generate_studio_images.py`
473+
- Installs: `pip install -r scripts/studio_images/requirements.txt`
474+
- Runs: `python scripts/studio_images/generate_studio_images.py --input-dir images --output-dir images/studio_full`
475+
476+
See `scripts/studio_images/README.md` for details.
477+
468478
### Project Structure
469479

470480
- **UI Code**: Located in `src/` directory

lib/server.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,72 @@ module.exports = async function ({ plants, nurseries }) {
7878
if (fs.existsSync(imagesDir)) {
7979
app.use("/images", express.static(imagesDir));
8080
}
81+
82+
// Resolve plant image based on the requested mode, with graceful fallback.
83+
// This avoids client-side 404 handling for background images and centralizes
84+
// the mapping between habitat (images/) and studio (images/studio_full/).
85+
//
86+
// Usage:
87+
// - /api/v1/plant-image/:id?mode=habitat&preview=1
88+
// - /api/v1/plant-image/:id?mode=studio&preview=0
89+
app.get("/api/v1/plant-image/:id", async (req, res) => {
90+
try {
91+
const id = String(req.params.id || "");
92+
// Basic traversal protection (allow spaces/etc, but disallow path tricks)
93+
if (!id || id.includes("..") || id.includes("/") || id.includes("\\")) {
94+
return res.status(400).json({ error: "Invalid plant id" });
95+
}
96+
97+
const mode = String(req.query.mode || "habitat") === "studio" ? "studio" : "habitat";
98+
const preview = String(req.query.preview || "0") === "1";
99+
100+
const studioDir = path.join(imagesDir, "studio_full");
101+
/** @type {string[]} */
102+
const candidatePaths = [];
103+
104+
if (mode === "studio") {
105+
// Studio images are generated with the same stem as the input (often the plant _id).
106+
// Support multiple formats since the generator can be configured.
107+
candidatePaths.push(
108+
path.join(studioDir, `${id}.webp`),
109+
path.join(studioDir, `${id}.jpg`),
110+
path.join(studioDir, `${id}.jpeg`),
111+
path.join(studioDir, `${id}.png`)
112+
);
113+
} else {
114+
// Habitat photos live at the root of /images
115+
if (preview) {
116+
candidatePaths.push(
117+
path.join(imagesDir, `${id}.preview.jpg`),
118+
path.join(imagesDir, `${id}.jpg`)
119+
);
120+
} else {
121+
candidatePaths.push(path.join(imagesDir, `${id}.jpg`));
122+
}
123+
}
124+
125+
let chosen = null;
126+
for (const p of candidatePaths) {
127+
if (p && fs.existsSync(p)) {
128+
chosen = p;
129+
break;
130+
}
131+
}
132+
133+
if (chosen) {
134+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
135+
return res.sendFile(chosen);
136+
}
137+
138+
// Fallback to the public missing image asset
139+
const missing = path.join(publicDir, "assets", "images", "missing-image.png");
140+
res.setHeader("Cache-Control", "public, max-age=300");
141+
return res.sendFile(missing);
142+
} catch (e) {
143+
console.error("error in /api/v1/plant-image:", e);
144+
return res.status(500).json({ error: "Failed to resolve plant image" });
145+
}
146+
});
81147

82148
app.use(express.json());
83149

scripts/ensure-plants-indexes.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,25 @@ main().catch((err) => {
6767

6868

6969

70+
71+
72+
73+
74+
75+
76+
77+
78+
79+
80+
81+
82+
83+
84+
85+
86+
87+
88+
89+
90+
91+

scripts/height-spread-source-policy.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,25 @@
100100
}
101101

102102

103+
104+
105+
106+
107+
108+
109+
110+
111+
112+
113+
114+
115+
116+
117+
118+
119+
120+
121+
122+
123+
124+

scripts/migrate-plants-clean-data.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,28 @@ main().catch((err) => {
8686

8787

8888

89+
90+
91+
92+
93+
94+
95+
96+
97+
98+
99+
100+
101+
102+
103+
104+
105+
106+
107+
108+
109+
110+
89111

90112

91113

scripts/studio_images/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Studio image generator (dev-only)
2+
3+
This folder contains a **dev-only** script that generates 2D “studio” images (single plant, side view, white background) from the raw photos in `images/`.
4+
5+
## Setup (Windows)
6+
7+
1. Ensure your repo root `.env` contains:
8+
- `GOOG_API_KEY=...`
9+
10+
2. Create a virtualenv and install deps:
11+
12+
```bash
13+
cd pa-wildflower-selector
14+
python -m venv .venv
15+
.venv\Scripts\activate
16+
pip install -r scripts/studio_images/requirements.txt
17+
```
18+
19+
## Run
20+
21+
Generate studio images into `images/studio_full` (skips ones already present):
22+
23+
```bash
24+
python scripts/studio_images/generate_studio_images.py --input-dir images --output-dir images/studio_full
25+
```
26+
27+
Higher quality (still fast model) tips:
28+
29+
- Preserve model output format (often PNG) to avoid extra compression:
30+
31+
```bash
32+
python scripts/studio_images/generate_studio_images.py --output-format keep
33+
```
34+
35+
- If using WebP, bump quality (bigger files):
36+
37+
```bash
38+
python scripts/studio_images/generate_studio_images.py --webp-quality 96
39+
```
40+
41+
By default, outputs are written as **`.webp`** (smaller + modern for web). You can change this:
42+
43+
```bash
44+
python scripts/studio_images/generate_studio_images.py --output-format jpg
45+
```
46+
47+
Dry-run (no API calls, no writes):
48+
49+
```bash
50+
python scripts/studio_images/generate_studio_images.py --dry-run --limit 10
51+
```
52+
53+
Use a custom prompt without editing the script:
54+
55+
```bash
56+
python scripts/studio_images/generate_studio_images.py --prompt-file scripts/studio_images/prompt.txt
57+
```
58+
59+
## Cropping (reduce whitespace)
60+
61+
By default the script **auto-crops** the generated image by trimming near-white margins, then adds a small padding.
62+
63+
- Disable: `--no-autocrop`
64+
- Crop algorithm: `--crop-mode bg-diff` (default) or `--crop-mode near-white`
65+
- Tune aggressiveness:
66+
- `--crop-mode bg-diff`: `--crop-threshold 8` (tighter) or `--crop-threshold 16` (looser)
67+
- `--crop-mode near-white`: `--crop-threshold 245` (looser) or `--crop-threshold 252` (tighter)
68+
- Tune padding: `--crop-padding 12`
69+
70+
Example:
71+
72+
```bash
73+
python scripts/studio_images/generate_studio_images.py --overwrite --crop-threshold 252 --crop-padding 12
74+
```
75+
76+
## Notes
77+
78+
- Inputs: `*.jpg`, `*.jpeg`, `*.png` in `images/`.
79+
- Skips: `*.preview.jpg` (unless you pass `--no-skip-preview`) and anything already generated in `images/studio_full/`.
80+
- Output format defaults to `.webp`. Use `--output-format keep` to preserve the model output type.
81+
Binary file not shown.

0 commit comments

Comments
 (0)