""" ACE-Step Model Downloader This module provides functionality to download models from HuggingFace Hub or ModelScope. It supports automatic downloading when models are not found locally, with intelligent fallback between download sources. """ import os import sys import hashlib import shutil import argparse from typing import Optional, List, Dict, Tuple from pathlib import Path from loguru import logger # ============================================================================= # Model Code File Sync (GitHub repo -> checkpoint directories) # ============================================================================= # Mapping from checkpoint directory name to source model variant in acestep/models/ _CHECKPOINT_TO_VARIANT: Dict[str, str] = { "acestep-v15-turbo": "turbo", "acestep-v15-sft": "sft", "acestep-v15-base": "base", # SFT variants (base-SFT uses the same model code as SFT) "acestep-v15-base-sft-fix-inst": "sft", # Turbo variants all share the turbo model code "acestep-v15-turbo-shift1": "turbo", "acestep-v15-turbo-shift3": "turbo", "acestep-v15-turbo-continuous": "turbo", "acestep-v15-turbo-fix-inst-shift3": "turbo", "acestep-v15-turbo-fix-inst-shift-continuous": "turbo", "acestep-v15-turbo-fix-inst-shift-dynamic": "turbo", "acestep-v15-turbo-rl": "turbo", # XL (4B DiT) variants have their own model code under acestep/models/xl_*/ "acestep-v15-xl-base": "xl_base", "acestep-v15-xl-sft": "xl_sft", "acestep-v15-xl-turbo": "xl_turbo", } def _get_models_source_dir() -> Path: """Get the acestep/models/ directory (authoritative source for model code).""" return Path(__file__).resolve().parent / "models" def _file_hash(filepath: Path) -> str: """Compute SHA-256 hash of a file's contents.""" h = hashlib.sha256() with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): h.update(chunk) return h.hexdigest() def _check_code_mismatch(model_name: str, checkpoints_dir) -> List[str]: """ Compare .py files in acestep/models/{variant}/ with those in the checkpoint directory. Args: model_name: Checkpoint directory name (e.g. "acestep-v15-turbo") checkpoints_dir: Path to the checkpoints root directory Returns: List of filenames that differ (empty list if all match or model_name is unknown) """ variant = _CHECKPOINT_TO_VARIANT.get(model_name) if variant is None: return [] source_dir = _get_models_source_dir() / variant if not source_dir.exists(): return [] if isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) target_dir = checkpoints_dir / model_name mismatched = [] for src_file in source_dir.glob("*.py"): if src_file.name == "__init__.py": continue dst_file = target_dir / src_file.name if not dst_file.exists(): mismatched.append(src_file.name) elif _file_hash(src_file) != _file_hash(dst_file): mismatched.append(src_file.name) return mismatched def _sync_model_code_files(model_name: str, checkpoints_dir) -> List[str]: """ Copy .py files from acestep/models/{variant}/ into the checkpoint directory, overwriting the HuggingFace-downloaded versions. Args: model_name: Checkpoint directory name (e.g. "acestep-v15-turbo") checkpoints_dir: Path to the checkpoints root directory Returns: List of filenames that were synced (empty if model_name is unknown or no source) """ variant = _CHECKPOINT_TO_VARIANT.get(model_name) if variant is None: return [] source_dir = _get_models_source_dir() / variant if not source_dir.exists(): logger.warning(f"[Model Sync] Source directory not found: {source_dir}") return [] if isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) target_dir = checkpoints_dir / model_name if not target_dir.exists(): logger.warning(f"[Model Sync] Target directory not found: {target_dir}") return [] synced = [] for src_file in source_dir.glob("*.py"): if src_file.name == "__init__.py": continue dst_file = target_dir / src_file.name shutil.copy2(src_file, dst_file) synced.append(src_file.name) logger.debug(f"[Model Sync] Synced {src_file.name} -> {dst_file}") return synced # ============================================================================= # Network Detection & Smart Download # ============================================================================= def _can_access_google(timeout: float = 3.0) -> bool: """ Check if Google is accessible (to determine HuggingFace vs ModelScope). Args: timeout: Connection timeout in seconds Returns: True if Google is accessible, False otherwise """ import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.settimeout(timeout) sock.connect(("www.google.com", 443)) return True except (socket.timeout, socket.error, OSError): return False finally: sock.close() def _download_from_huggingface_internal( repo_id: str, local_dir: Path, token: Optional[str] = None, ) -> None: """ Internal function to download from HuggingFace Hub. Args: repo_id: HuggingFace repository ID (e.g., "ACE-Step/Ace-Step1.5") local_dir: Local directory to save the model token: HuggingFace token for private repos (optional) Raises: Exception: If download fails """ from huggingface_hub import snapshot_download logger.info(f"[Model Download] Downloading from HuggingFace: {repo_id} -> {local_dir}") snapshot_download( repo_id=repo_id, local_dir=str(local_dir), local_dir_use_symlinks="auto", token=token, ) def _download_from_modelscope_internal( repo_id: str, local_dir: Path, ) -> None: """ Internal function to download from ModelScope. Args: repo_id: ModelScope repository ID (e.g., "ACE-Step/Ace-Step1.5") local_dir: Local directory to save the model Raises: Exception: If download fails """ from modelscope import snapshot_download logger.info(f"[Model Download] Downloading from ModelScope: {repo_id} -> {local_dir}") snapshot_download( model_id=repo_id, local_dir=str(local_dir), ) def _smart_download( repo_id: str, local_dir: Path, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """ Smart download with automatic fallback between HuggingFace and ModelScope. Automatically detects network environment and chooses the best download source. If the primary source fails, automatically falls back to the alternative. Args: repo_id: Repository ID (same format for both HF and ModelScope) local_dir: Local directory to save the model token: HuggingFace token for private repos (optional) prefer_source: Preferred download source ("huggingface", "modelscope", or None for auto-detect) Returns: Tuple of (success, message) """ # Ensure directory exists local_dir.mkdir(parents=True, exist_ok=True) # Determine primary source if prefer_source == "huggingface": use_huggingface_first = True logger.info("[Model Download] User preference: HuggingFace Hub") elif prefer_source == "modelscope": use_huggingface_first = False logger.info("[Model Download] User preference: ModelScope") else: # Auto-detect network environment can_access_google = _can_access_google() use_huggingface_first = can_access_google logger.info(f"[Model Download] Auto-detected: {'HuggingFace Hub' if can_access_google else 'ModelScope'}") if use_huggingface_first: logger.info("[Model Download] Using HuggingFace Hub...") try: _download_from_huggingface_internal(repo_id, local_dir, token) return True, f"Successfully downloaded from HuggingFace: {repo_id}" except Exception as e: logger.warning(f"[Model Download] HuggingFace download failed: {e}") logger.info("[Model Download] Falling back to ModelScope...") try: _download_from_modelscope_internal(repo_id, local_dir) return True, f"Successfully downloaded from ModelScope: {repo_id}" except Exception as e2: error_msg = f"Both HuggingFace and ModelScope downloads failed. HF: {e}, MS: {e2}" logger.error(error_msg) return False, error_msg else: logger.info("[Model Download] Using ModelScope...") try: _download_from_modelscope_internal(repo_id, local_dir) return True, f"Successfully downloaded from ModelScope: {repo_id}" except Exception as e: logger.warning(f"[Model Download] ModelScope download failed: {e}") logger.info("[Model Download] Falling back to HuggingFace Hub...") try: _download_from_huggingface_internal(repo_id, local_dir, token) return True, f"Successfully downloaded from HuggingFace: {repo_id}" except Exception as e2: error_msg = f"Both ModelScope and HuggingFace downloads failed. MS: {e}, HF: {e2}" logger.error(error_msg) return False, error_msg # ============================================================================= # Model Registry # ============================================================================= # Main model contains core components (vae, text_encoder, default DiT) MAIN_MODEL_REPO = "ACE-Step/Ace-Step1.5" # Sub-models that can be downloaded separately into the checkpoints directory SUBMODEL_REGISTRY: Dict[str, str] = { # LM models "acestep-5Hz-lm-0.6B": "ACE-Step/acestep-5Hz-lm-0.6B", "acestep-5Hz-lm-4B": "ACE-Step/acestep-5Hz-lm-4B", # DiT models "acestep-v15-turbo-shift3": "ACE-Step/acestep-v15-turbo-shift3", "acestep-v15-sft": "ACE-Step/acestep-v15-sft", "acestep-v15-base": "ACE-Step/acestep-v15-base", "acestep-v15-turbo-shift1": "ACE-Step/acestep-v15-turbo-shift1", "acestep-v15-turbo-continuous": "ACE-Step/acestep-v15-turbo-continuous", # XL (4B DiT) models "acestep-v15-xl-base": "ACE-Step/acestep-v15-xl-base", "acestep-v15-xl-sft": "ACE-Step/acestep-v15-xl-sft", "acestep-v15-xl-turbo": "ACE-Step/acestep-v15-xl-turbo", } # Components that come from the main model repo (ACE-Step/Ace-Step1.5) MAIN_MODEL_COMPONENTS = [ "acestep-v15-turbo", # Default DiT model "vae", # VAE for audio encoding/decoding "Qwen3-Embedding-0.6B", # Text encoder "acestep-5Hz-lm-1.7B", # Default LM model (1.7B) ] # Default LM model (included in main model) DEFAULT_LM_MODEL = "acestep-5Hz-lm-1.7B" # Optional community-finetuned VAE checkpoints. Each entry maps a short # variant id (also used as the on-disk subdirectory under # /) to its HuggingFace repo id. The bundled official VAE # stays at /vae/ and is referenced as variant id "official". VAE_REGISTRY: Dict[str, str] = { "scragvae": "scragnog/Ace-Step-1.5-ScragVAE", } # Variant id used for the bundled VAE that ships with the main model repo. DEFAULT_VAE_VARIANT = "official" def get_project_root() -> Path: """Get the project root directory. Returns the directory set by the ``ACESTEP_PROJECT_ROOT`` environment variable when present, otherwise the current working directory. Using the working directory (rather than ``__file__``) keeps the checkpoints folder next to where the user launched the process, regardless of whether the package was installed via ``pip install .`` or run from source. """ env_root = os.environ.get("ACESTEP_PROJECT_ROOT") if env_root: return Path(env_root).resolve() return Path(os.getcwd()) def get_checkpoints_dir(custom_dir: Optional[str] = None) -> Path: """Get the checkpoints directory path. Resolution order: 1. *custom_dir* argument (passed programmatically) 2. ``ACESTEP_CHECKPOINTS_DIR`` environment variable – allows users to share a single model directory across multiple ACE-Step installations, avoiding duplicate downloads that waste disk space. 3. ``/checkpoints`` (original default) """ if custom_dir: return Path(custom_dir) env_dir = os.environ.get("ACESTEP_CHECKPOINTS_DIR") if env_dir: return Path(env_dir).expanduser().resolve() return get_project_root() / "checkpoints" def _contains_model_weights(model_path: Path) -> bool: """Return whether a model directory contains at least one weights artifact. Args: model_path: Candidate model directory path. Returns: `True` when a known model weights file exists in the directory. """ weight_filenames = ( "model.safetensors", "model.safetensors.index.json", "pytorch_model.bin", "pytorch_model.bin.index.json", "diffusion_pytorch_model.safetensors", "diffusion_pytorch_model.safetensors.index.json", "diffusion_pytorch_model.bin", "diffusion_pytorch_model.bin.index.json", ) if not model_path.is_dir(): return False return any((model_path / filename).exists() for filename in weight_filenames) def check_main_model_exists(checkpoints_dir: Optional[Path] = None) -> bool: """ Check if the main model components exist in the checkpoints directory. Returns: True if all main model components contain weights, False otherwise. """ if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) for component in MAIN_MODEL_COMPONENTS: component_path = checkpoints_dir / component if not _contains_model_weights(component_path): return False return True def check_model_exists(model_name: str, checkpoints_dir: Optional[Path] = None) -> bool: """ Check if a specific model exists in the checkpoints directory. Args: model_name: Name of the model to check checkpoints_dir: Custom checkpoints directory (optional) Returns: True if the model exists, False otherwise. """ if not model_name: logger.warning("[check_model_exists] Empty model_name; treating as missing.") return False if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) model_path = checkpoints_dir / model_name return _contains_model_weights(model_path) def list_available_models() -> Dict[str, str]: """ List all available models for download. Returns: Dictionary mapping local names to HuggingFace repo IDs. """ models = { "main": MAIN_MODEL_REPO, **SUBMODEL_REGISTRY } return models def download_main_model( checkpoints_dir: Optional[Path] = None, force: bool = False, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """ Download the main ACE-Step model from HuggingFace or ModelScope. The main model includes: - acestep-v15-turbo (default DiT model) - vae (audio encoder/decoder) - Qwen3-Embedding-0.6B (text encoder) - acestep-5Hz-lm-1.7B (default LM model) Args: checkpoints_dir: Custom checkpoints directory (optional) force: Force re-download even if model exists token: HuggingFace token for private repos (optional) prefer_source: Preferred download source ("huggingface", "modelscope", or None for auto-detect) Returns: Tuple of (success, message) """ if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) # Ensure checkpoints directory exists checkpoints_dir.mkdir(parents=True, exist_ok=True) if not force and check_main_model_exists(checkpoints_dir): return True, f"Main model already exists at {checkpoints_dir}" print(f"Downloading main model from {MAIN_MODEL_REPO}...") print(f"Destination: {checkpoints_dir}") print("This may take a while depending on your internet connection...") # Use smart download with automatic fallback success, msg = _smart_download(MAIN_MODEL_REPO, checkpoints_dir, token, prefer_source) if success: # Sync model code files for all DiT components in the main model for component in MAIN_MODEL_COMPONENTS: if component in _CHECKPOINT_TO_VARIANT: synced = _sync_model_code_files(component, checkpoints_dir) if synced: logger.info(f"[Model Download] Synced code files for {component}: {synced}") return success, msg def download_submodel( model_name: str, checkpoints_dir: Optional[Path] = None, force: bool = False, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """ Download a specific sub-model from HuggingFace or ModelScope. Args: model_name: Name of the model to download (must be in SUBMODEL_REGISTRY) checkpoints_dir: Custom checkpoints directory (optional) force: Force re-download even if model exists token: HuggingFace token for private repos (optional) prefer_source: Preferred download source ("huggingface", "modelscope", or None for auto-detect) Returns: Tuple of (success, message) """ if model_name not in SUBMODEL_REGISTRY: available = ", ".join(SUBMODEL_REGISTRY.keys()) return False, f"Unknown model '{model_name}'. Available models: {available}" if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) # Ensure checkpoints directory exists checkpoints_dir.mkdir(parents=True, exist_ok=True) model_path = checkpoints_dir / model_name if not force and model_path.exists(): return True, f"Model '{model_name}' already exists at {model_path}" repo_id = SUBMODEL_REGISTRY[model_name] print(f"Downloading {model_name} from {repo_id}...") print(f"Destination: {model_path}") # Use smart download with automatic fallback success, msg = _smart_download(repo_id, model_path, token, prefer_source) if success and model_name in _CHECKPOINT_TO_VARIANT: # Sync model code files after successful download synced = _sync_model_code_files(model_name, checkpoints_dir) if synced: logger.info(f"[Model Download] Synced code files for {model_name}: {synced}") return success, msg def download_all_models( checkpoints_dir: Optional[Path] = None, force: bool = False, token: Optional[str] = None, ) -> Tuple[bool, List[str]]: """ Download all available models. Args: checkpoints_dir: Custom checkpoints directory (optional) force: Force re-download even if models exist token: HuggingFace token for private repos (optional) Returns: Tuple of (all_success, list of messages) """ if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) messages = [] all_success = True # Download main model first success, msg = download_main_model(checkpoints_dir, force, token) messages.append(msg) if not success: all_success = False # Download all sub-models for model_name in SUBMODEL_REGISTRY: success, msg = download_submodel(model_name, checkpoints_dir, force, token) messages.append(msg) if not success: all_success = False return all_success, messages def ensure_main_model( checkpoints_dir: Optional[Path] = None, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """ Ensure the main model is available, downloading if necessary. This function is designed to be called during initialization. It will only download if the model doesn't exist. Args: checkpoints_dir: Custom checkpoints directory (optional) token: HuggingFace token for private repos (optional) prefer_source: Preferred download source ("huggingface", "modelscope", or None for auto-detect) Returns: Tuple of (success, message) """ if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() if check_main_model_exists(checkpoints_dir): return True, "Main model is available" print("\n" + "=" * 60) print("Main model not found. Starting automatic download...") print("=" * 60 + "\n") return download_main_model(checkpoints_dir, token=token, prefer_source=prefer_source) def ensure_lm_model( model_name: Optional[str] = None, checkpoints_dir: Optional[Path] = None, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """ Ensure an LM model is available, downloading if necessary. Args: model_name: Name of the LM model (defaults to DEFAULT_LM_MODEL) checkpoints_dir: Custom checkpoints directory (optional) token: HuggingFace token for private repos (optional) prefer_source: Preferred download source ("huggingface", "modelscope", or None for auto-detect) Returns: Tuple of (success, message) """ if model_name is None: model_name = DEFAULT_LM_MODEL if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) if check_model_exists(model_name, checkpoints_dir): return True, f"LM model '{model_name}' is available" # Check if this is a known LM model if model_name not in SUBMODEL_REGISTRY: # Check if it might be a variant name for known_model in SUBMODEL_REGISTRY: if "lm" in known_model.lower() and model_name.lower() in known_model.lower(): model_name = known_model break else: return False, f"Unknown LM model: {model_name}" print("\n" + "=" * 60) print(f"LM model '{model_name}' not found. Starting automatic download...") print("=" * 60 + "\n") return download_submodel(model_name, checkpoints_dir, token=token, prefer_source=prefer_source) def ensure_dit_model( model_name: str, checkpoints_dir: Optional[Path] = None, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """ Ensure a DiT model is available, downloading if necessary. Args: model_name: Name of the DiT model checkpoints_dir: Custom checkpoints directory (optional) token: HuggingFace token for private repos (optional) prefer_source: Preferred download source ("huggingface", "modelscope", or None for auto-detect) Returns: Tuple of (success, message) """ if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) if check_model_exists(model_name, checkpoints_dir): return True, f"DiT model '{model_name}' is available" # Check if this is the default turbo model (part of main) if model_name == "acestep-v15-turbo": return ensure_main_model(checkpoints_dir, token, prefer_source) # Check if it's a known sub-model if model_name in SUBMODEL_REGISTRY: print("\n" + "=" * 60) print(f"DiT model '{model_name}' not found. Starting automatic download...") print("=" * 60 + "\n") return download_submodel(model_name, checkpoints_dir, token=token, prefer_source=prefer_source) if not model_name: return False, "Unknown DiT model: '' (pass None for default or choose a valid model)" return False, f"Unknown DiT model: {model_name}" def list_available_vae_variants() -> List[str]: """Return all selectable VAE variant ids (official first).""" return [DEFAULT_VAE_VARIANT, *VAE_REGISTRY.keys()] def resolve_vae_path(checkpoint_dir: "str | Path", vae_variant: Optional[str]) -> Path: """Resolve a VAE variant id (or absolute path) to its on-disk directory. Args: checkpoint_dir: Root checkpoints directory. vae_variant: Variant id (``"official"`` or a key in ``VAE_REGISTRY``) or an absolute filesystem path. ``None`` / ``""`` is treated as ``"official"``. Returns: Absolute path of the VAE checkpoint directory. Raises: ValueError: If ``vae_variant`` is not recognized. """ if isinstance(checkpoint_dir, str): checkpoint_dir = Path(checkpoint_dir) if not vae_variant: return checkpoint_dir / "vae" if os.path.isabs(vae_variant): return Path(vae_variant) if vae_variant == DEFAULT_VAE_VARIANT: return checkpoint_dir / "vae" if vae_variant in VAE_REGISTRY: return checkpoint_dir / vae_variant raise ValueError( f"Unknown VAE variant '{vae_variant}'. Available: " f"{', '.join(list_available_vae_variants())}" ) def check_vae_exists(vae_variant: str, checkpoints_dir: Optional[Path] = None) -> bool: """Return whether the requested VAE variant has weights on disk.""" if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) try: path = resolve_vae_path(checkpoints_dir, vae_variant) except ValueError: return False return _contains_model_weights(path) def download_vae( vae_variant: str, checkpoints_dir: Optional[Path] = None, force: bool = False, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """Download a community VAE variant into ``//``. The bundled ``"official"`` VAE is *not* downloaded here — it ships with the main model and is fetched by ``download_main_model``. """ if vae_variant == DEFAULT_VAE_VARIANT: return False, ( f"VAE variant '{DEFAULT_VAE_VARIANT}' ships with the main model; " "use download_main_model() instead." ) if vae_variant not in VAE_REGISTRY: available = ", ".join(VAE_REGISTRY.keys()) or "(none)" return False, f"Unknown VAE variant '{vae_variant}'. Available: {available}" if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) checkpoints_dir.mkdir(parents=True, exist_ok=True) target_path = checkpoints_dir / vae_variant if not force and _contains_model_weights(target_path): return True, f"VAE variant '{vae_variant}' already exists at {target_path}" repo_id = VAE_REGISTRY[vae_variant] print(f"Downloading VAE '{vae_variant}' from {repo_id}...") print(f"Destination: {target_path}") return _smart_download(repo_id, target_path, token, prefer_source) def ensure_vae_model( vae_variant: Optional[str] = None, checkpoints_dir: Optional[Path] = None, token: Optional[str] = None, prefer_source: Optional[str] = None, ) -> Tuple[bool, str]: """Ensure the requested VAE variant is on disk, downloading if needed. For ``"official"`` (or ``None``) this defers to ``ensure_main_model`` since the bundled VAE travels with the main model. For registered community variants this calls ``download_vae`` when the directory is missing. """ if not vae_variant or vae_variant == DEFAULT_VAE_VARIANT: return ensure_main_model(checkpoints_dir, token, prefer_source) if checkpoints_dir is None: checkpoints_dir = get_checkpoints_dir() elif isinstance(checkpoints_dir, str): checkpoints_dir = Path(checkpoints_dir) if check_vae_exists(vae_variant, checkpoints_dir): return True, f"VAE variant '{vae_variant}' is available" # Absolute paths are user-supplied and cannot be downloaded. Fail with a # clear, path-specific diagnostic instead of routing through download_vae, # which would surface a misleading "Unknown VAE variant" error. if os.path.isabs(vae_variant): path = Path(vae_variant) if not path.exists(): return False, f"VAE path '{vae_variant}' does not exist." return False, ( f"VAE path '{vae_variant}' does not contain VAE weights " "(expected diffusion_pytorch_model.safetensors)." ) print("\n" + "=" * 60) print(f"VAE variant '{vae_variant}' not found. Starting automatic download...") print("=" * 60 + "\n") return download_vae(vae_variant, checkpoints_dir, token=token, prefer_source=prefer_source) def print_model_list(): """Print formatted list of available models.""" print("\nAvailable Models for Download:") print("=" * 60) print("\nSupported Sources: HuggingFace Hub <-> ModelScope (auto-fallback)") print("\n[Main Model]") print(f" main -> {MAIN_MODEL_REPO}") print(" Contains: vae, Qwen3-Embedding-0.6B, acestep-v15-turbo, acestep-5Hz-lm-1.7B") print("\n[Optional LM Models]") for name, repo in SUBMODEL_REGISTRY.items(): if "lm" in name.lower(): print(f" {name} -> {repo}") print("\n[Optional DiT Models]") for name, repo in SUBMODEL_REGISTRY.items(): if "lm" not in name.lower(): print(f" {name} -> {repo}") if VAE_REGISTRY: print("\n[Optional VAEs]") print(f" official -> bundled in {MAIN_MODEL_REPO}") for name, repo in VAE_REGISTRY.items(): print(f" {name} -> {repo}") print("\n" + "=" * 60) def main(): """CLI entry point for model downloading.""" parser = argparse.ArgumentParser( description="Download ACE-Step models with automatic fallback (HuggingFace <-> ModelScope)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: acestep-download # Download main model (includes LM 1.7B) acestep-download --all # Download all available models acestep-download --model acestep-v15-sft # Download a specific model acestep-download --list # List all available models Network Detection: Automatically detects network environment and chooses the best download source: - Google accessible -> HuggingFace (fallback to ModelScope) - Google blocked -> ModelScope (fallback to HuggingFace) Shared checkpoints directory: Set ACESTEP_CHECKPOINTS_DIR to share models across multiple installations: export ACESTEP_CHECKPOINTS_DIR=~/ace-step-models Alternative using huggingface-cli: huggingface-cli download ACE-Step/Ace-Step1.5 --local-dir ./checkpoints huggingface-cli download ACE-Step/acestep-5Hz-lm-0.6B --local-dir ./checkpoints/acestep-5Hz-lm-0.6B """ ) parser.add_argument( "--model", "-m", type=str, help="Specific model to download (use --list to see available models)" ) parser.add_argument( "--all", "-a", action="store_true", help="Download all available models" ) parser.add_argument( "--list", "-l", action="store_true", help="List all available models" ) parser.add_argument( "--dir", "-d", type=str, default=None, help="Custom checkpoints directory (default: ./checkpoints)" ) parser.add_argument( "--force", "-f", action="store_true", help="Force re-download even if model exists" ) parser.add_argument( "--token", "-t", type=str, default=None, help="HuggingFace token for private repos" ) parser.add_argument( "--skip-main", action="store_true", help="Skip downloading the main model (only download specified sub-model)" ) args = parser.parse_args() # Handle --list if args.list: print_model_list() return 0 # Get checkpoints directory checkpoints_dir = get_checkpoints_dir(args.dir) if args.dir else get_checkpoints_dir() print(f"Checkpoints directory: {checkpoints_dir}") # Handle --all if args.all: success, messages = download_all_models(checkpoints_dir, args.force, args.token) for msg in messages: print(msg) return 0 if success else 1 # Handle --model if args.model: if args.model == "main": success, msg = download_main_model(checkpoints_dir, args.force, args.token) elif args.model in SUBMODEL_REGISTRY: # Download main model first if needed (unless --skip-main) if not args.skip_main and not check_main_model_exists(checkpoints_dir): print("Main model not found. Downloading main model first...") main_success, main_msg = download_main_model(checkpoints_dir, args.force, args.token) print(main_msg) if not main_success: return 1 success, msg = download_submodel(args.model, checkpoints_dir, args.force, args.token) else: print(f"Unknown model: {args.model}") print("Use --list to see available models") return 1 print(msg) return 0 if success else 1 # Default: download main model (includes default LM 1.7B) print("Downloading main model (includes vae, text encoder, DiT, and LM 1.7B)...") # Download main model success, msg = download_main_model(checkpoints_dir, args.force, args.token) print(msg) if success: print("\nDownload complete!") print(f"Models are available at: {checkpoints_dir}") return 0 if success else 1 if __name__ == "__main__": sys.exit(main())