Files

225 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: ./scripts/build_offline_bundle.sh [options]
Create the local asset bundle used by the offline installer.
The wheel bundle is taken from the selected Python environment's installed
runtime versions so the offline target matches the source ComfyUI runtime.
Options:
--python PATH Python executable to use for pip download (default: python3)
--bundle-root PATH Bundle root to populate (default: ./_offline)
--model-root PATH Existing HeartMuLa model root to copy into the bundle
--include-torch Download a torch wheel that matches the selected Python env
--skip-torchaudio Skip downloading a torchaudio wheel from the selected Python env
--help Show this help text
EOF
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PYTHON_BIN="python3"
BUNDLE_ROOT="$REPO_ROOT/_offline"
WHEEL_DIR="$BUNDLE_ROOT/wheels"
MODEL_BUNDLE_ROOT="$BUNDLE_ROOT/models/HeartMuLa"
LOCK_FILE="$BUNDLE_ROOT/requirements-lock.txt"
MODEL_SOURCE=""
INCLUDE_TORCH=0
INCLUDE_TORCHAUDIO=1
while [[ $# -gt 0 ]]; do
case "$1" in
--python)
PYTHON_BIN="$2"
shift 2
;;
--bundle-root)
BUNDLE_ROOT="$2"
WHEEL_DIR="$BUNDLE_ROOT/wheels"
MODEL_BUNDLE_ROOT="$BUNDLE_ROOT/models/HeartMuLa"
shift 2
;;
--model-root)
MODEL_SOURCE="$2"
shift 2
;;
--include-torch)
INCLUDE_TORCH=1
shift
;;
--skip-torchaudio)
INCLUDE_TORCHAUDIO=0
shift
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
mkdir -p "$WHEEL_DIR" "$BUNDLE_ROOT/models"
has_supported_generation_pair() {
local root="$1"
[[ -f "$root/HeartMuLa-RL-oss-3B-20260123/config.json" && -f "$root/HeartCodec-oss-20260123/config.json" ]] && return 0
[[ -f "$root/HeartMuLa-oss-3B-happy-new-year/config.json" && -f "$root/HeartCodec-oss-20260123/config.json" ]] && return 0
[[ -f "$root/HeartMuLa-oss-3B/config.json" && -f "$root/HeartCodec-oss/config.json" ]] && return 0
return 1
}
model_root_ready() {
local root="$1"
[[ -f "$root/gen_config.json" ]] || return 1
[[ -f "$root/tokenizer.json" ]] || return 1
[[ -f "$root/HeartTranscriptor-oss/config.json" ]] || return 1
has_supported_generation_pair "$root"
}
copy_model_bundle() {
local src="$1"
local dst="$2"
rm -rf "$dst"
if cp -cR "$src" "$dst" 2>/dev/null; then
return 0
fi
cp -R "$src" "$dst"
}
write_runtime_lockfile() {
HEARTMULA_INCLUDE_TORCH="$INCLUDE_TORCH" HEARTMULA_INCLUDE_TORCHAUDIO="$INCLUDE_TORCHAUDIO" "$PYTHON_BIN" - "$LOCK_FILE" <<'PY'
from importlib import metadata, util
import os
from pathlib import Path
import sys
pairs = [
("accelerate", "accelerate"),
("certifi", "certifi"),
("einops", "einops"),
("huggingface_hub", "huggingface_hub"),
("numpy", "numpy"),
("soundfile", "soundfile"),
("tokenizers", "tokenizers"),
]
if os.environ.get("HEARTMULA_INCLUDE_TORCH") == "1":
pairs.append(("torch", "torch"))
if os.environ.get("HEARTMULA_INCLUDE_TORCHAUDIO") == "1":
pairs.append(("torchaudio", "torchaudio"))
pairs.extend(
[
("torchao", "torchao"),
("torchtune", "torchtune"),
("tqdm", "tqdm"),
("transformers", "transformers"),
("vector_quantize_pytorch", "vector-quantize-pytorch"),
]
)
missing = []
lines = []
for module_name, package_name in pairs:
if util.find_spec(module_name) is None:
missing.append(package_name)
continue
version = metadata.version(package_name)
lines.append(f"{package_name}=={version}")
if missing:
raise SystemExit(
"Selected Python is missing required runtime packages: " + ", ".join(missing)
)
Path(sys.argv[1]).write_text("\n".join(lines) + "\n", encoding="utf-8")
PY
}
download_installed_wheel() {
local module_name="$1"
local package_name="$2"
local version
version="$($PYTHON_BIN - <<PY
import importlib
import importlib.util
if importlib.util.find_spec("$module_name") is None:
print("")
else:
module = importlib.import_module("$module_name")
print(getattr(module, "__version__", ""))
PY
)"
if [[ -z "$version" ]]; then
echo "Skipping $package_name wheel download because $module_name is not installed in $PYTHON_BIN"
return
fi
"$PYTHON_BIN" -m pip download --only-binary=:all: --dest "$WHEEL_DIR" "$package_name==$version"
}
write_runtime_lockfile
"$PYTHON_BIN" -m pip download --only-binary=:all: --dest "$WHEEL_DIR" -r "$LOCK_FILE"
if [[ "$INCLUDE_TORCHAUDIO" -eq 1 ]]; then
download_installed_wheel "torchaudio" "torchaudio"
fi
if [[ "$INCLUDE_TORCH" -eq 1 ]]; then
download_installed_wheel "torch" "torch"
fi
if [[ -n "$MODEL_SOURCE" ]]; then
if ! model_root_ready "$MODEL_SOURCE"; then
echo "Model source is missing required HeartMuLa assets: $MODEL_SOURCE" >&2
echo "Expected gen_config.json, tokenizer.json, HeartTranscriptor-oss/config.json, and one supported model+codec pair." >&2
exit 1
fi
copy_model_bundle "$MODEL_SOURCE" "$MODEL_BUNDLE_ROOT"
fi
WHEEL_COUNT="$(find "$WHEEL_DIR" -maxdepth 1 -type f | wc -l | tr -d ' ')"
MODEL_STATE="not included"
if [[ -d "$MODEL_BUNDLE_ROOT" ]]; then
MODEL_STATE="$MODEL_BUNDLE_ROOT"
fi
cat > "$BUNDLE_ROOT/bundle-manifest.txt" <<EOF
Created: $(date -u +%Y-%m-%dT%H:%M:%SZ)
Python: $PYTHON_BIN
Wheel directory: $WHEEL_DIR
Wheel count: $WHEEL_COUNT
Included torch wheel: $INCLUDE_TORCH
Included torchaudio wheel: $INCLUDE_TORCHAUDIO
Locked package list: $LOCK_FILE
Model bundle: $MODEL_STATE
EOF
cat <<EOF
Offline bundle ready.
Wheels: $WHEEL_DIR
Models: $MODEL_STATE
Locked package list: $LOCK_FILE
Manifest: $BUNDLE_ROOT/bundle-manifest.txt
Next step on the offline machine:
./scripts/install_offline.sh --python /path/to/comfyui/python --comfyui-root /path/to/ComfyUI
EOF