204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
"""M1-② 构建数据集
|
||
从源数据 (E:\\parquet\\) 拷贝代表图到 data/styles/,
|
||
并从 parquet 反查每个风格的代表 content/id,输出 data/styles_meta.json。
|
||
|
||
用法:
|
||
python build_dataset.py # 默认 copy 模式
|
||
python build_dataset.py --mode link # 同卷硬链接(更省空间)
|
||
python build_dataset.py --src E:\\parquet --dst ../data
|
||
"""
|
||
import argparse
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pyarrow.parquet as pq
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent # comfyui-metastyle-T8/
|
||
ILLEGAL = re.compile(r'[\\/:*?"<>|\x00-\x1f]+')
|
||
|
||
|
||
def parse_index_tsv(tsv_path: Path) -> dict[str, dict]:
|
||
out: dict[str, dict] = {}
|
||
with open(tsv_path, "r", encoding="utf-8") as f:
|
||
f.readline() # 表头
|
||
for line in f:
|
||
parts = line.rstrip("\n").split("\t")
|
||
if len(parts) < 4:
|
||
continue
|
||
style, row, sid, fname = parts[0], int(parts[1]), parts[2], parts[3]
|
||
out[style] = {"row": row, "id": sid, "filename": fname}
|
||
return out
|
||
|
||
|
||
def fetch_contents_by_rows(parquet_path: Path, rows_needed: set[int]) -> dict[int, str]:
|
||
pf = pq.ParquetFile(str(parquet_path))
|
||
n_groups = pf.metadata.num_row_groups
|
||
rg_offsets = []
|
||
acc = 0
|
||
for i in range(n_groups):
|
||
rg_offsets.append(acc)
|
||
acc += pf.metadata.row_group(i).num_rows
|
||
rg_offsets.append(acc)
|
||
|
||
rows_per_rg: dict[int, list[int]] = {}
|
||
for r in rows_needed:
|
||
lo, hi = 0, n_groups - 1
|
||
while lo <= hi:
|
||
mid = (lo + hi) // 2
|
||
if r < rg_offsets[mid]:
|
||
hi = mid - 1
|
||
elif r >= rg_offsets[mid + 1]:
|
||
lo = mid + 1
|
||
else:
|
||
rows_per_rg.setdefault(mid, []).append(r)
|
||
break
|
||
|
||
result: dict[int, str] = {}
|
||
total = len(rows_needed)
|
||
done = 0
|
||
for rg_idx in sorted(rows_per_rg.keys()):
|
||
wanted = rows_per_rg[rg_idx]
|
||
batch = pf.read_row_group(rg_idx, columns=["content"]).to_pylist()
|
||
rg_start = rg_offsets[rg_idx]
|
||
for r in wanted:
|
||
local = r - rg_start
|
||
if 0 <= local < len(batch):
|
||
result[r] = batch[local].get("content") or ""
|
||
done += len(wanted)
|
||
print(f" 反查 content: {done}/{total}", end="\r", flush=True)
|
||
print()
|
||
return result
|
||
|
||
|
||
def link_or_copy(src: Path, dst: Path, mode: str) -> bool:
|
||
if dst.exists():
|
||
return False
|
||
if mode == "link":
|
||
try:
|
||
os.link(src, dst)
|
||
return True
|
||
except OSError:
|
||
shutil.copy2(src, dst)
|
||
return True
|
||
shutil.copy2(src, dst)
|
||
return True
|
||
|
||
|
||
def idx_map_lookup(idx_map: dict, key: str) -> str | None:
|
||
if key in idx_map:
|
||
return key
|
||
for full in idx_map.keys():
|
||
cleaned = ILLEGAL.sub("_", full)[:80].strip(" .")
|
||
if cleaned == key:
|
||
return full
|
||
return None
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--src", default=r"E:\parquet")
|
||
ap.add_argument("--dst", default=str(ROOT / "data"))
|
||
ap.add_argument("--mode", choices=["copy", "link"], default="copy")
|
||
ap.add_argument("--parquet-name", default="train-00000.parquet")
|
||
args = ap.parse_args()
|
||
|
||
src = Path(args.src)
|
||
dst = Path(args.dst)
|
||
src_styles = src / "styles"
|
||
src_index = src_styles / "styles_index.tsv"
|
||
src_parquet = src / args.parquet_name
|
||
|
||
if not src_styles.exists():
|
||
sys.exit(f"[错误] 找不到源 styles 目录: {src_styles}")
|
||
if not src_parquet.exists():
|
||
sys.exit(f"[错误] 找不到 parquet: {src_parquet}")
|
||
|
||
dst_styles = dst / "styles"
|
||
dst.mkdir(parents=True, exist_ok=True)
|
||
dst_styles.mkdir(parents=True, exist_ok=True)
|
||
|
||
image_files = [f for f in os.listdir(src_styles)
|
||
if not f.endswith(".tsv")]
|
||
print(f"[信息] 源风格图片: {len(image_files)} 张")
|
||
|
||
idx_map: dict[str, dict] = {}
|
||
if src_index.exists():
|
||
idx_map = parse_index_tsv(src_index)
|
||
print(f"[信息] 解析 styles_index.tsv: {len(idx_map)} 条")
|
||
else:
|
||
print("[警告] 未找到 styles_index.tsv,content/id 字段将留空")
|
||
|
||
print(f"[信息] {args.mode} 图片到 {dst_styles} ...")
|
||
new_count = 0
|
||
for i, fname in enumerate(image_files):
|
||
s = src_styles / fname
|
||
d = dst_styles / fname
|
||
if link_or_copy(s, d, args.mode):
|
||
new_count += 1
|
||
if (i + 1) % 1000 == 0:
|
||
print(f" {i+1}/{len(image_files)}", end="\r", flush=True)
|
||
print(f" 完成,新增 {new_count} 个文件")
|
||
|
||
meta_path = dst / "styles_meta.json"
|
||
cached: dict = {}
|
||
if meta_path.exists():
|
||
try:
|
||
cached = json.loads(meta_path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
cached = {}
|
||
|
||
rows_needed: set[int] = set()
|
||
style_to_row: dict[str, int] = {}
|
||
style_to_id: dict[str, str] = {}
|
||
for fname in image_files:
|
||
key = os.path.splitext(fname)[0]
|
||
if key in cached and cached[key].get("sample_content"):
|
||
continue
|
||
full = idx_map_lookup(idx_map, key)
|
||
if full and idx_map.get(full):
|
||
rows_needed.add(idx_map[full]["row"])
|
||
style_to_row[key] = idx_map[full]["row"]
|
||
style_to_id[key] = idx_map[full]["id"]
|
||
|
||
print(f"[信息] 需要反查 content 的行数: {len(rows_needed)}")
|
||
contents: dict[int, str] = {}
|
||
if rows_needed:
|
||
contents = fetch_contents_by_rows(src_parquet, rows_needed)
|
||
|
||
meta = dict(cached)
|
||
for fname in image_files:
|
||
key = os.path.splitext(fname)[0]
|
||
if key in meta and meta[key].get("sample_content"):
|
||
continue
|
||
row = style_to_row.get(key)
|
||
sid = style_to_id.get(key, "")
|
||
content = contents.get(row, "") if row is not None else ""
|
||
prefix = ""
|
||
m = re.match(r"In the style of ([^,]+)", key, re.I)
|
||
if m:
|
||
prefix = m.group(1).strip()
|
||
meta[key] = {
|
||
"key": key,
|
||
"image": f"styles/{fname}",
|
||
"thumb": f"thumbs/{key}.webp",
|
||
"style_full": key,
|
||
"style_prefix": prefix,
|
||
"sample_id": sid,
|
||
"sample_row": row,
|
||
"sample_content": content,
|
||
}
|
||
|
||
meta_path.write_text(
|
||
json.dumps(meta, ensure_ascii=False, indent=2),
|
||
encoding="utf-8"
|
||
)
|
||
print(f"[完成] styles_meta.json 写入 {meta_path} 共 {len(meta)} 条")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|