Adnan commited on
Commit
af3ddb3
·
verified ·
1 Parent(s): 9d4b06f

Update image_generator.py

Browse files
Files changed (1) hide show
  1. image_generator.py +183 -194
image_generator.py CHANGED
@@ -1,206 +1,196 @@
1
  """
2
- TimeLapseForge — Image Generator Module
3
- Uses SDXL Turbo / SDXL Base for consistent timelapse panel generation.
4
- Key technique: img2img chaining with low strength for natural transitions.
5
  """
6
 
7
- import torch
8
  import gc
 
9
  from typing import List, Dict, Optional, Callable, Tuple
10
  from PIL import Image
11
 
12
- # Model cache
13
- _pipelines = {}
14
 
15
 
16
  def flush_memory():
17
- """Clear GPU and CPU memory caches."""
18
  gc.collect()
19
  if torch.cuda.is_available():
20
  torch.cuda.empty_cache()
21
 
22
 
23
- def get_device():
24
- """Get the best available device."""
25
- if torch.cuda.is_available():
26
- return "cuda"
27
- return "cpu"
28
 
 
29
 
30
- def get_pipeline(model_id: str = "stabilityai/sdxl-turbo", pipeline_type: str = "t2i"):
31
- """
32
- Load and cache the diffusion pipeline.
33
- Uses from_pipe for img2img to share components and save memory.
34
- """
35
- global _pipelines
36
  from diffusers import AutoPipelineForText2Image, AutoPipelineForImage2Image
37
 
38
  cache_key = f"{model_id}_{pipeline_type}"
39
-
40
- if cache_key not in _pipelines:
41
- device = get_device()
42
  dtype = torch.float16 if device == "cuda" else torch.float32
43
-
44
- # Determine variant
45
- variant = None
46
- if device == "cuda" and ("turbo" in model_id or "sdxl" in model_id.lower()):
47
- variant = "fp16"
48
 
49
  if pipeline_type == "t2i":
50
  try:
51
  pipe = AutoPipelineForText2Image.from_pretrained(
52
- model_id,
53
- torch_dtype=dtype,
54
- variant=variant,
55
  )
56
  except Exception:
57
- # Fallback without variant
58
  pipe = AutoPipelineForText2Image.from_pretrained(
59
- model_id,
60
- torch_dtype=dtype,
61
  )
62
  pipe = pipe.to(device)
63
- _pipelines[cache_key] = pipe
64
 
65
  elif pipeline_type == "i2i":
66
- # Try to reuse t2i components
67
  t2i_key = f"{model_id}_t2i"
68
- if t2i_key in _pipelines:
69
  try:
70
- pipe = AutoPipelineForImage2Image.from_pipe(_pipelines[t2i_key])
71
  except Exception:
72
  try:
73
  pipe = AutoPipelineForImage2Image.from_pretrained(
74
- model_id, torch_dtype=dtype, variant=variant,
75
  )
76
  except Exception:
77
  pipe = AutoPipelineForImage2Image.from_pretrained(
78
- model_id, torch_dtype=dtype,
79
  )
80
  pipe = pipe.to(device)
81
  else:
82
  try:
83
  pipe = AutoPipelineForImage2Image.from_pretrained(
84
- model_id, torch_dtype=dtype, variant=variant,
85
  )
86
  except Exception:
87
  pipe = AutoPipelineForImage2Image.from_pretrained(
88
- model_id, torch_dtype=dtype,
89
  )
90
  pipe = pipe.to(device)
91
- _pipelines[cache_key] = pipe
92
-
93
- return _pipelines[cache_key]
94
-
95
-
96
- def get_model_config(model_id: str) -> Dict:
97
- """Get optimal generation parameters for each model."""
98
- configs = {
99
- "stabilityai/sdxl-turbo": {
100
- "num_inference_steps": 4,
101
- "guidance_scale": 0.0,
102
- "default_resolution": (512, 512),
103
- "supports_guidance": False,
104
- },
105
- "stabilityai/stable-diffusion-xl-base-1.0": {
106
- "num_inference_steps": 25,
107
- "guidance_scale": 7.5,
108
- "default_resolution": (1024, 1024),
109
- "supports_guidance": True,
110
- },
111
- "runwayml/stable-diffusion-v1-5": {
112
- "num_inference_steps": 25,
113
- "guidance_scale": 7.5,
114
- "default_resolution": (512, 512),
115
- "supports_guidance": True,
116
- },
117
- }
118
- return configs.get(model_id, {
119
- "num_inference_steps": 20,
120
- "guidance_scale": 7.0,
121
  "default_resolution": (512, 512),
122
- "supports_guidance": True,
123
- })
124
 
125
 
 
 
 
 
126
  class ImageGenerator:
127
  """
128
- Generates timelapse panels using Stable Diffusion.
129
- Uses img2img chaining for visual consistency between panels.
130
  """
131
 
132
- def __init__(self, model_id: str = "stabilityai/sdxl-turbo"):
133
- self.model_id = model_id
134
- self.config = get_model_config(model_id)
135
- self.device = get_device()
136
-
137
- def generate_first_panel(
138
  self,
139
- prompt: str,
140
- negative_prompt: str = "",
141
- seed: int = 42,
142
- width: Optional[int] = None,
143
- height: Optional[int] = None,
144
- steps: Optional[int] = None,
145
- guidance: Optional[float] = None,
146
- ) -> Image.Image:
147
- """Generate the first panel using text-to-image."""
148
- pipe = get_pipeline(self.model_id, "t2i")
149
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  w = width or self.config["default_resolution"][0]
151
  h = height or self.config["default_resolution"][1]
152
- n_steps = steps or self.config["num_inference_steps"]
153
- cfg = guidance if guidance is not None else self.config["guidance_scale"]
154
- generator = torch.Generator(device=self.device).manual_seed(seed)
155
-
156
- kwargs = dict(
157
- prompt=prompt,
158
- num_inference_steps=n_steps,
159
- guidance_scale=cfg,
160
- width=w,
161
- height=h,
162
- generator=generator,
163
- )
164
  if negative_prompt:
165
  kwargs["negative_prompt"] = negative_prompt
166
-
167
- result = pipe(**kwargs)
168
- return result.images[0]
169
-
170
- def generate_next_panel(
171
- self,
172
- prompt: str,
173
- previous_image: Image.Image,
174
- negative_prompt: str = "",
175
- strength: float = 0.4,
176
- seed: int = 42,
177
- steps: Optional[int] = None,
178
- guidance: Optional[float] = None,
179
- ) -> Image.Image:
180
- """Generate the next panel using img2img from the previous panel."""
181
- pipe = get_pipeline(self.model_id, "i2i")
182
-
183
- n_steps = steps or self.config["num_inference_steps"]
184
- cfg = guidance if guidance is not None else self.config["guidance_scale"]
185
- generator = torch.Generator(device=self.device).manual_seed(seed)
186
-
187
- # Resize previous image to match model resolution
188
  target_w, target_h = self.config["default_resolution"]
189
- prev_resized = previous_image.resize((target_w, target_h), Image.LANCZOS)
190
-
191
- kwargs = dict(
192
- prompt=prompt,
193
- image=prev_resized,
194
- num_inference_steps=n_steps,
195
- guidance_scale=cfg,
196
- strength=strength,
197
- generator=generator,
198
- )
199
  if negative_prompt:
200
  kwargs["negative_prompt"] = negative_prompt
 
 
 
 
 
 
 
 
 
201
 
202
- result = pipe(**kwargs)
203
- return result.images[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  def generate_all_panels(
206
  self,
@@ -214,11 +204,7 @@ class ImageGenerator:
214
  progress_callback: Optional[Callable] = None,
215
  reference_image: Optional[Image.Image] = None,
216
  ) -> List[Image.Image]:
217
- """
218
- Generate all panels in sequence.
219
- Panel 1: text-to-image (or img2img from reference)
220
- Panel 2+: img2img from previous panel
221
- """
222
  images = []
223
 
224
  for i, prompt_data in enumerate(prompts):
@@ -229,37 +215,42 @@ class ImageGenerator:
229
  seed = base_seed + i
230
 
231
  try:
232
- if i == 0:
233
- if reference_image is not None:
234
- img = self.generate_next_panel(
235
- prompt=full_prompt,
236
- previous_image=reference_image,
237
- negative_prompt=neg_prompt,
238
- strength=max(strength, 0.5),
239
- seed=seed,
240
- steps=steps,
241
- guidance=guidance,
242
- )
 
243
  else:
244
- img = self.generate_first_panel(
245
- prompt=full_prompt,
246
- negative_prompt=neg_prompt,
247
- seed=seed,
248
- width=width,
249
- height=height,
250
- steps=steps,
251
- guidance=guidance,
252
  )
253
  else:
254
- img = self.generate_next_panel(
255
- prompt=full_prompt,
256
- previous_image=images[-1],
257
- negative_prompt=neg_prompt,
258
- strength=strength,
259
- seed=seed,
260
- steps=steps,
261
- guidance=guidance,
262
- )
 
 
 
 
 
 
 
 
 
263
 
264
  images.append(img)
265
 
@@ -268,8 +259,8 @@ class ImageGenerator:
268
  if images:
269
  images.append(images[-1].copy())
270
  else:
271
- fallback = Image.new("RGB", self.config["default_resolution"], (50, 50, 50))
272
- images.append(fallback)
273
 
274
  if progress_callback:
275
  progress_callback(i + 1, len(prompts))
@@ -278,14 +269,8 @@ class ImageGenerator:
278
  return images
279
 
280
  def regenerate_single_panel(
281
- self,
282
- panel_index: int,
283
- prompts: List[Dict[str, str]],
284
- existing_images: List[Image.Image],
285
- strength: float = 0.4,
286
- base_seed: int = 42,
287
- steps: Optional[int] = None,
288
- guidance: Optional[float] = None,
289
  ) -> Tuple[Image.Image, List[Image.Image]]:
290
  """Regenerate a single panel."""
291
  prompt_data = prompts[panel_index]
@@ -296,19 +281,23 @@ class ImageGenerator:
296
  seed = base_seed + panel_index
297
 
298
  if panel_index == 0:
299
- new_img = self.generate_first_panel(
300
- prompt=full_prompt, negative_prompt=neg_prompt,
301
- seed=seed, steps=steps, guidance=guidance,
302
- )
 
 
303
  else:
304
- new_img = self.generate_next_panel(
305
- prompt=full_prompt,
306
- previous_image=existing_images[panel_index - 1],
307
- negative_prompt=neg_prompt,
308
- strength=strength, seed=seed,
309
- steps=steps, guidance=guidance,
310
- )
 
 
311
 
312
- updated_images = existing_images.copy()
313
- updated_images[panel_index] = new_img
314
- return new_img, updated_images
 
1
  """
2
+ TimeLapseForge — Image Generator Module v2.0
3
+ Supports BOTH local (diffusers) and API-based generation.
4
+ Unified interface for all providers.
5
  """
6
 
 
7
  import gc
8
+ import torch
9
  from typing import List, Dict, Optional, Callable, Tuple
10
  from PIL import Image
11
 
12
+ from api_providers import get_provider, BaseProvider, PROVIDERS
 
13
 
14
 
15
  def flush_memory():
 
16
  gc.collect()
17
  if torch.cuda.is_available():
18
  torch.cuda.empty_cache()
19
 
20
 
21
+ # ═══════════════════════════════════════════
22
+ # LOCAL GENERATOR (Same as before — diffusers)
23
+ # ═══════════════════════════════════════════
 
 
24
 
25
+ _local_pipelines = {}
26
 
27
+ def get_local_pipeline(model_id, pipeline_type="t2i"):
28
+ global _local_pipelines
 
 
 
 
29
  from diffusers import AutoPipelineForText2Image, AutoPipelineForImage2Image
30
 
31
  cache_key = f"{model_id}_{pipeline_type}"
32
+ if cache_key not in _local_pipelines:
33
+ device = "cuda" if torch.cuda.is_available() else "cpu"
 
34
  dtype = torch.float16 if device == "cuda" else torch.float32
 
 
 
 
 
35
 
36
  if pipeline_type == "t2i":
37
  try:
38
  pipe = AutoPipelineForText2Image.from_pretrained(
39
+ model_id, torch_dtype=dtype, variant="fp16"
 
 
40
  )
41
  except Exception:
 
42
  pipe = AutoPipelineForText2Image.from_pretrained(
43
+ model_id, torch_dtype=dtype
 
44
  )
45
  pipe = pipe.to(device)
46
+ _local_pipelines[cache_key] = pipe
47
 
48
  elif pipeline_type == "i2i":
 
49
  t2i_key = f"{model_id}_t2i"
50
+ if t2i_key in _local_pipelines:
51
  try:
52
+ pipe = AutoPipelineForImage2Image.from_pipe(_local_pipelines[t2i_key])
53
  except Exception:
54
  try:
55
  pipe = AutoPipelineForImage2Image.from_pretrained(
56
+ model_id, torch_dtype=dtype, variant="fp16"
57
  )
58
  except Exception:
59
  pipe = AutoPipelineForImage2Image.from_pretrained(
60
+ model_id, torch_dtype=dtype
61
  )
62
  pipe = pipe.to(device)
63
  else:
64
  try:
65
  pipe = AutoPipelineForImage2Image.from_pretrained(
66
+ model_id, torch_dtype=dtype, variant="fp16"
67
  )
68
  except Exception:
69
  pipe = AutoPipelineForImage2Image.from_pretrained(
70
+ model_id, torch_dtype=dtype
71
  )
72
  pipe = pipe.to(device)
73
+ _local_pipelines[cache_key] = pipe
74
+
75
+ return _local_pipelines[cache_key]
76
+
77
+
78
+ LOCAL_MODELS = {
79
+ "stabilityai/sdxl-turbo": {
80
+ "num_inference_steps": 4, "guidance_scale": 0.0,
81
+ "default_resolution": (512, 512),
82
+ },
83
+ "stabilityai/stable-diffusion-xl-base-1.0": {
84
+ "num_inference_steps": 25, "guidance_scale": 7.5,
85
+ "default_resolution": (1024, 1024),
86
+ },
87
+ "runwayml/stable-diffusion-v1-5": {
88
+ "num_inference_steps": 25, "guidance_scale": 7.5,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  "default_resolution": (512, 512),
90
+ },
91
+ }
92
 
93
 
94
+ # ═══════════════════════════════════════════
95
+ # UNIFIED IMAGE GENERATOR
96
+ # ═══════════════════════════════════════════
97
+
98
  class ImageGenerator:
99
  """
100
+ Unified image generator supporting both local models and API providers.
 
101
  """
102
 
103
+ def __init__(
 
 
 
 
 
104
  self,
105
+ mode: str = "local", # "local" or "api"
106
+ # Local params
107
+ local_model_id: str = "stabilityai/sdxl-turbo",
108
+ # API params
109
+ provider_name: str = "openai",
110
+ api_key: str = "",
111
+ api_model: str = "",
112
+ custom_base_url: str = "",
113
+ custom_endpoint_url: str = "",
114
+ ):
115
+ self.mode = mode
116
+
117
+ if mode == "local":
118
+ self.local_model_id = local_model_id
119
+ self.config = LOCAL_MODELS.get(local_model_id, {
120
+ "num_inference_steps": 20, "guidance_scale": 7.0,
121
+ "default_resolution": (512, 512),
122
+ })
123
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
124
+ self.provider = None
125
+ else:
126
+ # API mode
127
+ kwargs = {}
128
+ if provider_name == "custom_openai":
129
+ kwargs["base_url"] = custom_base_url
130
+ elif provider_name == "direct_url":
131
+ kwargs["endpoint_url"] = custom_endpoint_url
132
+ self.provider = get_provider(provider_name, api_key, **kwargs)
133
+ self.api_model = api_model or self.provider.default_model
134
+ self.config = {"default_resolution": (1024, 1024)}
135
+ self.device = "cpu"
136
+
137
+ def _generate_local_t2i(self, prompt, negative_prompt="", seed=42,
138
+ width=None, height=None, steps=None, guidance=None):
139
+ pipe = get_local_pipeline(self.local_model_id, "t2i")
140
  w = width or self.config["default_resolution"][0]
141
  h = height or self.config["default_resolution"][1]
142
+ n_steps = steps or self.config.get("num_inference_steps", 20)
143
+ cfg = guidance if guidance is not None else self.config.get("guidance_scale", 7.0)
144
+ gen = torch.Generator(device=self.device).manual_seed(seed)
145
+
146
+ kwargs = dict(prompt=prompt, num_inference_steps=n_steps,
147
+ guidance_scale=cfg, width=w, height=h, generator=gen)
 
 
 
 
 
 
148
  if negative_prompt:
149
  kwargs["negative_prompt"] = negative_prompt
150
+ return pipe(**kwargs).images[0]
151
+
152
+ def _generate_local_i2i(self, prompt, prev_image, strength=0.4,
153
+ negative_prompt="", seed=42, steps=None, guidance=None):
154
+ pipe = get_local_pipeline(self.local_model_id, "i2i")
155
+ n_steps = steps or self.config.get("num_inference_steps", 20)
156
+ cfg = guidance if guidance is not None else self.config.get("guidance_scale", 7.0)
157
+ gen = torch.Generator(device=self.device).manual_seed(seed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  target_w, target_h = self.config["default_resolution"]
159
+ prev_resized = prev_image.resize((target_w, target_h), Image.LANCZOS)
160
+
161
+ kwargs = dict(prompt=prompt, image=prev_resized, num_inference_steps=n_steps,
162
+ guidance_scale=cfg, strength=strength, generator=gen)
 
 
 
 
 
 
163
  if negative_prompt:
164
  kwargs["negative_prompt"] = negative_prompt
165
+ return pipe(**kwargs).images[0]
166
+
167
+ def _generate_api_t2i(self, prompt, negative_prompt="", seed=None,
168
+ width=1024, height=1024, **kwargs):
169
+ return self.provider.generate_image(
170
+ prompt=prompt, negative_prompt=negative_prompt,
171
+ width=width, height=height, seed=seed,
172
+ model=self.api_model, **kwargs,
173
+ )
174
 
175
+ def _generate_api_i2i(self, prompt, prev_image, strength=0.4,
176
+ negative_prompt="", seed=None, **kwargs):
177
+ if self.provider.supports_img2img:
178
+ return self.provider.img2img(
179
+ prompt=prompt, image=prev_image, strength=strength,
180
+ negative_prompt=negative_prompt, seed=seed,
181
+ model=self.api_model, **kwargs,
182
+ )
183
+ else:
184
+ # Fallback: include description of previous state in prompt
185
+ enhanced_prompt = (
186
+ f"{prompt}, maintaining visual consistency with the previous frame, "
187
+ f"same camera angle, same lighting, same environment, subtle changes only"
188
+ )
189
+ return self._generate_api_t2i(
190
+ prompt=enhanced_prompt, negative_prompt=negative_prompt,
191
+ seed=seed, width=prev_image.width, height=prev_image.height,
192
+ **kwargs,
193
+ )
194
 
195
  def generate_all_panels(
196
  self,
 
204
  progress_callback: Optional[Callable] = None,
205
  reference_image: Optional[Image.Image] = None,
206
  ) -> List[Image.Image]:
207
+ """Generate all panels in sequence — local or API."""
 
 
 
 
208
  images = []
209
 
210
  for i, prompt_data in enumerate(prompts):
 
215
  seed = base_seed + i
216
 
217
  try:
218
+ if self.mode == "local":
219
+ if i == 0:
220
+ if reference_image is not None:
221
+ img = self._generate_local_i2i(
222
+ full_prompt, reference_image,
223
+ max(strength, 0.5), neg_prompt, seed, steps, guidance,
224
+ )
225
+ else:
226
+ img = self._generate_local_t2i(
227
+ full_prompt, neg_prompt, seed,
228
+ width, height, steps, guidance,
229
+ )
230
  else:
231
+ img = self._generate_local_i2i(
232
+ full_prompt, images[-1], strength,
233
+ neg_prompt, seed, steps, guidance,
 
 
 
 
 
234
  )
235
  else:
236
+ # API mode
237
+ if i == 0:
238
+ if reference_image is not None and self.provider.supports_img2img:
239
+ img = self._generate_api_i2i(
240
+ full_prompt, reference_image,
241
+ max(strength, 0.5), neg_prompt, seed,
242
+ )
243
+ else:
244
+ w = width or 1024
245
+ h = height or 1024
246
+ img = self._generate_api_t2i(
247
+ full_prompt, neg_prompt, seed, w, h,
248
+ )
249
+ else:
250
+ img = self._generate_api_i2i(
251
+ full_prompt, images[-1], strength,
252
+ neg_prompt, seed,
253
+ )
254
 
255
  images.append(img)
256
 
 
259
  if images:
260
  images.append(images[-1].copy())
261
  else:
262
+ res = self.config.get("default_resolution", (1024, 1024))
263
+ images.append(Image.new("RGB", res, (50, 50, 50)))
264
 
265
  if progress_callback:
266
  progress_callback(i + 1, len(prompts))
 
269
  return images
270
 
271
  def regenerate_single_panel(
272
+ self, panel_index, prompts, existing_images,
273
+ strength=0.4, base_seed=42, steps=None, guidance=None,
 
 
 
 
 
 
274
  ) -> Tuple[Image.Image, List[Image.Image]]:
275
  """Regenerate a single panel."""
276
  prompt_data = prompts[panel_index]
 
281
  seed = base_seed + panel_index
282
 
283
  if panel_index == 0:
284
+ if self.mode == "local":
285
+ new_img = self._generate_local_t2i(
286
+ full_prompt, neg_prompt, seed, steps=steps, guidance=guidance,
287
+ )
288
+ else:
289
+ new_img = self._generate_api_t2i(full_prompt, neg_prompt, seed)
290
  else:
291
+ prev = existing_images[panel_index - 1]
292
+ if self.mode == "local":
293
+ new_img = self._generate_local_i2i(
294
+ full_prompt, prev, strength, neg_prompt, seed, steps, guidance,
295
+ )
296
+ else:
297
+ new_img = self._generate_api_i2i(
298
+ full_prompt, prev, strength, neg_prompt, seed,
299
+ )
300
 
301
+ updated = existing_images.copy()
302
+ updated[panel_index] = new_img
303
+ return new_img, updated