Files
print_forge/scripts/refresh-public-photos.py
T
westfarn 1ca5a757d9
Deploy Beta / unit-tests (push) Successful in 31s
Deploy Beta / docker (push) Failing after 34s
Deploy Beta / deploy-beta (push) Skipped
Ship Print Forge shop site (colors, photos, 3D viewer) (#2)
## Summary

- Rebrand the Django client template as Print Forge (`client_site` → `print_forge`) with shop + shipping enabled.
- Product listings support multiple photos, color variants (shared price/description/STL, per-color stock and photos), and a photo-first / 3D-second gallery.
- Public pages use 3D printer / printed-toy photography instead of leftover t-shirt mockups; beta CI deploys on `master`.

Closes #1
Infra: [server-infra#27](ai_ml_operations/server-infra#27) (easy deploy beta).

## Test plan

- [ ] Product page shows photo first, 3D model second; color swatches swap photos and stock
- [ ] Portal can upload multiple photos and per-color qty/images; STL stays shared
- [ ] Public home/about/gallery have no t-shirt mockups
- [ ] `manage.py test` passes
- [ ] After server-infra#27: beta deploy to `print-forge-preview.aimloperations.com`

Reviewed-on: #2
2026-09-06 18:40:42 -07:00

175 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Replace public-site t-shirt mockups with 3D printer / printed-toy photos."""
from __future__ import annotations
import io
import urllib.request
from pathlib import Path
from PIL import Image, ImageOps
ROOT = Path(__file__).resolve().parent.parent
IMAGES = ROOT / "site" / "print_forge" / "static" / "images"
CACHE = ROOT / ".pexels-cache"
USER_HERO = [
Path(
"/home/westfarn/.cursor/projects/home-westfarn-Documents-repos-print-forge/assets/"
"pexels-jakubzerdzicki-20688553-82987c3e-5bc2-4514-9eb0-2eb9518d5d63.png"
),
Path(
"/home/westfarn/.cursor/projects/home-westfarn-Documents-repos-print-forge/assets/"
"pexels-jakubzerdzicki-24859620-43b11020-90b7-4d02-9c78-d28b2cc14a4e.png"
),
]
PEXELS = {
"figurine": 13624760,
"miniatures": 35063297,
"tray": 19583534,
"neon": 30720501,
"extruder": 31336922,
"spool": 30658376,
"lab": 23533991,
"neon2": 18296466,
"workshop": 30482193,
"nozzle": 23533982,
"hex": 30620861,
"orange_parts": 20688553,
"maker": 24859620,
}
UA = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
)
def fetch_pexels(photo_id: int) -> Path:
CACHE.mkdir(parents=True, exist_ok=True)
dest = CACHE / f"pexels-{photo_id}.jpg"
if dest.exists() and dest.stat().st_size > 20_000:
return dest
url = (
f"https://images.pexels.com/photos/{photo_id}/"
f"pexels-photo-{photo_id}.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=2000"
)
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "image/*"})
with urllib.request.urlopen(req, timeout=60) as resp:
data = resp.read()
if len(data) < 5000:
raise RuntimeError(f"Pexels {photo_id} too small ({len(data)} bytes)")
dest.write_bytes(data)
return dest
def open_image(path: Path) -> Image.Image:
image = Image.open(path)
image.load()
image = ImageOps.exif_transpose(image) or image
return image.convert("RGB")
def cover_crop(image: Image.Image, width: int, height: int, focus: str = "center") -> Image.Image:
src_w, src_h = image.size
scale = max(width / src_w, height / src_h)
new_w = max(width, int(src_w * scale + 0.5))
new_h = max(height, int(src_h * scale + 0.5))
resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
if focus == "left":
left = 0
elif focus == "right":
left = new_w - width
else:
left = (new_w - width) // 2
if focus == "top":
top = 0
elif focus == "bottom":
top = new_h - height
else:
top = (new_h - height) // 2
return resized.crop((left, top, left + width, top + height))
def save_jpeg(image: Image.Image, dest: Path, quality: int = 86) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
image.save(dest, format="JPEG", quality=quality, optimize=True, progressive=True)
def save_png(image: Image.Image, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
image.save(dest, format="PNG", optimize=True)
def main() -> None:
sources: dict[str, Image.Image] = {
"hero1": open_image(USER_HERO[0]),
"hero2": open_image(USER_HERO[1]),
}
failed = []
for name, photo_id in PEXELS.items():
try:
sources[name] = open_image(fetch_pexels(photo_id))
print(f"ok {name} {photo_id} {sources[name].size}")
except Exception as exc: # noqa: BLE001
failed.append(f"{name}/{photo_id}: {exc}")
print(f"fail {name} {photo_id}: {exc}")
def src(*names: str) -> Image.Image:
for name in names:
if name in sources:
return sources[name]
raise KeyError(names)
jobs = [
("slider-image-01-1132x657.jpg", src("hero1"), (1132, 657), "center", "jpeg"),
("slider-image-02-1132x657.jpg", src("hero2"), (1132, 657), "center", "jpeg"),
("box-nina-img-364x474.jpg", src("figurine", "tray", "hero1"), (364, 474), "center", "jpeg"),
("box-nina-img-mini-217x328.jpg", src("spool", "extruder", "hero2"), (217, 328), "center", "jpeg"),
("gallery-22-1200x800-original.jpg", src("figurine", "hero1"), (1200, 800), "center", "jpeg"),
("gallery-22-582x299.jpg", src("figurine", "hero1"), (582, 299), "center", "jpeg"),
("gallery-22-582x558.jpg", src("workshop", "hero2"), (582, 558), "center", "jpeg"),
("gallery-4-1200x800-original.jpg", src("miniatures", "tray", "hero1"), (1200, 800), "center", "jpeg"),
("gallery-23-287x252.jpg", src("miniatures", "tray", "hero1"), (287, 252), "center", "jpeg"),
("gallery-23-1200x800-original.jpg", src("tray", "miniatures", "hero1"), (1200, 800), "center", "jpeg"),
("quote-1-287x252.jpg", src("tray", "miniatures", "hero1"), (287, 252), "center", "jpeg"),
("gallery-20-800x1200-original.jpg", src("spool", "extruder", "hero2"), (800, 1200), "center", "jpeg"),
("gallery-20-287x252.jpg", src("spool", "extruder", "hero2"), (287, 252), "center", "jpeg"),
("gallery-2-1200x800-original.jpg", src("neon", "neon2", "hero2"), (1200, 800), "center", "jpeg"),
("gallery-21-287x252.jpg", src("neon", "neon2", "hero2"), (287, 252), "center", "jpeg"),
("gallery-21-1200x800-original.jpg", src("lab", "workshop", "hero2"), (1200, 800), "center", "jpeg"),
("video-1-549x384.jpg", src("nozzle", "extruder", "hero1"), (549, 384), "center", "jpeg"),
("image-1-333x262.jpg", src("tray", "miniatures", "hero1"), (333, 262), "center", "jpeg"),
("bg-breadcrumbs.jpg", src("hero2", "workshop", "lab"), (1920, 414), "center", "jpeg"),
("bg-image-1.jpg", src("hero2", "workshop", "lab"), (1920, 950), "center", "jpeg"),
("product-1-292x256.png", src("figurine", "hero1"), (292, 256), "center", "png"),
("product-2-292x256.png", src("miniatures", "tray", "hero1"), (292, 256), "center", "png"),
("product-3-292x256.png", src("tray", "hero1"), (292, 256), "center", "png"),
("product-4-292x256.png", src("hex", "neon", "hero2"), (292, 256), "center", "png"),
("product-5-292x256.png", src("spool", "extruder", "hero2"), (292, 256), "center", "png"),
("product-6-292x256.png", src("lab", "workshop", "hero2"), (292, 256), "center", "png"),
("product-mini-1-146x132.png", src("figurine", "hero1"), (146, 132), "center", "png"),
("product-mini-2-146x132.png", src("tray", "hero1"), (146, 132), "center", "png"),
("single-product-1-530x480.png", src("figurine", "hero1"), (530, 480), "center", "png"),
("single-product-2-530x480.png", src("miniatures", "tray", "hero1"), (530, 480), "center", "png"),
("single-product-3-530x480.png", src("hero1", "tray"), (530, 480), "center", "png"),
]
for filename, image, size, focus, kind in jobs:
framed = cover_crop(image, *size, focus=focus)
dest = IMAGES / filename
if kind == "png":
save_png(framed, dest)
else:
save_jpeg(framed, dest)
print(f"wrote {filename} {framed.size}")
if failed:
print("download warnings:")
for item in failed:
print(" ", item)
if __name__ == "__main__":
main()