Spaces:
Running
Running
Update app.py
Browse files
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
|
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 |
-
#
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
|
495 |
-
#
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
508 |
|
509 |
-
|
510 |
-
|
511 |
-
|
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"""
|