AlekseyCalvin commited on
Commit
0e0bd41
·
verified ·
1 Parent(s): b660280

Create deforum_engine.py

Browse files
Files changed (1) hide show
  1. deforum_engine.py +154 -0
deforum_engine.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ from diffusers import AutoPipelineForImage2Image, LCMScheduler, EulerAncestralDiscreteScheduler
4
+ from PIL import Image
5
+ import utils
6
+ import os
7
+ import uuid
8
+ import zipfile
9
+ import cv2
10
+ import gc
11
+
12
+ class DeforumRunner:
13
+ def __init__(self, device="cpu"):
14
+ self.device = device
15
+ self.pipe = None
16
+ self.stop_requested = False
17
+ self.current_model_config = (None, None, None)
18
+
19
+ def load_model(self, model_id, lora_id, scheduler_name):
20
+ """Loads model, LoRA, and scheduler dynamically."""
21
+ new_config = (model_id, lora_id, scheduler_name)
22
+ if new_config == self.current_model_config and self.pipe is not None:
23
+ return
24
+
25
+ print(f"Loading Model: {model_id}, LoRA: {lora_id}, Scheduler: {scheduler_name}")
26
+ if self.pipe:
27
+ del self.pipe
28
+ gc.collect()
29
+
30
+ try:
31
+ pipe = AutoPipelineForImage2Image.from_pretrained(
32
+ model_id, safety_checker=None, torch_dtype=torch.float32
33
+ )
34
+ except Exception as e:
35
+ print(f"Error loading model {model_id}: {e}. Falling back.")
36
+ pipe = AutoPipelineForImage2Image.from_pretrained(
37
+ "runwayml/stable-diffusion-v1-5", safety_checker=None, torch_dtype=torch.float32
38
+ )
39
+
40
+ if lora_id and lora_id != "None":
41
+ try:
42
+ pipe.load_lora_weights(lora_id)
43
+ pipe.fuse_lora()
44
+ print("LoRA loaded and fused.")
45
+ except Exception as e: print(f"Error loading LoRA: {e}")
46
+
47
+ if scheduler_name == "LCM":
48
+ pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
49
+ elif scheduler_name == "Euler A":
50
+ pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
51
+
52
+ pipe.to(self.device)
53
+ pipe.set_progress_bar_config(disable=True)
54
+ pipe.enable_attention_slicing() # Crucial for CPU memory
55
+ self.pipe = pipe
56
+ self.current_model_config = new_config
57
+ print("Pipeline ready.")
58
+
59
+ def stop(self):
60
+ self.stop_requested = True
61
+
62
+ def render(self, prompts, neg_prompt, max_frames, width, height,
63
+ zoom_s, angle_s, tx_s, ty_s, strength_s, noise_s,
64
+ fps, steps, cadence, color_mode, border_mode, init_image_upload,
65
+ model_id, lora_id, scheduler_name):
66
+
67
+ self.stop_requested = False
68
+ self.load_model(model_id, lora_id, scheduler_name)
69
+
70
+ # 1. Parse Schedules
71
+ keys = ['z', 'a', 'tx', 'ty', 'str', 'noi']
72
+ inputs = [zoom_s, angle_s, tx_s, ty_s, strength_s, noise_s]
73
+ sched = {k: utils.parse_weight_string(v, max_frames) for k, v in zip(keys, inputs)}
74
+
75
+ # 2. Setup
76
+ run_id = uuid.uuid4().hex[:6]
77
+ output_dir = f"output_{run_id}"
78
+ os.makedirs(output_dir, exist_ok=True)
79
+
80
+ if init_image_upload:
81
+ prev_img = init_image_upload.resize((width, height), Image.LANCZOS)
82
+ color_anchor = prev_img
83
+ else:
84
+ prev_img = None
85
+ color_anchor = None
86
+
87
+ generated_frames = []
88
+ print(f"Starting run {run_id}...")
89
+
90
+ # 3. Loop
91
+ for i in range(max_frames):
92
+ if self.stop_requested:
93
+ print("Generation stopped.")
94
+ break
95
+
96
+ # Get Params
97
+ z, a, tx, ty = sched['z'][i], sched['a'][i], sched['tx'][i], sched['ty'][i]
98
+ strength, noise = sched['str'][i], sched['noi'][i]
99
+ current_prompt = prompts[max(k for k in prompts.keys() if k <= i)]
100
+
101
+ # --- Authentic Deforum Loop ---
102
+ # 1. Warp Previous Frame
103
+ if prev_img is not None:
104
+ warped_img = utils.anim_frame_warp_2d(prev_img, {'angle': a, 'zoom': z, 'translation_x': tx, 'translation_y': ty}, border_mode)
105
+ else:
106
+ warped_img = Image.new("RGB", (width, height), (0,0,0))
107
+
108
+ # Decide: Diffusion or Just Warp (Cadence)
109
+ if i % cadence == 0:
110
+ # 2. Color Match & 3. Add Noise (Only before diffusion)
111
+ init_for_diff = utils.maintain_colors(warped_img, color_anchor, color_mode)
112
+ init_for_diff = utils.add_noise(init_for_diff, noise)
113
+
114
+ # 4. Diffusion (Img2Img)
115
+ # Use high strength for first frame if no init image provided
116
+ curr_strength = strength if prev_img is not None else 0.95
117
+
118
+ gen_image = self.pipe(
119
+ prompt=current_prompt, negative_prompt=neg_prompt,
120
+ image=init_for_diff, num_inference_steps=steps,
121
+ strength=curr_strength, guidance_scale=1.2, # Low CFG for LCM
122
+ width=width, height=height
123
+ ).images[0]
124
+ else:
125
+ # Cadence step: Just show the warped image (faster)
126
+ gen_image = warped_img
127
+
128
+ # Update state
129
+ prev_img = gen_image
130
+ if color_anchor is None: color_anchor = gen_image
131
+ generated_frames.append(gen_image)
132
+ yield gen_image, None, None
133
+
134
+ # 4. Finalize
135
+ vid_path = f"{output_dir}/video.mp4"
136
+ self.save_video(generated_frames, vid_path, fps)
137
+ zip_path = f"{output_dir}/frames.zip"
138
+ self.save_zip(generated_frames, zip_path)
139
+ yield generated_frames[-1], vid_path, zip_path
140
+
141
+ # (save_video and save_zip are the same as before, omitted for brevity)
142
+ def save_video(self, frames, path, fps):
143
+ if not frames: return
144
+ w, h = frames[0].size
145
+ out = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
146
+ for f in frames: out.write(cv2.cvtColor(np.array(f), cv2.COLOR_RGB2BGR))
147
+ out.release()
148
+ def save_zip(self, frames, path):
149
+ import io
150
+ with zipfile.ZipFile(path, 'w') as zf:
151
+ for i, f in enumerate(frames):
152
+ buf = io.BytesIO()
153
+ f.save(buf, format="PNG")
154
+ zf.writestr(f"{i:05d}.png", buf.getvalue())