ginipick commited on
Commit
3c21081
ยท
verified ยท
1 Parent(s): 7cc9db6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -72
app.py CHANGED
@@ -383,30 +383,94 @@ class WebtoonDatabase:
383
  conn.commit()
384
 
385
  # --- Image Generation ---
 
386
  class ImageGenerator:
387
  """Handle image generation using Replicate API with webtoon-focused prompts"""
388
 
389
  def __init__(self):
390
  self.generation_lock = Lock()
391
  self.active_generations = {}
 
 
 
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  def enhance_prompt_for_webtoon(self, prompt: str, panel_number: int, scene_type: str = "medium", genre: str = "๋กœ๋งจ์Šค") -> str:
394
  """Enhanced prompt for webtoon-style panels focusing on action and scenes"""
395
 
396
  # ์›นํˆฐ ์Šคํƒ€์ผ ๊ธฐ๋ณธ ์„ค์ •
397
- base_style = "webtoon style, manhwa illustration, clean line art, vibrant colors"
398
 
399
  # ์žฅ๋ฅด๋ณ„ ์Šคํƒ€์ผ ์กฐ์ •
400
  genre_styles = {
401
- "๋กœ๋งจ์Šค": "soft colors, romantic atmosphere, emotional lighting",
402
- "ํŒํƒ€์ง€": "dynamic action, magical effects, epic atmosphere",
403
- "์Šค๋ฆด๋Ÿฌ": "dark tones, dramatic shadows, suspenseful mood",
404
- "์ผ์ƒ": "warm colors, everyday scenes, comfortable atmosphere",
405
- "๊ฐœ๊ทธ": "exaggerated expressions, comedic style, bright colors",
406
- "์Šคํฌ์ธ ": "dynamic motion, athletic poses, energetic atmosphere",
407
- "๋ฌดํ˜‘": "martial arts action, eastern style, dramatic poses",
408
- "๋กœํŒ": "fantasy romance, elegant costumes, magical atmosphere",
409
- "ํ˜„ํŒ": "modern fantasy, urban setting, supernatural effects"
410
  }
411
 
412
  # ์”ฌ ํƒ€์ž…๋ณ„ ์นด๋ฉ”๋ผ ์•ต๊ธ€๊ณผ ๊ตฌ๋„
@@ -437,8 +501,8 @@ class ImageGenerator:
437
  else:
438
  action_emphasis = "clear composition, focused scene"
439
 
440
- # ์›นํˆฐ ํŒจ๋„ ํŠน์„ฑ
441
- panel_style = "single panel illustration, vertical scroll webtoon format, story panel"
442
 
443
  # ๋ฐฐ๊ฒฝ๊ณผ ํ™˜๊ฒฝ ๊ฐ•์กฐ
444
  if "establishing" in scene_type or "wide" in scene_type:
@@ -466,7 +530,7 @@ class ImageGenerator:
466
  def generate_image(self, prompt: str, panel_id: str, session_id: str,
467
  scene_type: str = "medium", genre: str = "๋กœ๋งจ์Šค",
468
  progress_callback=None) -> Dict[str, Any]:
469
- """Generate image using Qwen-Image API with webtoon optimization"""
470
  try:
471
  if not REPLICATE_API_TOKEN:
472
  logger.warning("No Replicate API token")
@@ -478,72 +542,135 @@ class ImageGenerator:
478
  # ์›นํˆฐ ์Šคํƒ€์ผ ํ”„๋กฌํ”„ํŠธ ๊ฐ•ํ™”
479
  enhanced_prompt = self.enhance_prompt_for_webtoon(prompt, panel_number, scene_type, genre)
480
 
481
- # Qwen-Image ํŒŒ๋ผ๋ฏธํ„ฐ (์›นํˆฐ ์ตœ์ ํ™”)
482
- input_params = {
483
- "prompt": enhanced_prompt,
484
- "negative_prompt": "low quality, blurry, distorted, bad anatomy, text errors",
485
- "num_inference_steps": 50, # ๋น ๋ฅธ ์ƒ์„ฑ์„ ์œ„ํ•ด ๋‚ฎ์ถค
486
- "guidance": 3.5, # ์‹ค์‚ฌ๊ฐ๊ณผ ์Šคํƒ€์ผ ๊ท ํ˜•
487
- "aspect_ratio": "9:16", # ์„ธ๋กœํ˜• ์›นํˆฐ
488
- "image_size": "optimize_for_quality",
489
- "go_fast": True, # ์†๋„ ์ตœ์ ํ™”
490
- "enhance_prompt": True, # ํ”„๋กฌํ”„ํŠธ ์ž๋™ ๊ฐœ์„ 
491
- "output_format": "jpg",
492
- "output_quality": 100
493
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
 
495
- # ์žฅ๋ฅด๋ณ„ LoRA ์ ์šฉ (์žˆ์„ ๊ฒฝ์šฐ)
496
- webtoon_loras = {
497
- "๋กœ๋งจ์Šค": "fofr/flux-anime-romance", # ์˜ˆ์‹œ
498
- "ํŒํƒ€์ง€": "fofr/flux-fantasy-art", # ์˜ˆ์‹œ
499
- }
500
-
501
- if genre in webtoon_loras:
502
- input_params["lora_weights"] = webtoon_loras[genre]
503
-
504
- output = replicate.run(
505
- "qwen/qwen-image",
506
- input=input_params
507
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
- if output:
510
- # Replicate returns different formats depending on the model
511
- # Handle both list and direct URL responses
512
- if isinstance(output, list) and len(output) > 0:
513
- # If it's a list, get the first item
514
- image_item = output[0]
515
- # Check if it has a url method or if it's already a string
516
- if hasattr(image_item, 'url') and callable(image_item.url):
517
- image_url = image_item.url()
518
- else:
519
- image_url = str(image_item)
520
- elif isinstance(output, str):
521
- # Direct string URL
522
- image_url = output
523
- else:
524
- # Try to convert to string as fallback
525
- image_url = str(output)
526
-
527
- # ์บ์‹œ ์ €์žฅ
528
- cache_key = f"{session_id}_{panel_id}"
529
- generated_images_cache[cache_key] = image_url
530
-
531
- logger.info(f"Successfully generated image for panel {panel_id}")
532
- return {
533
- "panel_id": panel_id,
534
- "status": "success",
535
- "image_url": image_url,
536
- "prompt": enhanced_prompt
537
- }
538
- else:
539
- logger.error(f"No output from Replicate for panel {panel_id}")
540
- return {"panel_id": panel_id, "status": "error", "message": "No output from model"}
541
-
542
 
543
  except Exception as e:
544
  logger.error(f"Image generation error: {e}")
545
  return {"panel_id": panel_id, "status": "error", "message": str(e)}
546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  # --- LLM Integration ---
548
  class WebtoonSystem:
549
  """Webtoon planning and storyboard generation system"""
 
383
  conn.commit()
384
 
385
  # --- Image Generation ---
386
+ # --- Image Generation Fixed Version ---
387
  class ImageGenerator:
388
  """Handle image generation using Replicate API with webtoon-focused prompts"""
389
 
390
  def __init__(self):
391
  self.generation_lock = Lock()
392
  self.active_generations = {}
393
+ # ์›นํˆฐ ํ‘œ์ค€ ํฌ๊ธฐ ์„ค์ • (690x1227px)
394
+ self.target_width = 690
395
+ self.target_height = 1227
396
 
397
+ def resize_image_from_url(self, image_url: str) -> str:
398
+ """Download image from URL, resize to 690x1227px, and return new URL"""
399
+ try:
400
+ import requests
401
+ from PIL import Image
402
+ import io as io_module
403
+ import base64
404
+
405
+ # ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
406
+ response = requests.get(image_url, timeout=30)
407
+ if response.status_code != 200:
408
+ logger.error(f"Failed to download image: {response.status_code}")
409
+ return image_url
410
+
411
+ # PIL Image๋กœ ๋ณ€ํ™˜
412
+ img = Image.open(io_module.BytesIO(response.content))
413
+
414
+ # 690x1227๋กœ ๋ฆฌ์‚ฌ์ด์ฆˆ (๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ํฌ๋กญ)
415
+ # ์›๋ณธ ๋น„์œจ ๊ณ„์‚ฐ
416
+ original_ratio = img.width / img.height
417
+ target_ratio = self.target_width / self.target_height
418
+
419
+ if original_ratio > target_ratio:
420
+ # ์›๋ณธ์ด ๋” ๋„“์Œ - ๋†’์ด ๋งž์ถ”๊ณ  ์ขŒ์šฐ ํฌ๋กญ
421
+ new_height = self.target_height
422
+ new_width = int(new_height * original_ratio)
423
+ img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
424
+ # ์ค‘์•™ ํฌ๋กญ
425
+ left = (new_width - self.target_width) // 2
426
+ img = img.crop((left, 0, left + self.target_width, self.target_height))
427
+ else:
428
+ # ์›๋ณธ์ด ๋” ์ข์Œ - ๋„ˆ๋น„ ๋งž์ถ”๊ณ  ์ƒํ•˜ ํฌ๋กญ
429
+ new_width = self.target_width
430
+ new_height = int(new_width / original_ratio)
431
+ img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
432
+ # ์ƒ๋‹จ ๊ธฐ์ค€ ํฌ๋กญ (์›นํˆฐ์€ ์ƒ๋‹จ์ด ์ค‘์š”)
433
+ img = img.crop((0, 0, self.target_width, self.target_height))
434
+
435
+ # ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
436
+ with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp_file:
437
+ img.save(tmp_file.name, 'JPEG', quality=95)
438
+
439
+ # Base64๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ URL ์ƒ์„ฑ
440
+ with open(tmp_file.name, 'rb') as f:
441
+ img_data = f.read()
442
+ base64_data = base64.b64encode(img_data).decode('utf-8')
443
+ data_url = f"data:image/jpeg;base64,{base64_data}"
444
+
445
+ # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
446
+ try:
447
+ os.unlink(tmp_file.name)
448
+ except:
449
+ pass
450
+
451
+ return data_url
452
+
453
+ except Exception as e:
454
+ logger.error(f"Error resizing image: {e}")
455
+ return image_url
456
+
457
  def enhance_prompt_for_webtoon(self, prompt: str, panel_number: int, scene_type: str = "medium", genre: str = "๋กœ๋งจ์Šค") -> str:
458
  """Enhanced prompt for webtoon-style panels focusing on action and scenes"""
459
 
460
  # ์›นํˆฐ ์Šคํƒ€์ผ ๊ธฐ๋ณธ ์„ค์ •
461
+ base_style = "webtoon style, manhwa illustration, clean line art, vibrant colors, vertical format"
462
 
463
  # ์žฅ๋ฅด๋ณ„ ์Šคํƒ€์ผ ์กฐ์ •
464
  genre_styles = {
465
+ "๋กœ๋งจ์Šค": "soft colors, romantic atmosphere, emotional lighting, shoujo manga style",
466
+ "ํŒํƒ€์ง€": "dynamic action, magical effects, epic atmosphere, detailed backgrounds",
467
+ "์Šค๋ฆด๋Ÿฌ": "dark tones, dramatic shadows, suspenseful mood, noir style",
468
+ "์ผ์ƒ": "warm colors, everyday scenes, comfortable atmosphere, slice of life",
469
+ "๊ฐœ๊ทธ": "exaggerated expressions, comedic style, bright colors, cartoon style",
470
+ "์Šคํฌ์ธ ": "dynamic motion, athletic poses, energetic atmosphere, action lines",
471
+ "๋ฌดํ˜‘": "martial arts action, eastern style, dramatic poses, wuxia aesthetic",
472
+ "๋กœํŒ": "fantasy romance, elegant costumes, magical atmosphere, ornate details",
473
+ "ํ˜„ํŒ": "modern fantasy, urban setting, supernatural effects, contemporary style"
474
  }
475
 
476
  # ์”ฌ ํƒ€์ž…๋ณ„ ์นด๋ฉ”๋ผ ์•ต๊ธ€๊ณผ ๊ตฌ๋„
 
501
  else:
502
  action_emphasis = "clear composition, focused scene"
503
 
504
+ # ์›นํˆฐ ํŒจ๋„ ํŠน์„ฑ (9:16 ๋น„์œจ ๊ฐ•์กฐ)
505
+ panel_style = "single panel illustration, 9:16 aspect ratio, vertical scroll webtoon format, story panel"
506
 
507
  # ๋ฐฐ๊ฒฝ๊ณผ ํ™˜๊ฒฝ ๊ฐ•์กฐ
508
  if "establishing" in scene_type or "wide" in scene_type:
 
530
  def generate_image(self, prompt: str, panel_id: str, session_id: str,
531
  scene_type: str = "medium", genre: str = "๋กœ๋งจ์Šค",
532
  progress_callback=None) -> Dict[str, Any]:
533
+ """Generate image using Replicate API with webtoon optimization and size control"""
534
  try:
535
  if not REPLICATE_API_TOKEN:
536
  logger.warning("No Replicate API token")
 
542
  # ์›นํˆฐ ์Šคํƒ€์ผ ํ”„๋กฌํ”„ํŠธ ๊ฐ•ํ™”
543
  enhanced_prompt = self.enhance_prompt_for_webtoon(prompt, panel_number, scene_type, genre)
544
 
545
+ # ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ ๋ชฉ๋ก (์šฐ์„ ์ˆœ์œ„ ์ˆœ)
546
+ models_to_try = [
547
+ {
548
+ "model": "black-forest-labs/flux-dev",
549
+ "params": {
550
+ "prompt": enhanced_prompt,
551
+ "aspect_ratio": "9:16",
552
+ "num_outputs": 1,
553
+ "guidance": 3.5,
554
+ "num_inference_steps": 28,
555
+ "output_format": "jpg",
556
+ "output_quality": 95
557
+ }
558
+ },
559
+ {
560
+ "model": "stability-ai/sdxl",
561
+ "params": {
562
+ "prompt": enhanced_prompt,
563
+ "negative_prompt": "low quality, blurry, distorted, bad anatomy, text errors, watermark",
564
+ "width": 768,
565
+ "height": 1344,
566
+ "num_inference_steps": 30,
567
+ "guidance_scale": 7.5,
568
+ "scheduler": "K_EULER",
569
+ "num_outputs": 1
570
+ }
571
+ },
572
+ {
573
+ "model": "playgroundai/playground-v2.5-1024px-aesthetic",
574
+ "params": {
575
+ "prompt": enhanced_prompt,
576
+ "width": 768,
577
+ "height": 1344,
578
+ "scheduler": "K_EULER_ANCESTRAL",
579
+ "guidance_scale": 3,
580
+ "num_inference_steps": 25,
581
+ "negative_prompt": "ugly, deformed, noisy, blurry, low contrast"
582
+ }
583
+ }
584
+ ]
585
 
586
+ # ๊ฐ ๋ชจ๋ธ ์‹œ๋„
587
+ for model_config in models_to_try:
588
+ try:
589
+ logger.info(f"Trying model: {model_config['model']}")
590
+
591
+ output = replicate.run(
592
+ model_config["model"],
593
+ input=model_config["params"]
594
+ )
595
+
596
+ if output:
597
+ # Replicate returns different formats depending on the model
598
+ if isinstance(output, list) and len(output) > 0:
599
+ image_item = output[0]
600
+ if hasattr(image_item, 'url'):
601
+ image_url = image_item.url() if callable(image_item.url) else str(image_item.url)
602
+ else:
603
+ image_url = str(image_item)
604
+ elif isinstance(output, str):
605
+ image_url = output
606
+ elif hasattr(output, 'url'):
607
+ image_url = output.url() if callable(output.url) else str(output.url)
608
+ else:
609
+ image_url = str(output)
610
+
611
+ # ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์กฐ์ • (690x1227)
612
+ logger.info(f"Resizing image to {self.target_width}x{self.target_height}")
613
+ resized_url = self.resize_image_from_url(image_url)
614
+
615
+ # ์บ์‹œ ์ €์žฅ
616
+ cache_key = f"{session_id}_{panel_id}"
617
+ generated_images_cache[cache_key] = resized_url
618
+
619
+ logger.info(f"Successfully generated and resized image for panel {panel_id}")
620
+ return {
621
+ "panel_id": panel_id,
622
+ "status": "success",
623
+ "image_url": resized_url,
624
+ "original_url": image_url,
625
+ "prompt": enhanced_prompt,
626
+ "model_used": model_config["model"]
627
+ }
628
+
629
+ except Exception as model_error:
630
+ logger.warning(f"Model {model_config['model']} failed: {model_error}")
631
+ continue
632
 
633
+ # ๋ชจ๋“  ๋ชจ๋ธ ์‹คํŒจ
634
+ logger.error(f"All models failed for panel {panel_id}")
635
+ return {"panel_id": panel_id, "status": "error", "message": "All models failed"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
 
637
  except Exception as e:
638
  logger.error(f"Image generation error: {e}")
639
  return {"panel_id": panel_id, "status": "error", "message": str(e)}
640
 
641
+ def generate_batch_images(self, panels: List[Dict], session_id: str,
642
+ character_profiles: Dict, genre: str = "๋กœ๋งจ์Šค",
643
+ progress_callback=None) -> List[Dict]:
644
+ """๋ฐฐ์น˜๋กœ ์—ฌ๋Ÿฌ ํŒจ๋„ ์ด๋ฏธ์ง€ ์ƒ์„ฑ"""
645
+ results = []
646
+ total = len(panels)
647
+
648
+ for i, panel in enumerate(panels):
649
+ if progress_callback:
650
+ progress_callback((i / total), f"ํŒจ๋„ {panel['number']}/{total} ์ƒ์„ฑ ์ค‘...")
651
+
652
+ panel_id = f"ep1_panel{panel['number']}"
653
+ prompt = panel.get('prompt_en', panel.get('prompt', ''))
654
+ scene_type = panel.get('scene_type', 'medium')
655
+
656
+ result = self.generate_image(
657
+ prompt=prompt,
658
+ panel_id=panel_id,
659
+ session_id=session_id,
660
+ scene_type=scene_type,
661
+ genre=genre
662
+ )
663
+
664
+ results.append(result)
665
+
666
+ # API ์ œํ•œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋Œ€๊ธฐ
667
+ time.sleep(1)
668
+
669
+ if progress_callback:
670
+ progress_callback(1.0, "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์™„๋ฃŒ!")
671
+
672
+ return results
673
+
674
  # --- LLM Integration ---
675
  class WebtoonSystem:
676
  """Webtoon planning and storyboard generation system"""