supli6669
feat: add Safebooru crawler and facexlib-based face crop/align tool, implement progress callback in streamlit app
296db8d | import os | |
| import shutil | |
| import requests | |
| from concurrent.futures import ThreadPoolExecutor | |
| # ---------------------------------------------------------------------- | |
| # Configuration | |
| # ---------------------------------------------------------------------- | |
| GAME_TAGS = { | |
| "zenless_zone_zero": "zenless_zone_zero", | |
| "wuthering_waves": "wuthering_waves", | |
| "honkai_impact_3": "honkai_impact_3rd", | |
| "genshin_impact": "genshin_impact", | |
| } | |
| IMAGES_PER_GAME = 600 | |
| MAX_WORKERS = 12 # Number of concurrent threads for downloading | |
| OUTPUT_ROOT = os.path.abspath( | |
| os.path.join(os.path.dirname(__file__), "game_characters") | |
| ) | |
| def download_single_image(post: dict, out_dir: str, headers: dict) -> bool: | |
| """Download a single image. Returns True if successful, False otherwise.""" | |
| file_url = post.get("file_url") | |
| post_id = post.get("id") | |
| if not file_url or not post_id: | |
| return False | |
| # Handle relative/absolute URL | |
| if file_url.startswith("//"): | |
| file_url = "https:" + file_url | |
| elif file_url.startswith("/"): | |
| file_url = "https://safebooru.org" + file_url | |
| elif not file_url.startswith("http"): | |
| file_url = "https://safebooru.org/" + file_url | |
| ext = os.path.splitext(file_url)[1] | |
| if not ext: | |
| ext = ".jpg" | |
| if ext.lower() not in [".jpg", ".jpeg", ".png", ".webp"]: | |
| return False | |
| dest_path = os.path.join(out_dir, f"{post_id}{ext.lower()}") | |
| # If already downloaded, skip | |
| if os.path.exists(dest_path): | |
| return True | |
| try: | |
| # stream=True to check Content-Length header first | |
| img_res = requests.get(file_url, headers=headers, timeout=10, stream=True) | |
| if img_res.status_code == 200: | |
| content_length = img_res.headers.get("Content-Length") | |
| if content_length: | |
| size_mb = int(content_length) / (1024 * 1024) | |
| # Skip files larger than 5MB to optimize download speeds | |
| if size_mb > 5.0: | |
| img_res.close() | |
| return False | |
| with open(dest_path, "wb") as f: | |
| shutil.copyfileobj(img_res.raw, f) | |
| return True | |
| else: | |
| return False | |
| except Exception: | |
| return False | |
| def crawl_safebooru(slug: str, tag: str, max_num: int) -> None: | |
| """Download images from Safebooru using DAPI JSON index concurrently.""" | |
| out_dir = os.path.join(OUTPUT_ROOT, slug) | |
| if os.path.exists(out_dir): | |
| shutil.rmtree(out_dir) | |
| os.makedirs(out_dir, exist_ok=True) | |
| print(f"[START] Safebooru tags search for '{tag}' -> '{slug}'") | |
| url = "https://safebooru.org/index.php" | |
| params = { | |
| "page": "dapi", | |
| "s": "post", | |
| "q": "index", | |
| "json": 1, | |
| "limit": max_num, | |
| "tags": tag | |
| } | |
| try: | |
| response = requests.get(url, params=params, timeout=15) | |
| response.raise_for_status() | |
| if not response.text.strip(): | |
| print(f"[WARN] No posts found for {tag}") | |
| return | |
| posts = response.json() | |
| if not isinstance(posts, list): | |
| print(f"[WARN] Expected list, got {type(posts)}") | |
| return | |
| print(f"Found {len(posts)} posts for '{tag}'. Downloading concurrently with {MAX_WORKERS} workers...") | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" | |
| } | |
| success_count = 0 | |
| with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: | |
| # Submit all tasks | |
| futures = [ | |
| executor.submit(download_single_image, post, out_dir, headers) | |
| for post in posts | |
| ] | |
| for future in futures: | |
| if future.result(): | |
| success_count += 1 | |
| if success_count % 50 == 0: | |
| print(f" Downloaded {success_count} images for {slug}...") | |
| print(f"[DONE] {slug}: {success_count} images saved to {out_dir}\n") | |
| except Exception as e: | |
| print(f"[ERROR] Failed to query Safebooru for {tag}: {e}") | |
| def main() -> None: | |
| print(f"Output root: {OUTPUT_ROOT}\n") | |
| for slug, tag in GAME_TAGS.items(): | |
| crawl_safebooru(slug, tag, IMAGES_PER_GAME) | |
| if __name__ == "__main__": | |
| main() | |