"""Упаковка MuScriptor в один самодостаточный архив. Внутри: веса, конфиг, карточка модели, все инструменты и manifest с хешем. Архив намеренно без сжатия: safetensors это уже плотные числа, gzip даёт ~2% выигрыша за десятки минут CPU, а манифест и хеши считаются на лету. Запуск: python tools/pack_bundle.py --model-dir muscriptor-large --out out/muscriptor-bundle.tar """ import argparse import hashlib import io import json import os import struct import sys import tarfile import time BUNDLE_VERSION = "1.0" CHUNK = 8 * 1024 * 1024 def human(size): for unit in ("Б", "КБ", "МБ", "ГБ", "ТБ"): if size < 1024 or unit == "ТБ": return "%.2f %s" % (size, unit) size /= 1024.0 def model_stats(path): """Размер, число тензоров и параметров — по заголовку, без чтения данных.""" with open(path, "rb") as handle: length = struct.unpack(" 0 else CHUNK) if block: self.digest.update(block) self.done += len(block) if self.total: percent = int(self.done * 100 / self.total) if percent != self.last: self.last = percent print("\r %s: %3d%% %s / %s" % (self.label, percent, human(self.done), human(self.total)), end="", flush=True) elif self.total: print("\r %s: 100%% %s" % (self.label, human(self.total)), flush=True) return block def main(): parser = argparse.ArgumentParser() parser.add_argument("--model-dir", default="muscriptor-large") parser.add_argument("--out", default="out/muscriptor-large-bundle.tar") parser.add_argument("--tools", default=None, help="папка с инструментами (по умолчанию рядом с этим скриптом)") parser.add_argument("--license-note", default=None) args = parser.parse_args() weights = os.path.join(args.model_dir, "model.safetensors") config = os.path.join(args.model_dir, "config.json") if not os.path.isfile(weights): raise SystemExit("нет весов: " + weights) print("[5%] считаю параметры по заголовку") stats = model_stats(weights) print(" тензоров %d, параметров %.1f млн, типы %s, вес %s" % (stats["tensors"], stats["parameters"] / 1e6, stats["dtypes"], human(stats["bytes"]))) tools_dir = args.tools or os.path.dirname(os.path.abspath(__file__)) tools = sorted(name for name in os.listdir(tools_dir) if name.endswith(".py")) print("[10%] инструменты: " + ", ".join(tools) if tools else "[10%] инструменты не найдены") os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) digest = hashlib.sha256() manifest = { "bundle_version": BUNDLE_VERSION, "built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "model": { "name": "MuScriptor Large", "source": "https://huggingface.co/MuScriptor/muscriptor-large", "variant": "large", "tensors": stats["tensors"], "parameters": stats["parameters"], "dtypes": stats["dtypes"], "weights_bytes": stats["bytes"], }, "license": { "weights": "CC-BY-NC-4.0", "code": "MIT", "note": "Веса некоммерческие: их нельзя использовать в платном продукте.", }, "files": ["model/model.safetensors", "model/config.json", "model/README.md", "manifest.json"], "tools": tools, } print("[15%] пишу архив " + args.out) with tarfile.open(args.out, "w") as archive: for name in tools: path = os.path.join(tools_dir, name) info = tarfile.TarInfo("tools/" + name) info.size = os.path.getsize(path) info.mtime = int(time.time()) with open(path, "rb") as handle: archive.addfile(info, handle) manifest["files"].append("tools/" + name) if args.license_note and os.path.isfile(args.license_note): data = open(args.license_note, "rb").read() info = tarfile.TarInfo("LICENSE_NOTICE.txt") info.size = len(data) info.mtime = int(time.time()) archive.addfile(info, io.BytesIO(data)) manifest["files"].append("LICENSE_NOTICE.txt") if os.path.isfile(config): data = open(config, "rb").read() info = tarfile.TarInfo("model/config.json") info.size = len(data) info.mtime = int(time.time()) archive.addfile(info, io.BytesIO(data)) card = os.path.join(args.model_dir, "README.md") if os.path.isfile(card): data = open(card, "rb").read() info = tarfile.TarInfo("model/README.md") info.size = len(data) info.mtime = int(time.time()) archive.addfile(info, io.BytesIO(data)) add_with_progress(archive, weights, "model/model.safetensors", digest, stats["bytes"], "веса") manifest["model"]["sha256"] = digest.hexdigest() blob = json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8") info = tarfile.TarInfo("manifest.json") info.size = len(blob) info.mtime = int(time.time()) archive.addfile(info, io.BytesIO(blob)) size = os.path.getsize(args.out) print("[100%%] готово: %s (%s)" % (args.out, human(size))) print(" sha256 весов: " + manifest["model"]["sha256"]) print(" в истории: python tools/pack_bundle.py --model-dir ... --out ...") return 0 if __name__ == "__main__": sys.exit(main())