# retoor import io import logging import imagehash from PIL import Image from molodetz import config from molodetz.attachments import blob_path, blob_url, write_blob from molodetz.database import generate_uid, media_by_kind, upsert_media from molodetz.database.media import get_media_by_file, retire_media logger = logging.getLogger(__name__) THUMB_WIDTH = 720 DUPLICATE_DISTANCE = 2 GALLERY = ( ("flyer-elite-twice.jpg", "flyer", "Elite twice. Know the code. Use the machine. Read every line it writes."), ("flyer-taste.jpg", "flyer", "Taste does not autocomplete."), ("flyer-demo.jpg", "flyer", "A demo is not a product."), ("flyer-hands.jpg", "flyer", "Your hands are not the craft."), ("meme-elite-galaxy-brain.jpg", "meme", "Getting elite twice: know every line before the AI writes it, review every line after, ship by lunch."), ("meme-classic-autocomplete.jpg", "meme", "I don't need autocomplete. Hand-typed. Artisanal. Not shipped."), ("meme-vibe-prompt-and-pray.jpg", "meme", "Prompt and pray: the same TypeError pasted into the chat 31 times. Suggested fix: learn what a function is."), ("meme-elite-choose-your-fighter.jpg", "meme", "Choose your fighter: the classic, the vibe coder, or the elite who writes code, uses AI and reads every diff."), ("meme-classic-starter-pack.jpg", "meme", "The 'AI is just fancy autocomplete' starter pack: 36 tmux panes, a vimrc older than the intern, zero pull requests."), ("meme-vibe-starter-pack.jpg", "meme", "The 'built it in a weekend' starter pack: tab, tab, accept all, API key committed to main, works on my machine."), ("meme-elite-build-log.jpg", "meme", "Elite twice. The model types. You decide. 1,184 lines drafted, every line read, 212 rejected."), ("meme-classic-chart.jpg", "meme", "Free-range, hand-typed code: keystrokes up, features shipped flat, opinions about AI off the chart."), ("meme-vibe-galaxy-brain.jpg", "meme", "Vibe debugging: read the error, paste the error, tab tab accept all, delete the repo and prompt a new one."), ("meme-elite-take.jpg", "meme", "AI replaced the part of me that typed. The part that thinks got a raise."), ("meme-classic-same-guy.jpg", "meme", "1987: compilers make programmers lazy. 2026: AI makes programmers lazy. Same guy. Still not shipping."), ("meme-vibe-git-blame.jpg", "meme", "Git blame says it was you. You say it was the vibes."), ("meme-elite-curve.jpg", "meme", "Pick your curve: classic stays flat, the vibe coder crashes at the first prod incident, elite plus AI keeps climbing."), ("meme-classic-certificate.jpg", "meme", "Certificate of manual labour. Time saved: 0 hours. Moral high ground: maximum."), ("meme-vibe-weekend.jpg", "meme", "A vibe coder's weekend: a SaaS on Friday, a public database by Sunday, a course about it on Monday."), ("meme-elite-achievement.jpg", "meme", "Achievement unlocked: this developer embraced AI and is now elite twice."), ("meme-classic-family.jpg", "meme", "Three generations of refusing the tool. Three generations of getting outshipped."), ("meme-vibe-priorities.jpg", "meme", "Vibe coder priorities: learning what a for loop does, nah. Typing 'make it work, no bugs, please' 30 times, yep."), ("meme-compiles.jpg", "meme", "It compiles. Ship it, king."), ("meme-senior.jpg", "meme", "My copilot is the senior."), ("meme-trophy.jpg", "meme", "Peak performance, 6 out of 10."), ("meme-todo.jpg", "meme", "Side projects, a love story."), ) RETIRED_GALLERY = ( "flyer-keuzes.jpg", "flyer-chat.jpg", "flyer-bijna.jpg", "flyer-assistent.jpg", "meme-vloer.jpg", "meme-split.jpg", "meme-cursus.jpg", "meme-zes.jpg", "meme-gesprek.jpg", "meme-product.jpg", "meme-bijna.jpg", "poster-thuis.jpg", "poster-vloer.jpg", "poster-cursus.jpg", "poster-luiheid.jpg", "poster-spel.jpg", "story.jpg", "take-chatlog.jpg", "take-copilot.jpg", "take-coping.jpg", ) def _thumbnail_bytes(image): copy = image.convert("RGB") copy.thumbnail((THUMB_WIDTH, THUMB_WIDTH * 2)) buffer = io.BytesIO() copy.save(buffer, format="WEBP", quality=82, method=4) return buffer.getvalue() def retire_gallery(): return sum(1 for filename in RETIRED_GALLERY if retire_media(filename)) def sync_gallery(): retire_gallery() seen_hashes = [] synced = 0 for position, (filename, kind, caption) in enumerate(GALLERY): source = config.MEDIA_SOURCE_DIR / filename if not source.exists(): logger.warning("gallery source missing: %s", source) continue existing = get_media_by_file(filename) with Image.open(source) as image: width, height = image.size phash = imagehash.phash(image) if any(phash - other <= DUPLICATE_DISTANCE for other in seen_hashes): logger.info("gallery duplicate skipped: %s", filename) continue seen_hashes.append(phash) thumb_directory = existing.get("thumb_directory") if existing else None thumb_name = existing.get("thumb_name") if existing else None changed = not existing or existing.get("phash") != str(phash) if changed or not thumb_directory or not blob_path(thumb_directory, thumb_name).exists(): thumb_directory, thumb_name = write_blob( existing["uid"] if existing else generate_uid(), "webp", _thumbnail_bytes(image) ) upsert_media(filename, kind, caption, position, width, height, str(phash), thumb_directory, thumb_name) synced += 1 return synced def missing_sources(): return [filename for filename, _, _ in GALLERY if not (config.MEDIA_SOURCE_DIR / filename).exists()] def gallery_items(kind): items = [] for row in media_by_kind(kind): thumb = blob_url(row["thumb_directory"], row["thumb_name"]) if row.get("thumb_directory") else None items.append( { "uid": row["uid"], "filename": row["filename"], "caption": row["caption"], "width": row["width"], "height": row["height"], "source_path": f"/static/media/{row['filename']}", "thumb_url": thumb, } ) return items