chore: move utility scripts under scripts (#1746)

This commit is contained in:
leejet
2026-07-05 18:48:18 +08:00
committed by GitHub
parent c674225b38
commit dff0e88265
11 changed files with 35 additions and 22 deletions

View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python
import argparse
import json
import math
import os
import struct
from collections import Counter
from pathlib import Path
import torch
from safetensors import safe_open
FLOAT_DTYPES = {
"BF16",
"F16",
"F32",
"F64",
"F8_E4M3",
"F8_E4M3FN",
"F8_E5M2",
}
FP8_DTYPES = {
"F8_E4M3",
"F8_E4M3FN",
"F8_E5M2",
}
DTYPE_SIZES = {
"BOOL": 1,
"U8": 1,
"I8": 1,
"F8_E4M3": 1,
"F8_E4M3FN": 1,
"F8_E5M2": 1,
"U16": 2,
"I16": 2,
"F16": 2,
"BF16": 2,
"U32": 4,
"I32": 4,
"F32": 4,
"U64": 8,
"I64": 8,
"F64": 8,
}
def read_safetensors_header(path: Path):
with path.open("rb") as f:
header_len = struct.unpack("<Q", f.read(8))[0]
header = f.read(header_len).decode("utf-8").rstrip()
return json.loads(header)
def numel(shape):
return math.prod(shape) if shape else 1
def scale_key_for_weight(name: str):
if name.endswith(".weight"):
return name[:-len(".weight")] + ".weight_scale"
if name.endswith("weight"):
return name + "_scale"
return None
def tensor_nbytes(dtype: str, shape):
return numel(shape) * DTYPE_SIZES[dtype]
def build_output_plan(header):
entries = {k: v for k, v in header.items() if k != "__metadata__"}
paired_scale_keys = set()
plan = []
for name, info in entries.items():
scale_key = scale_key_for_weight(name)
if info["dtype"] in FP8_DTYPES and scale_key in entries:
paired_scale_keys.add(scale_key)
for name, info in entries.items():
if name in paired_scale_keys:
continue
dtype = info["dtype"]
shape = info["shape"]
scale_key = scale_key_for_weight(name)
if dtype in FP8_DTYPES and scale_key in entries:
scale_info = entries[scale_key]
plan.append(
{
"name": name,
"source_dtype": dtype,
"output_dtype": "BF16",
"shape": shape,
"mode": "fp8_scaled_weight",
"scale_key": scale_key,
}
)
continue
if dtype in FLOAT_DTYPES:
plan.append(
{
"name": name,
"source_dtype": dtype,
"output_dtype": "BF16",
"shape": shape,
"mode": "float_to_bf16",
}
)
else:
plan.append(
{
"name": name,
"source_dtype": dtype,
"output_dtype": dtype,
"shape": shape,
"mode": "copy",
}
)
metadata = dict(header.get("__metadata__", {}) or {})
metadata["format"] = "pt"
metadata["conversion"] = "fp8_weight_scale_to_bf16"
output_header = {"__metadata__": metadata}
offset = 0
for item in plan:
size = tensor_nbytes(item["output_dtype"], item["shape"])
output_header[item["name"]] = {
"dtype": item["output_dtype"],
"shape": item["shape"],
"data_offsets": [offset, offset + size],
}
offset += size
return plan, output_header, offset
def write_tensor_bytes(out, tensor):
tensor = tensor.detach().cpu().contiguous()
if tensor.numel() == 0:
return
if tensor.dtype == torch.bfloat16:
tensor.view(torch.uint16).numpy().tofile(out)
elif tensor.dtype in (getattr(torch, "float8_e4m3fn", None), getattr(torch, "float8_e5m2", None)):
tensor.view(torch.uint8).numpy().tofile(out)
else:
tensor.numpy().tofile(out)
def scale_view_for_chunk(scale, chunk, first_dim_start=0, first_dim_end=None):
scale = scale.to(torch.float32)
if scale.numel() == 1:
return scale.reshape((1,) * chunk.ndim)
if chunk.ndim > 0 and scale.ndim == 1:
if first_dim_end is not None and scale.shape[0] >= first_dim_end:
scale = scale[first_dim_start:first_dim_end]
if scale.shape[0] == chunk.shape[0]:
return scale.reshape((scale.shape[0],) + (1,) * (chunk.ndim - 1))
return scale
def write_scaled_fp8_weight(out, weight, scale, chunk_rows):
if weight.ndim == 0:
result = weight.to(torch.float32) * scale_view_for_chunk(scale, weight)
write_tensor_bytes(out, result.to(torch.bfloat16))
return
rows = weight.shape[0]
for start in range(0, rows, chunk_rows):
end = min(start + chunk_rows, rows)
chunk = weight[start:end].to(torch.float32)
scale_view = scale_view_for_chunk(scale, chunk, start, end)
result = chunk * scale_view
write_tensor_bytes(out, result.to(torch.bfloat16))
def write_float_as_bf16(out, tensor, chunk_rows):
if tensor.dtype == torch.bfloat16:
write_tensor_bytes(out, tensor)
return
if tensor.ndim == 0:
write_tensor_bytes(out, tensor.to(torch.bfloat16))
return
rows = tensor.shape[0]
for start in range(0, rows, chunk_rows):
end = min(start + chunk_rows, rows)
write_tensor_bytes(out, tensor[start:end].to(torch.bfloat16))
def convert(input_path: Path, output_path: Path, chunk_rows: int, dry_run: bool):
header = read_safetensors_header(input_path)
plan, output_header, data_size = build_output_plan(header)
source_counts = Counter(item["source_dtype"] for item in plan)
output_counts = Counter(item["output_dtype"] for item in plan)
scaled_count = sum(item["mode"] == "fp8_scaled_weight" for item in plan)
dropped_scales = sum(item["mode"] == "fp8_scaled_weight" for item in plan)
header_bytes = json.dumps(output_header, separators=(",", ":")).encode("utf-8")
expected_size = 8 + len(header_bytes) + data_size
print(f"input: {input_path}")
print(f"output: {output_path}")
print(f"tensors written: {len(plan)}")
print(f"scaled fp8 weights dequantized: {scaled_count}")
print(f"weight_scale tensors dropped: {dropped_scales}")
print(f"source dtypes: {dict(sorted(source_counts.items()))}")
print(f"output dtypes: {dict(sorted(output_counts.items()))}")
print(f"expected output size: {expected_size / (1024 ** 3):.2f} GiB")
if dry_run:
return
if output_path.exists():
raise FileExistsError(f"{output_path} already exists; pass --overwrite to replace it")
tmp_path = output_path.with_suffix(output_path.suffix + ".tmp")
if tmp_path.exists():
raise FileExistsError(f"{tmp_path} already exists; remove it or choose another output")
with safe_open(str(input_path), framework="pt", device="cpu") as sf, tmp_path.open("wb") as out:
out.write(struct.pack("<Q", len(header_bytes)))
out.write(header_bytes)
for index, item in enumerate(plan, 1):
name = item["name"]
print(f"[{index:04d}/{len(plan):04d}] {name} -> {item['output_dtype']}")
tensor = sf.get_tensor(name)
if item["mode"] == "fp8_scaled_weight":
scale = sf.get_tensor(item["scale_key"])
write_scaled_fp8_weight(out, tensor, scale, chunk_rows)
elif item["mode"] == "float_to_bf16":
write_float_as_bf16(out, tensor, chunk_rows)
else:
write_tensor_bytes(out, tensor)
actual_size = out.tell()
if actual_size != expected_size:
tmp_path.unlink(missing_ok=True)
raise RuntimeError(f"wrote {actual_size} bytes, expected {expected_size} bytes")
tmp_path.replace(output_path)
print("done")
def main():
parser = argparse.ArgumentParser(
description="Convert an fp8 safetensors checkpoint with weight_scale tensors to bf16."
)
parser.add_argument("--input", default="ideogram4_fp8.safetensors", type=Path)
parser.add_argument("--output", default="ideogram4_bf16.safetensors", type=Path)
parser.add_argument("--chunk-rows", default=1024, type=int)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--overwrite", action="store_true")
args = parser.parse_args()
input_path = args.input.resolve()
output_path = args.output.resolve()
if args.chunk_rows < 1:
raise ValueError("--chunk-rows must be >= 1")
if not input_path.exists():
raise FileNotFoundError(input_path)
if args.overwrite and output_path.exists():
output_path.unlink()
convert(input_path, output_path, args.chunk_rows, args.dry_run)
if __name__ == "__main__":
main()

112
scripts/convert_qwen3_vl.py Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Convert a Qwen3-VL HF safetensors checkpoint into a sd.cpp-loadable form.
The HF dump prefixes text-tower keys with ``model.language_model.`` and
vision-tower keys with ``model.visual.``. sd.cpp expects ``model.<rest>`` for
the text side; the vision side is converted by sd.cpp's own
``convert_qwen3_vl_vision_name`` and is left as-is here.
Operates on raw safetensors bytes so any dtype (BF16/F16/F32) is preserved.
Usage:
python3 scripts/convert_qwen3_vl.py <hf_qwen3_vl_dir_or_safetensors> <output.safetensors>
"""
import argparse
import json
import os
import struct
import sys
def rewrite_key(key: str) -> str:
if key.startswith("model.language_model."):
return "model." + key[len("model.language_model."):]
return key
def read_safetensors_header(path: str):
with open(path, "rb") as f:
hdr_len = struct.unpack("<Q", f.read(8))[0]
hdr_bytes = f.read(hdr_len)
return json.loads(hdr_bytes), 8 + hdr_len
def collect_shard_paths(path: str):
if os.path.isdir(path):
index_path = os.path.join(path, "model.safetensors.index.json")
if os.path.isfile(index_path):
with open(index_path) as f:
idx = json.load(f)
return sorted({os.path.join(path, n) for n in idx["weight_map"].values()})
single = os.path.join(path, "model.safetensors")
if os.path.isfile(single):
return [single]
raise FileNotFoundError(f"No Qwen3-VL safetensors in {path}")
if os.path.isfile(path):
return [path]
raise FileNotFoundError(path)
def stage_tensors(input_path: str):
entries = []
for shard_path in collect_shard_paths(input_path):
hdr, data_off = read_safetensors_header(shard_path)
for key, info in hdr.items():
if key == "__metadata__":
continue
entries.append((rewrite_key(key), shard_path, data_off, info))
return entries
def write_consolidated(out_path: str, entries):
entries = sorted(entries, key=lambda e: e[0])
new_header = {}
cur_offset = 0
for new_key, shard_path, data_off, info in entries:
start, end = info["data_offsets"]
size = end - start
new_header[new_key] = {
"dtype": info["dtype"],
"shape": info["shape"],
"data_offsets": [cur_offset, cur_offset + size],
}
cur_offset += size
header_json = json.dumps(new_header, separators=(",", ":")).encode("utf-8")
pad = (-len(header_json)) % 8
header_json = header_json + (b" " * pad)
with open(out_path, "wb") as out:
out.write(struct.pack("<Q", len(header_json)))
out.write(header_json)
for new_key, shard_path, data_off, info in entries:
start, end = info["data_offsets"]
with open(shard_path, "rb") as src:
src.seek(data_off + start)
remaining = end - start
while remaining > 0:
chunk = src.read(min(8 * 1024 * 1024, remaining))
if not chunk:
raise IOError(f"Truncated tensor in {shard_path}")
out.write(chunk)
remaining -= len(chunk)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", help="HF Qwen3-VL directory or single safetensors file")
parser.add_argument("output", help="Output single safetensors path")
args = parser.parse_args()
entries = stage_tensors(args.input)
print(f"Tensors: {len(entries)}")
print(f"Writing -> {args.output}")
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
write_consolidated(args.output, entries)
print(f"Done. Output size: {os.path.getsize(args.output) / 1e9:.2f} GB")
if __name__ == "__main__":
main()

279
scripts/convert_sefi.py Normal file
View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""Convert a SeFi-Image diffusers checkpoint into a single sd.cpp-compatible safetensors.
Operates on raw safetensors bytes so any dtype (BF16, F32, ...) is preserved exactly.
No numpy or torch dependency required.
Usage:
python3 scripts/convert_sefi.py <sefi_diffusers_dir> <output.safetensors>
"""
import argparse
import json
import os
import re
import struct
import sys
_LINEAR_TO_LIN = re.compile(r"\.linear\.")
_SHARED_MOD_PREFIXES = (
"double_stream_modulation_img",
"double_stream_modulation_txt",
"single_stream_modulation",
)
def rewrite_transformer_key(key: str) -> str:
if key.startswith("backbone."):
key = key[len("backbone."):]
elif key.startswith("dual_time_embed."):
return key
if any(key.startswith(prefix + ".") for prefix in _SHARED_MOD_PREFIXES):
key = _LINEAR_TO_LIN.sub(".lin.", key, count=1)
if key == "context_embedder.weight":
return "txt_in.weight"
if key == "context_embedder.bias":
return "txt_in.bias"
if key == "x_embedder.weight":
return "img_in.weight"
if key == "x_embedder.bias":
return "img_in.bias"
if key == "proj_out.weight":
return "final_layer.linear.weight"
if key == "proj_out.bias":
return "final_layer.linear.bias"
if key == "norm_out.linear.weight":
return "final_layer.adaLN_modulation.1.weight"
if key == "norm_out.linear.bias":
return "final_layer.adaLN_modulation.1.bias"
m = re.match(r"transformer_blocks\.(\d+)\.(.*)$", key)
if m:
return _rewrite_double_stream(m.group(1), m.group(2))
m = re.match(r"single_transformer_blocks\.(\d+)\.(.*)$", key)
if m:
return _rewrite_single_stream(m.group(1), m.group(2))
return key
def _rewrite_double_stream(idx: str, tail: str) -> str:
dst = f"double_blocks.{idx}."
mapping = {
"norm1.linear.weight": "img_mod.lin.weight",
"norm1_context.linear.weight": "txt_mod.lin.weight",
"attn.norm_q.weight": "img_attn.norm.query_norm.scale",
"attn.norm_k.weight": "img_attn.norm.key_norm.scale",
"attn.norm_added_q.weight": "txt_attn.norm.query_norm.scale",
"attn.norm_added_k.weight": "txt_attn.norm.key_norm.scale",
"attn.to_out.0.weight": "img_attn.proj.weight",
"attn.to_add_out.weight": "txt_attn.proj.weight",
"ff.net.0.proj.weight": "img_mlp.0.weight",
"ff.net.2.weight": "img_mlp.2.weight",
"ff_context.net.0.proj.weight": "txt_mlp.0.weight",
"ff_context.net.2.weight": "txt_mlp.2.weight",
"ff.linear_in.weight": "img_mlp.0.weight",
"ff.linear_out.weight": "img_mlp.2.weight",
"ff_context.linear_in.weight": "txt_mlp.0.weight",
"ff_context.linear_out.weight": "txt_mlp.2.weight",
}
return dst + mapping.get(tail, tail)
# QKV triplets to fuse on output: source tails -> target fused tail.
# Each tuple is (q_tail, k_tail, v_tail, fused_target_tail).
QKV_DOUBLE_TRIPLETS = [
("attn.to_q.weight", "attn.to_k.weight", "attn.to_v.weight", "img_attn.qkv.weight"),
("attn.add_q_proj.weight", "attn.add_k_proj.weight", "attn.add_v_proj.weight", "txt_attn.qkv.weight"),
]
def _rewrite_single_stream(idx: str, tail: str) -> str:
dst = f"single_blocks.{idx}."
mapping = {
"norm.linear.weight": "modulation.lin.weight",
"attn.norm_q.weight": "norm.query_norm.scale",
"attn.norm_k.weight": "norm.key_norm.scale",
"attn.to_qkv_mlp_proj.weight": "linear1.weight",
"attn.to_out.weight": "linear2.weight",
}
return dst + mapping.get(tail, tail)
def read_safetensors_header(path: str):
"""Return (header dict, data start byte offset)."""
with open(path, "rb") as f:
hdr_len = struct.unpack("<Q", f.read(8))[0]
hdr_bytes = f.read(hdr_len)
return json.loads(hdr_bytes), 8 + hdr_len
def collect_shard_paths(directory: str, weight_pattern: str):
index_path = os.path.join(directory, f"{weight_pattern}.safetensors.index.json")
if os.path.isfile(index_path):
with open(index_path) as f:
idx = json.load(f)
return sorted({os.path.join(directory, n) for n in idx["weight_map"].values()})
single = os.path.join(directory, f"{weight_pattern}.safetensors")
if not os.path.isfile(single):
raise FileNotFoundError(f"No checkpoint at {directory}: missing {weight_pattern}")
return [single]
def stage_tensors_for_section(section_dir: str, rewrite_fn):
"""Return a list of (new_key, shard_path, data_start_offset, info_dict) entries.
A "qkv_fuse" pseudo-entry with three source descriptors is emitted when a
transformer_blocks.* split q/k/v triplet is found, so the writer can fuse
them into a single output tensor.
"""
entries = []
# First, index all raw keys per shard so we can detect qkv triplets.
raw_by_block = {} # block_idx -> {tail: (key, shard_path, data_off, info)}
raw_others = []
for shard_path in collect_shard_paths(section_dir, "diffusion_pytorch_model"):
hdr, data_off = read_safetensors_header(shard_path)
for key, info in hdr.items():
if key == "__metadata__":
continue
m = re.match(r"backbone\.transformer_blocks\.(\d+)\.(.*)$", key)
if m and any(m.group(2) in trip[:3] for trip in QKV_DOUBLE_TRIPLETS):
idx = m.group(1)
raw_by_block.setdefault(idx, {})[m.group(2)] = (key, shard_path, data_off, info)
else:
raw_others.append((key, shard_path, data_off, info))
for key, shard_path, data_off, info in raw_others:
new_key = rewrite_fn(key)
# Swap the (scale, shift) halves to (shift, scale) at conversion time so
# the on-disk weight matches BFL flux ordering and the runtime stays
# version-agnostic. norm_out.linear weight shape is [2*dim, dim] and bias
# is [2*dim]; both split along axis 0 (outermost == row-major outer).
if new_key in ("final_layer.adaLN_modulation.1.weight",
"final_layer.adaLN_modulation.1.bias"):
info = dict(info)
info["_chunk_swap_halves"] = True
entries.append((new_key, shard_path, data_off, info))
for block_idx, tails in raw_by_block.items():
for q_tail, k_tail, v_tail, fused_tail in QKV_DOUBLE_TRIPLETS:
if q_tail in tails and k_tail in tails and v_tail in tails:
q = tails[q_tail]; k = tails[k_tail]; v = tails[v_tail]
# Validate shapes match.
q_shape = q[3]["shape"]; k_shape = k[3]["shape"]; v_shape = v[3]["shape"]
if q_shape != k_shape or q_shape != v_shape:
raise ValueError(f"qkv shape mismatch at block {block_idx} {q_tail}: q={q_shape} k={k_shape} v={v_shape}")
fused_shape = [q_shape[0] * 3] + list(q_shape[1:])
fused_info = {
"dtype": q[3]["dtype"],
"shape": fused_shape,
"_qkv_sources": [q, k, v], # pseudo field consumed by writer
}
entries.append((f"double_blocks.{block_idx}.{fused_tail}",
None, None, fused_info))
del tails[q_tail]; del tails[k_tail]; del tails[v_tail]
# Anything left in tails was an unmatched single - pass through.
for tail, payload in tails.items():
entries.append((rewrite_fn(payload[0]),) + payload[1:])
return entries
_DTYPE_BYTES = {
"BF16": 2, "F16": 2, "F32": 4, "F64": 8,
"U8": 1, "I8": 1, "I16": 2, "I32": 4, "I64": 8,
"BOOL": 1,
}
def _total_bytes(info: dict) -> int:
if "_qkv_sources" in info:
elems = 1
for d in info["shape"]:
elems *= d
return elems * _DTYPE_BYTES[info["dtype"]]
start, end = info["data_offsets"]
return end - start
def write_consolidated(out_path: str, entries):
"""Write a single safetensors file by streaming raw bytes from each shard.
For qkv-fused entries, q/k/v are concatenated along axis 0 (row-major), so a
simple byte-level concatenation produces the correct fused layout for any
standard dtype.
"""
entries = sorted(entries, key=lambda e: e[0])
new_header = {}
cur_offset = 0
for new_key, shard_path, data_off, info in entries:
size = _total_bytes(info)
new_header[new_key] = {
"dtype": info["dtype"],
"shape": info["shape"],
"data_offsets": [cur_offset, cur_offset + size],
}
cur_offset += size
header_json = json.dumps(new_header, separators=(",", ":")).encode("utf-8")
pad = (-len(header_json)) % 8
header_json = header_json + (b" " * pad)
def copy_range(src_path, src_data_off, src_info, out, byte_range=None):
start, end = src_info["data_offsets"]
if byte_range is not None:
sub_start, sub_end = byte_range
start, end = start + sub_start, start + sub_end
with open(src_path, "rb") as src:
src.seek(src_data_off + start)
remaining = end - start
while remaining > 0:
chunk = src.read(min(8 * 1024 * 1024, remaining))
if not chunk:
raise IOError(f"Truncated tensor in {src_path}")
out.write(chunk)
remaining -= len(chunk)
with open(out_path, "wb") as out:
out.write(struct.pack("<Q", len(header_json)))
out.write(header_json)
for new_key, shard_path, data_off, info in entries:
if "_qkv_sources" in info:
for q_entry in info["_qkv_sources"]:
_, src_path, src_data_off, src_info = q_entry
copy_range(src_path, src_data_off, src_info, out)
elif info.get("_chunk_swap_halves"):
size = _total_bytes(info)
half = size // 2
if size != half * 2:
raise ValueError(f"{new_key}: odd byte size {size} cannot be split into halves")
copy_range(shard_path, data_off, info, out, byte_range=(half, size))
copy_range(shard_path, data_off, info, out, byte_range=(0, half))
else:
copy_range(shard_path, data_off, info, out)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input_dir", help="SeFi diffusers checkpoint directory")
parser.add_argument("output", help="Output transformer safetensors path (load via --diffusion-model)")
args = parser.parse_args()
transformer_entries = stage_tensors_for_section(
os.path.join(args.input_dir, "transformer"), rewrite_transformer_key)
print(f"Transformer tensors: {len(transformer_entries)}")
print(f"Writing {len(transformer_entries)} tensors -> {args.output}")
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
write_consolidated(args.output, transformer_entries)
print(f"Done. Output size: {os.path.getsize(args.output) / 1e9:.2f} GB")
if __name__ == "__main__":
main()

88
scripts/face_detect.py Normal file
View File

@@ -0,0 +1,88 @@
import os
import sys
import numpy as np
import torch
from diffusers.utils import load_image
# pip install insightface==0.7.3
from insightface.app import FaceAnalysis
from insightface.data import get_image as ins_get_image
from safetensors.torch import save_file
###
# https://github.com/cubiq/ComfyUI_IPAdapter_plus/issues/165#issue-2055829543
###
class FaceAnalysis2(FaceAnalysis):
# NOTE: allows setting det_size for each detection call.
# the model allows it but the wrapping code from insightface
# doesn't show it, and people end up loading duplicate models
# for different sizes where there is absolutely no need to
def get(self, img, max_num=0, det_size=(640, 640)):
if det_size is not None:
self.det_model.input_size = det_size
return super().get(img, max_num)
def analyze_faces(face_analysis: FaceAnalysis, img_data: np.ndarray, det_size=(640, 640)):
# NOTE: try detect faces, if no faces detected, lower det_size until it does
detection_sizes = [None] + [(size, size) for size in range(640, 256, -64)] + [(256, 256)]
for size in detection_sizes:
faces = face_analysis.get(img_data, det_size=size)
if len(faces) > 0:
return faces
return []
if __name__ == "__main__":
#face_detector = FaceAnalysis2(providers=['CUDAExecutionProvider'], allowed_modules=['detection', 'recognition'])
face_detector = FaceAnalysis2(providers=['CPUExecutionProvider'], allowed_modules=['detection', 'recognition'])
face_detector.prepare(ctx_id=0, det_size=(640, 640))
#input_folder_name = './scarletthead_woman'
input_folder_name = sys.argv[1]
image_basename_list = os.listdir(input_folder_name)
image_path_list = sorted([os.path.join(input_folder_name, basename) for basename in image_basename_list])
input_id_images = []
for image_path in image_path_list:
input_id_images.append(load_image(image_path))
id_embed_list = []
for img in input_id_images:
img = np.array(img)
img = img[:, :, ::-1]
faces = analyze_faces(face_detector, img)
if len(faces) > 0:
id_embed_list.append(torch.from_numpy((faces[0]['embedding'])))
if len(id_embed_list) == 0:
raise ValueError(f"No face detected in input image pool")
id_embeds = torch.stack(id_embed_list)
# for r in id_embeds:
# print(r)
# #torch.save(id_embeds, input_folder_name+'/id_embeds.pt');
# weights = dict()
# weights["id_embeds"] = id_embeds
# save_file(weights, input_folder_name+'/id_embeds.safetensors')
binary_data = id_embeds.numpy().tobytes()
two = 4
zero = 0
one = 1
tensor_name = "id_embeds"
# Write binary data to a file
with open(input_folder_name+'/id_embeds.bin', "wb") as f:
f.write(two.to_bytes(4, byteorder='little'))
f.write((len(tensor_name)).to_bytes(4, byteorder='little'))
f.write(zero.to_bytes(4, byteorder='little'))
f.write((id_embeds.shape[1]).to_bytes(4, byteorder='little'))
f.write((id_embeds.shape[0]).to_bytes(4, byteorder='little'))
f.write(one.to_bytes(4, byteorder='little'))
f.write(one.to_bytes(4, byteorder='little'))
f.write(tensor_name.encode('ascii'))
f.write(binary_data)

60
scripts/format-code.ps1 Normal file
View File

@@ -0,0 +1,60 @@
$patterns = @(
"src/*.cpp"
"src/*.h"
"src/*.hpp"
"src/conditioning/*.cpp"
"src/conditioning/*.h"
"src/conditioning/*.hpp"
"src/core/*.cpp"
"src/core/*.h"
"src/core/*.hpp"
"src/extensions/*.cpp"
"src/extensions/*.h"
"src/extensions/*.hpp"
"src/runtime/*.cpp"
"src/runtime/*.h"
"src/runtime/*.hpp"
"src/model/*/*.cpp"
"src/model/*/*.h"
"src/model/*/*.hpp"
"src/tokenizers/*.h"
"src/tokenizers/*.cpp"
"src/tokenizers/vocab/*.h"
"src/tokenizers/vocab/*.cpp"
"src/model_io/*.h"
"src/model_io/*.cpp"
"examples/cli/*.cpp"
"examples/cli/*.h"
"examples/server/*.cpp"
"examples/common/*.hpp"
"examples/common/*.h"
"examples/common/*.cpp"
)
Push-Location (Join-Path $PSScriptRoot "..")
try {
$root = (Get-Location).Path
foreach ($pattern in $patterns) {
$files = Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue | Sort-Object FullName
foreach ($file in $files) {
$relativePath = $file.FullName.Substring($root.Length).TrimStart('\', '/') -replace '\\', '/'
if ($relativePath -like "vocab*") {
continue
}
Write-Host "formatting '$relativePath'"
# if ($relativePath -ne "stable-diffusion.h") {
# clang-tidy -fix -p build_linux/ "$relativePath"
# }
& clang-format -style=file -i $relativePath
}
}
} finally {
Pop-Location
}

24
scripts/format-code.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
cd "$repo_root"
for f in src/*.cpp src/*.h src/*.hpp \
src/conditioning/*.cpp src/conditioning/*.h src/conditioning/*.hpp \
src/core/*.cpp src/core/*.h src/core/*.hpp \
src/extensions/*.cpp src/extensions/*.h src/extensions/*.hpp \
src/runtime/*.cpp src/runtime/*.h src/runtime/*.hpp \
src/model/*/*.cpp src/model/*/*.h src/model/*/*.hpp \
src/tokenizers/*.h src/tokenizers/*.cpp src/tokenizers/vocab/*.h src/tokenizers/vocab/*.cpp \
src/model_io/*.h src/model_io/*.cpp examples/cli/*.cpp examples/cli/*.h examples/server/*.cpp \
examples/common/*.hpp examples/common/*.h examples/common/*.cpp; do
[[ -e "$f" ]] || continue
[[ "$f" == vocab* ]] && continue
echo "formatting '$f'"
# if [ "$f" != "stable-diffusion.h" ]; then
# clang-tidy -fix -p build_linux/ "$f"
# fi
clang-format -style=file -i "$f"
done

134
scripts/pulid_extract_id.py Normal file
View File

@@ -0,0 +1,134 @@
"""
Precompute a PuLID-Flux identity embedding from a single source portrait.
Writes a gguf file (a single tensor `pulid_id`) that stable-diffusion.cpp's
`--pulid-id-embedding` flag consumes.
Dependencies (recommended: vendor rather than pip-install due to upstream
packaging quirks):
- torch + safetensors
- The ToTheBeginning/PuLID repository's `pulid/` package and `eva_clip/`.
Put them on PYTHONPATH or sys.path before running this script.
- insightface, facexlib, torchvision, opencv-python, huggingface_hub, gguf
- numpy, Pillow
Usage:
python scripts/pulid_extract_id.py \\
--portrait /path/to/source-photo.jpg \\
--pulid-weights /path/to/pulid_flux_v0.9.1.safetensors \\
--out /path/to/source.pulidembd
The portrait must contain a clearly visible face. insightface's antelopev2
detector will be auto-downloaded on first run.
"""
from __future__ import annotations
import argparse
import os
import sys
from types import SimpleNamespace
def extract(portrait_path: str, pulid_weights: str) -> "torch.Tensor":
import numpy as np
import torch
from PIL import Image
from pulid.pipeline_flux import PuLIDPipeline
if torch.cuda.is_available():
device, onnx_provider = "cuda", "gpu"
else:
device, onnx_provider = "cpu", "cpu"
print(f"device={device}", flush=True)
# PuLIDPipeline only attaches pulid_ca attributes to `dit` during
# construction; get_id_embedding() never runs Flux, so a dummy object is
# enough and avoids importing/building a Flux skeleton.
print("instantiating PuLIDPipeline with a dummy Flux object", flush=True)
dit = SimpleNamespace()
pulid = PuLIDPipeline(dit=dit,
device=device,
weight_dtype=torch.bfloat16,
onnx_provider=onnx_provider)
print(f"loading PuLID weights from {pulid_weights}", flush=True)
pulid.load_pretrain(pretrain_path=pulid_weights, version="v0.9.1")
print(f"extracting ID embedding from {portrait_path}", flush=True)
face_img = np.array(Image.open(portrait_path).convert("RGB"))
id_embedding, _ = pulid.get_id_embedding(face_img)
print(f"id embedding shape={tuple(id_embedding.shape)} dtype={id_embedding.dtype}",
flush=True)
if id_embedding.ndim == 3 and id_embedding.shape[0] == 1:
id_embedding = id_embedding[0]
return id_embedding
def write_embd(tensor, out_path: str, dtype_choice: str) -> None:
import gguf
import torch
if tensor.ndim != 2:
raise ValueError(f"expected (num_tokens, token_dim); got {tuple(tensor.shape)}")
num_tokens, token_dim = tensor.shape
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
writer = gguf.GGUFWriter(out_path, arch="pulid")
writer.add_uint32("pulid.version", 1)
if dtype_choice == "fp16":
arr = tensor.to(torch.float16).contiguous().cpu().numpy()
writer.add_tensor("pulid_id", arr)
elif dtype_choice == "fp32":
arr = tensor.to(torch.float32).contiguous().cpu().numpy()
writer.add_tensor("pulid_id", arr)
elif dtype_choice == "bf16":
raw = tensor.to(torch.bfloat16).contiguous().view(torch.uint16).cpu().numpy()
writer.add_tensor("pulid_id", raw,
raw_shape=(int(num_tokens), int(token_dim)),
raw_dtype=gguf.GGMLQuantizationType.BF16)
else:
raise ValueError(f"unknown --dtype {dtype_choice}")
writer.write_header_to_file()
writer.write_kv_data_to_file()
writer.write_tensors_to_file()
writer.close()
print(f"wrote {out_path}: gguf, tensor pulid_id [{token_dim}, {num_tokens}] {dtype_choice}",
flush=True)
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--portrait", required=True,
help="Path to the source portrait image (JPG/PNG).")
ap.add_argument("--pulid-weights", required=True,
help="Path to pulid_flux_v0.9.x.safetensors.")
ap.add_argument("--out", required=True,
help="Output path for the .pulidembd binary.")
ap.add_argument("--dtype", default="fp16",
choices=["fp16", "bf16", "fp32"],
help="Storage dtype (default fp16; produces ~131 KB).")
args = ap.parse_args()
if not os.path.exists(args.portrait):
print(f"ERROR: portrait not found at {args.portrait}", file=sys.stderr)
return 2
if not os.path.exists(args.pulid_weights):
print(f"ERROR: PuLID weights not found at {args.pulid_weights}", file=sys.stderr)
return 3
embedding = extract(args.portrait, args.pulid_weights)
write_embd(embedding, args.out, args.dtype)
return 0
if __name__ == "__main__":
raise SystemExit(main())

234
scripts/remove_utf8_bom.py Normal file
View File

@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""Remove UTF-8 BOMs from files under a directory.
By default this scans the current working directory recursively and skips
repository areas that should not be touched by ordinary maintenance scripts.
Only files whose first three bytes are the UTF-8 BOM are rewritten.
"""
import argparse
import os
import shutil
import sys
import tempfile
from pathlib import Path
UTF8_BOM = b"\xef\xbb\xbf"
DEFAULT_EXCLUDED_DIR_NAMES = {
".git",
".hg",
".svn",
".mypy_cache",
".pytest_cache",
"__pycache__",
"test",
}
DEFAULT_EXCLUDED_DIR_PREFIXES = {
"build",
}
DEFAULT_EXCLUDED_REL_DIRS = {
"examples/server/frontend",
"ggml",
"models",
"src/vocab",
"thirdparty",
}
def rel_posix(path: Path, root: Path) -> str:
try:
return path.relative_to(root).as_posix()
except ValueError:
return path.as_posix()
def should_skip_dir(
path: Path,
root: Path,
excluded_rel_dirs: set[str],
excluded_names: set[str],
excluded_prefixes: set[str],
) -> bool:
rel = rel_posix(path, root)
return (
path.name in excluded_names
or rel in excluded_rel_dirs
or any(path.name.startswith(prefix) for prefix in excluded_prefixes)
)
def iter_files(
root: Path,
recursive: bool,
excluded_rel_dirs: set[str],
excluded_names: set[str],
excluded_prefixes: set[str],
follow_symlinks: bool,
):
if recursive:
for dirpath, dirnames, filenames in os.walk(root, followlinks=follow_symlinks):
current_dir = Path(dirpath)
dirnames[:] = [
name
for name in dirnames
if not should_skip_dir(
current_dir / name,
root,
excluded_rel_dirs,
excluded_names,
excluded_prefixes,
)
]
for filename in filenames:
path = current_dir / filename
if path.is_symlink() and not follow_symlinks:
continue
yield path
else:
for path in root.iterdir():
if path.is_file() and (follow_symlinks or not path.is_symlink()):
yield path
def has_utf8_bom(path: Path) -> bool:
with path.open("rb") as f:
return f.read(len(UTF8_BOM)) == UTF8_BOM
def strip_utf8_bom(path: Path) -> None:
tmp_path = None
try:
with path.open("rb") as src:
if src.read(len(UTF8_BOM)) != UTF8_BOM:
return
fd, tmp_name = tempfile.mkstemp(
prefix=f".{path.name}.",
suffix=".tmp",
dir=str(path.parent),
)
tmp_path = Path(tmp_name)
with os.fdopen(fd, "wb") as dst:
shutil.copyfileobj(src, dst, length=1024 * 1024)
shutil.copystat(path, tmp_path, follow_symlinks=False)
os.replace(tmp_path, path)
tmp_path = None
finally:
if tmp_path is not None:
try:
tmp_path.unlink()
except FileNotFoundError:
pass
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Scan files and convert UTF-8 BOM files to UTF-8 without BOM.",
)
parser.add_argument(
"root",
nargs="?",
default=".",
help="Directory to scan. Defaults to the current directory.",
)
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Only list files that would be converted.",
)
parser.add_argument(
"--no-recursive",
action="store_true",
help="Only scan files directly under root.",
)
parser.add_argument(
"--include-repo-excluded",
action="store_true",
help="Do not skip default repository excluded directories.",
)
parser.add_argument(
"--exclude-dir",
action="append",
default=[],
metavar="DIR",
help="Additional directory name or root-relative path to skip. Can be used multiple times.",
)
parser.add_argument(
"--follow-symlinks",
action="store_true",
help="Follow symlinked directories and files.",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Only print the final summary.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
root = Path(args.root).resolve()
if not root.is_dir():
print(f"error: not a directory: {root}", file=sys.stderr)
return 2
excluded_names = set()
excluded_rel_dirs = set()
excluded_prefixes = set()
if not args.include_repo_excluded:
excluded_names.update(DEFAULT_EXCLUDED_DIR_NAMES)
excluded_rel_dirs.update(DEFAULT_EXCLUDED_REL_DIRS)
excluded_prefixes.update(DEFAULT_EXCLUDED_DIR_PREFIXES)
for item in args.exclude_dir:
normalized = Path(item).as_posix().strip("/")
if "/" in normalized:
excluded_rel_dirs.add(normalized)
else:
excluded_names.add(normalized)
scanned = 0
converted = 0
errors = 0
for path in iter_files(
root=root,
recursive=not args.no_recursive,
excluded_rel_dirs=excluded_rel_dirs,
excluded_names=excluded_names,
excluded_prefixes=excluded_prefixes,
follow_symlinks=args.follow_symlinks,
):
scanned += 1
try:
if not has_utf8_bom(path):
continue
converted += 1
rel = rel_posix(path, root)
if args.dry_run:
if not args.quiet:
print(f"would convert: {rel}")
else:
strip_utf8_bom(path)
if not args.quiet:
print(f"converted: {rel}")
except OSError as exc:
errors += 1
print(f"error: {rel_posix(path, root)}: {exc}", file=sys.stderr)
action = "would convert" if args.dry_run else "converted"
print(f"scanned {scanned} file(s), {action} {converted}, errors {errors}")
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())