Kohya_ss_2 / sd-scripts /tests /download_training_data.py
ChBysk's picture
Upload folder using huggingface_hub (part 2)
622d48e verified
Raw
History Blame Contribute Delete
7.28 kB
#!/usr/bin/env python3
"""
Download training images from common-canvas/commoncatalog-cc-by.
Images are saved in DreamBooth folder format so they can be used directly
with train_network.py / sdxl_train_network.py.
Output structure:
<out_dir>/<repeats>_<concept>/
image_00000.jpg
image_00000.caption
...
Usage:
python3 tests/download_training_data.py --out-dir tests/downloaded_data
python3 tests/download_training_data.py --out-dir tests/downloaded_data --n 500 --repeats 5 --concept photography
python3 tests/download_training_data.py --min-size 1024 # filter for 1024x1024+
"""
import argparse
import os
import sys
import urllib.request
import urllib.error
from pathlib import Path
# os._exit(0) is used at the end of main() to avoid a crash on exit caused by
# HuggingFace datasets' PyArrow background prefetch threads — they crash during
# normal Python interpreter teardown with "terminate called without an active
# exception". os._exit() bypasses teardown and kills them cleanly.
def build_caption(record: dict) -> str:
"""Combine available text fields into a training caption."""
parts = []
title = (record.get("title") or "").strip()
description = (record.get("description") or "").strip()
tags = (record.get("usertags") or "").strip()
if title:
parts.append(title)
if description and description != title:
parts.append(description)
if tags:
tag_list = [t.strip() for t in tags.split() if t.strip()]
if tag_list:
parts.append(", ".join(tag_list))
return ", ".join(parts) if parts else "photograph"
def download_image(url: str, dest_path: str, timeout: int = 15, min_size: int = 0) -> bool:
"""
Download a single image. Returns True on success.
If min_size > 0, opens the saved file with PIL and verifies both dimensions
are at least min_size pixels. Deletes the file and returns False if not.
This catches cases where the dataset metadata dimensions don't match what
the server actually serves (e.g. Flickr returning a scaled-down version).
"""
try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = resp.read()
if len(data) < 1024:
return False
with open(dest_path, "wb") as f:
f.write(data)
except (urllib.error.URLError, OSError, TimeoutError):
return False
if min_size > 0:
try:
from PIL import Image
with Image.open(dest_path) as im:
w, h = im.size
if w < min_size or h < min_size:
os.remove(dest_path)
return False
except Exception:
os.remove(dest_path)
return False
return True
def main():
parser = argparse.ArgumentParser(description="Download commoncatalog training images")
parser.add_argument("--out-dir", default=os.path.join(os.path.dirname(__file__), "downloaded_data"),
help="Root output directory")
parser.add_argument("--n", type=int, default=100,
help="Number of images to successfully download")
parser.add_argument("--repeats", type=int, default=1,
help="DreamBooth repeat count (prefix of subfolder name)")
parser.add_argument("--concept", type=str, default="photo",
help="Concept token (suffix of subfolder name)")
parser.add_argument("--timeout", type=int, default=15,
help="Per-image download timeout in seconds")
parser.add_argument("--max-attempts", type=int, default=0,
help="Stop after this many dataset rows regardless of success (0 = unlimited)")
parser.add_argument("--min-size", type=int, default=1024,
help="Minimum width AND height in pixels (default: 1024). Use 0 to disable.")
args = parser.parse_args()
try:
from datasets import load_dataset
except ImportError:
print("Error: 'datasets' package not installed. Run: pip install datasets", file=sys.stderr)
sys.exit(1)
folder_name = f"{args.repeats}_{args.concept}"
out_dir = Path(args.out_dir) / folder_name
out_dir.mkdir(parents=True, exist_ok=True)
print(f"Streaming commoncatalog-cc-by dataset...")
print(f"Target: {args.n} images >= {args.min_size}px → {out_dir}")
print()
dataset = load_dataset(
"common-canvas/commoncatalog-cc-by",
streaming=True,
)
saved = 0
attempted = 0
skipped_status = 0
skipped_size = 0
skipped_download = 0
# Wrap iteration in try/finally so we always delete the iterator explicitly
# before Python exit. The HuggingFace streaming backend uses PyArrow threads
# that crash with "terminate called without an active exception" if they are
# still running during interpreter teardown.
stream_iter = iter(dataset["train"])
try:
for record in stream_iter:
if saved >= args.n:
break
if args.max_attempts > 0 and attempted >= args.max_attempts:
break
attempted += 1
if record.get("status") != "success":
skipped_status += 1
continue
# Size filter: the dataset carries width/height of the original image
if args.min_size > 0:
w = record.get("original_width") or record.get("width") or 0
h = record.get("original_height") or record.get("height") or 0
if w < args.min_size or h < args.min_size:
skipped_size += 1
continue
# Prefer the original-resolution URL over the scaled thumbnail
url = record.get("url") or record.get("downloadurl") or ""
if not url:
skipped_download += 1
continue
ext = record.get("ext") or "jpg"
img_name = f"image_{saved:05d}.{ext}"
img_path = out_dir / img_name
cap_path = out_dir / f"image_{saved:05d}.caption"
if download_image(url, str(img_path), timeout=args.timeout, min_size=args.min_size):
caption = build_caption(record)
cap_path.write_text(caption, encoding="utf-8")
saved += 1
print(f" [{saved:4d}/{args.n}] {img_name} \"{caption[:72]}\"")
else:
skipped_download += 1
finally:
del stream_iter
print()
print(f"Done: {saved} saved, {attempted} rows examined")
print(f" skipped (status) : {skipped_status}")
print(f" skipped (size) : {skipped_size}")
print(f" skipped (download): {skipped_download}")
print(f"Data directory: {out_dir}")
# Flush before bypassing Python teardown — os._exit() skips atexit handlers
# including the normal stdout flush, so we do it explicitly here.
sys.stdout.flush()
# Bypass Python interpreter teardown to prevent PyArrow background threads
# from crashing with "terminate called without an active exception".
os._exit(0)
if __name__ == "__main__":
main()