# Copyright 2026 The AnyFlow Team, NVIDIA Corp., and The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Convert AnyFlow training checkpoints to the diffusers ``save_pretrained`` layout.

The AnyFlow training pipeline emits ``.pt`` files containing an ``ema`` key whose value is a flat state
dict for the transformer. This script:

1. Loads the matching base Wan2.1 pipeline from the Hub (provides VAE, tokenizer, and text encoder).
2. Constructs an ``AnyFlowTransformer3DModel`` with the right config flags for the chosen variant.
3. Loads the ``ema`` weights into the transformer.
4. Wraps everything in an ``AnyFlowPipeline`` (bidirectional) or ``AnyFlowFARPipeline`` (FAR causal).
5. Calls ``pipeline.save_pretrained(output_dir)``.

Example:

```bash
python scripts/convert_anyflow_to_diffusers.py \\
    --variant AnyFlow-FAR-Wan2.1-1.3B-Diffusers \\
    --ckpt /path/to/anyflow-checkpoint.pt \\
    --output-dir /path/to/output/AnyFlow-FAR-Wan2.1-1.3B-Diffusers
```
"""

import argparse
import logging
import os

import torch

from diffusers import (
    AnyFlowFARPipeline,
    AnyFlowFARTransformer3DModel,
    AnyFlowPipeline,
    AnyFlowTransformer3DModel,
    FlowMapEulerDiscreteScheduler,
)


logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")


# Per-variant configuration. ``base_model`` is fetched from the Hub to source the matching VAE / text encoder.
VARIANTS = {
    "AnyFlow-FAR-Wan2.1-1.3B-Diffusers": {
        "base_model": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
        "transformer_cls": AnyFlowFARTransformer3DModel,
        "transformer_kwargs": {
            "full_chunk_limit": 3,
            "compressed_patch_size": [1, 4, 4],
            "chunk_partition": [1, 3, 3, 3, 3, 3, 3, 2],
        },
        "pipeline_cls": AnyFlowFARPipeline,
    },
    "AnyFlow-FAR-Wan2.1-14B-Diffusers": {
        "base_model": "Wan-AI/Wan2.1-T2V-14B-Diffusers",
        "transformer_cls": AnyFlowFARTransformer3DModel,
        "transformer_kwargs": {
            "full_chunk_limit": 3,
            "compressed_patch_size": [1, 4, 4],
            "chunk_partition": [1, 3, 3, 3, 3, 3, 3, 2],
        },
        "pipeline_cls": AnyFlowFARPipeline,
    },
    "AnyFlow-Wan2.1-T2V-1.3B-Diffusers": {
        "base_model": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
        "transformer_cls": AnyFlowTransformer3DModel,
        "transformer_kwargs": {},
        "pipeline_cls": AnyFlowPipeline,
    },
    "AnyFlow-Wan2.1-T2V-14B-Diffusers": {
        "base_model": "Wan-AI/Wan2.1-T2V-14B-Diffusers",
        "transformer_cls": AnyFlowTransformer3DModel,
        "transformer_kwargs": {},
        "pipeline_cls": AnyFlowPipeline,
    },
}


def build_pipeline(variant: str, ckpt_path: str):
    if variant not in VARIANTS:
        raise ValueError(f"Unknown variant {variant!r}. Choices: {list(VARIANTS)}.")
    spec = VARIANTS[variant]

    transformer = spec["transformer_cls"].from_pretrained(
        spec["base_model"],
        subfolder="transformer",
        gate_value=0.25,
        deltatime_type="r",
        **spec["transformer_kwargs"],
    )
    # NVlabs/AnyFlow training checkpoints are wrapped Python objects (the `ema` key carries metadata
    # alongside tensors), so the unpickle is required. Only run this script on checkpoints you trust.
    state_dict = torch.load(ckpt_path, map_location="cpu", weights_only=False)["ema"]
    missing, unexpected = transformer.load_state_dict(state_dict, strict=False)
    if unexpected:
        logger.warning(
            "Unexpected keys in state dict (ignored): %s%s",
            unexpected[:5],
            "..." if len(unexpected) > 5 else "",
        )
    if missing:
        logger.warning(
            "Missing keys not loaded from state dict: %s%s",
            missing[:5],
            "..." if len(missing) > 5 else "",
        )

    scheduler = FlowMapEulerDiscreteScheduler(num_train_timesteps=1000, shift=5.0)

    pipeline = spec["pipeline_cls"].from_pretrained(
        spec["base_model"],
        transformer=transformer,
        scheduler=scheduler,
    )
    return pipeline


def main():
    parser = argparse.ArgumentParser(
        description="Convert an AnyFlow training checkpoint into a diffusers pipeline directory."
    )
    parser.add_argument(
        "--variant",
        required=True,
        choices=list(VARIANTS),
        help="Which AnyFlow variant the checkpoint corresponds to.",
    )
    parser.add_argument(
        "--ckpt",
        required=True,
        help="Path to the AnyFlow training checkpoint (a .pt file containing an 'ema' key).",
    )
    parser.add_argument(
        "--output-dir",
        required=True,
        help="Destination directory for pipeline.save_pretrained.",
    )
    args = parser.parse_args()

    os.makedirs(args.output_dir, exist_ok=True)
    pipeline = build_pipeline(args.variant, args.ckpt)
    pipeline.save_pretrained(args.output_dir)
    logger.info("Saved %s pipeline to %s", args.variant, args.output_dir)


if __name__ == "__main__":
    main()
