NANI-Nithin commited on
Commit
07fb364
Β·
1 Parent(s): 85ae7dd

feat: Add photo path handling in poster generation and enhance collage creation

Browse files
Files changed (3) hide show
  1. app.py +6 -1
  2. app/services/image_gen.py +166 -4
  3. requirements.txt +1 -0
app.py CHANGED
@@ -389,6 +389,7 @@ def upload_photo(
389
  SESSION_STORE[session_id]["photos"].append({
390
  "photo_id": photo_id,
391
  "photo_name": photo_name,
 
392
  "caption": caption,
393
  "task_id": task_id,
394
  })
@@ -513,7 +514,11 @@ def generate_poster_from_session(session_id: str):
513
  )
514
  result = generate_story(packet, session_id=session_id)
515
 
516
- poster_url, status = generate_poster_sync(session_id, result["poster_prompt"])
 
 
 
 
517
  session["poster_url"] = poster_url
518
  return poster_url, status
519
 
 
389
  SESSION_STORE[session_id]["photos"].append({
390
  "photo_id": photo_id,
391
  "photo_name": photo_name,
392
+ "photo_path": photo_file if isinstance(photo_file, str) else "",
393
  "caption": caption,
394
  "task_id": task_id,
395
  })
 
514
  )
515
  result = generate_story(packet, session_id=session_id)
516
 
517
+ poster_url, status = generate_poster_sync(
518
+ session_id,
519
+ result["poster_prompt"],
520
+ photo_paths=[p.get("photo_path", "") for p in photos if p.get("photo_path")],
521
+ )
522
  session["poster_url"] = poster_url
523
  return poster_url, status
524
 
app/services/image_gen.py CHANGED
@@ -1,7 +1,10 @@
1
- """Black Forest Labs FLUX poster generation module.
2
 
3
- Generates a recap poster image from a text prompt using FLUX offline
4
- via ``diffusers`` (same lazy-download pattern as generator.py / minicpm.py).
 
 
 
5
 
6
  Sponsor alignment: Black Forest Labs FLUX produces a visible demo artifact
7
  (the recap poster) rather than being hidden in backend code.
@@ -155,21 +158,180 @@ def generate_poster(poster_prompt: str) -> Optional[str]:
155
  return None
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  def generate_poster_sync(
159
  session_id: str,
160
  poster_prompt: str,
 
161
  ) -> tuple[Optional[str], str]:
162
  """Generate a poster and return (image_path, status_message).
163
 
164
- This is a convenience wrapper for use in the pipeline entry point.
 
 
165
 
166
  Args:
167
  session_id: Session identifier (for logging).
168
  poster_prompt: Text prompt for the image.
 
169
 
170
  Returns:
171
  Tuple of (image_path_or_None, status_message).
172
  """
 
 
 
 
 
 
 
 
 
 
 
 
173
  image_path = generate_poster(poster_prompt)
174
 
175
  if image_path:
 
1
+ """Poster generation module β€” two strategies:
2
 
3
+ 1. **Photo collage** (default) β€” builds an attractive collage from the
4
+ player's uploaded gameplay photos using Pillow. Instant, free, and
5
+ shows *real* game moments.
6
+ 2. **FLUX text-to-image** (fallback) β€” generates an AI poster from a
7
+ text prompt via ``diffusers``. Requires GPU and the FLUX model.
8
 
9
  Sponsor alignment: Black Forest Labs FLUX produces a visible demo artifact
10
  (the recap poster) rather than being hidden in backend code.
 
158
  return None
159
 
160
 
161
+ # ── Photo collage poster (uses uploaded gameplay photos) ─────────────────
162
+
163
+ def generate_collage_poster(
164
+ photo_paths: list[str],
165
+ title: str = "CityQuest",
166
+ game_title: str = "",
167
+ ) -> Optional[str]:
168
+ """Build an attractive photo collage poster from uploaded gameplay photos.
169
+
170
+ Uses Pillow only (no GPU required). Falls back to FLUX text-to-image
171
+ if Pillow is unavailable or no photos are provided.
172
+
173
+ Args:
174
+ photo_paths: List of filesystem paths to uploaded player photos.
175
+ title: Overlay title text (e.g. "CityQuest AI").
176
+ game_title: The game's title from generation.
177
+
178
+ Returns:
179
+ Filesystem path (as string) to the saved poster, or ``None``.
180
+ """
181
+ if not photo_paths:
182
+ return None
183
+
184
+ # Filter to only files that actually exist
185
+ valid = [p for p in photo_paths if Path(p).exists()]
186
+ if not valid:
187
+ return None
188
+
189
+ try:
190
+ from PIL import Image, ImageDraw, ImageFont
191
+
192
+ POSTER_DIR.mkdir(parents=True, exist_ok=True)
193
+
194
+ # Poster dimensions
195
+ W, H = 1024, 768
196
+ poster = Image.new("RGB", (W, H), (20, 20, 30))
197
+ draw = ImageDraw.Draw(poster)
198
+
199
+ # Layout: pick grid based on photo count
200
+ n = len(valid)
201
+ if n == 1:
202
+ cols, rows = 1, 1
203
+ elif n == 2:
204
+ cols, rows = 2, 1
205
+ elif n <= 4:
206
+ cols, rows = 2, 2
207
+ elif n <= 6:
208
+ cols, rows = 3, 2
209
+ else:
210
+ cols, rows = 4, 2
211
+
212
+ padding = 8
213
+ header_h = 80 # space for title overlay at top
214
+ footer_h = 60 # space for bottom overlay
215
+ grid_w = W - 2 * padding
216
+ grid_h = H - header_h - footer_h - 2 * padding
217
+ cell_w = grid_w // cols
218
+ cell_h = grid_h // rows
219
+
220
+ # Paste photos into grid
221
+ placed = 0
222
+ for idx, path in enumerate(valid[: cols * rows]):
223
+ try:
224
+ img = Image.open(path).convert("RGB")
225
+ # Resize to fit cell, preserve aspect ratio (cover)
226
+ ratio = max(cell_w / img.width, cell_h / img.height)
227
+ new_w = int(img.width * ratio)
228
+ new_h = int(img.height * ratio)
229
+ img = img.resize((new_w, new_h), Image.LANCZOS)
230
+ # Center-crop to cell size
231
+ left = (new_w - cell_w) // 2
232
+ top = (new_h - cell_h) // 2
233
+ img = img.crop((left, top, left + cell_w, top + cell_h))
234
+ # Place in grid
235
+ col = placed % cols
236
+ row = placed // cols
237
+ x = padding + col * cell_w
238
+ y = header_h + padding + row * cell_h
239
+ poster.paste(img, (x, y))
240
+ placed += 1
241
+ except Exception as exc:
242
+ print(f"[image_gen] Skipping photo {path}: {exc}")
243
+
244
+ if placed == 0:
245
+ return None
246
+
247
+ # Semi-transparent header overlay
248
+ overlay = Image.new("RGBA", (W, header_h), (0, 0, 0, 180))
249
+ poster.paste(
250
+ Image.alpha_composite(
251
+ Image.new("RGBA", (W, header_h), (0, 0, 0, 0)),
252
+ overlay,
253
+ ).convert("RGB"),
254
+ (0, 0),
255
+ )
256
+
257
+ # Semi-transparent footer overlay
258
+ footer_overlay = Image.new("RGBA", (W, footer_h), (0, 0, 0, 180))
259
+ poster.paste(
260
+ Image.alpha_composite(
261
+ Image.new("RGBA", (W, footer_h), (0, 0, 0, 0)),
262
+ footer_overlay,
263
+ ).convert("RGB"),
264
+ (0, H - footer_h),
265
+ )
266
+
267
+ # Draw title text
268
+ draw = ImageDraw.Draw(poster)
269
+ display_title = game_title or title
270
+ try:
271
+ font_large = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 36)
272
+ font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
273
+ except (OSError, IOError):
274
+ # Fallback if system fonts unavailable
275
+ font_large = ImageFont.load_default()
276
+ font_small = ImageFont.load_default()
277
+
278
+ # Title (centered top)
279
+ bbox = draw.textbbox((0, 0), display_title, font=font_large)
280
+ tw = bbox[2] - bbox[0]
281
+ draw.text(((W - tw) // 2, 20), display_title, fill=(255, 255, 255), font=font_large)
282
+
283
+ # Footer text
284
+ footer_text = f"{placed} photo{'s' if placed != 1 else ''} from gameplay"
285
+ bbox2 = draw.textbbox((0, 0), footer_text, font=font_small)
286
+ fw = bbox2[2] - bbox2[0]
287
+ draw.text(((W - fw) // 2, H - 45), footer_text, fill=(200, 200, 200), font=font_small)
288
+
289
+ # Save
290
+ filename = f"poster_{uuid.uuid4().hex[:8]}.png"
291
+ filepath = POSTER_DIR / filename
292
+ poster.save(str(filepath), quality=95)
293
+ print(f"[image_gen] Collage poster saved -> {filepath} ({placed} photos)")
294
+ return str(filepath)
295
+
296
+ except ImportError:
297
+ print("[image_gen] Pillow not installed β€” cannot create collage")
298
+ return None
299
+ except Exception as e:
300
+ print(f"[image_gen] Collage generation failed: {type(e).__name__}: {e}")
301
+ return None
302
+
303
+
304
  def generate_poster_sync(
305
  session_id: str,
306
  poster_prompt: str,
307
+ photo_paths: Optional[list[str]] = None,
308
  ) -> tuple[Optional[str], str]:
309
  """Generate a poster and return (image_path, status_message).
310
 
311
+ Strategy:
312
+ 1. If uploaded photos exist β†’ build a photo collage (instant, no GPU).
313
+ 2. Otherwise β†’ fall back to FLUX text-to-image (needs GPU).
314
 
315
  Args:
316
  session_id: Session identifier (for logging).
317
  poster_prompt: Text prompt for the image.
318
+ photo_paths: Optional list of uploaded gameplay photo file paths.
319
 
320
  Returns:
321
  Tuple of (image_path_or_None, status_message).
322
  """
323
+ # ── Strategy 1: Photo collage from uploaded gameplay photos ─────────
324
+ if photo_paths:
325
+ print(f"[image_gen] Attempting collage from {len(photo_paths)} photo(s)")
326
+ collage = generate_collage_poster(photo_paths)
327
+ if collage:
328
+ return collage, (
329
+ f"**Poster created from your gameplay photos!** πŸ“Έ\n"
330
+ f"Saved as `{collage}` β€” refresh the Recap tab to view."
331
+ )
332
+ print("[image_gen] Collage failed, falling back to FLUX text-to-image")
333
+
334
+ # ── Strategy 2: FLUX text-to-image (GPU required) ─────────────────
335
  image_path = generate_poster(poster_prompt)
336
 
337
  if image_path:
requirements.txt CHANGED
@@ -13,6 +13,7 @@ diffusers[torch]>=0.30.0
13
  transformers>=5.4.0
14
  accelerate
15
  safetensors
 
16
  # Cohere Transcribe ASR (voice journal) β€” gated model, requires HF_TOKEN
17
  soundfile
18
  librosa
 
13
  transformers>=5.4.0
14
  accelerate
15
  safetensors
16
+ Pillow>=9.0
17
  # Cohere Transcribe ASR (voice journal) β€” gated model, requires HF_TOKEN
18
  soundfile
19
  librosa