VSEARCH / scripts /gen-thumbnail.py
ml-intern's picture
SEO + social-share thumbnail: ảnh ghép 12 ảnh mới nhất tự động
c913ae2
Raw History Blame Contribute Delete
3.93 kB
#!/usr/bin/env python3
"""
Generate the social-share thumbnail for the semantic-image-field Space.
Always the CURRENT 12 newest photographs: 8 newest interiors (by created_at)
from bep40/vai-avatar2-interior + 4 newest added products (by _added_at)
from bep40/grob-products-updated. Composed as a 4x3 grid at 1200x675 (2:1
social-card crop). Written to BOTH public/ and dist/ so whichever path the
static Space serves, the resolved URL exists.
Intended to run from a scheduled HF job (or locally) BEFORE the catalogue
sync commits, so the committed thumbnail is refreshed at least as often as
the mirror itself.
"""
import io, json, os, sys, urllib.request
INTERIOR_INDEX = "https://huggingface.co/datasets/bep40/vai-avatar2-interior/resolve/main/index.json"
INTERIOR_API = "https://bep40-vai-avatar2-interior-api.hf.space"
PRODUCTS_URL = "https://huggingface.co/datasets/bep40/grob-products-updated/resolve/main/products_url_added.json"
N_INTERIORS = 8
N_PRODUCTS = 4
W, H = 1200, 675
GAP = 6
COLS, ROWS = 4, 3
BG = (17, 20, 28)
UA = {"User-Agent": "V.AI-STUDIO-thumbnail/1.0"}
def fetch_json(url, timeout=25):
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode("utf-8"))
def fetch_bytes(url, timeout=25):
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read()
def main():
from PIL import Image
interiors = fetch_json(INTERIOR_INDEX)
products = fetch_json(PRODUCTS_URL)
dated = [r for r in interiors
if isinstance(r, dict) and isinstance(r.get("created_at"), str)
and r.get("created_at") and r.get("image_id")]
dated.sort(key=lambda r: r["created_at"], reverse=True)
top_i = dated[:N_INTERIORS]
prods = [p for p in products
if isinstance(p, dict) and isinstance(p.get("_added_at"), str)
and p.get("_added_at") and (p.get("imgs") or p.get("i"))]
prods.sort(key=lambda p: str(p["_added_at"]), reverse=True)
top_p = prods[:N_PRODUCTS]
urls = [f"{INTERIOR_API}/image/{r['image_id']}.jpg" for r in top_i]
urls += [(p.get("imgs") or [p.get("i")])[0] for p in top_p]
cell_w = (W - GAP * (COLS - 1)) // COLS
cell_h = (H - GAP * (ROWS - 1)) // ROWS
canvas = Image.new("RGB", (W, H), BG)
cells = []
for u in urls:
try:
data = fetch_bytes(u)
im = Image.open(io.BytesIO(data))
if im.mode not in ("RGB", "RGBA"):
im = im.convert("RGB")
if im.mode == "RGBA":
im = im.convert("RGB")
cells.append(im)
except Exception as e:
print(f"skip {u[:80]}: {e}", file=sys.stderr)
if not cells:
print("FATAL: no images could be fetched", file=sys.stderr)
sys.exit(1)
idx = 0
for row in range(ROWS):
for col in range(COLS):
if idx >= len(cells):
break
im = cells[idx]
idx += 1
iw, ih = im.size
scale = max(cell_w / iw, cell_h / ih)
sw, sh = max(1, round(iw * scale)), max(1, round(ih * scale))
im2 = im.resize((sw, sh), Image.LANCZOS)
left = (sw - cell_w) // 2
top = (sh - cell_h) // 2
im2 = im2.crop((left, top, left + cell_w, top + cell_h))
canvas.paste(im2, (col * (cell_w + GAP), row * (cell_h + GAP)))
out = io.BytesIO()
canvas.save(out, format="PNG", optimize=True)
png = out.getvalue()
os.makedirs("public/share", exist_ok=True)
os.makedirs("dist/share", exist_ok=True)
for path in ("public/share/og-image.png", "dist/share/og-image.png"):
with open(path, "wb") as f:
f.write(png)
print(f"OK wrote {len(png)} bytes {W}x{H} from {len(cells)} images")
if __name__ == "__main__":
main()