Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
233 lines
8.5 KiB
Python
233 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import types
|
|
import warnings
|
|
from fractions import Fraction
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import torch
|
|
import yaml
|
|
from safetensors.torch import save_file
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
REFERENCE_ROOT = REPO_ROOT / "reference" / "demucs"
|
|
REMOTE_ROOT = REFERENCE_ROOT / "demucs" / "remote"
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert original Demucs .th checkpoints into audio.cpp-friendly safetensors + JSON configs."
|
|
)
|
|
parser.add_argument(
|
|
"--name",
|
|
default="htdemucs",
|
|
help="Reference model or bag name, e.g. htdemucs, htdemucs_ft, or htdemucs_6s.",
|
|
)
|
|
parser.add_argument("--repo", default="models/htdemucs", help="Directory containing original .th checkpoints.")
|
|
parser.add_argument("--output-dir", default="models/htdemucs", help="Directory to write converted artifacts.")
|
|
return parser.parse_args()
|
|
|
|
|
|
def install_import_stubs() -> None:
|
|
if "openunmix.filtering" not in sys.modules:
|
|
filtering = types.ModuleType("openunmix.filtering")
|
|
|
|
def _wiener_stub(*_args: Any, **_kwargs: Any) -> Any:
|
|
raise RuntimeError("openunmix.filtering.wiener stub should not be called during Demucs conversion")
|
|
|
|
filtering.wiener = _wiener_stub
|
|
openunmix = types.ModuleType("openunmix")
|
|
openunmix.filtering = filtering
|
|
sys.modules["openunmix"] = openunmix
|
|
sys.modules["openunmix.filtering"] = filtering
|
|
|
|
if "julius" not in sys.modules:
|
|
julius = types.ModuleType("julius")
|
|
|
|
def _resample_frac_stub(*_args: Any, **_kwargs: Any) -> Any:
|
|
raise RuntimeError("julius.resample_frac stub should not be called during Demucs conversion")
|
|
|
|
julius.resample_frac = _resample_frac_stub
|
|
sys.modules["julius"] = julius
|
|
|
|
if "dora.log" not in sys.modules:
|
|
log = types.ModuleType("dora.log")
|
|
|
|
def _fatal_stub(message: str) -> None:
|
|
raise RuntimeError(message)
|
|
|
|
log.fatal = _fatal_stub
|
|
dora = types.ModuleType("dora")
|
|
dora.log = log
|
|
sys.modules["dora"] = dora
|
|
sys.modules["dora.log"] = log
|
|
|
|
|
|
def load_package(path: Path) -> dict[str, Any]:
|
|
install_import_stubs()
|
|
sys.path.insert(0, str(REFERENCE_ROOT))
|
|
try:
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore")
|
|
package = torch.load(path, map_location="cpu", weights_only=False)
|
|
finally:
|
|
try:
|
|
sys.path.remove(str(REFERENCE_ROOT))
|
|
except ValueError:
|
|
pass
|
|
if not isinstance(package, dict):
|
|
raise RuntimeError(f"unexpected Demucs package type in {path}: {type(package)}")
|
|
required = {"klass", "args", "kwargs", "state"}
|
|
missing = required.difference(package)
|
|
if missing:
|
|
raise RuntimeError(f"Demucs package {path} is missing keys: {sorted(missing)}")
|
|
return package
|
|
|
|
|
|
def find_checkpoint(repo_dir: Path, signature: str) -> Path:
|
|
matches = sorted(repo_dir.glob(f"{signature}*.th"))
|
|
if not matches:
|
|
raise RuntimeError(f"Could not find checkpoint for signature '{signature}' in {repo_dir}")
|
|
if len(matches) > 1:
|
|
exact = [path for path in matches if path.stem == signature or path.stem.startswith(signature + "-")]
|
|
if len(exact) == 1:
|
|
return exact[0]
|
|
raise RuntimeError(f"Ambiguous checkpoints for signature '{signature}' in {repo_dir}: {matches}")
|
|
return matches[0]
|
|
|
|
|
|
def normalize_value(value: Any) -> Any:
|
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
return value
|
|
if isinstance(value, Fraction):
|
|
return float(value)
|
|
if isinstance(value, Path):
|
|
return str(value)
|
|
if isinstance(value, (list, tuple)):
|
|
return [normalize_value(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {str(key): normalize_value(item) for key, item in value.items()}
|
|
return repr(value)
|
|
|
|
|
|
def export_model_config(package: dict[str, Any], checkpoint_path: Path, signature: str) -> dict[str, Any]:
|
|
klass = package["klass"]
|
|
kwargs = {str(key): normalize_value(value) for key, value in package["kwargs"].items()}
|
|
return {
|
|
"model_type": "demucs",
|
|
"class_name": getattr(klass, "__name__", str(klass)),
|
|
"signature": signature,
|
|
"checkpoint_file": checkpoint_path.name,
|
|
"args": [normalize_value(value) for value in package["args"]],
|
|
"kwargs": kwargs,
|
|
"sources": normalize_value(kwargs.get("sources")),
|
|
"samplerate": normalize_value(kwargs.get("samplerate")),
|
|
"audio_channels": normalize_value(kwargs.get("audio_channels")),
|
|
"segment": normalize_value(kwargs.get("segment")),
|
|
}
|
|
|
|
|
|
def export_state_tensors(package: dict[str, Any]) -> dict[str, torch.Tensor]:
|
|
state = package["state"]
|
|
if not isinstance(state, dict):
|
|
raise RuntimeError(f"unexpected Demucs state payload type: {type(state)}")
|
|
converted: dict[str, torch.Tensor] = {}
|
|
for key, value in state.items():
|
|
if key == "__quantized":
|
|
raise RuntimeError("Demucs quantized checkpoints are not supported by this converter yet")
|
|
if not isinstance(value, torch.Tensor):
|
|
raise RuntimeError(f"unexpected non-tensor entry in Demucs state: {key} -> {type(value)}")
|
|
converted[str(key)] = value.detach().cpu().contiguous()
|
|
return converted
|
|
|
|
|
|
def load_bag_manifest(name: str) -> dict[str, Any] | None:
|
|
yaml_path = REMOTE_ROOT / f"{name}.yaml"
|
|
if not yaml_path.is_file():
|
|
return None
|
|
payload = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
if not isinstance(payload, dict) or "models" not in payload:
|
|
raise RuntimeError(f"invalid Demucs bag manifest: {yaml_path}")
|
|
return payload
|
|
|
|
|
|
def convert_single(signature: str, repo_dir: Path, output_root: Path) -> dict[str, Any]:
|
|
checkpoint_path = find_checkpoint(repo_dir, signature)
|
|
package = load_package(checkpoint_path)
|
|
state = export_state_tensors(package)
|
|
|
|
model_dir = output_root / signature
|
|
model_dir.mkdir(parents=True, exist_ok=True)
|
|
save_file(state, str(model_dir / "model.safetensors"))
|
|
(model_dir / "config.json").write_text(
|
|
json.dumps(export_model_config(package, checkpoint_path, signature), indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
return {
|
|
"signature": signature,
|
|
"checkpoint_file": checkpoint_path.name,
|
|
"output_dir": str(model_dir.relative_to(output_root)),
|
|
"tensor_count": len(state),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
repo_dir = REPO_ROOT / args.repo if not Path(args.repo).is_absolute() else Path(args.repo)
|
|
output_root = REPO_ROOT / args.output_dir if not Path(args.output_dir).is_absolute() else Path(args.output_dir)
|
|
if not repo_dir.is_dir():
|
|
raise RuntimeError(f"checkpoint repo directory does not exist: {repo_dir}")
|
|
|
|
bag = load_bag_manifest(args.name)
|
|
output_dir = output_root / args.name
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
if bag is None:
|
|
converted = [convert_single(args.name, repo_dir, output_dir)]
|
|
manifest = {
|
|
"model_type": "demucs_single",
|
|
"name": args.name,
|
|
"model": converted[0],
|
|
}
|
|
else:
|
|
signatures = bag["models"]
|
|
if not isinstance(signatures, list) or not signatures:
|
|
raise RuntimeError(f"Demucs bag manifest has invalid model list: {args.name}")
|
|
converted = [convert_single(str(signature), repo_dir, output_dir) for signature in signatures]
|
|
weights = normalize_value(bag.get("weights"))
|
|
segment = normalize_value(bag.get("segment"))
|
|
if len(converted) == 1 and weights is None and segment is None:
|
|
manifest = {
|
|
"model_type": "demucs_single_alias",
|
|
"name": args.name,
|
|
"alias_of": converted[0]["signature"],
|
|
"model": converted[0],
|
|
}
|
|
else:
|
|
manifest = {
|
|
"model_type": "demucs_bag",
|
|
"name": args.name,
|
|
"models": converted,
|
|
"weights": weights,
|
|
"segment": segment,
|
|
}
|
|
|
|
(output_dir / "manifest.json").write_text(
|
|
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(f"name={args.name}")
|
|
print(f"repo_dir={repo_dir}")
|
|
print(f"output_dir={output_dir}")
|
|
print(f"model_count={len(converted)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|