Files
smthemex-ComfyUI_ChronoEdit_SM/Chrono/scripts/run_inference.py
T
2025-11-05 20:42:24 +08:00

242 lines
11 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
import argparse
from functools import partial
import torch.distributed as dist
import torch as th
import os
import json
import time
import chronoedit._ext.imaginaire.utils.distributed
from megatron.core import parallel_state
from chronoedit._ext.imaginaire.visualize.video import save_img_or_video
from chronoedit._src.datasets.utils import VIDEO_RES_SIZE_INFO
from chronoedit._src.utils.get_t5_emb import get_text_embedding
from chronoedit._src.utils.model_loader import load_model_from_checkpoint
from chronoedit._src.utils.misc import read_and_process_image
th.enable_grad(False)
DEFAULT_NEGATIVE_PROMPT = "The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality."
DEFAULT_POSITIVE_PROMPT = "A close-up shot captures a vibrant yellow scrubber vigorously working on a grimy plate, its bristles moving in circular motions to lift stubborn grease and food residue. The dish, once covered in remnants of a hearty meal, gradually reveals its original glossy surface. Suds form and bubble around the scrubber, creating a satisfying visual of cleanliness in progress. The sound of scrubbing fills the air, accompanied by the gentle clinking of the dish against the sink. As the scrubber continues its task, the dish transforms, gleaming under the bright kitchen lights, symbolizing the triumph of cleanliness over mess."
UMT5_NEGATIVE_PROMPT = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="ChronoEdit inference script")
parser.add_argument("--experiment", type=str, default="???", help="inference only config")
parser.add_argument("--guidance", type=float, default=5.0, help="Guidance value")
parser.add_argument("--lora_paths", type=str, default=None, nargs="+", help="Path to the lora weights")
parser.add_argument("--lora_weights", type=float, default=None, nargs="+", help="Path to the lora weights")
parser.add_argument("--prompt", type=str, default="", help="Editing Prompt")
parser.add_argument("--input_image_fp", type=str, default="", help="Input image path")
parser.add_argument("--shift", type=float, default=5.0, help="Shift value")
parser.add_argument("--neg_prompt", type=str, default="", help="Input image path")
parser.add_argument(
"--checkpoint_path",
type=str,
default="",
help="Path to the checkpoint. If not provided, will use the one specify in the config",
)
parser.add_argument("--num_steps", type=int, default=50, help="sampler steps")
parser.add_argument("--num_samples", type=int, default=1, help="Number of samples to generate")
parser.add_argument("--num_frames", type=int, default=2, help="Number of frames in the video")
parser.add_argument("--save_root", type=str, default="./", help="Path to save the results")
parser.add_argument("--cache_dir", type=str, default="/project/cosmos/tianshic/imaginaire4-cache", help="Path to cache the text embeddings")
parser.add_argument("--resolution", type=str, default="720p", help="Resolution of the video")
parser.add_argument("--fps", type=int, default=10, help="FPS of the video and model")
parser.add_argument("--drop_video_step", type=int, default=None, help="The step to drop the video")
return parser.parse_args()
def sample_batch_video(resolution, batch_size, num_frames, fps):
h, w = VIDEO_RES_SIZE_INFO[resolution]["9,16"]
data_batch = {
"dataset_name": "video_data",
"video": th.randint(0, 256, (batch_size, 3, num_frames, h, w), dtype=th.uint8).cuda(),
"fps": th.ones((batch_size,)).cuda() * fps,
"padding_mask": th.zeros(batch_size, 1, h, w).cuda(),
}
return data_batch
def get_sample_batch(
num_frames: int = 93,
resolution: str = "720p",
batch_size: int = 1,
fps: int = 30,
) -> th.Tensor:
data_batch = sample_batch_video(resolution, batch_size, num_frames, fps)
for k, v in data_batch.items():
if isinstance(v, th.Tensor) and th.is_floating_point(data_batch[k]):
data_batch[k] = v.cuda().to(dtype=th.bfloat16)
return data_batch
if __name__ == "__main__":
args = parse_arguments()
if "{" in args.save_root:
try:
_ = args.save_root.format(g=args.guidance, s=args.shift, n=args.num_steps)
except KeyError:
raise ValueError(f"Invalid save_root: {args.save_root}, please use {{g,s,n}} in the save_root")
th.backends.cuda.preferred_linalg_library(backend="magma")
chronoedit._ext.imaginaire.utils.distributed.init()
world_size = dist.get_world_size()
print(f"world_size: {world_size}")
if world_size > 1:
parallel_state.initialize_model_parallel(context_parallel_size=world_size)
process_group = parallel_state.get_context_parallel_group()
# instantiate model, config and load checkpoint
if args.lora_paths:
experiment_opts = ["model.config.net.postpone_checkpoint=True"]
else:
experiment_opts = []
model, config = load_model_from_checkpoint(
experiment_name=args.experiment,
checkpoint_path=args.checkpoint_path,
enable_fsdp=False,
config_file="chronoedit/_src/configs/chronoedit/config.py",
experiment_opts=experiment_opts,
)
if world_size > 1:
model.net.enable_context_parallel(process_group)
if args.lora_paths:
lora_names = []
for lora_path in args.lora_paths:
lora_name = model.load_lora_weights(lora_path)
lora_names.append(lora_name)
model.set_weights_and_activate_adapters(lora_names, args.lora_weights)
model.net.enable_selective_checkpoint(model.net.sac_config, model.net.blocks)
embedding_func = partial(get_text_embedding, text_encoder_class='umT5')
if dist.get_rank() == 0:
avg_list = {}
save_path = f"{args.save_root}/{args.input_image_fp.split('/')[-1].split('.')[0]}_edited"
sample_info = {}
sample_info['prompt'] = args.prompt
data_batch = get_sample_batch(
num_frames=model.tokenizer.get_pixel_num_frames(model.get_num_video_latent_frames()),
resolution=args.resolution,
batch_size=args.num_samples,
fps=args.fps,
)
data_batch["video"] = read_and_process_image(args.input_image_fp, [data_batch['video'].shape[3], data_batch['video'].shape[4]], args.num_frames).cuda()
if "is_preprocessed" in data_batch:
del data_batch["is_preprocessed"]
assert args.num_frames == 2 or args.num_frames == 26, f"num_frames must be 2 or 26, but got {args.num_frames}"
is_video_prior = True if args.num_frames == 26 else False
data_batch["is_video_prior"] = is_video_prior
data_batch['caption'] = [args.prompt]
#### prepare text embeddings ####
text_emb = embedding_func(prompts = args.prompt, cache_dir=args.cache_dir)
th.distributed.barrier()
data_batch["t5_text_embeddings"] = text_emb.to(dtype=th.bfloat16).cuda()
if args.neg_prompt:
args.neg_prompt = UMT5_NEGATIVE_PROMPT
text_emb = embedding_func(prompts = args.neg_prompt)
data_batch["neg_t5_text_embeddings"] = text_emb.to(dtype=th.bfloat16).cuda()
print("Using negative prompt: ", args.neg_prompt)
is_negative_prompt = True
sample_info['neg_prompt'] = args.neg_prompt
else:
print("Using empty negative prompt")
is_negative_prompt = False
sample_info['neg_prompt'] = ""
if dist.get_rank() == 0:
key = f"{args.guidance:.2f}_{args.shift:.2f}_{args.num_steps:d}"
if key not in avg_list:
avg_list[key] = {
"diffusion": [],
"decode": [],
"total": [],
}
this_list = avg_list[key]
if "{" in args.save_root:
save_root = args.save_root.format(g=args.guidance, s=args.shift, n=args.num_steps)
else:
save_root = args.save_root
if dist.get_rank() == 0:
os.makedirs(os.path.dirname(save_path), exist_ok=True)
with open(f"{save_path}.json", "w") as f:
json.dump(sample_info, f, indent=2)
# generate samples
dist.barrier()
th.cuda.synchronize()
start_time = time.time()
sample_info['steps'] = args.guidance
sample_info['guidance'] = args.num_steps
sample_info['shift'] = args.shift
sample_info['drop_video_step'] = args.drop_video_step
sample = model.generate_samples_from_batch(
data_batch,
guidance=args.guidance,
state_shape=[16, 2 + (args.num_frames - 2) // 4, data_batch['video'].shape[3] // 8, data_batch['video'].shape[4] // 8],
seed=1,
num_steps=args.num_steps,
is_negative_prompt=is_negative_prompt,
shift=args.shift,
drop_video_step=args.drop_video_step,
)
dist.barrier()
th.cuda.synchronize()
if is_video_prior and args.drop_video_step is None:
first_frame_sample = sample[:, :, :1, :, :]
last_frame_sample = sample[:, :, -1:, :, :]
sample_edit = th.cat([first_frame_sample, last_frame_sample], dim=2)
sample_edit = model.decode(sample_edit).contiguous()
sample_video = sample[:, :, :-1, :, :]
sample_video = model.decode(sample_video).contiguous()
video = th.cat([sample_video, sample_edit[:, :, 1:, :, :]], dim=2)
else:
video = model.decode(sample)
dist.barrier()
th.cuda.synchronize()
if not os.path.exists(save_root):
os.makedirs(save_root, exist_ok=True)
for i in range(args.num_samples):
save_img_or_video((1.0 + video[i]) / 2, save_path, quality=8, fps=args.fps)
save_img_or_video((1.0 + video[i, :, -1:]) / 2, save_path)
if world_size > 1:
parallel_state.destroy_model_parallel()
dist.destroy_process_group()