# Download all poses at https://old.reddit.com/r/unstable_diffusion/comments/1xhfrxq/how_to_use_pose_reference_images_to_create/

> Source: <https://gist.github.com/Hunt3rseeker/ec89a421869baacb6096332b42c89093>
> Published: 2026-10-28 16:51:38+00:00

|  | #!/usr/bin/env python3 | 
|  | """Download every original-size PNG from https://ai-pose-gallery.pages.dev/ | 
|  | and sort them into folders using the site's own series / category data. | 
|  | No extra packages needed (standard library only). | 
|  | Usage: | 
|  | python download_poses_sorted.py # download + sort + focus folders | 
|  | python download_poses_sorted.py --no-focus # skip the _by_focus folders | 
|  | python download_poses_sorted.py --out D:\\poses | 
|  | Re-running skips files that are already downloaded, so you can stop and | 
|  | resume at any time. | 
|  | Result: | 
|  | pose_pngs/ | 
|  | solo/full_body_standard/<category>/ (365 images) | 
|  | solo/full_body_sexy/<category>/ (399 images) | 
|  | solo/upper_body/<category>/ (400 images) | 
|  | duo/<category>/ (275 images) | 
|  | _by_focus/<tag>/ (keyword-based extras, see FOCUS below) | 
|  | catalog.csv (file name -> Japanese pose name, category, angle) | 
|  | Each pose has 4-5 angle images (front/high/low/side/diag); they sit next to each | 
|  | other in the same folder because they share the pose number in the file name. | 
|  | """ | 
|  | import argparse | 
|  | import csv | 
|  | import json | 
|  | import os | 
|  | import re | 
|  | import shutil | 
|  | import sys | 
|  | import time | 
|  | import urllib.request | 
|  | from collections import Counter | 
|  | from concurrent.futures import ThreadPoolExecutor, as_completed | 
|  | from pathlib import Path | 
|  | BASE = "https://ai-pose-gallery.pages.dev/" | 
|  | HEADERS = {"User-Agent": "Mozilla/5.0"} | 
|  | WORKERS = 6 # parallel downloads; keep modest to be polite to the host | 
|  | # Site series (系統) -> top-level folder | 
|  | SERIES = { | 
|  | "全身1人・標準系": "solo/full_body_standard", | 
|  | "全身1人・セクシー系": "solo/full_body_sexy", | 
|  | "上半身1人": "solo/upper_body", | 
|  | "2人絡み": "duo", | 
|  | } | 
|  | # Site category (分類) -> folder name | 
|  | CATEGORY = { | 
|  | # solo, full body (standard + sexy) | 
|  | "立ち": "standing", | 
|  | "座り": "sitting", | 
|  | "しゃがみ": "crouching", | 
|  | "膝立ち": "kneeling", | 
|  | "膝立ち・四つん這い": "kneeling_all_fours", | 
|  | "寝そべり": "lying", | 
|  | "前かがみ": "leaning_forward", | 
|  | "のけぞり・反り": "arched_back", | 
|  | "ひねり": "twisting", | 
|  | "横座り・女の子座り": "side_sitting", | 
|  | "特殊": "special", | 
|  | # solo, upper body | 
|  | "かわいい系": "cute", | 
|  | "セクシー・露骨寄り系": "sexy_explicit", | 
|  | "胸寄せ・谷間強調系": "chest_emphasis", | 
|  | # duo | 
|  | "立位": "standing", | 
|  | "立位密着": "standing_close", | 
|  | "座位": "seated", | 
|  | "座位密着": "seated_close", | 
|  | "対面座位": "seated_face_to_face", | 
|  | "背面座位": "seated_back_facing", | 
|  | "正常位": "missionary", | 
|  | "後背位": "from_behind", | 
|  | "騎乗位": "on_top", | 
|  | "側位": "side_lying", | 
|  | "密着": "close_embrace", | 
|  | "顔寄せ系": "faces_close", | 
|  | } | 
|  | # Extra keyword-based views, built as hard links (or copies if links fail) in | 
|  | # _by_focus/<tag>/. These match the Japanese POSE NAME, so they are rough: | 
|  | # edit the patterns freely. Only a handful of poses mention the buttocks by name. | 
|  | FOCUS = { | 
|  | "butt": r"お尻\|ヒップ\|尻", | 
|  | "chest": r"胸\|バスト\|谷間", | 
|  | "face": r"顔", | 
|  | "legs": r"脚\|足\|太もも\|太腿\|開脚", | 
|  | "back": r"背中", | 
|  | } | 
|  | # Whole site categories that also count for a focus tag | 
|  | FOCUS_CATEGORY = { | 
|  | "chest": {"胸寄せ・谷間強調系"}, | 
|  | "face": {"顔寄せ系"}, | 
|  | } | 
|  | def get(url): | 
|  | req = urllib.request.Request(url, headers=HEADERS) | 
|  | with urllib.request.urlopen(req, timeout=60) as r: | 
|  | return r.read() | 
|  | def safe(name): | 
|  | return re.sub(r'[\\/:*?"<>\|]+', "_", name).strip() or "other" | 
|  | def fetch_catalog(): | 
|  | html = get(BASE).decode("utf-8", "replace") | 
|  | items, seen = [], set() | 
|  | # The gallery page embeds its catalog as {"id": ..., "系統": ..., "原寸": ...} objects. | 
|  | for m in re.finditer(r'\{"id":[^{}]*\}', html): | 
|  | try: | 
|  | o = json.loads(m.group(0)) | 
|  | except ValueError: | 
|  | continue | 
|  | if o.get("原寸") and o.get("id") not in seen: | 
|  | seen.add(o["id"]) | 
|  | items.append(o) | 
|  | return items | 
|  | def folder_for(o): | 
|  | series = SERIES.get(o.get("系統"), safe(o.get("系統", "other"))) | 
|  | if series == "duo": | 
|  | cat = CATEGORY.get(o.get("分類"), safe(o.get("分類", "other"))) | 
|  | return Path("duo") / cat | 
|  | cat = CATEGORY.get(o.get("分類"), safe(o.get("分類", "other"))) | 
|  | return Path(*series.split("/")) / cat | 
|  | def target_path(out, o): | 
|  | return out / folder_for(o) / Path(o["原寸"]).name | 
|  | def download(out, o): | 
|  | dest = target_path(out, o) | 
|  | if dest.exists() and dest.stat().st_size > 0: | 
|  | return "skipped" | 
|  | dest.parent.mkdir(parents=True, exist_ok=True) | 
|  | err = None | 
|  | for attempt in range(3): | 
|  | try: | 
|  | data = get(BASE + o["原寸"]) | 
|  | tmp = dest.with_suffix(".part") | 
|  | tmp.write_bytes(data) | 
|  | tmp.replace(dest) | 
|  | return "ok" | 
|  | except Exception as e: # network hiccup: wait and retry | 
|  | err = e | 
|  | time.sleep(1 + attempt) | 
|  | return f"failed: {err}" | 
|  | def write_catalog(out, items): | 
|  | with open(out / "catalog.csv", "w", newline="", encoding="utf-8-sig") as f: | 
|  | w = csv.writer(f) | 
|  | w.writerow(["file", "folder", "series_ja", "category_ja", "pose_number", "pose_name_ja", "angle"]) | 
|  | for o in items: | 
|  | name = Path(o["原寸"]).name | 
|  | angle = Path(o["原寸"]).stem.rsplit("_", 1)[-1] | 
|  | w.writerow([name, folder_for(o).as_posix(), o.get("系統", ""), o.get("分類", ""), | 
|  | o.get("ポーズ番号", ""), o.get("ポーズ名", ""), angle]) | 
|  | def build_focus(out, items): | 
|  | counts = Counter() | 
|  | for o in items: | 
|  | src = target_path(out, o) | 
|  | if not src.exists(): | 
|  | continue | 
|  | for tag, pattern in FOCUS.items(): | 
|  | if re.search(pattern, o.get("ポーズ名", "")) or o.get("分類") in FOCUS_CATEGORY.get(tag, ()): | 
|  | dst_dir = out / "_by_focus" / tag | 
|  | dst_dir.mkdir(parents=True, exist_ok=True) | 
|  | dst = dst_dir / src.name | 
|  | if dst.exists(): | 
|  | continue | 
|  | try: | 
|  | os.link(src, dst) # no extra disk space | 
|  | except OSError: | 
|  | shutil.copy2(src, dst) | 
|  | counts[tag] += 1 | 
|  | return counts | 
|  | def main(): | 
|  | ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | 
|  | ap.add_argument("--out", default=str(Path(__file__).resolve().parent / "pose_pngs"), | 
|  | help="output folder (default: pose_pngs next to this script)") | 
|  | ap.add_argument("--no-focus", action="store_true", help="do not build the _by_focus folders") | 
|  | args = ap.parse_args() | 
|  | out = Path(args.out) | 
|  | items = fetch_catalog() | 
|  | if not items: | 
|  | sys.exit("Could not read the image catalog from the page (site layout may have changed).") | 
|  | out.mkdir(parents=True, exist_ok=True) | 
|  | print(f"Found {len(items)} images. Saving to {out}") | 
|  | done = failed = 0 | 
|  | with ThreadPoolExecutor(max_workers=WORKERS) as pool: | 
|  | futures = {pool.submit(download, out, o): o for o in items} | 
|  | for i, fut in enumerate(as_completed(futures), 1): | 
|  | result = fut.result() | 
|  | if result.startswith("failed"): | 
|  | failed += 1 | 
|  | print(f" {futures[fut]['原寸']}: {result}") | 
|  | else: | 
|  | done += 1 | 
|  | if i % 50 == 0 or i == len(items): | 
|  | print(f" {i}/{len(items)} processed") | 
|  | write_catalog(out, items) | 
|  | if not args.no_focus: | 
|  | counts = build_focus(out, items) | 
|  | if counts: | 
|  | print("Focus folders: " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items()))) | 
|  | print(f"Finished: {done} saved/skipped, {failed} failed.") | 
|  | if failed: | 
|  | print("Run the script again to retry the failed ones.") | 
|  | if __name__ == "__main__": | 
|  | main() |
