Spaces:
Running
Running
import gradio as gr | |
import os | |
import json | |
import requests | |
from datetime import datetime | |
import time | |
from typing import List, Dict, Any, Generator, Tuple, Optional, Set | |
import logging | |
import re | |
import tempfile | |
from pathlib import Path | |
import sqlite3 | |
import hashlib | |
import threading | |
from contextlib import contextmanager | |
from dataclasses import dataclass, field, asdict | |
from collections import defaultdict | |
import random | |
from huggingface_hub import HfApi, upload_file, hf_hub_download | |
import replicate | |
from PIL import Image | |
import io as io_module | |
import base64 | |
import concurrent.futures | |
from threading import Lock | |
# --- Logging setup --- | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
logger = logging.getLogger(__name__) | |
# --- Document export imports --- | |
try: | |
from docx import Document | |
from docx.shared import Inches, Pt, RGBColor, Mm | |
from docx.enum.text import WD_ALIGN_PARAGRAPH | |
from docx.enum.style import WD_STYLE_TYPE | |
from docx.oxml.ns import qn | |
from docx.oxml import OxmlElement | |
DOCX_AVAILABLE = True | |
except ImportError: | |
DOCX_AVAILABLE = False | |
logger.warning("python-docx not installed. DOCX export will be disabled.") | |
# --- Environment variables and constants --- | |
FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "") | |
REPLICATE_API_TOKEN = os.getenv("REPLICATE_API_TOKEN", "") | |
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") | |
API_URL = "https://api.fireworks.ai/inference/v1/chat/completions" | |
MODEL_ID = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507" | |
DB_PATH = "webtoon_sessions_v1.db" | |
# Initialize Replicate client if token exists | |
if REPLICATE_API_TOKEN: | |
os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN | |
# Target settings for webtoon | |
TARGET_EPISODES = 40 # 40ํ ์๊ฒฐ | |
PANELS_PER_EPISODE = 30 # ๊ฐ ํ๋น 30๊ฐ ํจ๋ | |
TARGET_PANELS = TARGET_EPISODES * PANELS_PER_EPISODE # ์ด 1200 ํจ๋ | |
# Webtoon genres | |
WEBTOON_GENRES = { | |
"๋ก๋งจ์ค": "Romance", | |
"๋กํ": "Romance Fantasy", | |
"ํํ์ง": "Fantasy", | |
"ํํ": "Modern Fantasy", | |
"๋ฌดํ": "Martial Arts", | |
"์ค๋ฆด๋ฌ": "Thriller", | |
"์ผ์": "Slice of Life", | |
"๊ฐ๊ทธ": "Comedy", | |
"์คํฌ์ธ ": "Sports" | |
} | |
# Celebrity face references for character design | |
CELEBRITY_FACES = { | |
"male": [ | |
"ํฐ ํฌ๋ฃจ์ฆ", "๋ธ๋๋ ํผํธ", "๋ ์ค๋๋ฅด๋ ๋์นดํ๋ฆฌ์ค", "๋ผ์ด์ธ ๊ณ ์ฌ๋ง", | |
"ํฌ๋ฆฌ์ค ํด์ค์์ค", "๋ก๋ฒํธ ๋ค์ฐ๋ ์ฃผ๋์ด", "ํฌ๋ฆฌ์ค ์๋ฐ์ค", "ํฐ ํ๋ค์คํด", | |
"๋ฒ ๋ค๋ํธ ์ปด๋ฒ๋ฐฐ์น", "ํค์๋ ๋ฆฌ๋ธ์ค", "์ด๋ณํ", "๊ณต์ ", "๋ฐ์์ค", "์ก์ค๊ธฐ" | |
], | |
"female": [ | |
"์ค์นผ๋ ์ํ์จ", "์ ๋ง ์์จ", "์ ๋ํผ ๋ก๋ ์ค", "๊ฐค ๊ฐ๋", | |
"๋ง๊ณ ๋ก๋น", "์ ๋ง ์คํค", "์ค ํด์์จ์ด", "๋ํ๋ฆฌ ํฌํธ๋ง", | |
"์ ์งํ", "์กํ๊ต", "๊นํ๋ฆฌ", "์์ด์ ", "์์ง", "ํ์ํฌ" | |
] | |
} | |
# --- Environment validation --- | |
if not FIREWORKS_API_KEY: | |
logger.error("FIREWORKS_API_KEY not set. Application will not work properly.") | |
FIREWORKS_API_KEY = "dummy_token_for_testing" | |
if not REPLICATE_API_TOKEN: | |
logger.warning("REPLICATE_API_TOKEN not set. Image generation will be disabled.") | |
# --- Global variables --- | |
db_lock = threading.Lock() | |
generated_images_cache = {} # Cache for generated images | |
# --- Genre-specific prompts and elements --- | |
GENRE_ELEMENTS = { | |
"๋ก๋งจ์ค": { | |
"key_elements": ["๊ฐ์ ์ ", "์คํด์ ํํด", "๋ฌ์ฝคํ ์๊ฐ", "์งํฌ", "๊ณ ๋ฐฑ"], | |
"visual_styles": ["์ํํธ ํค", "ํ์คํ ", "๊ฝ ๋ฐฐ๊ฒฝ", "๋น๋ง์ธ ํจ๊ณผ", "๋ถํ๋น ํํฐ"], | |
"panel_types": ["ํด๋ก์ฆ์ ๊ฐ์ ์ท", "ํฌ์ท", "์ ํด๋ก์ฆ์ ", "๋๋น ๊ตํ", "๋ฐฑํ๊ทธ"], | |
"typical_scenes": ["์นดํ ๋ฐ์ดํธ", "์ฐ์ฐ ์ฌ", "๋ถ๊ฝ๋์ด", "์ฅ์ ๊ณ ๋ฐฑ", "๊ณตํญ ์ด๋ณ"] | |
}, | |
"๋กํ": { | |
"key_elements": ["ํ๊ท/๋น์", "๋๋ ์ค", "๋ฌด๋ํ", "๋ง๋ฒ", "์ ๋ถ ์์น"], | |
"visual_styles": ["ํ๋ คํ ์์", "์ ๋ฝํ ๋ฐฐ๊ฒฝ", "๋ฐ์ง์ด ํจ๊ณผ", "๋ง๋ฒ์ง", "์ฑ ๋ฐฐ๊ฒฝ"], | |
"panel_types": ["์ ์ ์ท", "๋๋ ์ค ๋ํ ์ผ", "๋ง๋ฒ ์ดํํธ", "ํ์์ฌ", "์ถฉ๊ฒฉ ๋ฆฌ์ก์ "], | |
"typical_scenes": ["๋ฌด๋ํ์ฅ", "์ ์ ์ฐ์ฑ ", "์์ฌ", "๋ง๋ฒ ์์ ", "์ํ์ค"] | |
}, | |
"ํํ์ง": { | |
"key_elements": ["๋ง๋ฒ์ฒด๊ณ", "๋ ๋ฒจ์ ", "๋์ ", "๊ธธ๋", "๋ชจํ"], | |
"visual_styles": ["๋ค์ด๋๋ฏน ์ก์ ", "์ดํํธ ๊ฐ์กฐ", "๋ชฌ์คํฐ ๋์์ธ", "ํํ์ง ๋ฐฐ๊ฒฝ", "๋น ํจ๊ณผ"], | |
"panel_types": ["์ก์ ์ฌ", "ํ์ท ์ ํฌ", "์คํฌ ๋ฐ๋", "๋ชฌ์คํฐ ๋ฑ์ฅ", "ํ์์ "], | |
"typical_scenes": ["๋์ ์ ๊ตฌ", "๋ณด์ค์ ", "๊ธธ๋ ํ๊ด", "์๋ จ์ฅ", "์์ดํ ํ๋"] | |
}, | |
"ํํ": { | |
"key_elements": ["๊ฒ์ดํธ", "ํํฐ", "๊ฐ์ฑ", "ํ๋ ๋์", "๋ฅ๋ ฅ"], | |
"visual_styles": ["๋์ ๋ฐฐ๊ฒฝ", "๋ค์จ ํจ๊ณผ", "ํ๋์ ์ก์ ", "ํน์ ํจ๊ณผ", "์ด๋ฐ ํํ์ง"], | |
"panel_types": ["๋์ ์ ๊ฒฝ", "๋ฅ๋ ฅ ๋ฐํ", "๊ฒ์ดํธ ์ถํ", "์ ํฌ ์ก์ ", "์ผ์ ๋๋น"], | |
"typical_scenes": ["๊ฒ์ดํธ ํ์ฅ", "ํํฐ ํํ", "ํ๋ จ์ฅ", "๋ณ์", "ํ๊ต"] | |
}, | |
"๋ฌดํ": { | |
"key_elements": ["๋ฌด๊ณต", "๋ฌธํ", "๊ฐํธ", "๋ณต์", "์ํ"], | |
"visual_styles": ["๋์ํ", "๋จน ํจ๊ณผ", "๊ธฐ ํํ", "์ค๊ตญํ ์์", "์ฐ์ํ ๋ฐฐ๊ฒฝ"], | |
"panel_types": ["๊ฒ์ ๋์", "๊ฒฝ๊ณต์ ", "๊ธฐ๊ณต ์๋ จ", "๋๊ฒฐ ๊ตฌ๋", "ํญ๋ฐ ์ดํํธ"], | |
"typical_scenes": ["๋ฌด๋ฆผ๋งน", "๊ฐ์", "์ ๋ฒฝ", "ํญํฌ ์๋ จ", "๋น๋ฌด๋ํ"] | |
}, | |
"์ค๋ฆด๋ฌ": { | |
"key_elements": ["์์คํ์ค", "๊ณตํฌ", "์ถ๊ฒฉ", "์ฌ๋ฆฌ์ ", "๋ฐ์ "], | |
"visual_styles": ["์ด๋์ด ํค", "๊ทธ๋ฆผ์ ๊ฐ์กฐ", "๋๋น ํจ๊ณผ", "๋ถ์ํ ๊ตฌ๋", "๋ถ์์ ๊ฐ์กฐ"], | |
"panel_types": ["๊ทน๋จ ํด๋ก์ฆ์ ", "Dutch angle", "์ค๋ฃจ์ฃ", "์ถฉ๊ฒฉ ์ปท", "๊ณตํฌ ์ฐ์ถ"], | |
"typical_scenes": ["์ด๋์ด ๊ณจ๋ชฉ", "ํ๊ฑด๋ฌผ", "์งํ์ค", "์ถ๊ฒฉ์ฌ", "๋์น ์ํฉ"] | |
}, | |
"์ผ์": { | |
"key_elements": ["์ผ์", "๊ณต๊ฐ", "์์ํ ์ฌ๋ฏธ", "๊ด๊ณ", "์ฑ์ฅ"], | |
"visual_styles": ["๋ฐ๋ปํ ์๊ฐ", "๋ถ๋๋ฌ์ด ์ ", "์ผ์ ๋ฐฐ๊ฒฝ", "์บ์ฃผ์ผ", "ํธ์ํ ๊ตฌ๋"], | |
"panel_types": ["์ผ์ ์ปท", "๋ฆฌ์ก์ ", "๋ํ์ฌ", "๋ฐฐ๊ฒฝ ๋ฌ์ฌ", "๊ฐ์ ํํ"], | |
"typical_scenes": ["์ง", "ํ๊ต", "ํ์ฌ", "๋๋ค", "ํธ์์ "] | |
}, | |
"๊ฐ๊ทธ": { | |
"key_elements": ["๊ฐ๊ทธ", "ํจ๋ฌ๋", "๊ณผ์ฅ", "๋ฐ์ ", "์ฌ๋ฉ์คํฑ"], | |
"visual_styles": ["๊ณผ์ฅ๋ ํ์ ", "๋ฐํฌ๋ฅด๋ฉ", "ํจ๊ณผ์ ", "๋งํ์ ์ฐ์ถ", "ํ๊ฒฉ ๊ตฌ๋"], | |
"panel_types": ["๊ณผ์ฅ ๋ฆฌ์ก์ ", "๊ฐ๊ทธ ์ปท", "ํจ๋ฌ๋", "์ถฉ๊ฒฉ ํ์ ", "๋ง๊ฐ์ง"], | |
"typical_scenes": ["๊ฐ๊ทธ ์ํฉ", "์ผ์ ๋ถ๊ดด", "์คํด ์ํฉ", "์ถ๊ฒฉ์ ", "๋จ์ฒด ๊ฐ๊ทธ"] | |
}, | |
"์คํฌ์ธ ": { | |
"key_elements": ["๊ฒฝ๊ธฐ", "ํ๋ จ", "ํ์ํฌ", "๋ผ์ด๋ฒ", "์ฑ์ฅ"], | |
"visual_styles": ["๋ค์ด๋๋ฏน", "์คํผ๋์ ", "๋ ํํ", "๊ทผ์ก ๋ฌ์ฌ", "๊ฒฝ๊ธฐ์ฅ"], | |
"panel_types": ["์ก์ ์ปท", "๊ฒฐ์ ์ ์๊ฐ", "์ ์ ๋์", "ํ์ ํด๋ก์ฆ์ ", "๊ฒฝ๊ธฐ ์ ๊ฒฝ"], | |
"typical_scenes": ["๊ฒฝ๊ธฐ์ฅ", "ํ๋ จ์ฅ", "๋ผ์ปค๋ฃธ", "๋ฒค์น", "์์๋"] | |
} | |
} | |
# --- Data classes --- | |
class CharacterProfile: | |
"""Character profile with celebrity lookalike""" | |
name: str | |
role: str | |
personality: str | |
appearance: str | |
celebrity_lookalike: str | |
gender: str | |
class WebtoonBible: | |
"""Webtoon story bible for maintaining consistency""" | |
genre: str = "" | |
title: str = "" | |
characters: Dict[str, CharacterProfile] = field(default_factory=dict) | |
settings: Dict[str, str] = field(default_factory=dict) | |
plot_points: List[Dict[str, Any]] = field(default_factory=list) | |
episode_hooks: Dict[int, str] = field(default_factory=dict) | |
genre_elements: Dict[str, Any] = field(default_factory=dict) | |
visual_style: Dict[str, Any] = field(default_factory=dict) | |
panel_compositions: List[str] = field(default_factory=list) | |
class StoryboardPanel: | |
"""Individual storyboard panel with unique ID""" | |
panel_number: int | |
scene_type: str # wide, close-up, medium, establishing | |
image_prompt: str # Image generation prompt with character descriptions | |
panel_id: str = "" # Unique panel identifier | |
dialogue: List[str] = field(default_factory=list) | |
narration: str = "" | |
sound_effects: List[str] = field(default_factory=list) | |
emotion_notes: str = "" | |
camera_angle: str = "" | |
background: str = "" | |
characters_in_scene: List[str] = field(default_factory=list) | |
generated_image_url: str = "" # URL of generated image | |
class EpisodeStoryboard: | |
"""Complete storyboard for one episode""" | |
episode_number: int | |
title: str | |
panels: List[StoryboardPanel] = field(default_factory=list) | |
total_panels: int = 30 | |
cliffhanger: str = "" | |
# --- Core logic classes --- | |
class WebtoonTracker: | |
"""Webtoon narrative and storyboard tracker""" | |
def __init__(self): | |
self.story_bible = WebtoonBible() | |
self.episode_storyboards: Dict[int, EpisodeStoryboard] = {} | |
self.episodes: Dict[int, str] = {} | |
self.total_panel_count = 0 | |
self.character_profiles: Dict[str, CharacterProfile] = {} | |
def set_genre(self, genre: str): | |
"""Set the webtoon genre""" | |
self.story_bible.genre = genre | |
self.story_bible.genre_elements = GENRE_ELEMENTS.get(genre, {}) | |
def add_character(self, character: CharacterProfile): | |
"""Add character with celebrity lookalike""" | |
self.character_profiles[character.name] = character | |
self.story_bible.characters[character.name] = character | |
def add_storyboard(self, episode_num: int, storyboard: EpisodeStoryboard): | |
"""Add episode storyboard""" | |
self.episode_storyboards[episode_num] = storyboard | |
self.total_panel_count += len(storyboard.panels) | |
class WebtoonDatabase: | |
"""Database management for webtoon system""" | |
def init_db(): | |
with sqlite3.connect(DB_PATH) as conn: | |
conn.execute("PRAGMA journal_mode=WAL") | |
cursor = conn.cursor() | |
# Sessions table with genre | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS sessions ( | |
session_id TEXT PRIMARY KEY, | |
user_query TEXT NOT NULL, | |
genre TEXT NOT NULL, | |
language TEXT NOT NULL, | |
title TEXT, | |
created_at TEXT DEFAULT (datetime('now')), | |
updated_at TEXT DEFAULT (datetime('now')), | |
status TEXT DEFAULT 'active', | |
current_episode INTEGER DEFAULT 0, | |
total_episodes INTEGER DEFAULT 40, | |
planning_doc TEXT, | |
story_bible TEXT, | |
visual_style TEXT, | |
character_profiles TEXT | |
) | |
''') | |
# Storyboards table | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS storyboards ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
episode_number INTEGER NOT NULL, | |
title TEXT, | |
storyboard_data TEXT, | |
panel_count INTEGER DEFAULT 30, | |
status TEXT DEFAULT 'pending', | |
created_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES sessions(session_id), | |
UNIQUE(session_id, episode_number) | |
) | |
''') | |
# Panels table with image data | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS panels ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
episode_number INTEGER NOT NULL, | |
panel_number INTEGER NOT NULL, | |
scene_type TEXT, | |
image_prompt TEXT, | |
dialogue TEXT, | |
narration TEXT, | |
sound_effects TEXT, | |
generated_image TEXT, | |
created_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES sessions(session_id) | |
) | |
''') | |
conn.commit() | |
def get_db(): | |
with db_lock: | |
conn = sqlite3.connect(DB_PATH, timeout=30.0) | |
conn.row_factory = sqlite3.Row | |
try: | |
yield conn | |
finally: | |
conn.close() | |
def create_session(user_query: str, genre: str, language: str) -> str: | |
session_id = hashlib.md5(f"{user_query}{genre}{datetime.now()}".encode()).hexdigest() | |
with WebtoonDatabase.get_db() as conn: | |
conn.cursor().execute( | |
'''INSERT INTO sessions (session_id, user_query, genre, language) | |
VALUES (?, ?, ?, ?)''', | |
(session_id, user_query, genre, language) | |
) | |
conn.commit() | |
return session_id | |
def save_storyboard(session_id: str, episode_num: int, storyboard: EpisodeStoryboard): | |
with WebtoonDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
# Save storyboard | |
cursor.execute(''' | |
INSERT INTO storyboards (session_id, episode_number, title, | |
storyboard_data, panel_count, status) | |
VALUES (?, ?, ?, ?, ?, 'complete') | |
ON CONFLICT(session_id, episode_number) | |
DO UPDATE SET title=?, storyboard_data=?, panel_count=?, status='complete' | |
''', (session_id, episode_num, storyboard.title, | |
json.dumps(asdict(storyboard)), len(storyboard.panels), | |
storyboard.title, json.dumps(asdict(storyboard)), len(storyboard.panels))) | |
# Save individual panels | |
for panel in storyboard.panels: | |
cursor.execute(''' | |
INSERT INTO panels (session_id, episode_number, panel_number, | |
scene_type, image_prompt, dialogue, narration, sound_effects) | |
VALUES (?, ?, ?, ?, ?, ?, ?, ?) | |
''', (session_id, episode_num, panel.panel_number, | |
panel.scene_type, panel.image_prompt, | |
json.dumps(panel.dialogue), panel.narration, | |
json.dumps(panel.sound_effects))) | |
conn.commit() | |
def save_character_profiles(session_id: str, profiles: Dict[str, CharacterProfile]): | |
with WebtoonDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
profiles_json = json.dumps({name: asdict(profile) for name, profile in profiles.items()}) | |
cursor.execute( | |
"UPDATE sessions SET character_profiles = ? WHERE session_id = ?", | |
(profiles_json, session_id) | |
) | |
conn.commit() | |
# --- Image Generation --- | |
class ImageGenerator: | |
"""Handle image generation using Replicate API with multi-threading""" | |
def __init__(self): | |
self.generation_lock = Lock() | |
self.active_generations = {} | |
def generate_image(self, prompt: str, panel_id: str, session_id: str, progress_callback=None) -> Dict[str, Any]: | |
"""Generate image using Replicate API with progress callback""" | |
try: | |
if not REPLICATE_API_TOKEN: | |
logger.warning("No Replicate API token, returning placeholder") | |
return {"panel_id": panel_id, "status": "error", "message": "No API token"} | |
logger.info(f"Generating image for panel {panel_id}") | |
if progress_callback: | |
progress_callback(f"ํจ๋ {panel_id.split('_panel')[1]} ์ด๋ฏธ์ง ์์ฑ ์ค...") | |
# Run the model | |
input_params = { | |
"prompt": prompt, | |
"num_inference_steps": 25, | |
"guidance_scale": 7.5 | |
} | |
output = replicate.run( | |
"stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", | |
input=input_params | |
) | |
# Get the image URL | |
if output and len(output) > 0: | |
image_url = output[0] if isinstance(output[0], str) else str(output[0]) | |
# Cache the image | |
cache_key = f"{session_id}_{panel_id}" | |
generated_images_cache[cache_key] = image_url | |
logger.info(f"Successfully generated image for panel {panel_id}") | |
return { | |
"panel_id": panel_id, | |
"status": "success", | |
"image_url": image_url, | |
"prompt": prompt | |
} | |
return {"panel_id": panel_id, "status": "error", "message": "No output from model"} | |
except Exception as e: | |
logger.error(f"Image generation error for panel {panel_id}: {e}") | |
return {"panel_id": panel_id, "status": "error", "message": str(e)} | |
def generate_multiple_images(self, panel_prompts: List[Dict], session_id: str, | |
max_workers: int = 3, progress_callback=None) -> List[Dict]: | |
"""Generate multiple images in parallel with progress tracking""" | |
results = [] | |
total = len(panel_prompts) | |
completed = 0 | |
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: | |
# Submit all tasks | |
future_to_panel = {} | |
for i, panel_data in enumerate(panel_prompts): | |
panel_id = panel_data['panel_id'] | |
prompt = panel_data['prompt'] | |
future = executor.submit( | |
self.generate_image, | |
prompt, | |
panel_id, | |
session_id, | |
None # Individual progress not tracked in parallel | |
) | |
future_to_panel[future] = panel_data | |
# Collect results as they complete | |
for future in concurrent.futures.as_completed(future_to_panel): | |
panel_data = future_to_panel[future] | |
try: | |
result = future.result(timeout=60) | |
results.append(result) | |
completed += 1 | |
if progress_callback: | |
progress_callback(f"์งํ์ค: {completed}/{total} ํจ๋ ์๋ฃ") | |
except concurrent.futures.TimeoutError: | |
results.append({ | |
"panel_id": panel_data['panel_id'], | |
"status": "error", | |
"message": "Generation timeout" | |
}) | |
completed += 1 | |
except Exception as e: | |
results.append({ | |
"panel_id": panel_data['panel_id'], | |
"status": "error", | |
"message": str(e) | |
}) | |
completed += 1 | |
# Sort results by panel_id to maintain order | |
results.sort(key=lambda x: int(x['panel_id'].split('_panel')[1])) | |
return results | |
# --- LLM Integration --- | |
class WebtoonSystem: | |
"""Webtoon planning and storyboard generation system""" | |
def __init__(self): | |
self.api_key = FIREWORKS_API_KEY | |
self.api_url = API_URL | |
self.model_id = MODEL_ID | |
self.tracker = WebtoonTracker() | |
self.current_session_id = None | |
self.image_generator = ImageGenerator() | |
WebtoonDatabase.init_db() | |
def create_headers(self): | |
return { | |
"Accept": "application/json", | |
"Content-Type": "application/json", | |
"Authorization": f"Bearer {self.api_key}" | |
} | |
def assign_celebrity_lookalikes(self, characters: List[Dict]) -> Dict[str, CharacterProfile]: | |
"""Assign celebrity lookalikes to characters""" | |
profiles = {} | |
used_celebrities = [] | |
for char in characters: | |
gender = char.get('gender', 'male') | |
available_celebrities = [c for c in CELEBRITY_FACES.get(gender, []) | |
if c not in used_celebrities] | |
if not available_celebrities: | |
available_celebrities = CELEBRITY_FACES.get(gender, []) | |
celebrity = random.choice(available_celebrities) | |
used_celebrities.append(celebrity) | |
profile = CharacterProfile( | |
name=char.get('name', ''), | |
role=char.get('role', ''), | |
personality=char.get('personality', ''), | |
appearance=char.get('appearance', ''), | |
celebrity_lookalike=celebrity, | |
gender=gender | |
) | |
profiles[profile.name] = profile | |
self.tracker.add_character(profile) | |
return profiles | |
# --- Prompt generation functions --- | |
def create_planning_prompt(self, query: str, genre: str, language: str) -> str: | |
"""Create initial planning prompt for webtoon with character profiles""" | |
genre_info = GENRE_ELEMENTS.get(genre, {}) | |
lang_prompts = { | |
"Korean": f"""ํ๊ตญ ์นํฐ ์์ฅ์ ๊ฒจ๋ฅํ {genre} ์ฅ๋ฅด ์นํฐ์ ๊ธฐํํ์ธ์. | |
**[ํต์ฌ ์คํ ๋ฆฌ ์ค์ - ๋ฐ๋์ ์ด ๋ด์ฉ์ ์ค์ฌ์ผ๋ก ์ ๊ฐํ์ธ์]** | |
{query} | |
**์ฅ๋ฅด:** {genre} | |
**๋ชฉํ:** 40ํ ์๊ฒฐ ์นํฐ | |
โ ๏ธ **์ค์**: | |
1. ์์ ์ ์๋ ์คํ ๋ฆฌ ์ค์ ์ ๋ฐ๋์ ๊ธฐ๋ฐ์ผ๋ก ํ์ฌ ํ๋กฏ์ ๊ตฌ์ฑํ์ธ์. | |
2. ๊ฐ ์บ๋ฆญํฐ์ ์ฑ๋ณ(gender)์ ๋ช ํํ ์ง์ ํ์ธ์ (male/female). | |
3. ๋ฐ๋์ 40ํ ์ ์ฒด ๊ตฌ์ฑ์์ ๋ชจ๋ ์์ฑํ์ธ์. | |
**์ฅ๋ฅด ํ์ ์์:** | |
- ํต์ฌ ์์: {', '.join(genre_info.get('key_elements', []))} | |
- ๋น์ฃผ์ผ ์คํ์ผ: {', '.join(genre_info.get('visual_styles', []))} | |
- ์ฃผ์ ์ฌ: {', '.join(genre_info.get('typical_scenes', []))} | |
๋ค์ ํ์์ผ๋ก ์์ฑํ์ธ์: | |
๐ **์ํ ์ ๋ชฉ:** [์ํฉํธ ์๋ ์ ๋ชฉ] | |
๐จ **๋น์ฃผ์ผ ์ปจ์ :** | |
- ๊ทธ๋ฆผ์ฒด: [์ํ์ ์ด์ธ๋ฆฌ๋ ๊ทธ๋ฆผ์ฒด] | |
- ์๊ฐ: [์ฃผ์ ์์ ํค] | |
- ์บ๋ฆญํฐ ๋์์ธ ํน์ง: [์ฃผ์ธ๊ณต๋ค์ ๋น์ฃผ์ผ ํน์ง] | |
๐ฅ **์ฃผ์ ์บ๋ฆญํฐ:** (๊ฐ ์บ๋ฆญํฐ๋ง๋ค ์ฑ๋ณ์ ๋ฐ๋์ ๋ช ์!) | |
- ์ฃผ์ธ๊ณต: [์ด๋ฆ] - ์ฑ๋ณ: [male/female] - [์ธ๋ชจ ํน์ง, ์ฑ๊ฒฉ, ๋ชฉํ] | |
- ์บ๋ฆญํฐ2: [์ด๋ฆ] - ์ฑ๋ณ: [male/female] - [์ญํ , ํน์ง] | |
- ์บ๋ฆญํฐ3: [์ด๋ฆ] - ์ฑ๋ณ: [male/female] - [์ญํ , ํน์ง] | |
๐ **์๋์์ค:** | |
[3-4์ค๋ก ์ ์ฒด ์คํ ๋ฆฌ ์์ฝ] | |
๐ **40ํ ์ ์ฒด ๊ตฌ์ฑ์:** (๋ฐ๋์ 40ํ ๋ชจ๋ ์์ฑ!) | |
๊ฐ ํ๋ณ๋ก ํต์ฌ ์ฌ๊ฑด๊ณผ ํด๋ฆฌํํ์ด๋ฅผ ํฌํจํ์ฌ ์์ฑํ์ธ์. | |
1ํ: [์ ๋ชฉ] - [ํต์ฌ ์ฌ๊ฑด] - ํด๋ฆฌํํ์ด: [์ถฉ๊ฒฉ์ ์ธ ๋ง๋ฌด๋ฆฌ] | |
2ํ: [์ ๋ชฉ] - [ํต์ฌ ์ฌ๊ฑด] - ํด๋ฆฌํํ์ด: [์ถฉ๊ฒฉ์ ์ธ ๋ง๋ฌด๋ฆฌ] | |
3ํ: [์ ๋ชฉ] - [ํต์ฌ ์ฌ๊ฑด] - ํด๋ฆฌํํ์ด: [์ถฉ๊ฒฉ์ ์ธ ๋ง๋ฌด๋ฆฌ] | |
... | |
(์ค๊ฐ ์๋ตํ์ง ๋ง๊ณ ๋ชจ๋ ํ๋ฅผ ์์ฑ) | |
... | |
38ํ: [์ ๋ชฉ] - [ํต์ฌ ์ฌ๊ฑด] - ํด๋ฆฌํํ์ด: [์ถฉ๊ฒฉ์ ์ธ ๋ง๋ฌด๋ฆฌ] | |
39ํ: [์ ๋ชฉ] - [ํต์ฌ ์ฌ๊ฑด] - ํด๋ฆฌํํ์ด: [์ถฉ๊ฒฉ์ ์ธ ๋ง๋ฌด๋ฆฌ] | |
40ํ: [์ ๋ชฉ] - [ํต์ฌ ์ฌ๊ฑด] - [๋๋จ์์ ๋ง๋ฌด๋ฆฌ] | |
โ ๏ธ ์ ๋ ์๋ตํ์ง ๋ง๊ณ 40ํ ๋ชจ๋ ์์ฑํ์ธ์!""", | |
"English": f"""Plan a Korean-style webtoon for {genre} genre. | |
**[Core Story Setting - MUST base the story on this]** | |
{query} | |
**Genre:** {genre} | |
**Goal:** 40 episodes webtoon | |
โ ๏ธ **IMPORTANT**: | |
1. You MUST base the plot on the story setting provided above. | |
2. Clearly specify each character's gender (male/female). | |
3. MUST write all 40 episodes structure. | |
**Genre Requirements:** | |
- Key elements: {', '.join(genre_info.get('key_elements', []))} | |
- Visual styles: {', '.join(genre_info.get('visual_styles', []))} | |
- Typical scenes: {', '.join(genre_info.get('typical_scenes', []))} | |
Format as follows: | |
๐ **Title:** [Impactful title] | |
๐จ **Visual Concept:** | |
- Art style: [Suitable art style] | |
- Color tone: [Main color palette] | |
- Character design: [Visual characteristics] | |
๐ฅ **Main Characters:** (Must specify gender for each!) | |
- Protagonist: [Name] - Gender: [male/female] - [Appearance, personality, goal] | |
- Character2: [Name] - Gender: [male/female] - [Role, traits] | |
- Character3: [Name] - Gender: [male/female] - [Role, traits] | |
๐ **Synopsis:** | |
[3-4 line story summary] | |
๐ **40 Episode Structure:** (MUST write all 40 episodes!) | |
Include key events and cliffhangers for each episode. | |
Episode 1: [Title] - [Key event] - Cliffhanger: [Shocking ending] | |
Episode 2: [Title] - [Key event] - Cliffhanger: [Shocking ending] | |
... | |
(Don't skip, write all episodes) | |
... | |
Episode 40: [Title] - [Key event] - [Grand finale] | |
โ ๏ธ Don't abbreviate, write all 40 episodes!""" | |
} | |
return lang_prompts.get(language, lang_prompts["Korean"]) | |
def create_storyboard_prompt(self, episode_num: int, plot_outline: str, | |
genre: str, language: str, character_profiles: Dict[str, CharacterProfile]) -> str: | |
"""Create prompt for episode storyboard with character descriptions""" | |
genre_info = GENRE_ELEMENTS.get(genre, {}) | |
# Create character description string | |
char_descriptions = "\n".join([ | |
f"- {name}: {profile.celebrity_lookalike} ๋ฎ์ ์ผ๊ตด์ {profile.gender}" | |
for name, profile in character_profiles.items() | |
]) | |
lang_prompts = { | |
"Korean": f"""์นํฐ {episode_num}ํ ์คํ ๋ฆฌ๋ณด๋๋ฅผ 30๊ฐ ํจ๋๋ก ์์ฑํ์ธ์. | |
**์ฅ๋ฅด:** {genre} | |
**1ํ ๋ด์ฉ:** {self._extract_episode_plan(plot_outline, episode_num)} | |
**์บ๋ฆญํฐ ์ผ๊ตด ์ค์ :** | |
{char_descriptions} | |
โ ๏ธ **์ค์**: | |
1. ๋ฐ๋์ 30๊ฐ ํจ๋์ ๋ชจ๋ ์์ฑํ์ธ์! | |
2. ์บ๋ฆญํฐ๊ฐ ๋ฑ์ฅํ ๋๋ง๋ค "์บ๋ฆญํฐ์ด๋ฆ(์ ๋ช ์ธ ๋ฎ์ ์ผ๊ตด์ ์ฑ๋ณ)" ํ์์ผ๋ก ์์ฑํ์ธ์! | |
์์: "ํ๊ธธ๋(ํฐ ํฌ๋ฃจ์ฆ ๋ฎ์ ์ผ๊ตด์ ๋จ์)์ด ๊ฑฐ๋ฆฌ๋ฅผ ๊ฑท๊ณ ์๋ค" | |
**ํจ๋ ๊ตฌ์ฑ ์ง์นจ:** | |
- ์ด 30๊ฐ ํจ๋๋ก ๊ตฌ์ฑ | |
- ๋ค์ํ ์ท ์ฌ์ด์ฆ ํ์ฉ | |
- ์ฅ๋ฅด ํน์ฑ์ ๋ง๋ ์ฐ์ถ: {', '.join(genre_info.get('panel_types', []))} | |
**๊ฐ ํจ๋๋ณ๋ก ๋ค์์ ํฌํจํ์ฌ ์์ฑ:** | |
ํจ๋ 1: | |
- ์ท ํ์ : [establishing/wide/medium/close_up ๋ฑ] | |
- ์ด๋ฏธ์ง ํ๋กฌํํธ: [์บ๋ฆญํฐ ์ค๋ช ํฌํจํ ์์ธํ ํ๊ธ ์ด๋ฏธ์ง ์์ฑ ํ๋กฌํํธ] | |
- ๋์ฌ: [์บ๋ฆญํฐ ๋์ฌ๊ฐ ์๋ค๋ฉด] | |
- ๋๋ ์ด์ : [ํด์ค์ด ์๋ค๋ฉด] | |
- ํจ๊ณผ์: [ํ์ํ ํจ๊ณผ์] | |
- ๋ฐฐ๊ฒฝ: [๋ฐฐ๊ฒฝ ์ค๋ช ] | |
ํจ๋ 2: | |
(์์ ๊ฐ์ ํ์) | |
...์ด๋ฐ ์์ผ๋ก 30๊ฐ ํจ๋ ๋ชจ๋ ์์ฑ | |
ํจ๋ 30: | |
(์์ ๊ฐ์ ํ์) | |
โ ๏ธ ๋ฐ๋์ 30๊ฐ ํจ๋์ ๋ชจ๋ ์์ฑํ์ธ์. ์๋ตํ์ง ๋ง์ธ์!""", | |
"English": f"""Create Episode {episode_num} storyboard with 30 panels. | |
**Genre:** {genre} | |
**Episode content:** {self._extract_episode_plan(plot_outline, episode_num)} | |
**Character Face Settings:** | |
{char_descriptions} | |
โ ๏ธ **IMPORTANT**: | |
1. Must write all 30 panels! | |
2. Always describe characters as "CharacterName (celebrity lookalike face gender)"! | |
Example: "John (Tom Cruise lookalike male) walking down the street" | |
**Panel composition guidelines:** | |
- Total 30 panels | |
- Various shot sizes | |
- Genre-appropriate directing: {', '.join(genre_info.get('panel_types', []))} | |
**For each panel include:** | |
Panel 1: | |
- Shot type: [establishing/wide/medium/close_up etc] | |
- Image prompt: [Detailed prompt with character descriptions] | |
- Dialogue: [Character dialogue if any] | |
- Narration: [Narration if any] | |
- Sound effects: [Required sound effects] | |
- Background: [Background description] | |
Panel 2: | |
(Same format) | |
...continue for all 30 panels | |
Panel 30: | |
(Same format) | |
โ ๏ธ Must write all 30 panels. Don't skip any!""" | |
} | |
return lang_prompts.get(language, lang_prompts["Korean"]) | |
def _extract_episode_plan(self, plot_outline: str, episode_num: int) -> str: | |
"""Extract specific episode plan from outline""" | |
lines = plot_outline.split('\n') | |
episode_section = [] | |
capturing = False | |
patterns = [ | |
f"{episode_num}ํ:", f"Episode {episode_num}:", | |
f"์ {episode_num}ํ:", f"EP{episode_num}:", | |
f"{episode_num}.", f"[{episode_num}]" | |
] | |
next_patterns = [ | |
f"{episode_num+1}ํ:", f"Episode {episode_num+1}:", | |
f"์ {episode_num+1}ํ:", f"EP{episode_num+1}:", | |
f"{episode_num+1}.", f"[{episode_num+1}]" | |
] | |
for line in lines: | |
if any(pattern in line for pattern in patterns): | |
capturing = True | |
episode_section.append(line) | |
elif capturing and any(pattern in line for pattern in next_patterns): | |
break | |
elif capturing: | |
episode_section.append(line) | |
if episode_section: | |
return '\n'.join(episode_section) | |
return f"์ํผ์๋ {episode_num} ๋ด์ฉ์ ํ๋กฏ์์ ์ฐธ์กฐํ์ฌ ์์ฑํ์ธ์." | |
def parse_characters_from_planning(self, planning_doc: str) -> List[Dict]: | |
"""Parse character information from planning document""" | |
characters = [] | |
lines = planning_doc.split('\n') | |
in_character_section = False | |
current_char = {} | |
for line in lines: | |
if '์ฃผ์ ์บ๋ฆญํฐ' in line or 'Main Characters' in line: | |
in_character_section = True | |
continue | |
elif in_character_section and ('์๋์์ค' in line or 'Synopsis' in line): | |
if current_char: | |
characters.append(current_char) | |
break | |
elif in_character_section and line.strip(): | |
# Parse character line | |
if '์ฑ๋ณ:' in line or 'Gender:' in line: | |
if current_char: | |
characters.append(current_char) | |
parts = line.split('-') | |
if len(parts) >= 2: | |
name = parts[0].strip().replace('์ฃผ์ธ๊ณต:', '').replace('์บ๋ฆญํฐ', '').strip() | |
# Extract gender | |
gender = 'male' # default | |
if 'female' in line.lower() or '์ฌ' in line: | |
gender = 'female' | |
elif 'male' in line.lower() or '๋จ' in line: | |
gender = 'male' | |
current_char = { | |
'name': name, | |
'gender': gender, | |
'role': parts[1].strip() if len(parts) > 1 else '', | |
'personality': parts[2].strip() if len(parts) > 2 else '', | |
'appearance': parts[3].strip() if len(parts) > 3 else '' | |
} | |
if current_char and current_char not in characters: | |
characters.append(current_char) | |
# Ensure at least 3 characters | |
while len(characters) < 3: | |
characters.append({ | |
'name': f'์บ๋ฆญํฐ{len(characters)+1}', | |
'gender': 'male' if len(characters) % 2 == 0 else 'female', | |
'role': '์กฐ์ฐ', | |
'personality': '์ผ๋ฐ์ ', | |
'appearance': 'ํ๋ฒํ ์ธ๋ชจ' | |
}) | |
return characters | |
# --- LLM call functions --- | |
def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str: | |
full_content = "" | |
for chunk in self.call_llm_streaming(messages, role, language): | |
full_content += chunk | |
if full_content.startswith("โ"): | |
raise Exception(f"LLM Call Failed: {full_content}") | |
return full_content | |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, | |
language: str) -> Generator[str, None, None]: | |
try: | |
system_prompts = self.get_system_prompts(language) | |
full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] | |
max_tokens = 15000 if role == "storyboarder" else 10000 | |
payload = { | |
"model": self.model_id, | |
"messages": full_messages, | |
"max_tokens": max_tokens, | |
"temperature": 0.7, | |
"top_p": 1, | |
"top_k": 40, | |
"presence_penalty": 0, | |
"frequency_penalty": 0, | |
"stream": True | |
} | |
response = requests.post( | |
self.api_url, | |
headers=self.create_headers(), | |
json=payload, | |
stream=True, | |
timeout=180 | |
) | |
if response.status_code != 200: | |
yield f"โ API Error (Status Code: {response.status_code})" | |
return | |
buffer = "" | |
for line in response.iter_lines(): | |
if not line: | |
continue | |
try: | |
line_str = line.decode('utf-8').strip() | |
if not line_str.startswith("data: "): | |
continue | |
data_str = line_str[6:] | |
if data_str == "[DONE]": | |
break | |
data = json.loads(data_str) | |
choices = data.get("choices", []) | |
if choices and choices[0].get("delta", {}).get("content"): | |
content = choices[0]["delta"]["content"] | |
buffer += content | |
if len(buffer) >= 50 or '\n' in buffer: | |
yield buffer | |
buffer = "" | |
time.sleep(0.01) | |
except Exception as e: | |
logger.error(f"Chunk processing error: {str(e)}") | |
continue | |
if buffer: | |
yield buffer | |
except Exception as e: | |
logger.error(f"Streaming error: {type(e).__name__}: {str(e)}") | |
yield f"โ Error occurred: {str(e)}" | |
def get_system_prompts(self, language: str) -> Dict[str, str]: | |
"""System prompts for webtoon roles""" | |
base_prompts = { | |
"Korean": { | |
"planner": """๋น์ ์ ํ๊ตญ ์นํฐ ์์ฅ์ ์๋ฒฝํ ์ดํดํ๋ ์นํฐ ๊ธฐํ์์ ๋๋ค. | |
๋ ์๋ฅผ ์ฌ๋ก์ก๋ ์คํ ๋ฆฌ์ ๋น์ฃผ์ผ ์ฐ์ถ์ ๊ธฐํํฉ๋๋ค. | |
40ํ ์๊ฒฐ ๊ตฌ์กฐ๋ก ์๋ฒฝํ ๊ธฐ์น์ ๊ฒฐ์ ์ค๊ณํฉ๋๋ค. | |
๊ฐ ํ๋ง๋ค ๊ฐ๋ ฅํ ํด๋ฆฌํํ์ด๋ก ๋ค์ ํ๋ฅผ ๊ธฐ๋ํ๊ฒ ๋ง๋ญ๋๋ค. | |
์บ๋ฆญํฐ์ ์ฑ๋ณ์ ๋ช ํํ ์ง์ ํฉ๋๋ค. | |
โ ๏ธ ๊ฐ์ฅ ์ค์ํ ์์น: | |
1. ์ฌ์ฉ์๊ฐ ์ ๊ณตํ ์คํ ๋ฆฌ ์ค์ ์ ์ ๋์ ์ผ๋ก ์ฐ์ ์ํ๊ณ , ์ด๋ฅผ ์ค์ฌ์ผ๋ก ๋ชจ๋ ํ๋กฏ์ ๊ตฌ์ฑํฉ๋๋ค. | |
2. ๋ฐ๋์ 40ํ ์ ์ฒด ๊ตฌ์ฑ์์ ๋ชจ๋ ์์ฑํฉ๋๋ค. ์๋ตํ์ง ์์ต๋๋ค.""", | |
"storyboarder": """๋น์ ์ ์นํฐ ์คํ ๋ฆฌ๋ณด๋ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
30๊ฐ ํจ๋๋ก ํ ํ๋ฅผ ์๋ฒฝํ๊ฒ ๊ตฌ์ฑํฉ๋๋ค. | |
์ธ๋ก ์คํฌ๋กค์ ์ต์ ํ๋ ์ฐ์ถ์ ํฉ๋๋ค. | |
๊ฐ ํจ๋๋ง๋ค ์บ๋ฆญํฐ์ ์ ๋ช ์ธ ๋ฎ์๊ผด ์ค์ ์ ํฌํจํ ์์ธํ ์ด๋ฏธ์ง ํ๋กฌํํธ๋ฅผ ์์ฑํฉ๋๋ค. | |
โ ๏ธ ๊ฐ์ฅ ์ค์ํ ์์น: | |
1. ๋ฐ๋์ 30๊ฐ ํจ๋์ ๋ชจ๋ ์์ฑํฉ๋๋ค. | |
2. ์บ๋ฆญํฐ๊ฐ ๋ฑ์ฅํ ๋๋ง๋ค "์บ๋ฆญํฐ์ด๋ฆ(์ ๋ช ์ธ ๋ฎ์ ์ผ๊ตด์ ์ฑ๋ณ)" ํ์์ผ๋ก ์์ฑํฉ๋๋ค.""" | |
}, | |
"English": { | |
"planner": """You perfectly understand the Korean webtoon market. | |
Design stories and visual direction that captivate readers. | |
Create perfect story structure in 40 episodes. | |
Make readers anticipate next episode with strong cliffhangers. | |
Clearly specify character genders. | |
โ ๏ธ Most important principles: | |
1. Absolutely prioritize the user's story setting and build all plots around it. | |
2. Must write all 40 episodes structure. Don't skip.""", | |
"storyboarder": """You are a webtoon storyboard specialist. | |
Perfectly compose one episode with 30 panels. | |
Write detailed image prompts including celebrity lookalike descriptions. | |
โ ๏ธ Most important principles: | |
1. Must write all 30 panels. | |
2. Always describe characters as "CharacterName (celebrity lookalike face gender)".""" | |
} | |
} | |
return base_prompts.get(language, base_prompts["Korean"]) | |
# --- Main process --- | |
def process_webtoon_stream(self, query: str, genre: str, language: str, | |
session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str, Dict], None, None]: | |
"""Webtoon planning and storyboard generation process""" | |
try: | |
if not session_id: | |
self.current_session_id = WebtoonDatabase.create_session(query, genre, language) | |
self.tracker.set_genre(genre) | |
logger.info(f"Created new session: {self.current_session_id}") | |
self.original_query = query | |
else: | |
self.current_session_id = session_id | |
# Phase 1: Generate planning document (40ํ ๊ตฌ์ฑ ํฌํจ) | |
yield "๐ฌ ์นํฐ ๊ธฐํ์ ์์ฑ ์ค... (40ํ ์ ์ฒด ๊ตฌ์ฑ ํฌํจ)", "", f"์ฅ๋ฅด: {genre}", self.current_session_id, {} | |
planning_prompt = self.create_planning_prompt(query, genre, language) | |
planning_doc = self.call_llm_sync( | |
[{"role": "user", "content": planning_prompt}], | |
"planner", language | |
) | |
self.planning_doc = planning_doc | |
# Parse characters and assign celebrity lookalikes | |
characters = self.parse_characters_from_planning(planning_doc) | |
character_profiles = self.assign_celebrity_lookalikes(characters) | |
# Save character profiles | |
WebtoonDatabase.save_character_profiles(self.current_session_id, character_profiles) | |
yield "โ ๊ธฐํ์ ์์ฑ! (40ํ ๊ตฌ์ฑ ์๋ฃ)", planning_doc, "40ํ ๊ตฌ์ฑ ์๋ฃ", self.current_session_id, character_profiles | |
# Phase 2: Generate Episode 1 Storyboard | |
yield "๐จ 1ํ ์คํ ๋ฆฌ๋ณด๋ ์์ฑ ์ค... (30๊ฐ ํจ๋)", planning_doc, "30๊ฐ ํจ๋ ๊ตฌ์ฑ ์ค", self.current_session_id, character_profiles | |
storyboard_prompt = self.create_storyboard_prompt(1, planning_doc, genre, language, character_profiles) | |
storyboard_content = self.call_llm_sync( | |
[{"role": "user", "content": storyboard_prompt}], | |
"storyboarder", language | |
) | |
# Parse storyboard into structured format | |
storyboard = self.parse_storyboard(storyboard_content, 1, character_profiles) | |
# Save to database | |
WebtoonDatabase.save_storyboard(self.current_session_id, 1, storyboard) | |
yield "๐ ์์ฑ! (๊ธฐํ์ + 1ํ ์คํ ๋ฆฌ๋ณด๋)", planning_doc, storyboard_content, self.current_session_id, character_profiles | |
except Exception as e: | |
logger.error(f"Webtoon generation error: {e}", exc_info=True) | |
yield f"โ ์ค๋ฅ ๋ฐ์: {e}", "", "", self.current_session_id, {} | |
def parse_storyboard(self, content: str, episode_num: int, character_profiles: Dict[str, CharacterProfile]) -> EpisodeStoryboard: | |
"""Parse storyboard text into structured format with unique panel IDs""" | |
storyboard = EpisodeStoryboard(episode_number=episode_num, title=f"{episode_num}ํ") | |
panels = [] | |
current_panel = None | |
panel_number = 0 | |
lines = content.split('\n') | |
for line in lines: | |
if 'ํจ๋' in line or 'Panel' in line: | |
if current_panel: | |
panels.append(current_panel) | |
panel_number += 1 | |
# Create unique panel ID | |
panel_id = f"ep{episode_num}_panel{panel_number}" | |
current_panel = StoryboardPanel( | |
panel_number=panel_number, | |
scene_type="medium", | |
image_prompt="", | |
panel_id=panel_id # Add unique panel_id | |
) | |
elif current_panel: | |
if '์ด๋ฏธ์ง ํ๋กฌํํธ:' in line or 'Image prompt:' in line: | |
current_panel.image_prompt = line.split(':', 1)[1].strip() | |
elif '๋์ฌ:' in line or 'Dialogue:' in line: | |
dialogue = line.split(':', 1)[1].strip() | |
if dialogue: | |
current_panel.dialogue.append(dialogue) | |
elif '๋๋ ์ด์ :' in line or 'Narration:' in line: | |
current_panel.narration = line.split(':', 1)[1].strip() | |
elif 'ํจ๊ณผ์:' in line or 'Sound effects:' in line: | |
effects = line.split(':', 1)[1].strip() | |
if effects: | |
current_panel.sound_effects.append(effects) | |
elif '์ท ํ์ :' in line or 'Shot type:' in line: | |
current_panel.scene_type = line.split(':', 1)[1].strip() | |
if current_panel: | |
panels.append(current_panel) | |
storyboard.panels = panels[:30] | |
return storyboard | |
# --- Format storyboard for display --- | |
def format_storyboard_for_display(storyboard_content: str, character_profiles: Dict[str, CharacterProfile], session_id: str) -> str: | |
"""Format storyboard content for panel display""" | |
formatted_panels = [] | |
panel_texts = [] | |
current_panel = {} | |
lines = storyboard_content.split('\n') | |
for line in lines: | |
if 'ํจ๋' in line or 'Panel' in line: | |
if current_panel: | |
panel_texts.append(current_panel) | |
current_panel = {'number': len(panel_texts) + 1} | |
elif '์ด๋ฏธ์ง ํ๋กฌํํธ:' in line or 'Image prompt:' in line: | |
current_panel['prompt'] = line.split(':', 1)[1].strip() | |
elif '๋์ฌ:' in line or 'Dialogue:' in line: | |
current_panel['dialogue'] = line.split(':', 1)[1].strip() | |
elif '๋๋ ์ด์ :' in line or 'Narration:' in line: | |
current_panel['narration'] = line.split(':', 1)[1].strip() | |
elif 'ํจ๊ณผ์:' in line or 'Sound effects:' in line: | |
current_panel['effects'] = line.split(':', 1)[1].strip() | |
elif '์ท ํ์ :' in line or 'Shot type:' in line: | |
current_panel['shot'] = line.split(':', 1)[1].strip() | |
if current_panel: | |
panel_texts.append(current_panel) | |
# Format each panel for display | |
for panel in panel_texts[:30]: | |
panel_num = panel.get('number', '?') | |
panel_html = f""" | |
<div style="border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; border-radius: 8px; background: #f9f9f9;"> | |
<h4 style="color: #764ba2; margin-top: 0;">๐ฌ ํจ๋ {panel_num}</h4> | |
<p><strong>์ท ํ์ :</strong> {panel.get('shot', 'N/A')}</p> | |
<p><strong>์ด๋ฏธ์ง ํ๋กฌํํธ:</strong><br> | |
<code style="background: #fff; padding: 8px; display: block; border-radius: 4px; font-size: 12px;"> | |
{panel.get('prompt', 'N/A')} | |
</code></p> | |
{f"<p><strong>๋์ฌ:</strong> {panel.get('dialogue')}</p>" if panel.get('dialogue') else ""} | |
{f"<p><strong>๋๋ ์ด์ :</strong> {panel.get('narration')}</p>" if panel.get('narration') else ""} | |
{f"<p><strong>ํจ๊ณผ์:</strong> {panel.get('effects')}</p>" if panel.get('effects') else ""} | |
</div> | |
""" | |
formatted_panels.append(panel_html) | |
return ''.join(formatted_panels) | |
# --- Export functions --- | |
def export_to_txt(planning_doc: str, storyboard: str, genre: str, title: str = "") -> str: | |
"""Export webtoon planning and storyboard to TXT format""" | |
content = f"{'=' * 50}\n" | |
content += f"{title if title else genre + ' ์นํฐ'}\n" | |
content += f"{'=' * 50}\n\n" | |
content += f"์ฅ๋ฅด: {genre}\n" | |
content += f"์ด 40ํ ๊ธฐํ\n" | |
content += f"{'=' * 50}\n\n" | |
content += "๐ ๊ธฐํ์ (40ํ ์ ์ฒด ๊ตฌ์ฑ)\n\n" | |
content += planning_doc | |
content += f"\n\n{'=' * 50}\n\n" | |
content += "๐จ 1ํ ์คํ ๋ฆฌ๋ณด๋ (30 ํจ๋)\n\n" | |
content += storyboard | |
return content | |
def generate_random_webtoon_theme(genre: str, language: str) -> str: | |
"""Generate random webtoon theme""" | |
templates = { | |
"๋ก๋งจ์ค": [ | |
"์ฌ๋ฒ 3์ธ ์์ฌ์ ์ ์ ์ฌ์์ ๋น๋ฐ ๊ณ์ฝ์ฐ์ ", | |
"๊ณ ๋ฑํ๊ต ๋ ์ฒซ์ฌ๋๊ณผ 10๋ ๋ง์ ์ฌํ", | |
"๋ํ ๊ฒ์ฌ์ ์ดํ ๋ณํธ์ฌ์ ๋ฒ์ ๋ก๋งจ์ค" | |
], | |
"๋กํ": [ | |
"์ ๋ ๋ก ๋น์ํ๋๋ฐ 1๋ ํ ์ฒํ ์์ ", | |
"ํ๊ทํ ํฉ๋ , ๋ฒ๋ ค์ง ์์์ ์์ก๋ค", | |
"๊ณ์ฝ๊ฒฐํผํ ๋ถ๋ถ ๊ณต์์ด ์ง์ฐฉ๋จ์ด ๋์๋ค" | |
], | |
"ํํ์ง": [ | |
"F๊ธ ํํฐ๊ฐ SSS๊ธ ๋คํฌ๋ก๋งจ์๋ก ๊ฐ์ฑ", | |
"100์ธต ํ์ ์ญ์ฃผํํ๋ ํ๊ท์", | |
"๋ฒ๊ทธ๋ก ์ต๊ฐ์ด ๋ ๊ฒ์ ์ NPC" | |
], | |
"ํํ": [ | |
"๋ฌด๋ฅ๋ ฅ์์ธ ์ค ์์๋๋ฐ SSS๊ธ ์์ฐ์ง", | |
"๊ฒ์ดํธ ์์์ 10๋ , ๋์์จ ์ต๊ฐ์", | |
"ํํฐ ๊ณ ๋ฑํ๊ต์ ์จ๊ฒจ์ง ๋ญํน 1์" | |
], | |
"๋ฌดํ": [ | |
"์ฒํ์ ์ผ๋ฌธ ๋ง๋ด๊ฐ ๋ง๊ต ๊ต์ฃผ ์ ์๊ฐ ๋๋ค", | |
"100๋ ์ ์ผ๋ก ํ๊ทํ ํ์ฐํ ์ฅ๋ฌธ์ธ", | |
"ํ๊ธ ๋ฌด๊ณต์ผ๋ก ์ฒํ๋ฅผ ์ ํจํ๋ค" | |
], | |
"์ค๋ฆด๋ฌ": [ | |
"ํ๊ต์ ๊ฐํ ๋์ฐฝํ, ํ ๋ช ์ฉ ์ฌ๋ผ์ง๋ค", | |
"ํ์๋ฃจํ ์ ์ฐ์์ด์ธ๋ฒ ์ฐพ๊ธฐ", | |
"๋ด ๋จํธ์ด ์ฌ์ด์ฝํจ์ค์๋ค" | |
], | |
"์ผ์": [ | |
"ํธ์์ ์๋ฐ์์ ์์ํ ์ผ์", | |
"30๋ ์ง์ฅ์ธ์ ํด์ฌ ์ค๋น ์ผ๊ธฐ", | |
"์ฐ๋ฆฌ ๋๋ค ๊ณ ์์ด๋ค์ ๋น๋ฐ ํ์" | |
], | |
"๊ฐ๊ทธ": [ | |
"์ด์ธ๊ณ ์ฉ์ฌ์ธ๋ฐ ์คํฏ์ด ์ด์ํ๋ค", | |
"์ฐ๋ฆฌ ํ๊ต ์ ์๋์ ์ ์ง ๋ง์", | |
"์ข๋น ์ํฌ์นผ๋ฆฝ์ค์ธ๋ฐ ๋๋ง ๊ฐ๊ทธ ์บ๋ฆญํฐ" | |
], | |
"์คํฌ์ธ ": [ | |
"๋ฒค์น ๋ฉค๋ฒ์์ ์์ด์ค๊ฐ ๋๊ธฐ๊น์ง", | |
"์ฌ์ ์ผ๊ตฌ๋ถ ์ฐฝ์ค๊ธฐ", | |
"์ํด ์ ์์ ์ฝ์น ๋์ ๊ธฐ" | |
] | |
} | |
genre_themes = templates.get(genre, templates["๋ก๋งจ์ค"]) | |
return random.choice(genre_themes) | |
# --- Gradio interface --- | |
def create_interface(): | |
with gr.Blocks(theme=gr.themes.Soft(), title="K-Webtoon Storyboard Generator") as interface: | |
gr.HTML(""" | |
<style> | |
.main-header { | |
text-align: center; | |
margin-bottom: 2rem; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
padding: 2rem; | |
border-radius: 15px; | |
color: white; | |
} | |
.header-title { | |
font-size: 3rem; | |
margin-bottom: 1rem; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.2); | |
} | |
.character-profile { | |
background: #f0f0f0; | |
padding: 10px; | |
margin: 5px 0; | |
border-radius: 8px; | |
} | |
.panel-image-container { | |
min-height: 400px; | |
background: #fff; | |
border: 2px dashed #ddd; | |
border-radius: 8px; | |
margin-bottom: 20px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.episode-structure { | |
background: #f5f5f5; | |
padding: 15px; | |
border-radius: 8px; | |
margin: 10px 0; | |
max-height: 500px; | |
overflow-y: auto; | |
} | |
</style> | |
<div class="main-header"> | |
<h1 class="header-title">๐จ K-Webtoon Storyboard Generator</h1> | |
<p class="header-subtitle">ํ๊ตญํ ์นํฐ ๊ธฐํ ๋ฐ ์คํ ๋ฆฌ๋ณด๋ ์๋ ์์ฑ ์์คํ </p> | |
<p style="font-size: 14px; opacity: 0.9;">๐ 40ํ ์ ์ฒด ๊ตฌ์ฑ + ๐ฌ 1ํ 30ํจ๋ ์คํ ๋ฆฌ๋ณด๋</p> | |
</div> | |
""") | |
# State | |
current_session_id = gr.State(None) | |
planning_state = gr.State("") | |
storyboard_state = gr.State("") | |
character_profiles_state = gr.State({}) | |
with gr.Tab("๐ ๊ธฐํ์ ์์ฑ (40ํ ๊ตฌ์ฑ)"): | |
with gr.Group(): | |
gr.Markdown("### ๐ฏ ์นํฐ ์ค์ ") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
genre_select = gr.Radio( | |
choices=list(WEBTOON_GENRES.keys()), | |
value="๋ก๋งจ์ค", | |
label="์ฅ๋ฅด ์ ํ", | |
info="์ํ๋ ์ฅ๋ฅด๋ฅผ ์ ํํ์ธ์" | |
) | |
query_input = gr.Textbox( | |
label="์คํ ๋ฆฌ ์ฝ์ ํธ", | |
placeholder="์นํฐ์ ๊ธฐ๋ณธ ์ค์ ์ด๋ ์ฃผ์ ๋ฅผ ์ ๋ ฅํ์ธ์...", | |
lines=3 | |
) | |
with gr.Row(): | |
random_btn = gr.Button("๐ฒ ๋๋ค ํ ๋ง", variant="secondary") | |
submit_btn = gr.Button("๐ ๊ธฐํ ์์", variant="primary", size="lg") | |
with gr.Column(scale=1): | |
language_select = gr.Radio( | |
choices=["Korean", "English"], | |
value="Korean", | |
label="์ธ์ด" | |
) | |
gr.Markdown(""" | |
**๐ญ ์บ๋ฆญํฐ ์ผ๊ตด ์ค์ ** | |
๊ฐ ์บ๋ฆญํฐ์ ์ ๋ช ์ธ ๋ฎ์๊ผด์ด | |
์๋์ผ๋ก ๋ฐฐ์ ๋ฉ๋๋ค. | |
**๐ ์์ฑ ๋ด์ฉ** | |
- 40ํ ์ ์ฒด ๊ตฌ์ฑ์ | |
- ๊ฐ ํ๋ณ ํด๋ฆฌํํ์ด | |
- ์บ๋ฆญํฐ ํ๋กํ | |
- 1ํ 30ํจ๋ ์คํ ๋ฆฌ๋ณด๋ | |
""") | |
status_text = gr.Textbox( | |
label="์งํ ์ํฉ", | |
interactive=False, | |
value="์ฅ๋ฅด๋ฅผ ์ ํํ๊ณ ์ฝ์ ํธ๋ฅผ ์ ๋ ฅํ์ธ์" | |
) | |
# Planning output with better visibility for 40 episodes | |
gr.Markdown("### ๐ ์นํฐ ๊ธฐํ์ (40ํ ์ ์ฒด ๊ตฌ์ฑ)") | |
planning_display = gr.Markdown("*๊ธฐํ์์ด ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค (40ํ ์ ์ฒด ๊ตฌ์ฑ ํฌํจ)*") | |
# Character profiles display | |
character_display = gr.HTML(label="์บ๋ฆญํฐ ํ๋กํ") | |
with gr.Row(): | |
download_format = gr.Radio( | |
choices=["TXT"], | |
value="TXT", | |
label="๋ค์ด๋ก๋ ํ์" | |
) | |
download_btn = gr.Button("๐ฅ ๋ค์ด๋ก๋", variant="secondary") | |
download_file = gr.File(visible=False) | |
with gr.Tab("๐ฌ 1ํ ์คํ ๋ฆฌ๋ณด๋ (30ํจ๋)"): | |
gr.Markdown(""" | |
### ๐ 1ํ ์คํ ๋ฆฌ๋ณด๋ (30 ํจ๋) | |
์ข์ธก: ํ ์คํธ ๋ฐ ์ด๋ฏธ์ง ํ๋กฌํํธ | ์ฐ์ธก: ์์ฑ๋ ์ด๋ฏธ์ง | |
""") | |
# Progress bar for image generation | |
progress_bar = gr.Progress(visible=False) | |
generation_status = gr.Textbox( | |
label="์ด๋ฏธ์ง ์์ฑ ์ํ", | |
value="๋๊ธฐ ์ค...", | |
interactive=False, | |
visible=False | |
) | |
with gr.Row(): | |
with gr.Column(): | |
storyboard_text_display = gr.HTML( | |
value="<p style='color: #999; text-align: center; padding: 50px;'>๊ธฐํ์ ์์ฑ ํ ์คํ ๋ฆฌ๋ณด๋๊ฐ ์์ฑ๋ฉ๋๋ค</p>", | |
label="ํจ๋ ํ ์คํธ & ํ๋กฌํํธ" | |
) | |
with gr.Column(): | |
image_display_area = gr.HTML( | |
value=""" | |
<div style="padding: 20px; background: #fafafa; border: 2px dashed #ddd; border-radius: 10px; min-height: 800px;"> | |
<p style="color: #999; text-align: center;">๐ผ๏ธ ์ด๋ฏธ์ง ์์ฑ ๊ณต๊ฐ</p> | |
<p style="color: #aaa; text-align: center; font-size: 12px;">๊ฐ ํจ๋์ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ ค๋ฉด ์๋ ๋ฒํผ์ ํด๋ฆญํ์ธ์</p> | |
</div> | |
""", | |
label="์์ฑ๋ ์ด๋ฏธ์ง" | |
) | |
# Image generation controls with progress | |
with gr.Row(): | |
generate_all_btn = gr.Button("๐จ ๋ชจ๋ ์ด๋ฏธ์ง ์์ฑ (30๊ฐ ํจ๋)", variant="primary") | |
clear_images_btn = gr.Button("๐๏ธ ์ด๋ฏธ์ง ์ด๊ธฐํ", variant="secondary") | |
generation_progress = gr.Number(value=0, label="์์ฑ ์งํ๋ฅ (%)", visible=False) | |
# Event handlers | |
def process_query(query, genre, language, session_id): | |
system = WebtoonSystem() | |
planning = "" | |
storyboard = "" | |
character_profiles = {} | |
for planning_content, storyboard_content, status, new_session_id, profiles in system.process_webtoon_stream(query, genre, language, session_id): | |
planning = planning_content | |
storyboard = storyboard_content | |
character_profiles = profiles | |
yield planning, storyboard, status, new_session_id, character_profiles | |
def format_character_profiles(profiles: Dict[str, CharacterProfile]) -> str: | |
"""Format character profiles for display""" | |
if not profiles: | |
return "" | |
html = "<h3>๐ญ ์บ๋ฆญํฐ ํ๋กํ</h3>" | |
for name, profile in profiles.items(): | |
html += f""" | |
<div class="character-profile"> | |
<strong>{name}</strong> - {profile.celebrity_lookalike} ๋ฎ์ ์ผ๊ตด | |
<br>์ญํ : {profile.role} | |
<br>์ฑ๊ฒฉ: {profile.personality} | |
</div> | |
""" | |
return html | |
def update_storyboard_display(storyboard_content, character_profiles, session_id): | |
"""Update storyboard display with formatted panels""" | |
if not storyboard_content or storyboard_content == "": | |
return "<p style='color: #999; text-align: center; padding: 50px;'>๊ธฐํ์ ์์ฑ ํ ์คํ ๋ฆฌ๋ณด๋๊ฐ ์์ฑ๋ฉ๋๋ค</p>" | |
return format_storyboard_for_display(storyboard_content, character_profiles, session_id) | |
def handle_random_theme(genre, language): | |
return generate_random_webtoon_theme(genre, language) | |
def handle_download(download_format, session_id, planning, storyboard, genre): | |
"""Handle download request""" | |
try: | |
title = f"{genre} ์นํฐ" | |
content = export_to_txt(planning, storyboard, genre, title) | |
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', | |
suffix='.txt', delete=False) as f: | |
f.write(content) | |
return f.name | |
except Exception as e: | |
logger.error(f"Download error: {e}") | |
gr.Warning(f"๋ค์ด๋ก๋ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}") | |
return None | |
def generate_all_images_with_progress(session_id, storyboard_content, character_profiles, progress=gr.Progress()): | |
"""Generate images for all panels with progress tracking""" | |
if not REPLICATE_API_TOKEN: | |
return "<p style='color: red; text-align: center; padding: 20px;'>โ ๏ธ Replicate API ํ ํฐ์ด ์ค์ ๋์ง ์์์ต๋๋ค.</p>" | |
if not storyboard_content: | |
return "<p style='color: orange; text-align: center; padding: 20px;'>โ ๏ธ ๋จผ์ ์คํ ๋ฆฌ๋ณด๋๋ฅผ ์์ฑํ์ธ์.</p>" | |
# Parse storyboard to extract prompts | |
panel_prompts = [] | |
lines = storyboard_content.split('\n') | |
current_panel_num = 0 | |
current_prompt = "" | |
for line in lines: | |
if 'ํจ๋' in line or 'Panel' in line: | |
if current_prompt and current_panel_num > 0: | |
panel_prompts.append({ | |
'panel_id': f"ep1_panel{current_panel_num}", | |
'panel_num': current_panel_num, | |
'prompt': current_prompt | |
}) | |
current_panel_num += 1 | |
current_prompt = "" | |
elif '์ด๋ฏธ์ง ํ๋กฌํํธ:' in line or 'Image prompt:' in line: | |
current_prompt = line.split(':', 1)[1].strip() | |
# Add last panel if exists | |
if current_prompt and current_panel_num > 0: | |
panel_prompts.append({ | |
'panel_id': f"ep1_panel{current_panel_num}", | |
'panel_num': current_panel_num, | |
'prompt': current_prompt | |
}) | |
# Limit to 30 panels | |
panel_prompts = panel_prompts[:30] | |
total_panels = len(panel_prompts) | |
if total_panels == 0: | |
return "<p style='color: orange; text-align: center; padding: 20px;'>โ ๏ธ ํจ๋ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.</p>" | |
# Initialize progress | |
progress(0, desc=f"์ด๋ฏธ์ง ์์ฑ ์ค๋น ์ค... (์ด {total_panels}๊ฐ ํจ๋)") | |
# Generate images with progress tracking | |
image_generator = ImageGenerator() | |
image_html = "<div style='padding: 20px;'>" | |
image_html += f"<h3>๐ผ๏ธ ์ด๋ฏธ์ง ์์ฑ ๊ฒฐ๊ณผ</h3>" | |
results = [] | |
# Generate images one by one with progress updates | |
for i, panel_data in enumerate(panel_prompts): | |
progress((i / total_panels), desc=f"ํจ๋ {panel_data['panel_num']}/{total_panels} ์์ฑ ์ค...") | |
try: | |
result = image_generator.generate_image( | |
panel_data['prompt'], | |
panel_data['panel_id'], | |
session_id | |
) | |
results.append(result) | |
except Exception as e: | |
results.append({ | |
"panel_id": panel_data['panel_id'], | |
"status": "error", | |
"message": str(e) | |
}) | |
# Small delay to prevent API rate limiting | |
time.sleep(0.5) | |
progress(1.0, desc="์ด๋ฏธ์ง ์์ฑ ์๋ฃ!") | |
# Display results | |
success_count = sum(1 for r in results if r['status'] == 'success') | |
image_html += f"<p style='color: green; font-weight: bold;'>โ ์ฑ๊ณต: {success_count}/{len(results)} ํจ๋</p>" | |
# Grid layout for images | |
image_html += "<div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-top: 20px;'>" | |
for result in results: | |
panel_num = int(result['panel_id'].split('_panel')[1]) | |
if result['status'] == 'success': | |
image_html += f""" | |
<div style="border: 1px solid #ddd; padding: 15px; border-radius: 8px; background: white;"> | |
<h4 style="margin-top: 0; color: #764ba2;">ํจ๋ {panel_num}</h4> | |
<img src="{result['image_url']}" style="width: 100%; border-radius: 8px; margin-bottom: 10px;"> | |
<details style="margin-top: 10px;"> | |
<summary style="cursor: pointer; color: #667eea;">ํ๋กฌํํธ ๋ณด๊ธฐ</summary> | |
<p style="font-size: 11px; color: #666; margin-top: 5px; padding: 5px; background: #f5f5f5; border-radius: 4px;"> | |
{result.get('prompt', '')[:150]}... | |
</p> | |
</details> | |
</div> | |
""" | |
else: | |
image_html += f""" | |
<div style="border: 1px solid #fee; padding: 15px; border-radius: 8px; background: #fff5f5;"> | |
<h4 style="margin-top: 0; color: #d00;">ํจ๋ {panel_num} - ์์ฑ ์คํจ</h4> | |
<p style="color: #d00; font-size: 14px;">โ {result.get('message', 'Unknown error')}</p> | |
</div> | |
""" | |
image_html += "</div></div>" | |
return image_html | |
def clear_all_images(): | |
"""Clear all generated images""" | |
return """ | |
<div style="padding: 20px; background: #fafafa; border: 2px dashed #ddd; border-radius: 10px; min-height: 800px;"> | |
<p style="color: #999; text-align: center;">๐ผ๏ธ ์ด๋ฏธ์ง ์์ฑ ๊ณต๊ฐ</p> | |
<p style="color: #aaa; text-align: center; font-size: 12px;">์ด๋ฏธ์ง๊ฐ ์ด๊ธฐํ๋์์ต๋๋ค</p> | |
</div> | |
""" | |
# Connect events | |
submit_btn.click( | |
fn=process_query, | |
inputs=[query_input, genre_select, language_select, current_session_id], | |
outputs=[planning_state, storyboard_state, status_text, current_session_id, character_profiles_state] | |
).then( | |
fn=lambda x: x, | |
inputs=[planning_state], | |
outputs=[planning_display] | |
).then( | |
fn=format_character_profiles, | |
inputs=[character_profiles_state], | |
outputs=[character_display] | |
).then( | |
fn=update_storyboard_display, | |
inputs=[storyboard_state, character_profiles_state, current_session_id], | |
outputs=[storyboard_text_display] | |
) | |
random_btn.click( | |
fn=handle_random_theme, | |
inputs=[genre_select, language_select], | |
outputs=[query_input] | |
) | |
download_btn.click( | |
fn=handle_download, | |
inputs=[download_format, current_session_id, planning_state, storyboard_state, genre_select], | |
outputs=[download_file] | |
).then( | |
fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False), | |
inputs=[download_file], | |
outputs=[download_file] | |
) | |
generate_all_btn.click( | |
fn=generate_all_images_with_progress, | |
inputs=[current_session_id, storyboard_state, character_profiles_state], | |
outputs=[image_display_area] | |
) | |
clear_images_btn.click( | |
fn=clear_all_images, | |
inputs=[], | |
outputs=[image_display_area] | |
) | |
return interface | |
# Main | |
if __name__ == "__main__": | |
logger.info("K-Webtoon Storyboard Generator Starting...") | |
logger.info("=" * 60) | |
# Environment check | |
logger.info(f"API Endpoint: {API_URL}") | |
logger.info(f"Model: {MODEL_ID}") | |
logger.info(f"Target: {TARGET_EPISODES} episodes, {PANELS_PER_EPISODE} panels per episode") | |
logger.info("Genres: " + ", ".join(WEBTOON_GENRES.keys())) | |
if REPLICATE_API_TOKEN: | |
logger.info("Replicate API: Configured โ") | |
else: | |
logger.warning("Replicate API: Not configured (Image generation disabled)") | |
logger.info("=" * 60) | |
# Initialize database | |
logger.info("Initializing database...") | |
WebtoonDatabase.init_db() | |
logger.info("Database ready.") | |
# Launch interface | |
interface = create_interface() | |
interface.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False | |
) |