Files
ai-video-editor/scripts/export_joyvasa_audio.py
Anupam MedirattaandClaude Sonnet 4.6 d689b89687 fix: replace safetensors load with weights_only=True in audio export script
export_joyvasa_audio.py was using safetensors.torch.load_file() on a
native PyTorch checkpoint, which fails with SafetensorError. Replace with
torch.load(..., weights_only=True) and an explicit add_safe_globals
allowlist ([argparse.Namespace, PosixPath]), consistent with the other
two JoyVASA export scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 12:40:44 +05:30

145 lines
5.2 KiB
Python

"""Export JoyVASA's pinned Chinese HuBERT audio path to ONNX.
The graph accepts one already padded four-second 16 kHz waveform (64,080
samples) and produces the 100 x 256 audio feature window consumed by the
diffusion denoiser. Padding is kept outside the graph so the browser can apply
JoyVASA's small reflect pad deterministically before inference.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from pathlib import Path, PosixPath
import numpy as np
import onnx
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import HubertConfig
EXPECTED_CHECKPOINT_SHA256 = "9dd869329725caedf5f0c13dd383abec1e385f566d8afe2047b141f604844e80"
EXPECTED_HUBERT_CONFIG_SHA256 = "f6bd1bdef239518d022edcef31243acf344a2afa33d5428e16f221cac09cbcd0"
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
class JoyVASAAudioFeatures(nn.Module):
def __init__(self, encoder: nn.Module, projection: nn.Module):
super().__init__()
self.encoder = encoder
self.projection = projection
def forward(self, audio_padded: torch.Tensor) -> torch.Tensor:
# JoyVASA first asks HuBERT for 200 frames, then back-resamples to the
# 100 motion frames used for a four-second, 25 fps diffusion window.
hidden = self.encoder(audio_padded, 25, frame_num=200).last_hidden_state
hidden = F.interpolate(
hidden.transpose(1, 2), size=100, align_corners=False, mode="linear"
).transpose(1, 2)
return self.projection(hidden)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", type=Path, required=True)
parser.add_argument("--checkpoint", type=Path, required=True)
parser.add_argument("--hubert-config", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
checkpoint_hash = sha256(args.checkpoint)
config_hash = sha256(args.hubert_config)
if checkpoint_hash != EXPECTED_CHECKPOINT_SHA256:
raise RuntimeError(f"Unexpected checkpoint SHA-256: {checkpoint_hash}")
if config_hash != EXPECTED_HUBERT_CONFIG_SHA256:
raise RuntimeError(f"Unexpected HuBERT config SHA-256: {config_hash}")
sys.path.insert(0, str(args.source))
from src.modules.hubert import HubertModel
torch.serialization.add_safe_globals([argparse.Namespace, PosixPath])
payload = torch.load(args.checkpoint, map_location="cpu", weights_only=True)
config = HubertConfig.from_json_file(str(args.hubert_config))
encoder = HubertModel(config).eval()
encoder_prefix = "audio_encoder."
encoder_state = {
key[len(encoder_prefix):]: value
for key, value in payload["model"].items()
if key.startswith(encoder_prefix)
}
missing, unexpected = encoder.load_state_dict(encoder_state, strict=True)
if missing or unexpected:
raise RuntimeError(f"Encoder state mismatch, missing={missing}, unexpected={unexpected}")
projection = nn.Linear(768, 256).eval()
projection.load_state_dict(
{
"weight": payload["model"]["audio_feature_map.weight"],
"bias": payload["model"]["audio_feature_map.bias"],
},
strict=True,
)
model = JoyVASAAudioFeatures(encoder, projection).eval()
torch.manual_seed(20260710)
audio = torch.randn(1, 64_080)
with torch.no_grad():
reference = model(audio).cpu().numpy()
args.output.parent.mkdir(parents=True, exist_ok=True)
torch.onnx.export(
model,
(audio,),
args.output,
input_names=["audio_padded"],
output_names=["audio_features"],
opset_version=17,
do_constant_folding=True,
export_params=True,
)
graph = onnx.load(args.output, load_external_data=False)
onnx.checker.check_model(graph)
np.savez_compressed(
args.output.with_suffix(".reference.npz"),
audio_padded=audio.numpy(),
audio_features=reference,
)
np.savez_compressed(
args.output.with_suffix(".conditioning.npz"),
start_audio_feat=payload["model"]["start_audio_feat"].numpy(),
start_motion_feat=payload["model"]["start_motion_feat"].numpy(),
null_audio_feat=payload["model"]["null_audio_feat"].numpy(),
)
metadata = {
"checkpoint_sha256": checkpoint_hash,
"hubert_config_sha256": config_hash,
"onnx_sha256": sha256(args.output),
"onnx_bytes": args.output.stat().st_size,
"opset": 17,
"sample_rate": 16_000,
"unpadded_samples": 64_000,
"padded_samples": 64_080,
"motion_fps": 25,
"motion_frames": 100,
"nodes": len(graph.graph.node),
"inputs": {item.name: [dim.dim_value for dim in item.type.tensor_type.shape.dim] for item in graph.graph.input},
"outputs": {item.name: [dim.dim_value for dim in item.type.tensor_type.shape.dim] for item in graph.graph.output},
}
args.output.with_suffix(".json").write_text(json.dumps(metadata, indent=2), encoding="utf-8")
print(json.dumps(metadata, indent=2))
if __name__ == "__main__":
main()