AGI-WebToon-KOREA / app-BACKUP.py
openfree's picture
Update app-BACKUP.py
01ba201 verified
raw
history blame
76.7 kB
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__)
# --- 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
PANELS_PER_EPISODE = 30
TARGET_PANELS = TARGET_EPISODES * PANELS_PER_EPISODE
# 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": [
{"kr": "ํ†ฐ ํฌ๋ฃจ์ฆˆ", "en": "Tom Cruise"},
{"kr": "๋ธŒ๋ž˜๋“œ ํ”ผํŠธ", "en": "Brad Pitt"},
{"kr": "๋ ˆ์˜ค๋‚˜๋ฅด๋„ ๋””์นดํ”„๋ฆฌ์˜ค", "en": "Leonardo DiCaprio"},
{"kr": "๋ผ์ด์–ธ ๊ณ ์Šฌ๋ง", "en": "Ryan Gosling"},
{"kr": "ํฌ๋ฆฌ์Šค ํ—ด์Šค์›Œ์Šค", "en": "Chris Hemsworth"},
{"kr": "๋กœ๋ฒ„ํŠธ ๋‹ค์šฐ๋‹ˆ ์ฃผ๋‹ˆ์–ด", "en": "Robert Downey Jr"},
{"kr": "ํฌ๋ฆฌ์Šค ์—๋ฐ˜์Šค", "en": "Chris Evans"},
{"kr": "ํ†ฐ ํžˆ๋“ค์Šคํ„ด", "en": "Tom Hiddleston"},
{"kr": "๋ฒ ๋„ค๋”•ํŠธ ์ปด๋ฒ„๋ฐฐ์น˜", "en": "Benedict Cumberbatch"},
{"kr": "ํ‚ค์•„๋ˆ„ ๋ฆฌ๋ธŒ์Šค", "en": "Keanu Reeves"},
{"kr": "์ด๋ณ‘ํ—Œ", "en": "Lee Byung-hun"},
{"kr": "๊ณต์œ ", "en": "Gong Yoo"},
{"kr": "๋ฐ•์„œ์ค€", "en": "Park Seo-joon"},
{"kr": "์†ก์ค‘๊ธฐ", "en": "Song Joong-ki"}
],
"female": [
{"kr": "์Šค์นผ๋ › ์š”ํ•œ์Šจ", "en": "Scarlett Johansson"},
{"kr": "์— ๋งˆ ์™“์Šจ", "en": "Emma Watson"},
{"kr": "์ œ๋‹ˆํผ ๋กœ๋ Œ์Šค", "en": "Jennifer Lawrence"},
{"kr": "๊ฐค ๊ฐ€๋—", "en": "Gal Gadot"},
{"kr": "๋งˆ๊ณ  ๋กœ๋น„", "en": "Margot Robbie"},
{"kr": "์— ๋งˆ ์Šคํ†ค", "en": "Emma Stone"},
{"kr": "์•ค ํ•ด์„œ์›จ์ด", "en": "Anne Hathaway"},
{"kr": "๋‚˜ํƒˆ๋ฆฌ ํฌํŠธ๋งŒ", "en": "Natalie Portman"},
{"kr": "์ „์ง€ํ˜„", "en": "Jun Ji-hyun"},
{"kr": "์†กํ˜œ๊ต", "en": "Song Hye-kyo"},
{"kr": "๊น€ํƒœ๋ฆฌ", "en": "Kim Tae-ri"},
{"kr": "์•„์ด์œ ", "en": "IU"},
{"kr": "์ˆ˜์ง€", "en": "Suzy"},
{"kr": "ํ•œ์†Œํฌ", "en": "Han So-hee"}
]
}
# 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 = {}
panel_images_state = {}
character_consistency_map = {}
# 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 ---
@dataclass
class CharacterProfile:
"""Character profile with celebrity lookalike"""
name: str
role: str
personality: str
appearance: str
celebrity_lookalike_kr: str
celebrity_lookalike_en: str
gender: str
detailed_appearance: str = ""
@dataclass
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)
@dataclass
class StoryboardPanel:
"""Individual storyboard panel with unique ID"""
panel_number: int
scene_type: str
image_prompt: str
image_prompt_en: str = ""
panel_id: str = ""
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 = ""
@dataclass
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"""
@staticmethod
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA journal_mode=WAL")
cursor = conn.cursor()
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,
character_consistency TEXT
)
''')
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)
)
''')
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,
image_prompt_en 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()
@staticmethod
@contextmanager
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()
@staticmethod
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
@staticmethod
def save_storyboard(session_id: str, episode_num: int, storyboard: EpisodeStoryboard):
with WebtoonDatabase.get_db() as conn:
cursor = conn.cursor()
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)))
for panel in storyboard.panels:
cursor.execute('''
INSERT INTO panels (session_id, episode_number, panel_number,
scene_type, image_prompt, image_prompt_en,
dialogue, narration, sound_effects)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (session_id, episode_num, panel.panel_number,
panel.scene_type, panel.image_prompt, panel.image_prompt_en,
json.dumps(panel.dialogue), panel.narration,
json.dumps(panel.sound_effects)))
conn.commit()
@staticmethod
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()
@staticmethod
def save_character_consistency(session_id: str, consistency_map: Dict):
"""์บ๋ฆญํ„ฐ ์ผ๊ด€์„ฑ ์ •๋ณด ์ €์žฅ"""
with WebtoonDatabase.get_db() as conn:
cursor = conn.cursor()
consistency_json = json.dumps(consistency_map)
cursor.execute(
"UPDATE sessions SET character_consistency = ? WHERE session_id = ?",
(consistency_json, session_id)
)
conn.commit()
# --- Image Generation ---
class ImageGenerator:
"""Handle image generation using Replicate API with improved prompts"""
def __init__(self):
self.generation_lock = Lock()
self.active_generations = {}
def enhance_prompt_for_webtoon(self, prompt: str, panel_number: int, scene_type: str = "medium") -> str:
"""Enhanced prompt for story-driven webtoon panel generation"""
# ์Šคํ† ๋ฆฌ ์ค‘์‹ฌ์˜ ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
base_style = "professional webtoon art, Korean manhwa style, high quality digital art"
# ์”ฌ ํƒ€์ž…๋ณ„ ๊ฐ•์กฐ์ 
scene_emphasis = {
"establishing": "wide environmental shot, detailed background, atmospheric scene",
"wide": "full scene view, multiple elements, environmental storytelling",
"medium": "balanced composition, character and environment, narrative focus",
"close_up": "emotional expression, detailed facial features, intimate moment",
"extreme_close_up": "dramatic detail, intense emotion, impactful moment"
}
# ์›นํˆฐ ํŠนํ™” ์Šคํƒ€์ผ
webtoon_style = "clean line art, vibrant colors, dynamic composition, professional illustration"
# ์Šคํ† ๋ฆฌ ์ค‘์‹ฌ ๊ฐ•์กฐ
story_focus = "narrative scene, story moment, sequential art panel"
# ์”ฌ ํƒ€์ž… ์ ์šฉ
scene_desc = scene_emphasis.get(scene_type, scene_emphasis["medium"])
# ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
enhanced_prompt = f"{base_style}, {scene_desc}, {prompt}, {webtoon_style}, {story_focus}"
# ๊ธธ์ด ์ œํ•œ
if len(enhanced_prompt) > 500:
enhanced_prompt = f"{base_style}, {prompt[:350]}, {story_focus}"
return enhanced_prompt
def generate_image(self, prompt: str, panel_id: str, session_id: str,
scene_type: str = "medium", progress_callback=None) -> Dict[str, Any]:
"""Generate image using Replicate API with enhanced quality settings"""
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} - Scene type: {scene_type}")
# ํŒจ๋„ ๋ฒˆํ˜ธ ์ถ”์ถœ
panel_number = int(panel_id.split('_panel')[1]) if '_panel' in panel_id else 1
# ํ”„๋กฌํ”„ํŠธ ๊ฐœ์„  (์Šคํ† ๋ฆฌ ์ค‘์‹ฌ)
enhanced_prompt = self.enhance_prompt_for_webtoon(prompt, panel_number, scene_type)
logger.info(f"Enhanced prompt: {enhanced_prompt[:150]}...")
if progress_callback:
progress_callback(f"ํŒจ๋„ {panel_number} ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...")
# Enhanced negative prompt for better quality
negative_prompt = (
"text, watermark, signature, logo, writing, letters, words, "
"low quality, blurry, pixelated, distorted, deformed, "
"bad anatomy, bad proportions, extra limbs, missing limbs, "
"duplicate, multiple panels, split screen, collage, "
"comic strip layout, speech bubbles, dialogue boxes"
)
# Run SDXL with optimized parameters
input_params = {
"prompt": enhanced_prompt,
"negative_prompt": negative_prompt,
"num_inference_steps": 35, # ํ’ˆ์งˆ ํ–ฅ์ƒ
"guidance_scale": 9.0, # ํ”„๋กฌํ”„ํŠธ ์ถฉ์‹ค๋„ ํ–ฅ์ƒ
"width": 768, # ์›นํˆฐ ์„ธ๋กœํ˜•
"height": 1024,
"scheduler": "DPMSolverMultistep", # ๋” ๋‚˜์€ ํ’ˆ์งˆ
"refine": "expert_ensemble_refiner", # SDXL refiner ์‚ฌ์šฉ
"high_noise_frac": 0.8,
"prompt_strength": 0.9,
"num_outputs": 1
}
output = replicate.run(
"stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b",
input=input_params
)
if output and len(output) > 0:
image_url = output[0] if isinstance(output[0], str) else str(output[0])
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": enhanced_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)}
# --- 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()
self.character_consistency_map = {}
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 with English names"""
profiles = {}
used_celebrities = []
for char in characters:
gender = char.get('gender', 'male')
available_celebrities = [c for c in CELEBRITY_FACES.get(gender, [])
if c['kr'] not in [u['kr'] for u in used_celebrities]]
if not available_celebrities:
available_celebrities = CELEBRITY_FACES.get(gender, [])
celebrity = random.choice(available_celebrities)
used_celebrities.append(celebrity)
detailed_appearance = f"{celebrity['en']} lookalike face, {char.get('appearance', '')}"
profile = CharacterProfile(
name=char.get('name', ''),
role=char.get('role', ''),
personality=char.get('personality', ''),
appearance=char.get('appearance', ''),
celebrity_lookalike_kr=celebrity['kr'],
celebrity_lookalike_en=celebrity['en'],
gender=gender,
detailed_appearance=detailed_appearance
)
profiles[profile.name] = profile
self.tracker.add_character(profile)
self.character_consistency_map[profile.name] = {
'kr': f"{profile.name}({celebrity['kr']} ๋‹ฎ์€ ์–ผ๊ตด์˜ {gender})",
'en': f"{profile.name} ({celebrity['en']} lookalike {gender})",
'appearance': detailed_appearance
}
return profiles
def translate_prompt_to_english(self, korean_prompt: str, character_profiles: Dict[str, CharacterProfile]) -> str:
"""ํ•œ๊ธ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์˜์–ด๋กœ ๋ฒˆ์—ญ"""
try:
english_prompt = korean_prompt
for name, profile in character_profiles.items():
korean_pattern = f"{name}\\([^)]+\\)"
english_replacement = f"{name} ({profile.celebrity_lookalike_en} lookalike {profile.gender})"
english_prompt = re.sub(korean_pattern, english_replacement, english_prompt)
translation_prompt = f"""Translate this Korean webtoon panel description to English.
Focus on visual elements and actions, not just characters.
Korean: {english_prompt}
English translation:"""
messages = [{"role": "user", "content": translation_prompt}]
translated = self.call_llm_sync(messages, "translator", "English")
return translated.strip()
except Exception as e:
logger.error(f"Translation error: {e}")
return korean_prompt
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ํ™” ์ „์ฒด ๊ตฌ์„ฑ์•ˆ์„ ๋ชจ๋‘ ์ž‘์„ฑํ•˜์„ธ์š”.
4. ๊ฐ ์บ๋ฆญํ„ฐ์˜ ์™ธ๋ชจ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ๋ฌ˜์‚ฌํ•˜์„ธ์š”.
**์žฅ๋ฅด ํ•„์ˆ˜ ์š”์†Œ:**
- ํ•ต์‹ฌ ์š”์†Œ: {', '.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.
4. Describe each character's appearance in detail.
**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 and appearance for each!)
- Protagonist: [Name] - Gender: [male/female] - Appearance: [height, build, hair color, eye color, features] - Personality: [traits] - Goal: [character goal]
- Character2: [Name] - Gender: [male/female] - Appearance: [detailed description] - Role: [role] - Traits: [traits]
- Character3: [Name] - Gender: [male/female] - Appearance: [detailed description] - Role: [role] - Traits: [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 story-driven focus"""
genre_info = GENRE_ELEMENTS.get(genre, {})
char_descriptions = "\n".join([
f"- {name}: ํ•ญ์ƒ '{profile.celebrity_lookalike_kr} ๋‹ฎ์€ ์–ผ๊ตด'๋กœ ๋ฌ˜์‚ฌ. {profile.appearance}"
for name, profile in character_profiles.items()
])
lang_prompts = {
"Korean": f"""์›นํˆฐ {episode_num}ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ 30๊ฐœ ํŒจ๋„๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
**์žฅ๋ฅด:** {genre}
**{episode_num}ํ™” ๋‚ด์šฉ:** {self._extract_episode_plan(plot_outline, episode_num)}
**์บ๋ฆญํ„ฐ ์ผ๊ด€์„ฑ ๊ทœ์น™:**
{char_descriptions}
โš ๏ธ **์ ˆ๋Œ€ ๊ทœ์น™ - ์Šคํ† ๋ฆฌ ์ค‘์‹ฌ ์ „๊ฐœ**:
1. ๋ฐ˜๋“œ์‹œ 30๊ฐœ ํŒจ๋„์„ ๋ชจ๋‘ ์ž‘์„ฑํ•˜์„ธ์š”!
2. ๊ฐ ํŒจ๋„์€ **์Šคํ† ๋ฆฌ ์ „๊ฐœ์˜ ํ•œ ์ˆœ๊ฐ„**์„ ๋‹ด์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค!
3. ๋‹จ์ˆœ ์ธ๋ฌผ ์ƒท์ด ์•„๋‹Œ **์‚ฌ๊ฑด๊ณผ ํ–‰๋™ ์ค‘์‹ฌ**์œผ๋กœ ๊ตฌ์„ฑ!
4. ๋ฐฐ๊ฒฝ, ์ƒํ™ฉ, ์•ก์…˜์„ ๊ตฌ์ฒด์ ์œผ๋กœ ๋ฌ˜์‚ฌ!
5. ์บ๋ฆญํ„ฐ๊ฐ€ ๋‚˜์˜ฌ ๋•Œ๋งˆ๋‹ค "์บ๋ฆญํ„ฐ์ด๋ฆ„(์œ ๋ช…์ธ ๋‹ฎ์€ ์–ผ๊ตด)" ํ˜•์‹ ์œ ์ง€!
**ํŒจ๋„ ๊ตฌ์„ฑ ๊ฐ€์ด๋“œ:**
- establishing shot (์ „์ฒด ์ƒํ™ฉ/๋ฐฐ๊ฒฝ): 4-5๊ฐœ
- wide shot (์ „์‹ /ํ™˜๊ฒฝ): 8-10๊ฐœ
- medium shot (์ƒ๋ฐ˜์‹ /๋Œ€ํ™”): 10-12๊ฐœ
- close-up (์–ผ๊ตด/๊ฐ์ •): 5-6๊ฐœ
- extreme close-up (๋””ํ…Œ์ผ): 2-3๊ฐœ
**๊ฐ ํŒจ๋„ ์ž‘์„ฑ ํ˜•์‹:**
ํŒจ๋„ 1:
- ์ƒท ํƒ€์ž…: [establishing/wide/medium/close_up/extreme_close_up ์ค‘ ํ•˜๋‚˜]
- ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ: [๊ตฌ์ฒด์ ์ธ ์žฅ๋ฉด ๋ฌ˜์‚ฌ - ๋ฐฐ๊ฒฝ, ํ–‰๋™, ๋ถ„์œ„๊ธฐ ํฌํ•จ]
- ๋Œ€์‚ฌ: [์บ๋ฆญํ„ฐ ๋Œ€์‚ฌ]
- ๋‚˜๋ ˆ์ด์…˜: [ํ•ด์„ค]
- ํšจ๊ณผ์Œ: [ํšจ๊ณผ์Œ]
- ๋ฐฐ๊ฒฝ: [๊ตฌ์ฒด์  ๋ฐฐ๊ฒฝ]
...30๊ฐœ ํŒจ๋„ ๋ชจ๋‘ ์ž‘์„ฑ
โš ๏ธ ์Šคํ† ๋ฆฌ ์ง„ํ–‰์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด์ง€๋„๋ก ๊ตฌ์„ฑํ•˜์„ธ์š”!""",
"English": f"""Create Episode {episode_num} storyboard with 30 panels.
**Genre:** {genre}
**Episode content:** {self._extract_episode_plan(plot_outline, episode_num)}
**Character Consistency Rules:**
{char_descriptions}
โš ๏ธ **ABSOLUTE RULES - Story-Driven Focus**:
1. Must write all 30 panels!
2. Each panel must capture **a moment in story progression**!
3. Focus on **events and actions**, not just character shots!
4. Describe backgrounds, situations, actions specifically!
5. Always maintain "Name (celebrity lookalike)" format!
**Panel Composition Guide:**
- establishing shot (overall scene/setting): 4-5 panels
- wide shot (full body/environment): 8-10 panels
- medium shot (upper body/dialogue): 10-12 panels
- close-up (face/emotion): 5-6 panels
- extreme close-up (detail): 2-3 panels
**Panel format:**
Panel 1:
- Shot type: [one of: establishing/wide/medium/close_up/extreme_close_up]
- Image prompt: [Specific scene description - include background, action, atmosphere]
- Dialogue: [Character dialogue]
- Narration: [Narration]
- Sound effects: [Effects]
- Background: [Specific background]
...write all 30 panels
โš ๏ธ Ensure natural story flow across panels!"""
}
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():
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()
gender = 'male'
if 'female' in line.lower() or '์—ฌ' in line:
gender = 'female'
elif 'male' in line.lower() or '๋‚จ' in line:
gender = 'male'
appearance = ''
for part in parts:
if '์™ธ๋ชจ:' in part or 'Appearance:' in part:
appearance = part.split(':', 1)[1].strip() if ':' in part else part.strip()
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': appearance
}
if current_char and current_char not in characters:
characters.append(current_char)
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
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
if role == "translator":
max_tokens = 2000
payload = {
"model": self.model_id,
"messages": full_messages,
"max_tokens": max_tokens,
"temperature": 0.7 if role != "translator" else 0.3,
"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 with story-driven emphasis"""
base_prompts = {
"Korean": {
"planner": """๋‹น์‹ ์€ ํ•œ๊ตญ ์›นํˆฐ ์‹œ์žฅ์„ ์™„๋ฒฝํžˆ ์ดํ•ดํ•˜๋Š” ์›นํˆฐ ๊ธฐํš์ž์ž…๋‹ˆ๋‹ค.
๋…์ž๋ฅผ ์‚ฌ๋กœ์žก๋Š” ์Šคํ† ๋ฆฌ์™€ ๋น„์ฃผ์–ผ ์—ฐ์ถœ์„ ๊ธฐํšํ•ฉ๋‹ˆ๋‹ค.
40ํ™” ์™„๊ฒฐ ๊ตฌ์กฐ๋กœ ์™„๋ฒฝํ•œ ๊ธฐ์Šน์ „๊ฒฐ์„ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
๊ฐ ํ™”๋งˆ๋‹ค ๊ฐ•๋ ฅํ•œ ํด๋ฆฌํ”„ํ–‰์–ด๋กœ ๋‹ค์Œ ํ™”๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
์บ๋ฆญํ„ฐ์˜ ์„ฑ๋ณ„๊ณผ ์™ธ๋ชจ๋ฅผ ๋ช…ํ™•ํžˆ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
โš ๏ธ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™:
1. ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์Šคํ† ๋ฆฌ ์„ค์ •์„ ์ ˆ๋Œ€์ ์œผ๋กœ ์šฐ์„ ์‹œํ•˜๊ณ , ์ด๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๋ชจ๋“  ํ”Œ๋กฏ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
2. ๋ฐ˜๋“œ์‹œ 40ํ™” ์ „์ฒด ๊ตฌ์„ฑ์•ˆ์„ ๋ชจ๋‘ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ƒ๋žตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
3. ๊ฐ ์บ๋ฆญํ„ฐ์˜ ์™ธ๋ชจ๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ๋ฌ˜์‚ฌํ•ฉ๋‹ˆ๋‹ค.""",
"storyboarder": """๋‹น์‹ ์€ ์›นํˆฐ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
30๊ฐœ ํŒจ๋„๋กœ ํ•œ ํ™”๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
์„ธ๋กœ ์Šคํฌ๋กค์— ์ตœ์ ํ™”๋œ ์—ฐ์ถœ์„ ํ•ฉ๋‹ˆ๋‹ค.
โš ๏ธ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™ - ์Šคํ† ๋ฆฌ ์ค‘์‹ฌ ์ „๊ฐœ:
1. ๋ฐ˜๋“œ์‹œ 30๊ฐœ ํŒจ๋„์„ ๋ชจ๋‘ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
2. ๊ฐ ํŒจ๋„์€ **์Šคํ† ๋ฆฌ ์ง„ํ–‰์˜ ํ•œ ์ˆœ๊ฐ„**์„ ๋‹ด์Šต๋‹ˆ๋‹ค.
3. ๋‹จ์ˆœ ์ธ๋ฌผ ์ƒท์ด ์•„๋‹Œ **์‚ฌ๊ฑด, ํ–‰๋™, ์ƒํ™ฉ ์ค‘์‹ฌ**์œผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
4. ๋ฐฐ๊ฒฝ๊ณผ ํ™˜๊ฒฝ์„ ๊ตฌ์ฒด์ ์œผ๋กœ ๋ฌ˜์‚ฌํ•ฉ๋‹ˆ๋‹ค.
5. ๋‹ค์–‘ํ•œ ์ƒท ํƒ€์ž…์„ ํ™œ์šฉํ•˜์—ฌ ์—ญ๋™์ ์œผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
6. ์บ๋ฆญํ„ฐ๊ฐ€ ๋‚˜์˜ฌ ๋•Œ๋งˆ๋‹ค "์บ๋ฆญํ„ฐ์ด๋ฆ„(์œ ๋ช…์ธ ๋‹ฎ์€ ์–ผ๊ตด)" ํ˜•์‹์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.""",
"translator": """You are a professional translator specializing in webtoon and visual content.
Translate Korean webtoon panel descriptions to English while maintaining:
- Focus on actions and scenes, not just characters
- Visual details and camera angles
- Environmental descriptions
- Emotional nuances
- Keep celebrity lookalike descriptions consistent"""
},
"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 and appearances.
โš ๏ธ 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.
3. Describe each character's appearance in detail.""",
"storyboarder": """You are a webtoon storyboard specialist.
Perfectly compose one episode with 30 panels.
โš ๏ธ Most important principles - Story-Driven Focus:
1. Must write all 30 panels.
2. Each panel captures **a moment in story progression**.
3. Focus on **events, actions, situations**, not just character shots.
4. Describe backgrounds and environments specifically.
5. Use varied shot types for dynamic composition.
6. Always maintain "CharacterName (celebrity lookalike)" format.""",
"translator": """You are a professional translator specializing in webtoon and visual content.
Translate Korean webtoon panel descriptions to English while maintaining:
- Focus on actions and scenes, not just characters
- Visual details and camera angles
- Environmental descriptions
- Emotional nuances
- Keep celebrity lookalike descriptions consistent"""
}
}
return base_prompts.get(language, base_prompts["Korean"])
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
yield "", "", f"๐ŸŽฌ ์›นํˆฐ ๊ธฐํš์•ˆ ์ž‘์„ฑ ์ค‘... (40ํ™” ์ „์ฒด ๊ตฌ์„ฑ ํฌํ•จ) - ์žฅ๋ฅด: {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
characters = self.parse_characters_from_planning(planning_doc)
character_profiles = self.assign_celebrity_lookalikes(characters)
WebtoonDatabase.save_character_profiles(self.current_session_id, character_profiles)
WebtoonDatabase.save_character_consistency(self.current_session_id, self.character_consistency_map)
yield planning_doc, "", "โœ… ๊ธฐํš์•ˆ ์™„์„ฑ! (40ํ™” ๊ตฌ์„ฑ ์™„๋ฃŒ)", self.current_session_id, character_profiles
yield planning_doc, "", "๐ŸŽจ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ž‘์„ฑ ์ค‘... (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
)
storyboard = self.parse_storyboard(storyboard_content, 1, character_profiles)
WebtoonDatabase.save_storyboard(self.current_session_id, 1, storyboard)
yield planning_doc, storyboard_content, "๐ŸŽ‰ ์™„์„ฑ! (๊ธฐํš์•ˆ + 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ)", 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 scene types"""
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:
if current_panel.image_prompt and not current_panel.image_prompt_en:
current_panel.image_prompt_en = self.translate_prompt_to_english(
current_panel.image_prompt, character_profiles
)
panels.append(current_panel)
panel_number += 1
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
)
elif current_panel:
if '์ƒท ํƒ€์ž…:' in line or 'Shot type:' in line:
shot = line.split(':', 1)[1].strip().lower()
if 'establishing' in shot:
current_panel.scene_type = "establishing"
elif 'wide' in shot:
current_panel.scene_type = "wide"
elif 'close' in shot and 'extreme' in shot:
current_panel.scene_type = "extreme_close_up"
elif 'close' in shot:
current_panel.scene_type = "close_up"
else:
current_panel.scene_type = "medium"
elif '์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:' in line or 'Image prompt:' in line:
prompt = line.split(':', 1)[1].strip()
for char_name, consistency in self.character_consistency_map.items():
if char_name in prompt and consistency['kr'] not in prompt:
prompt = prompt.replace(char_name, consistency['kr'])
current_panel.image_prompt = prompt
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)
if current_panel:
if current_panel.image_prompt and not current_panel.image_prompt_en:
current_panel.image_prompt_en = self.translate_prompt_to_english(
current_panel.image_prompt, character_profiles
)
panels.append(current_panel)
storyboard.panels = panels[:30]
return storyboard
# Export functions
def export_planning_to_txt(planning_doc: str, genre: str, title: str = "") -> str:
"""Export only planning document (40 episodes structure) to TXT"""
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 += planning_doc
return content
def export_storyboard_to_txt(storyboard: str, genre: str, episode_num: int = 1) -> str:
"""Export only storyboard (30 panels) to TXT"""
content = f"{'=' * 50}\n"
content += f"{genre} ์›นํˆฐ - {episode_num}ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ\n"
content += f"{'=' * 50}\n\n"
content += f"์ด 30๊ฐœ ํŒจ๋„\n"
content += f"{'=' * 50}\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)
# Parse storyboard panels for display
def parse_storyboard_panels(storyboard_content, character_profiles=None):
"""Parse storyboard content into structured panel data"""
if not storyboard_content:
return []
panels = []
lines = storyboard_content.split('\n')
current_panel = None
panel_num = 0
for i, line in enumerate(lines):
if any(keyword in line for keyword in ['ํŒจ๋„', 'Panel']) and any(char.isdigit() for char in line):
if current_panel and current_panel.get('prompt'):
panels.append(current_panel)
numbers = re.findall(r'\d+', line)
panel_num = int(numbers[0]) if numbers else panel_num + 1
current_panel = {
'number': panel_num,
'shot': '',
'prompt': '',
'prompt_en': '',
'dialogue': '',
'narration': '',
'effects': '',
'image_url': None,
'scene_type': 'medium'
}
elif current_panel:
if '์ƒท ํƒ€์ž…:' in line or 'Shot type:' in line.lower():
shot = line.split(':', 1)[1].strip() if ':' in line else ''
current_panel['shot'] = shot
# Determine scene type
if 'establishing' in shot.lower():
current_panel['scene_type'] = 'establishing'
elif 'wide' in shot.lower():
current_panel['scene_type'] = 'wide'
elif 'extreme' in shot.lower() and 'close' in shot.lower():
current_panel['scene_type'] = 'extreme_close_up'
elif 'close' in shot.lower():
current_panel['scene_type'] = 'close_up'
else:
current_panel['scene_type'] = 'medium'
elif '์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:' in line or 'Image prompt:' in line.lower():
current_panel['prompt'] = line.split(':', 1)[1].strip() if ':' in line else ''
elif '๋Œ€์‚ฌ:' in line or 'Dialogue:' in line.lower():
current_panel['dialogue'] = line.split(':', 1)[1].strip() if ':' in line else ''
elif '๋‚˜๋ ˆ์ด์…˜:' in line or 'Narration:' in line.lower():
current_panel['narration'] = line.split(':', 1)[1].strip() if ':' in line else ''
elif 'ํšจ๊ณผ์Œ:' in line or 'Sound effect:' in line.lower():
current_panel['effects'] = line.split(':', 1)[1].strip() if ':' in line else ''
if current_panel and current_panel.get('prompt'):
panels.append(current_panel)
return panels[:30]
# Gradio interface with side-by-side panel layout
def create_interface():
with gr.Blocks(theme=gr.themes.Soft(), title="K-Webtoon Storyboard Generator", css="""
.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);
}
.panel-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
.panel-text-box {
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 15px;
background: #f9f9f9;
height: 400px;
overflow-y: auto;
}
.panel-image-box {
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 15px;
background: white;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.panel-header {
font-size: 18px;
font-weight: bold;
color: #764ba2;
margin-bottom: 10px;
border-bottom: 2px solid #764ba2;
padding-bottom: 5px;
}
.panel-content {
font-size: 14px;
line-height: 1.8;
}
.panel-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
}
.shot-type {
display: inline-block;
background: #764ba2;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-bottom: 5px;
}
.panel-dialogue {
background: #fff;
padding: 8px;
border-left: 3px solid #667eea;
margin: 5px 0;
}
.panel-narration {
font-style: italic;
color: #666;
margin: 5px 0;
}
.panel-effects {
color: #ff6b6b;
font-weight: bold;
margin: 5px 0;
}
""") as interface:
gr.HTML("""
<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 variables
current_session_id = gr.State(None)
planning_state = gr.State("")
storyboard_state = gr.State("")
character_profiles_state = gr.State({})
panel_data_state = gr.State([])
webtoon_system = gr.State(None)
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ํ™” ์ „์ฒด ๊ตฌ์„ฑ์•ˆ
- ๊ฐ ํ™”๋ณ„ ํด๋ฆฌํ”„ํ–‰์–ด
- ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„
""")
status_text = gr.Textbox(
label="์ง„ํ–‰ ์ƒํ™ฉ",
interactive=False,
value="์žฅ๋ฅด๋ฅผ ์„ ํƒํ•˜๊ณ  ์ฝ˜์…‰ํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
)
# Planning output
gr.Markdown("### ๐Ÿ“– ์›นํˆฐ ๊ธฐํš์•ˆ (40ํ™” ์ „์ฒด ๊ตฌ์„ฑ)")
planning_display = gr.Textbox(
label="๊ธฐํš์•ˆ",
lines=20,
max_lines=50,
interactive=False,
value="๊ธฐํš์•ˆ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค (40ํ™” ์ „์ฒด ๊ตฌ์„ฑ ํฌํ•จ)"
)
character_display = gr.HTML(label="์บ๋ฆญํ„ฐ ํ”„๋กœํ•„")
with gr.Row():
download_planning_btn = gr.Button("๐Ÿ“ฅ ๊ธฐํš์•ˆ ๋‹ค์šด๋กœ๋“œ (40ํ™” ๊ตฌ์„ฑ)", variant="secondary")
planning_download_file = gr.File(visible=False)
with gr.Tab("๐ŸŽฌ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ (30ํŒจ๋„)"):
gr.Markdown("""
### ๐Ÿ“‹ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ - 30๊ฐœ ํŒจ๋„
๊ฐ ํŒจ๋„์€ ์Šคํ† ๋ฆฌ ์ „๊ฐœ์— ๋”ฐ๋ผ ๊ตฌ์„ฑ๋˜๋ฉฐ, ์ขŒ์ธก์— ํ…์ŠคํŠธ, ์šฐ์ธก์— ์ด๋ฏธ์ง€๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
""")
# Control buttons
with gr.Row():
apply_edits_btn = gr.Button("โœ… ํŽธ์ง‘ ๋‚ด์šฉ ์ ์šฉ", variant="secondary")
generate_selected_btn = gr.Button("๐ŸŽจ ์„ ํƒํ•œ ํŒจ๋„ ์ด๋ฏธ์ง€ ์ƒ์„ฑ", variant="secondary")
generate_all_images_btn = gr.Button("๐ŸŽจ ๋ชจ๋“  ํŒจ๋„ ์ด๋ฏธ์ง€ ์ƒ์„ฑ", variant="primary", size="lg")
download_storyboard_btn = gr.Button("๐Ÿ“ฅ ์Šคํ† ๋ฆฌ๋ณด๋“œ ๋‹ค์šด๋กœ๋“œ", variant="secondary")
clear_images_btn = gr.Button("๐Ÿ—‘๏ธ ์ด๋ฏธ์ง€ ์ดˆ๊ธฐํ™”", variant="secondary")
storyboard_download_file = gr.File(visible=False)
generation_progress = gr.Textbox(label="์ง„ํ–‰ ์ƒํ™ฉ", interactive=False, visible=False)
# Editable storyboard display
storyboard_editor = gr.Textbox(
label="์Šคํ† ๋ฆฌ๋ณด๋“œ ํŽธ์ง‘๊ธฐ",
lines=15,
max_lines=30,
interactive=True,
placeholder="์Šคํ† ๋ฆฌ๋ณด๋“œ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ž์œ ๋กญ๊ฒŒ ํŽธ์ง‘ํ•˜์„ธ์š”.",
visible=False
)
# Panel selector
panel_selector = gr.CheckboxGroup(
label="์ด๋ฏธ์ง€ ์ƒ์„ฑํ•  ํŒจ๋„ ์„ ํƒ",
choices=[f"ํŒจ๋„ {i}" for i in range(1, 31)],
value=[],
visible=False
)
# Panels display area with side-by-side layout
panels_display = gr.HTML(label="ํŒจ๋„ ํ‘œ์‹œ", value="<p>์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์—ฌ๊ธฐ์— ํŒจ๋„์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.</p>")
# Helper functions
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, system
def format_character_profiles(profiles: Dict[str, CharacterProfile]) -> str:
if not profiles:
return ""
html = "<h3>๐ŸŽญ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„</h3>"
for name, profile in profiles.items():
html += f"""
<div style="background: #f0f0f0; padding: 10px; margin: 5px 0; border-radius: 8px;">
<strong>{name}</strong> - {profile.celebrity_lookalike_kr} ๋‹ฎ์€ ์–ผ๊ตด ({profile.celebrity_lookalike_en})
<br>์—ญํ• : {profile.role}
<br>์„ฑ๊ฒฉ: {profile.personality}
<br>์™ธ๋ชจ: {profile.appearance}
</div>
"""
return html
def display_panels_side_by_side(panel_data):
"""Display panels in side-by-side format"""
if not panel_data:
return "<p>ํŒจ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ๋จผ์ € ์ƒ์„ฑํ•˜์„ธ์š”.</p>"
html = ""
for panel in panel_data:
html += f"""
<div class="panel-container">
<div class="panel-text-box">
<div class="panel-header">ํŒจ๋„ {panel['number']}</div>
<div class="panel-content">
<span class="shot-type">{panel.get('shot', 'medium')}</span>
<div><strong>์žฅ๋ฉด:</strong> {panel.get('prompt', '')}</div>
{f'<div class="panel-dialogue"><strong>๋Œ€์‚ฌ:</strong> {panel["dialogue"]}</div>' if panel.get('dialogue') else ''}
{f'<div class="panel-narration"><strong>๋‚˜๋ ˆ์ด์…˜:</strong> {panel["narration"]}</div>' if panel.get('narration') else ''}
{f'<div class="panel-effects"><strong>ํšจ๊ณผ์Œ:</strong> {panel["effects"]}</div>' if panel.get('effects') else ''}
</div>
</div>
<div class="panel-image-box">
{f'<img src="{panel["image_url"]}" class="panel-image" alt="Panel {panel["number"]}" />' if panel.get('image_url') else '<p style="color: #999;">์ด๋ฏธ์ง€๊ฐ€ ์•„์ง ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค</p>'}
</div>
</div>
"""
return html
def apply_edited_storyboard(edited_text, session_id, character_profiles):
"""Parse and apply edited storyboard text"""
if not edited_text:
return [], "<p>์Šคํ† ๋ฆฌ๋ณด๋“œ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.</p>", gr.update(visible=False), gr.update(visible=False)
panel_data = parse_storyboard_panels(edited_text, character_profiles)
if panel_data:
html = display_panels_side_by_side(panel_data)
panel_choices = [f"ํŒจ๋„ {p['number']}" for p in panel_data]
return panel_data, html, gr.update(visible=True, choices=panel_choices, value=[]), gr.update(visible=True, value=edited_text)
return [], "<p>ํŒจ๋„์„ ํŒŒ์‹ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</p>", gr.update(visible=False), gr.update(visible=True)
def generate_selected_panel_images(panel_data, selected_panels, session_id, character_profiles, webtoon_system, progress=gr.Progress()):
"""Generate images for selected panels only"""
if not REPLICATE_API_TOKEN:
return display_panels_side_by_side(panel_data), gr.update(visible=True, value="โš ๏ธ Replicate API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
if not panel_data:
return display_panels_side_by_side(panel_data), gr.update(visible=True, value="โš ๏ธ ํŒจ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
if not selected_panels:
return display_panels_side_by_side(panel_data), gr.update(visible=True, value="โš ๏ธ ์ƒ์„ฑํ•  ํŒจ๋„์„ ์„ ํƒํ•˜์„ธ์š”.")
if not webtoon_system:
webtoon_system = WebtoonSystem()
selected_numbers = [int(p.split()[1]) for p in selected_panels]
total = len(selected_numbers)
successful = 0
for i, panel in enumerate(panel_data):
if panel['number'] in selected_numbers:
idx = selected_numbers.index(panel['number'])
progress((idx / total), desc=f"ํŒจ๋„ {panel['number']} ์ƒ์„ฑ ์ค‘...")
if panel.get('prompt'):
try:
# Translate if needed
if not panel.get('prompt_en'):
panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
panel['prompt'], character_profiles
)
# Generate with scene type
result = webtoon_system.image_generator.generate_image(
panel['prompt_en'],
f"ep1_panel{panel['number']}",
session_id,
scene_type=panel.get('scene_type', 'medium')
)
if result['status'] == 'success':
panel['image_url'] = result['image_url']
successful += 1
except Exception as e:
logger.error(f"Error generating panel {panel['number']}: {e}")
time.sleep(0.5)
progress(1.0, desc=f"์™„๋ฃŒ! {successful}/{total} ํŒจ๋„ ์ƒ์„ฑ ์„ฑ๊ณต")
return display_panels_side_by_side(panel_data), gr.update(visible=False)
def generate_all_panel_images(panel_data, session_id, character_profiles, webtoon_system, progress=gr.Progress()):
"""Generate images for all panels"""
if not REPLICATE_API_TOKEN:
return display_panels_side_by_side(panel_data), gr.update(visible=True, value="โš ๏ธ Replicate API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
if not panel_data:
return display_panels_side_by_side(panel_data), gr.update(visible=True, value="โš ๏ธ ํŒจ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
if not webtoon_system:
webtoon_system = WebtoonSystem()
total_panels = len(panel_data)
successful = 0
failed = 0
for i, panel in enumerate(panel_data):
progress((i / total_panels), desc=f"ํŒจ๋„ {panel['number']}/{total_panels} ์ƒ์„ฑ ์ค‘...")
if panel.get('prompt'):
try:
# Translate if needed
if not panel.get('prompt_en'):
panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
panel['prompt'], character_profiles
)
# Generate with scene type
result = webtoon_system.image_generator.generate_image(
panel['prompt_en'],
f"ep1_panel{panel['number']}",
session_id,
scene_type=panel.get('scene_type', 'medium')
)
if result['status'] == 'success':
panel['image_url'] = result['image_url']
successful += 1
else:
failed += 1
except Exception as e:
logger.error(f"Error generating panel {panel['number']}: {e}")
failed += 1
time.sleep(0.5) # Rate limiting
progress(1.0, desc=f"์™„๋ฃŒ! ์„ฑ๊ณต: {successful}, ์‹คํŒจ: {failed}")
return display_panels_side_by_side(panel_data), gr.update(visible=False)
def clear_all_images(panel_data):
for panel in panel_data:
panel['image_url'] = None
return display_panels_side_by_side(panel_data)
def handle_random_theme(genre, language):
return generate_random_webtoon_theme(genre, language)
def download_planning(session_id, planning, genre):
try:
title = f"{genre} ์›นํˆฐ"
content = export_planning_to_txt(planning, genre, title)
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='_planning.txt', delete=False) as f:
f.write(content)
return f.name
except Exception as e:
logger.error(f"Download error: {e}")
return None
def download_storyboard_from_editor(edited_text, genre):
try:
content = export_storyboard_to_txt(edited_text, genre, 1)
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='_storyboard.txt', delete=False) as f:
f.write(content)
return f.name
except Exception as e:
logger.error(f"Download error: {e}")
return None
# 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, webtoon_system]
).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=lambda x: x,
inputs=[storyboard_state],
outputs=[storyboard_editor]
).then(
fn=apply_edited_storyboard,
inputs=[storyboard_state, current_session_id, character_profiles_state],
outputs=[panel_data_state, panels_display, panel_selector, storyboard_editor]
)
# Apply edits button
apply_edits_btn.click(
fn=apply_edited_storyboard,
inputs=[storyboard_editor, current_session_id, character_profiles_state],
outputs=[panel_data_state, panels_display, panel_selector, storyboard_editor]
).then(
fn=lambda: gr.update(visible=True, value="ํŽธ์ง‘ ๋‚ด์šฉ์ด ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."),
outputs=[generation_progress]
)
# Generate selected images
generate_selected_btn.click(
fn=lambda: gr.update(visible=True, value="์„ ํƒํ•œ ํŒจ๋„ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ์ž‘..."),
outputs=[generation_progress]
).then(
fn=generate_selected_panel_images,
inputs=[panel_data_state, panel_selector, current_session_id, character_profiles_state, webtoon_system],
outputs=[panels_display, generation_progress]
)
# Generate all images
generate_all_images_btn.click(
fn=lambda: gr.update(visible=True, value="๋ชจ๋“  ํŒจ๋„ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ์ž‘..."),
outputs=[generation_progress]
).then(
fn=generate_all_panel_images,
inputs=[panel_data_state, current_session_id, character_profiles_state, webtoon_system],
outputs=[panels_display, generation_progress]
)
# Clear images
clear_images_btn.click(
fn=clear_all_images,
inputs=[panel_data_state],
outputs=[panels_display]
)
# Random theme
random_btn.click(
fn=handle_random_theme,
inputs=[genre_select, language_select],
outputs=[query_input]
)
# Downloads
download_planning_btn.click(
fn=download_planning,
inputs=[current_session_id, planning_state, genre_select],
outputs=[planning_download_file]
).then(
fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
inputs=[planning_download_file],
outputs=[planning_download_file]
)
download_storyboard_btn.click(
fn=download_storyboard_from_editor,
inputs=[storyboard_editor, genre_select],
outputs=[storyboard_download_file]
).then(
fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
inputs=[storyboard_download_file],
outputs=[storyboard_download_file]
)
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
)