openfree commited on
Commit
9b51d82
·
verified ·
1 Parent(s): 6e94210

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -362
app.py CHANGED
@@ -28,19 +28,6 @@ from threading import Lock
28
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
29
  logger = logging.getLogger(__name__)
30
 
31
- # --- Document export imports ---
32
- try:
33
- from docx import Document
34
- from docx.shared import Inches, Pt, RGBColor, Mm
35
- from docx.enum.text import WD_ALIGN_PARAGRAPH
36
- from docx.enum.style import WD_STYLE_TYPE
37
- from docx.oxml.ns import qn
38
- from docx.oxml import OxmlElement
39
- DOCX_AVAILABLE = True
40
- except ImportError:
41
- DOCX_AVAILABLE = False
42
- logger.warning("python-docx not installed. DOCX export will be disabled.")
43
-
44
  # --- Environment variables and constants ---
45
  FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
46
  REPLICATE_API_TOKEN = os.getenv("REPLICATE_API_TOKEN", "")
@@ -54,9 +41,9 @@ if REPLICATE_API_TOKEN:
54
  os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN
55
 
56
  # Target settings for webtoon
57
- TARGET_EPISODES = 40 # 40화 완결
58
- PANELS_PER_EPISODE = 30 # 각 화당 30개 패널
59
- TARGET_PANELS = TARGET_EPISODES * PANELS_PER_EPISODE # 총 1200 패널
60
 
61
  # Webtoon genres
62
  WEBTOON_GENRES = {
@@ -71,7 +58,7 @@ WEBTOON_GENRES = {
71
  "스포츠": "Sports"
72
  }
73
 
74
- # Celebrity face references for character design - 영어 이름 매핑 추가
75
  CELEBRITY_FACES = {
76
  "male": [
77
  {"kr": "톰 크루즈", "en": "Tom Cruise"},
@@ -107,7 +94,7 @@ CELEBRITY_FACES = {
107
  ]
108
  }
109
 
110
- # --- Environment validation ---
111
  if not FIREWORKS_API_KEY:
112
  logger.error("FIREWORKS_API_KEY not set. Application will not work properly.")
113
  FIREWORKS_API_KEY = "dummy_token_for_testing"
@@ -115,13 +102,13 @@ if not FIREWORKS_API_KEY:
115
  if not REPLICATE_API_TOKEN:
116
  logger.warning("REPLICATE_API_TOKEN not set. Image generation will be disabled.")
117
 
118
- # --- Global variables ---
119
  db_lock = threading.Lock()
120
- generated_images_cache = {} # Cache for generated images
121
- panel_images_state = {} # Global state for panel images
122
- character_consistency_map = {} # 캐릭터 일관성 맵
123
 
124
- # --- Genre-specific prompts and elements ---
125
  GENRE_ELEMENTS = {
126
  "로맨스": {
127
  "key_elements": ["감정선", "오해와 화해", "달콤한 순간", "질투", "고백"],
@@ -187,10 +174,10 @@ class CharacterProfile:
187
  role: str
188
  personality: str
189
  appearance: str
190
- celebrity_lookalike_kr: str # 한글 이름
191
- celebrity_lookalike_en: str # 영어 이름
192
  gender: str
193
- detailed_appearance: str = "" # 상세 외모 설명
194
 
195
  @dataclass
196
  class WebtoonBible:
@@ -209,10 +196,10 @@ class WebtoonBible:
209
  class StoryboardPanel:
210
  """Individual storyboard panel with unique ID"""
211
  panel_number: int
212
- scene_type: str # wide, close-up, medium, establishing
213
- image_prompt: str # Image generation prompt with character descriptions
214
- image_prompt_en: str = "" # 영어 번역된 프롬프트
215
- panel_id: str = "" # Unique panel identifier
216
  dialogue: List[str] = field(default_factory=list)
217
  narration: str = ""
218
  sound_effects: List[str] = field(default_factory=list)
@@ -220,7 +207,7 @@ class StoryboardPanel:
220
  camera_angle: str = ""
221
  background: str = ""
222
  characters_in_scene: List[str] = field(default_factory=list)
223
- generated_image_url: str = "" # URL of generated image
224
 
225
  @dataclass
226
  class EpisodeStoryboard:
@@ -264,7 +251,6 @@ class WebtoonDatabase:
264
  conn.execute("PRAGMA journal_mode=WAL")
265
  cursor = conn.cursor()
266
 
267
- # Sessions table with genre and character consistency
268
  cursor.execute('''
269
  CREATE TABLE IF NOT EXISTS sessions (
270
  session_id TEXT PRIMARY KEY,
@@ -285,7 +271,6 @@ class WebtoonDatabase:
285
  )
286
  ''')
287
 
288
- # Storyboards table
289
  cursor.execute('''
290
  CREATE TABLE IF NOT EXISTS storyboards (
291
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -301,7 +286,6 @@ class WebtoonDatabase:
301
  )
302
  ''')
303
 
304
- # Panels table with image data and English prompts
305
  cursor.execute('''
306
  CREATE TABLE IF NOT EXISTS panels (
307
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -350,7 +334,6 @@ class WebtoonDatabase:
350
  with WebtoonDatabase.get_db() as conn:
351
  cursor = conn.cursor()
352
 
353
- # Save storyboard
354
  cursor.execute('''
355
  INSERT INTO storyboards (session_id, episode_number, title,
356
  storyboard_data, panel_count, status)
@@ -361,7 +344,6 @@ class WebtoonDatabase:
361
  json.dumps(asdict(storyboard)), len(storyboard.panels),
362
  storyboard.title, json.dumps(asdict(storyboard)), len(storyboard.panels)))
363
 
364
- # Save individual panels with English prompts
365
  for panel in storyboard.panels:
366
  cursor.execute('''
367
  INSERT INTO panels (session_id, episode_number, panel_number,
@@ -406,59 +388,82 @@ class ImageGenerator:
406
  self.generation_lock = Lock()
407
  self.active_generations = {}
408
 
409
- def enhance_prompt_for_webtoon(self, prompt: str, panel_number: int) -> str:
410
- """Enhance prompt for single-scene webtoon panel generation"""
411
- # 기본 스타일 접두사
412
- base_style = "Japanese animation style, webtoon panel, single scene, clear composition"
 
 
 
 
 
 
 
 
 
 
413
 
414
- # 단일 장면 강조 키워드 추가
415
- single_scene_emphasis = "ONE scene only, focused composition, no split scenes, no multiple moments"
416
 
417
- # 웹툰 특화 키워드
418
- webtoon_keywords = "vertical scroll webtoon style, manhwa art, clean lines, vibrant colors"
419
 
420
- # 카메라 앵글 강조
421
- camera_focus = "single camera angle, consistent perspective"
422
 
423
  # 최종 프롬프트 구성
424
- enhanced_prompt = f"{base_style}, {single_scene_emphasis}, {prompt}, {webtoon_keywords}, {camera_focus}"
425
 
426
- # 너무 긴 프롬프트 제한 (SDXL 권장 길이)
427
  if len(enhanced_prompt) > 500:
428
- # 핵심 부분만 유지
429
- enhanced_prompt = f"{base_style}, {prompt[:350]}, {single_scene_emphasis}"
430
 
431
  return enhanced_prompt
432
 
433
- def generate_image(self, prompt: str, panel_id: str, session_id: str, progress_callback=None) -> Dict[str, Any]:
434
- """Generate image using Replicate API with enhanced webtoon prompts"""
 
435
  try:
436
  if not REPLICATE_API_TOKEN:
437
  logger.warning("No Replicate API token, returning placeholder")
438
  return {"panel_id": panel_id, "status": "error", "message": "No API token"}
439
 
440
- logger.info(f"Generating image for panel {panel_id}")
441
 
442
  # 패널 번호 추출
443
  panel_number = int(panel_id.split('_panel')[1]) if '_panel' in panel_id else 1
444
 
445
- # 프롬프트 개선 (이미 영어로 번역된 프롬프트 사용)
446
- enhanced_prompt = self.enhance_prompt_for_webtoon(prompt, panel_number)
447
 
448
  logger.info(f"Enhanced prompt: {enhanced_prompt[:150]}...")
449
 
450
  if progress_callback:
451
  progress_callback(f"패널 {panel_number} 이미지 생성 중...")
452
 
453
- # Run the model with enhanced parameters
 
 
 
 
 
 
 
 
 
454
  input_params = {
455
  "prompt": enhanced_prompt,
456
- "negative_prompt": "multiple scenes, split screen, collage, montage, comic strip layout, multiple panels, confused composition, overlapping scenes, blurry, bad anatomy, ugly, deformed, text, watermark",
457
- "num_inference_steps": 30, # 품질 향상을 위해 증가
458
- "guidance_scale": 8.5, # 프롬프트 충실도 증가
459
- "width": 768, # 웹툰 세로형 비율
460
  "height": 1024,
461
- "scheduler": "K_EULER_ANCESTRAL" # 더 나은 결과를 위한 스케줄러
 
 
 
 
462
  }
463
 
464
  output = replicate.run(
@@ -466,11 +471,9 @@ class ImageGenerator:
466
  input=input_params
467
  )
468
 
469
- # Get the image URL
470
  if output and len(output) > 0:
471
  image_url = output[0] if isinstance(output[0], str) else str(output[0])
472
 
473
- # Cache the image
474
  cache_key = f"{session_id}_{panel_id}"
475
  generated_images_cache[cache_key] = image_url
476
 
@@ -498,7 +501,7 @@ class WebtoonSystem:
498
  self.tracker = WebtoonTracker()
499
  self.current_session_id = None
500
  self.image_generator = ImageGenerator()
501
- self.character_consistency_map = {} # 캐릭터 일관성 맵
502
  WebtoonDatabase.init_db()
503
 
504
  def create_headers(self):
@@ -524,7 +527,6 @@ class WebtoonSystem:
524
  celebrity = random.choice(available_celebrities)
525
  used_celebrities.append(celebrity)
526
 
527
- # 상세 외모 설명 생성
528
  detailed_appearance = f"{celebrity['en']} lookalike face, {char.get('appearance', '')}"
529
 
530
  profile = CharacterProfile(
@@ -541,7 +543,6 @@ class WebtoonSystem:
541
  profiles[profile.name] = profile
542
  self.tracker.add_character(profile)
543
 
544
- # 캐릭터 일관성 맵에 저장
545
  self.character_consistency_map[profile.name] = {
546
  'kr': f"{profile.name}({celebrity['kr']} 닮은 얼굴의 {gender})",
547
  'en': f"{profile.name} ({celebrity['en']} lookalike {gender})",
@@ -553,16 +554,14 @@ class WebtoonSystem:
553
  def translate_prompt_to_english(self, korean_prompt: str, character_profiles: Dict[str, CharacterProfile]) -> str:
554
  """한글 프롬프트를 영어로 번역"""
555
  try:
556
- # 캐릭터 이름을 영어 설명으로 대체
557
  english_prompt = korean_prompt
558
  for name, profile in character_profiles.items():
559
  korean_pattern = f"{name}\\([^)]+\\)"
560
  english_replacement = f"{name} ({profile.celebrity_lookalike_en} lookalike {profile.gender})"
561
  english_prompt = re.sub(korean_pattern, english_replacement, english_prompt)
562
 
563
- # LLM을 통한 번역
564
  translation_prompt = f"""Translate this Korean webtoon panel description to English.
565
- Keep character descriptions and maintain the visual details.
566
  Korean: {english_prompt}
567
  English translation:"""
568
 
@@ -572,10 +571,8 @@ English translation:"""
572
  return translated.strip()
573
  except Exception as e:
574
  logger.error(f"Translation error: {e}")
575
- # 폴백: 기본 번역 시도
576
- return korean_prompt # 번역 실패시 원본 반환
577
 
578
- # --- Prompt generation functions ---
579
  def create_planning_prompt(self, query: str, genre: str, language: str) -> str:
580
  """Create initial planning prompt for webtoon with character profiles"""
581
  genre_info = GENRE_ELEMENTS.get(genre, {})
@@ -685,10 +682,9 @@ Episode 40: [Title] - [Key event] - [Grand finale]
685
 
686
  def create_storyboard_prompt(self, episode_num: int, plot_outline: str,
687
  genre: str, language: str, character_profiles: Dict[str, CharacterProfile]) -> str:
688
- """Create prompt for episode storyboard with single-scene focus and character consistency"""
689
  genre_info = GENRE_ELEMENTS.get(genre, {})
690
 
691
- # Create character description string with consistency
692
  char_descriptions = "\n".join([
693
  f"- {name}: 항상 '{profile.celebrity_lookalike_kr} 닮은 얼굴'로 묘사. {profile.appearance}"
694
  for name, profile in character_profiles.items()
@@ -700,80 +696,72 @@ Episode 40: [Title] - [Key event] - [Grand finale]
700
  **장르:** {genre}
701
  **{episode_num}화 내용:** {self._extract_episode_plan(plot_outline, episode_num)}
702
 
703
- **캐릭터 일관성 규칙 (매우 중요!):**
704
  {char_descriptions}
705
 
706
- ⚠️ **절대 규칙**:
707
  1. 반드시 30개 패널을 모두 작성하세요!
708
- 2. 각 패널은 **단 하나의 장면**만 담아야 합니다!
709
- 3. 캐릭터가 나올 때마다 반드시 "캐릭터이름(유명인 닮은 얼굴의 성별)" 형식으로 작성!
710
- 4. 캐릭터 외모는 처음부터 끝까지 완전히 일관되게 유지!
711
- 5. 이미지 프롬프트는 구체적이고 시각적으로 명확하게!
712
 
713
- **패널 구성 지침:**
714
- - 30개 패널로 구성
715
- - 패널은 독립적인 단일 장면
716
- - 장르 특성: {', '.join(genre_info.get('panel_types', []))}
 
 
717
 
718
- **각 패널별로 다음을 포함하여 작성:**
719
 
720
  패널 1:
721
  - 샷 타입: [establishing/wide/medium/close_up/extreme_close_up 중 하나]
722
- - 이미지 프롬프트: [단일 장면, 캐릭터는 반드시 "이름(유명인 닮은 얼굴의 성별)" 형식]
723
  - 대사: [캐릭터 대사]
724
  - 나레이션: [해설]
725
  - 효과음: [효과음]
726
  - 배경: [구체적 배경]
727
 
728
- 패널 2:
729
- (동일한 형식, 캐릭터 일관성 유지)
730
-
731
  ...30개 패널 모두 작성
732
 
733
- 패널 30:
734
- (동일한 형식, 캐릭터 일관성 유지)
735
-
736
- ⚠️ 모든 패널에서 캐릭터 묘사를 일관되게 유지하세요!""",
737
 
738
  "English": f"""Create Episode {episode_num} storyboard with 30 panels.
739
 
740
  **Genre:** {genre}
741
  **Episode content:** {self._extract_episode_plan(plot_outline, episode_num)}
742
 
743
- **Character Consistency Rules (VERY IMPORTANT!):**
744
  {char_descriptions}
745
 
746
- ⚠️ **ABSOLUTE RULES**:
747
  1. Must write all 30 panels!
748
- 2. Each panel must contain **ONLY ONE SCENE**!
749
- 3. Always describe characters as "Name (celebrity lookalike face gender)"!
750
- 4. Keep character appearance completely consistent!
751
- 5. Image prompts must be specific and visually clear!
752
 
753
- **Panel composition:**
754
- - Total 30 panels
755
- - Each panel is independent single scene
756
- - Genre traits: {', '.join(genre_info.get('panel_types', []))}
 
 
757
 
758
- **For each panel include:**
759
 
760
  Panel 1:
761
  - Shot type: [one of: establishing/wide/medium/close_up/extreme_close_up]
762
- - Image prompt: [Single scene, characters as "Name (celebrity lookalike face gender)"]
763
  - Dialogue: [Character dialogue]
764
  - Narration: [Narration]
765
  - Sound effects: [Effects]
766
  - Background: [Specific background]
767
 
768
- Panel 2:
769
- (Same format, maintain character consistency)
770
-
771
  ...write all 30 panels
772
 
773
- Panel 30:
774
- (Same format, maintain character consistency)
775
-
776
- ⚠️ Keep character descriptions consistent across all panels!"""
777
  }
778
 
779
  return lang_prompts.get(language, lang_prompts["Korean"])
@@ -827,7 +815,6 @@ Panel 30:
827
  characters.append(current_char)
828
  break
829
  elif in_character_section and line.strip():
830
- # Parse character line
831
  if '성별:' in line or 'Gender:' in line:
832
  if current_char:
833
  characters.append(current_char)
@@ -836,14 +823,12 @@ Panel 30:
836
  if len(parts) >= 2:
837
  name = parts[0].strip().replace('주인공:', '').replace('캐릭터', '').strip()
838
 
839
- # Extract gender
840
- gender = 'male' # default
841
  if 'female' in line.lower() or '여' in line:
842
  gender = 'female'
843
  elif 'male' in line.lower() or '남' in line:
844
  gender = 'male'
845
 
846
- # Extract appearance
847
  appearance = ''
848
  for part in parts:
849
  if '외모:' in part or 'Appearance:' in part:
@@ -860,7 +845,6 @@ Panel 30:
860
  if current_char and current_char not in characters:
861
  characters.append(current_char)
862
 
863
- # Ensure at least 3 characters
864
  while len(characters) < 3:
865
  characters.append({
866
  'name': f'캐릭터{len(characters)+1}',
@@ -872,7 +856,6 @@ Panel 30:
872
 
873
  return characters
874
 
875
- # --- LLM call functions ---
876
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
877
  full_content = ""
878
  for chunk in self.call_llm_streaming(messages, role, language):
@@ -952,7 +935,7 @@ Panel 30:
952
  yield f"❌ Error occurred: {str(e)}"
953
 
954
  def get_system_prompts(self, language: str) -> Dict[str, str]:
955
- """System prompts for webtoon roles with single-scene emphasis"""
956
  base_prompts = {
957
  "Korean": {
958
  "planner": """당신은 한국 웹툰 시장을 완벽히 이해하는 웹툰 기획자입니다.
@@ -969,18 +952,20 @@ Panel 30:
969
  "storyboarder": """당신은 웹툰 스토리보드 전문가입니다.
970
  30개 패널로 한 화를 완벽하게 구성합니다.
971
  세로 스크롤에 최적화된 연출을 합니다.
972
- 각 패널마다 캐릭터의 유명인 닮은꼴 설정을 일관되게 포함합니다.
973
 
974
- ⚠️ 가장 중요한 원칙:
975
  1. 반드시 30개 패널을 모두 작성합니다.
976
- 2. 각 패널은 **단 하나의 장면**만 담습니다.
977
- 3. 캐릭터가 나올 때마다 반드시 "캐릭터이름(유명인 닮은 얼굴의 성별)" 형식을 유지합니다.
978
- 4. 모든 패널에서 캐릭터 외모를 완벽히 일관되게 유지합니다.""",
 
 
979
 
980
  "translator": """You are a professional translator specializing in webtoon and visual content.
981
  Translate Korean webtoon panel descriptions to English while maintaining:
982
- - Character names and descriptions
983
  - Visual details and camera angles
 
984
  - Emotional nuances
985
  - Keep celebrity lookalike descriptions consistent"""
986
  },
@@ -998,18 +983,20 @@ Clearly specify character genders and appearances.
998
 
999
  "storyboarder": """You are a webtoon storyboard specialist.
1000
  Perfectly compose one episode with 30 panels.
1001
- Include consistent celebrity lookalike descriptions for characters.
1002
 
1003
- ⚠️ Most important principles:
1004
  1. Must write all 30 panels.
1005
- 2. Each panel contains **ONLY ONE SCENE**.
1006
- 3. Always maintain "CharacterName (celebrity lookalike face gender)" format.
1007
- 4. Keep character appearances perfectly consistent across all panels.""",
 
 
1008
 
1009
  "translator": """You are a professional translator specializing in webtoon and visual content.
1010
  Translate Korean webtoon panel descriptions to English while maintaining:
1011
- - Character names and descriptions
1012
  - Visual details and camera angles
 
1013
  - Emotional nuances
1014
  - Keep celebrity lookalike descriptions consistent"""
1015
  }
@@ -1017,7 +1004,6 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1017
 
1018
  return base_prompts.get(language, base_prompts["Korean"])
1019
 
1020
- # --- Main process ---
1021
  def process_webtoon_stream(self, query: str, genre: str, language: str,
1022
  session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str, Dict], None, None]:
1023
  """Webtoon planning and storyboard generation process"""
@@ -1030,7 +1016,6 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1030
  else:
1031
  self.current_session_id = session_id
1032
 
1033
- # Phase 1 시작 알림
1034
  yield "", "", f"🎬 웹툰 기획안 작성 중... (40화 전체 구성 포함) - 장르: {genre}", self.current_session_id, {}
1035
 
1036
  planning_prompt = self.create_planning_prompt(query, genre, language)
@@ -1041,18 +1026,14 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1041
 
1042
  self.planning_doc = planning_doc
1043
 
1044
- # Parse characters and assign celebrity lookalikes
1045
  characters = self.parse_characters_from_planning(planning_doc)
1046
  character_profiles = self.assign_celebrity_lookalikes(characters)
1047
 
1048
- # Save character profiles and consistency map
1049
  WebtoonDatabase.save_character_profiles(self.current_session_id, character_profiles)
1050
  WebtoonDatabase.save_character_consistency(self.current_session_id, self.character_consistency_map)
1051
 
1052
- # 기획안 완성 시점
1053
  yield planning_doc, "", "✅ 기획안 완성! (40화 구성 완료)", self.current_session_id, character_profiles
1054
 
1055
- # Phase 2: 1화 스토리보드 생성
1056
  yield planning_doc, "", "🎨 1화 스토리보드 작성 중... (30개 패널)", self.current_session_id, character_profiles
1057
 
1058
  storyboard_prompt = self.create_storyboard_prompt(1, planning_doc, genre, language, character_profiles)
@@ -1061,13 +1042,10 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1061
  "storyboarder", language
1062
  )
1063
 
1064
- # Parse storyboard into structured format
1065
  storyboard = self.parse_storyboard(storyboard_content, 1, character_profiles)
1066
 
1067
- # Save to database
1068
  WebtoonDatabase.save_storyboard(self.current_session_id, 1, storyboard)
1069
 
1070
- # 최종 완료
1071
  yield planning_doc, storyboard_content, "🎉 완성! (기획안 + 1화 스토리보드)", self.current_session_id, character_profiles
1072
 
1073
  except Exception as e:
@@ -1075,7 +1053,7 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1075
  yield "", "", f"❌ 오류 발생: {e}", self.current_session_id, {}
1076
 
1077
  def parse_storyboard(self, content: str, episode_num: int, character_profiles: Dict[str, CharacterProfile]) -> EpisodeStoryboard:
1078
- """Parse storyboard text into structured format with English translation"""
1079
  storyboard = EpisodeStoryboard(episode_number=episode_num, title=f"{episode_num}화")
1080
 
1081
  panels = []
@@ -1086,7 +1064,6 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1086
  for line in lines:
1087
  if '패널' in line or 'Panel' in line:
1088
  if current_panel:
1089
- # Translate Korean prompt to English before saving
1090
  if current_panel.image_prompt and not current_panel.image_prompt_en:
1091
  current_panel.image_prompt_en = self.translate_prompt_to_english(
1092
  current_panel.image_prompt, character_profiles
@@ -1102,12 +1079,22 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1102
  panel_id=panel_id
1103
  )
1104
  elif current_panel:
1105
- if '이미지 프롬프트:' in line or 'Image prompt:' in line:
 
 
 
 
 
 
 
 
 
 
 
 
1106
  prompt = line.split(':', 1)[1].strip()
1107
- # 캐릭터 일관성 확인 및 적용
1108
  for char_name, consistency in self.character_consistency_map.items():
1109
  if char_name in prompt and consistency['kr'] not in prompt:
1110
- # 캐릭터 이름만 있고 설명이 없으면 추가
1111
  prompt = prompt.replace(char_name, consistency['kr'])
1112
  current_panel.image_prompt = prompt
1113
  elif '대사:' in line or 'Dialogue:' in line:
@@ -1120,11 +1107,8 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1120
  effects = line.split(':', 1)[1].strip()
1121
  if effects:
1122
  current_panel.sound_effects.append(effects)
1123
- elif '샷 타입:' in line or 'Shot type:' in line:
1124
- current_panel.scene_type = line.split(':', 1)[1].strip()
1125
 
1126
  if current_panel:
1127
- # Translate last panel
1128
  if current_panel.image_prompt and not current_panel.image_prompt_en:
1129
  current_panel.image_prompt_en = self.translate_prompt_to_english(
1130
  current_panel.image_prompt, character_profiles
@@ -1134,7 +1118,7 @@ Translate Korean webtoon panel descriptions to English while maintaining:
1134
  storyboard.panels = panels[:30]
1135
  return storyboard
1136
 
1137
- # --- Export functions ---
1138
  def export_planning_to_txt(planning_doc: str, genre: str, title: str = "") -> str:
1139
  """Export only planning document (40 episodes structure) to TXT"""
1140
  content = f"{'=' * 50}\n"
@@ -1213,148 +1197,150 @@ def generate_random_webtoon_theme(genre: str, language: str) -> str:
1213
  genre_themes = templates.get(genre, templates["로맨스"])
1214
  return random.choice(genre_themes)
1215
 
1216
- # --- Helper functions for panel parsing (continued) ---
1217
  def parse_storyboard_panels(storyboard_content, character_profiles=None):
1218
- """Parse storyboard content into structured panel data with English translation"""
1219
- if not storyboard_content:
1220
- print("No storyboard content provided")
1221
- return []
1222
-
1223
- panels = []
1224
- lines = storyboard_content.split('\n')
1225
- current_panel = None
1226
- panel_num = 0
1227
-
1228
- print(f"Parsing {len(lines)} lines of storyboard content")
1229
-
1230
- for i, line in enumerate(lines):
1231
- # 패널 감지
1232
- if any(keyword in line for keyword in ['패널', 'Panel']) and any(char.isdigit() for char in line):
1233
- if current_panel and current_panel.get('prompt'):
1234
- panels.append(current_panel)
1235
- print(f"Saved panel {current_panel['number']} with prompt: {current_panel['prompt'][:50]}...")
1236
-
1237
- # 패널 번호 추출
1238
- numbers = re.findall(r'\d+', line)
1239
- panel_num = int(numbers[0]) if numbers else panel_num + 1
1240
-
1241
- current_panel = {
1242
- 'number': panel_num,
1243
- 'shot': '',
1244
- 'prompt': '',
1245
- 'prompt_en': '', # 영어 번역 프롬프트
1246
- 'dialogue': '',
1247
- 'narration': '',
1248
- 'effects': '',
1249
- 'image_url': None
1250
- }
1251
- print(f"Started parsing panel {panel_num}")
1252
- elif current_panel:
1253
- low = line.lower()
1254
-
1255
- if '샷 타입' in line or 'shot type' in low:
1256
- if any(sep in line for sep in [':', ':', '-', '–', '—']):
1257
- current_panel['shot'] = re.split(r'[::\-–—]', line, maxsplit=1)[1].strip()
1258
-
1259
- # 이미지 프롬프트 감지
1260
- elif (('이미지' in line and ('프롬' in line or '설명' in line)) or
1261
- ('image prompt' in low) or
1262
- re.search(r'\bprompt\s*[::\-–—]', low)):
1263
- if any(sep in line for sep in [':', ':', '-', '–', '—']):
1264
- prompt_text = re.split(r'[::\-–—]', line, maxsplit=1)[1].strip()
1265
- if prompt_text:
1266
- current_panel['prompt'] = prompt_text
1267
- print(f"Found prompt for panel {panel_num}: {prompt_text[:50]}...")
1268
- elif i + 1 < len(lines):
1269
- next_line = lines[i + 1].strip()
1270
- if next_line and not any(k in next_line.lower() for k in ['샷', 'shot', '대사', 'dialogue', '나레이션', 'narration', '효과음', 'sound', '패널', 'panel', '배경', 'background']):
1271
- current_panel['prompt'] = next_line
1272
- print(f"Found prompt on next line for panel {panel_num}: {next_line[:50]}...")
1273
-
1274
- elif '대사' in line or 'dialogue' in low:
1275
- if any(sep in line for sep in [':', ':', '-', '–', '—']):
1276
- current_panel['dialogue'] = re.split(r'[::\-–—]', line, maxsplit=1)[1].strip()
1277
-
1278
- elif '나레이션' in line or 'narration' in low:
1279
- if any(sep in line for sep in [':', ':', '-', '–', '—']):
1280
- current_panel['narration'] = re.split(r'[::\-–—]', line, maxsplit=1)[1].strip()
1281
-
1282
- elif '효과음' in line or 'sound effect' in low:
1283
- if any(sep in line for sep in [':', ':', '-', '–', '—']):
1284
- current_panel['effects'] = re.split(r'[::\-–—]', line, maxsplit=1)[1].strip()
1285
-
1286
- # 마지막 패널 추가
1287
- if current_panel and current_panel.get('prompt'):
1288
- panels.append(current_panel)
1289
- print(f"Saved last panel {current_panel['number']}")
1290
-
1291
- # 프롬프트가 없는 패널 제거 및 번호 정리
1292
- valid_panels = []
1293
- for i, panel in enumerate(panels):
1294
- if panel.get('prompt'):
1295
- if panel['number'] == 0:
1296
- panel['number'] = i + 1
1297
- valid_panels.append(panel)
1298
-
1299
- print(f"Total parsed panels with prompts: {len(valid_panels)}")
1300
- return valid_panels[:30]
1301
-
1302
- # --- Gradio interface ---
1303
- # --- Gradio interface ---
1304
- def create_interface():
1305
- with gr.Blocks(theme=gr.themes.Soft(), title="K-Webtoon Storyboard Generator") as interface:
1306
- gr.HTML("""
1307
- <style>
1308
- .main-header {
1309
- text-align: center;
1310
- margin-bottom: 2rem;
1311
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1312
- padding: 2rem;
1313
- border-radius: 15px;
1314
- color: white;
1315
- }
1316
-
1317
- .header-title {
1318
- font-size: 3rem;
1319
- margin-bottom: 1rem;
1320
- text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
1321
- }
1322
-
1323
- .character-profile {
1324
- background: #f0f0f0;
1325
- padding: 10px;
1326
- margin: 5px 0;
1327
- border-radius: 8px;
1328
- }
1329
-
1330
- .panel-container {
1331
- border: 2px solid #ddd;
1332
- border-radius: 10px;
1333
- padding: 20px;
1334
- margin-bottom: 20px;
1335
- background: white;
1336
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
1337
- }
1338
 
1339
- .panel-header {
1340
- font-size: 18px;
1341
- font-weight: bold;
1342
- color: #764ba2;
1343
- margin-bottom: 15px;
1344
- border-bottom: 2px solid #764ba2;
1345
- padding-bottom: 10px;
1346
- }
1347
 
1348
- .episode-structure {
1349
- background: #f5f5f5;
1350
- padding: 15px;
1351
- border-radius: 8px;
1352
- margin: 10px 0;
1353
- max-height: 500px;
1354
- overflow-y: auto;
 
 
 
1355
  }
1356
- </style>
1357
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1358
  <div class="main-header">
1359
  <h1 class="header-title">🎨 K-Webtoon Storyboard Generator</h1>
1360
  <p class="header-subtitle">한국형 웹툰 기획 및 스토리보드 자동 생성 시스템</p>
@@ -1438,12 +1424,13 @@ def create_interface():
1438
  with gr.Tab("🎬 1화 스토리보드 (30패널)"):
1439
  gr.Markdown("""
1440
  ### 📋 1화 스토리보드 - 30개 패널
1441
- 패널의 텍스트를 편집할 있습니다. 편집 '적용' 버튼을 클릭하세요.
1442
  """)
1443
 
1444
  # Control buttons
1445
  with gr.Row():
1446
  apply_edits_btn = gr.Button("✅ 편집 내용 적용", variant="secondary")
 
1447
  generate_all_images_btn = gr.Button("🎨 모든 패널 이미지 생성", variant="primary", size="lg")
1448
  download_storyboard_btn = gr.Button("📥 스토리보드 다운로드", variant="secondary")
1449
  clear_images_btn = gr.Button("🗑️ 이미지 초기화", variant="secondary")
@@ -1454,22 +1441,23 @@ def create_interface():
1454
  # Editable storyboard display
1455
  storyboard_editor = gr.Textbox(
1456
  label="스토리보드 편집기",
1457
- lines=30,
1458
- max_lines=50,
1459
  interactive=True,
1460
- placeholder="스토리보드가 생성되면 여기에 표시됩니다. 자유롭게 편집하세요."
 
1461
  )
1462
 
1463
- # Panel images gallery
1464
- gr.Markdown("### 🖼️ 생성된 이미지")
1465
- images_gallery = gr.Gallery(
1466
- label="패널 이미지",
1467
- show_label=False,
1468
- elem_id="gallery",
1469
- columns=5,
1470
- rows=6,
1471
- height="auto"
1472
  )
 
 
 
1473
 
1474
  # Helper functions
1475
  def process_query(query, genre, language, session_id):
@@ -1491,7 +1479,7 @@ def create_interface():
1491
  html = "<h3>🎭 캐릭터 프로필</h3>"
1492
  for name, profile in profiles.items():
1493
  html += f"""
1494
- <div class="character-profile">
1495
  <strong>{name}</strong> - {profile.celebrity_lookalike_kr} 닮은 얼굴 ({profile.celebrity_lookalike_en})
1496
  <br>역할: {profile.role}
1497
  <br>성격: {profile.personality}
@@ -1500,63 +1488,107 @@ def create_interface():
1500
  """
1501
  return html
1502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1503
  def apply_edited_storyboard(edited_text, session_id, character_profiles):
1504
  """Parse and apply edited storyboard text"""
1505
  if not edited_text:
1506
- return [], "스토리보드가 비어있습니다."
1507
 
1508
- # Parse the edited text back into panel data
1509
- panel_data = []
1510
- lines = edited_text.split('\n')
1511
- current_panel = None
1512
 
1513
- for line in lines:
1514
- if '패널' in line and any(char.isdigit() for char in line):
1515
- if current_panel and current_panel.get('prompt'):
1516
- panel_data.append(current_panel)
1517
-
1518
- numbers = re.findall(r'\d+', line)
1519
- panel_num = int(numbers[0]) if numbers else len(panel_data) + 1
1520
-
1521
- current_panel = {
1522
- 'number': panel_num,
1523
- 'shot': '',
1524
- 'prompt': '',
1525
- 'prompt_en': '',
1526
- 'dialogue': '',
1527
- 'narration': '',
1528
- 'effects': '',
1529
- 'image_url': None
1530
- }
1531
- elif current_panel:
1532
- if '샷 타입:' in line or 'Shot type:' in line.lower():
1533
- current_panel['shot'] = line.split(':', 1)[1].strip() if ':' in line else ''
1534
- elif '이미지 프롬프트:' in line or 'Image prompt:' in line.lower():
1535
- current_panel['prompt'] = line.split(':', 1)[1].strip() if ':' in line else ''
1536
- elif '대사:' in line or 'Dialogue:' in line.lower():
1537
- current_panel['dialogue'] = line.split(':', 1)[1].strip() if ':' in line else ''
1538
- elif '나레이션:' in line or 'Narration:' in line.lower():
1539
- current_panel['narration'] = line.split(':', 1)[1].strip() if ':' in line else ''
1540
- elif '효과음:' in line or 'Sound effect:' in line.lower():
1541
- current_panel['effects'] = line.split(':', 1)[1].strip() if ':' in line else ''
1542
 
1543
- if current_panel and current_panel.get('prompt'):
1544
- panel_data.append(current_panel)
 
 
 
 
 
 
 
1545
 
1546
- return panel_data[:30], f"✅ {len(panel_data)}개 패널이 적용되었습니다."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1547
 
1548
- def generate_all_panel_images_from_editor(panel_data, session_id, character_profiles, webtoon_system, progress=gr.Progress()):
1549
- """Generate images for all panels from edited data"""
1550
  if not REPLICATE_API_TOKEN:
1551
- return [], gr.update(visible=True, value="⚠️ Replicate API 토큰이 설정되지 않았습니다.")
1552
 
1553
  if not panel_data:
1554
- return [], gr.update(visible=True, value="⚠️ 패널 데이터가 없습니다.")
1555
 
1556
  if not webtoon_system:
1557
  webtoon_system = WebtoonSystem()
1558
 
1559
- generated_images = []
1560
  total_panels = len(panel_data)
1561
  successful = 0
1562
  failed = 0
@@ -1566,28 +1598,22 @@ def create_interface():
1566
 
1567
  if panel.get('prompt'):
1568
  try:
1569
- # Translate to English if needed
1570
  if not panel.get('prompt_en'):
1571
  panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
1572
  panel['prompt'], character_profiles
1573
  )
1574
 
1575
- # Generate image
1576
  result = webtoon_system.image_generator.generate_image(
1577
  panel['prompt_en'],
1578
  f"ep1_panel{panel['number']}",
1579
- session_id
 
1580
  )
1581
 
1582
  if result['status'] == 'success':
1583
- # Download and convert image
1584
- import requests
1585
- from PIL import Image
1586
- from io import BytesIO
1587
-
1588
- response = requests.get(result['image_url'])
1589
- img = Image.open(BytesIO(response.content))
1590
- generated_images.append((img, f"Panel {panel['number']}"))
1591
  successful += 1
1592
  else:
1593
  failed += 1
@@ -1598,10 +1624,12 @@ def create_interface():
1598
  time.sleep(0.5) # Rate limiting
1599
 
1600
  progress(1.0, desc=f"완료! 성공: {successful}, 실패: {failed}")
1601
- return generated_images, gr.update(visible=False)
1602
 
1603
- def clear_all_images():
1604
- return []
 
 
1605
 
1606
  def handle_random_theme(genre, language):
1607
  return generate_random_webtoon_theme(genre, language)
@@ -1648,32 +1676,47 @@ def create_interface():
1648
  fn=lambda x: x,
1649
  inputs=[storyboard_state],
1650
  outputs=[storyboard_editor]
 
 
 
 
1651
  )
1652
 
1653
  # Apply edits button
1654
  apply_edits_btn.click(
1655
  fn=apply_edited_storyboard,
1656
  inputs=[storyboard_editor, current_session_id, character_profiles_state],
1657
- outputs=[panel_data_state, generation_progress]
1658
  ).then(
1659
  fn=lambda: gr.update(visible=True, value="편집 내용이 적용되었습니다."),
1660
  outputs=[generation_progress]
1661
  )
1662
 
 
 
 
 
 
 
 
 
 
 
1663
  # Generate all images
1664
  generate_all_images_btn.click(
1665
- fn=lambda: gr.update(visible=True, value="이미지 생성 시작..."),
1666
  outputs=[generation_progress]
1667
  ).then(
1668
- fn=generate_all_panel_images_from_editor,
1669
  inputs=[panel_data_state, current_session_id, character_profiles_state, webtoon_system],
1670
- outputs=[images_gallery, generation_progress]
1671
  )
1672
 
1673
  # Clear images
1674
  clear_images_btn.click(
1675
  fn=clear_all_images,
1676
- outputs=[images_gallery]
 
1677
  )
1678
 
1679
  # Random theme
 
28
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
29
  logger = logging.getLogger(__name__)
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # --- Environment variables and constants ---
32
  FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
33
  REPLICATE_API_TOKEN = os.getenv("REPLICATE_API_TOKEN", "")
 
41
  os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN
42
 
43
  # Target settings for webtoon
44
+ TARGET_EPISODES = 40
45
+ PANELS_PER_EPISODE = 30
46
+ TARGET_PANELS = TARGET_EPISODES * PANELS_PER_EPISODE
47
 
48
  # Webtoon genres
49
  WEBTOON_GENRES = {
 
58
  "스포츠": "Sports"
59
  }
60
 
61
+ # Celebrity face references for character design
62
  CELEBRITY_FACES = {
63
  "male": [
64
  {"kr": "톰 크루즈", "en": "Tom Cruise"},
 
94
  ]
95
  }
96
 
97
+ # Environment validation
98
  if not FIREWORKS_API_KEY:
99
  logger.error("FIREWORKS_API_KEY not set. Application will not work properly.")
100
  FIREWORKS_API_KEY = "dummy_token_for_testing"
 
102
  if not REPLICATE_API_TOKEN:
103
  logger.warning("REPLICATE_API_TOKEN not set. Image generation will be disabled.")
104
 
105
+ # Global variables
106
  db_lock = threading.Lock()
107
+ generated_images_cache = {}
108
+ panel_images_state = {}
109
+ character_consistency_map = {}
110
 
111
+ # Genre-specific prompts and elements
112
  GENRE_ELEMENTS = {
113
  "로맨스": {
114
  "key_elements": ["감정선", "오해와 화해", "달콤한 순간", "질투", "고백"],
 
174
  role: str
175
  personality: str
176
  appearance: str
177
+ celebrity_lookalike_kr: str
178
+ celebrity_lookalike_en: str
179
  gender: str
180
+ detailed_appearance: str = ""
181
 
182
  @dataclass
183
  class WebtoonBible:
 
196
  class StoryboardPanel:
197
  """Individual storyboard panel with unique ID"""
198
  panel_number: int
199
+ scene_type: str
200
+ image_prompt: str
201
+ image_prompt_en: str = ""
202
+ panel_id: str = ""
203
  dialogue: List[str] = field(default_factory=list)
204
  narration: str = ""
205
  sound_effects: List[str] = field(default_factory=list)
 
207
  camera_angle: str = ""
208
  background: str = ""
209
  characters_in_scene: List[str] = field(default_factory=list)
210
+ generated_image_url: str = ""
211
 
212
  @dataclass
213
  class EpisodeStoryboard:
 
251
  conn.execute("PRAGMA journal_mode=WAL")
252
  cursor = conn.cursor()
253
 
 
254
  cursor.execute('''
255
  CREATE TABLE IF NOT EXISTS sessions (
256
  session_id TEXT PRIMARY KEY,
 
271
  )
272
  ''')
273
 
 
274
  cursor.execute('''
275
  CREATE TABLE IF NOT EXISTS storyboards (
276
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
286
  )
287
  ''')
288
 
 
289
  cursor.execute('''
290
  CREATE TABLE IF NOT EXISTS panels (
291
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
334
  with WebtoonDatabase.get_db() as conn:
335
  cursor = conn.cursor()
336
 
 
337
  cursor.execute('''
338
  INSERT INTO storyboards (session_id, episode_number, title,
339
  storyboard_data, panel_count, status)
 
344
  json.dumps(asdict(storyboard)), len(storyboard.panels),
345
  storyboard.title, json.dumps(asdict(storyboard)), len(storyboard.panels)))
346
 
 
347
  for panel in storyboard.panels:
348
  cursor.execute('''
349
  INSERT INTO panels (session_id, episode_number, panel_number,
 
388
  self.generation_lock = Lock()
389
  self.active_generations = {}
390
 
391
+ def enhance_prompt_for_webtoon(self, prompt: str, panel_number: int, scene_type: str = "medium") -> str:
392
+ """Enhanced prompt for story-driven webtoon panel generation"""
393
+
394
+ # 스토리 중심의 프롬프트 구성
395
+ base_style = "professional webtoon art, Korean manhwa style, high quality digital art"
396
+
397
+ # 씬 타입별 강조점
398
+ scene_emphasis = {
399
+ "establishing": "wide environmental shot, detailed background, atmospheric scene",
400
+ "wide": "full scene view, multiple elements, environmental storytelling",
401
+ "medium": "balanced composition, character and environment, narrative focus",
402
+ "close_up": "emotional expression, detailed facial features, intimate moment",
403
+ "extreme_close_up": "dramatic detail, intense emotion, impactful moment"
404
+ }
405
 
406
+ # 웹툰 특화 스타일
407
+ webtoon_style = "clean line art, vibrant colors, dynamic composition, professional illustration"
408
 
409
+ # 스토리 중심 강조
410
+ story_focus = "narrative scene, story moment, sequential art panel"
411
 
412
+ # 타입 적용
413
+ scene_desc = scene_emphasis.get(scene_type, scene_emphasis["medium"])
414
 
415
  # 최종 프롬프트 구성
416
+ enhanced_prompt = f"{base_style}, {scene_desc}, {prompt}, {webtoon_style}, {story_focus}"
417
 
418
+ # 길이 제한
419
  if len(enhanced_prompt) > 500:
420
+ enhanced_prompt = f"{base_style}, {prompt[:350]}, {story_focus}"
 
421
 
422
  return enhanced_prompt
423
 
424
+ def generate_image(self, prompt: str, panel_id: str, session_id: str,
425
+ scene_type: str = "medium", progress_callback=None) -> Dict[str, Any]:
426
+ """Generate image using Replicate API with enhanced quality settings"""
427
  try:
428
  if not REPLICATE_API_TOKEN:
429
  logger.warning("No Replicate API token, returning placeholder")
430
  return {"panel_id": panel_id, "status": "error", "message": "No API token"}
431
 
432
+ logger.info(f"Generating image for panel {panel_id} - Scene type: {scene_type}")
433
 
434
  # 패널 번호 추출
435
  panel_number = int(panel_id.split('_panel')[1]) if '_panel' in panel_id else 1
436
 
437
+ # 프롬프트 개선 (스토리 중심)
438
+ enhanced_prompt = self.enhance_prompt_for_webtoon(prompt, panel_number, scene_type)
439
 
440
  logger.info(f"Enhanced prompt: {enhanced_prompt[:150]}...")
441
 
442
  if progress_callback:
443
  progress_callback(f"패널 {panel_number} 이미지 생성 중...")
444
 
445
+ # Enhanced negative prompt for better quality
446
+ negative_prompt = (
447
+ "text, watermark, signature, logo, writing, letters, words, "
448
+ "low quality, blurry, pixelated, distorted, deformed, "
449
+ "bad anatomy, bad proportions, extra limbs, missing limbs, "
450
+ "duplicate, multiple panels, split screen, collage, "
451
+ "comic strip layout, speech bubbles, dialogue boxes"
452
+ )
453
+
454
+ # Run SDXL with optimized parameters
455
  input_params = {
456
  "prompt": enhanced_prompt,
457
+ "negative_prompt": negative_prompt,
458
+ "num_inference_steps": 35, # 품질 향상
459
+ "guidance_scale": 9.0, # 프롬프트 충실도 향상
460
+ "width": 768, # 웹툰 세로형
461
  "height": 1024,
462
+ "scheduler": "DPMSolverMultistep", # 더 나은 품질
463
+ "refine": "expert_ensemble_refiner", # SDXL refiner 사용
464
+ "high_noise_frac": 0.8,
465
+ "prompt_strength": 0.9,
466
+ "num_outputs": 1
467
  }
468
 
469
  output = replicate.run(
 
471
  input=input_params
472
  )
473
 
 
474
  if output and len(output) > 0:
475
  image_url = output[0] if isinstance(output[0], str) else str(output[0])
476
 
 
477
  cache_key = f"{session_id}_{panel_id}"
478
  generated_images_cache[cache_key] = image_url
479
 
 
501
  self.tracker = WebtoonTracker()
502
  self.current_session_id = None
503
  self.image_generator = ImageGenerator()
504
+ self.character_consistency_map = {}
505
  WebtoonDatabase.init_db()
506
 
507
  def create_headers(self):
 
527
  celebrity = random.choice(available_celebrities)
528
  used_celebrities.append(celebrity)
529
 
 
530
  detailed_appearance = f"{celebrity['en']} lookalike face, {char.get('appearance', '')}"
531
 
532
  profile = CharacterProfile(
 
543
  profiles[profile.name] = profile
544
  self.tracker.add_character(profile)
545
 
 
546
  self.character_consistency_map[profile.name] = {
547
  'kr': f"{profile.name}({celebrity['kr']} 닮은 얼굴의 {gender})",
548
  'en': f"{profile.name} ({celebrity['en']} lookalike {gender})",
 
554
  def translate_prompt_to_english(self, korean_prompt: str, character_profiles: Dict[str, CharacterProfile]) -> str:
555
  """한글 프롬프트를 영어로 번역"""
556
  try:
 
557
  english_prompt = korean_prompt
558
  for name, profile in character_profiles.items():
559
  korean_pattern = f"{name}\\([^)]+\\)"
560
  english_replacement = f"{name} ({profile.celebrity_lookalike_en} lookalike {profile.gender})"
561
  english_prompt = re.sub(korean_pattern, english_replacement, english_prompt)
562
 
 
563
  translation_prompt = f"""Translate this Korean webtoon panel description to English.
564
+ Focus on visual elements and actions, not just characters.
565
  Korean: {english_prompt}
566
  English translation:"""
567
 
 
571
  return translated.strip()
572
  except Exception as e:
573
  logger.error(f"Translation error: {e}")
574
+ return korean_prompt
 
575
 
 
576
  def create_planning_prompt(self, query: str, genre: str, language: str) -> str:
577
  """Create initial planning prompt for webtoon with character profiles"""
578
  genre_info = GENRE_ELEMENTS.get(genre, {})
 
682
 
683
  def create_storyboard_prompt(self, episode_num: int, plot_outline: str,
684
  genre: str, language: str, character_profiles: Dict[str, CharacterProfile]) -> str:
685
+ """Create prompt for episode storyboard with story-driven focus"""
686
  genre_info = GENRE_ELEMENTS.get(genre, {})
687
 
 
688
  char_descriptions = "\n".join([
689
  f"- {name}: 항상 '{profile.celebrity_lookalike_kr} 닮은 얼굴'로 묘사. {profile.appearance}"
690
  for name, profile in character_profiles.items()
 
696
  **장르:** {genre}
697
  **{episode_num}화 내용:** {self._extract_episode_plan(plot_outline, episode_num)}
698
 
699
+ **캐릭터 일관성 규칙:**
700
  {char_descriptions}
701
 
702
+ ⚠️ **절대 규칙 - 스토리 중심 전개**:
703
  1. 반드시 30개 패널을 모두 작성하세요!
704
+ 2. 각 패널은 **스토리 전개의 순간**을 담아야 합니다!
705
+ 3. 단순 인물 샷이 아닌 **사건과 행동 중심**으로 구성!
706
+ 4. 배경, 상황, 액션을 구체적으로 묘사!
707
+ 5. 캐릭터가 나올 때마다 "캐릭터이름(유명인 닮은 얼굴)" 형식 유지!
708
 
709
+ **패널 구성 가이드:**
710
+ - establishing shot (전체 상황/배경): 4-5개
711
+ - wide shot (전신/환경): 8-10개
712
+ - medium shot (상반신/대화): 10-12개
713
+ - close-up (얼굴/감정): 5-6개
714
+ - extreme close-up (디테일): 2-3개
715
 
716
+ **각 패널 작성 형식:**
717
 
718
  패널 1:
719
  - 샷 타입: [establishing/wide/medium/close_up/extreme_close_up 중 하나]
720
+ - 이미지 프롬프트: [구체적인 장면 묘사 - 배경, 행동, 분위기 포함]
721
  - 대사: [캐릭터 대사]
722
  - 나레이션: [해설]
723
  - 효과음: [효과음]
724
  - 배경: [구체적 배경]
725
 
 
 
 
726
  ...30개 패널 모두 작성
727
 
728
+ ⚠️ 스토리 진행이 자연스럽게 이어지도록 구성하세요!""",
 
 
 
729
 
730
  "English": f"""Create Episode {episode_num} storyboard with 30 panels.
731
 
732
  **Genre:** {genre}
733
  **Episode content:** {self._extract_episode_plan(plot_outline, episode_num)}
734
 
735
+ **Character Consistency Rules:**
736
  {char_descriptions}
737
 
738
+ ⚠️ **ABSOLUTE RULES - Story-Driven Focus**:
739
  1. Must write all 30 panels!
740
+ 2. Each panel must capture **a moment in story progression**!
741
+ 3. Focus on **events and actions**, not just character shots!
742
+ 4. Describe backgrounds, situations, actions specifically!
743
+ 5. Always maintain "Name (celebrity lookalike)" format!
744
 
745
+ **Panel Composition Guide:**
746
+ - establishing shot (overall scene/setting): 4-5 panels
747
+ - wide shot (full body/environment): 8-10 panels
748
+ - medium shot (upper body/dialogue): 10-12 panels
749
+ - close-up (face/emotion): 5-6 panels
750
+ - extreme close-up (detail): 2-3 panels
751
 
752
+ **Panel format:**
753
 
754
  Panel 1:
755
  - Shot type: [one of: establishing/wide/medium/close_up/extreme_close_up]
756
+ - Image prompt: [Specific scene description - include background, action, atmosphere]
757
  - Dialogue: [Character dialogue]
758
  - Narration: [Narration]
759
  - Sound effects: [Effects]
760
  - Background: [Specific background]
761
 
 
 
 
762
  ...write all 30 panels
763
 
764
+ ⚠️ Ensure natural story flow across panels!"""
 
 
 
765
  }
766
 
767
  return lang_prompts.get(language, lang_prompts["Korean"])
 
815
  characters.append(current_char)
816
  break
817
  elif in_character_section and line.strip():
 
818
  if '성별:' in line or 'Gender:' in line:
819
  if current_char:
820
  characters.append(current_char)
 
823
  if len(parts) >= 2:
824
  name = parts[0].strip().replace('주인공:', '').replace('캐릭터', '').strip()
825
 
826
+ gender = 'male'
 
827
  if 'female' in line.lower() or '여' in line:
828
  gender = 'female'
829
  elif 'male' in line.lower() or '남' in line:
830
  gender = 'male'
831
 
 
832
  appearance = ''
833
  for part in parts:
834
  if '외모:' in part or 'Appearance:' in part:
 
845
  if current_char and current_char not in characters:
846
  characters.append(current_char)
847
 
 
848
  while len(characters) < 3:
849
  characters.append({
850
  'name': f'캐릭터{len(characters)+1}',
 
856
 
857
  return characters
858
 
 
859
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
860
  full_content = ""
861
  for chunk in self.call_llm_streaming(messages, role, language):
 
935
  yield f"❌ Error occurred: {str(e)}"
936
 
937
  def get_system_prompts(self, language: str) -> Dict[str, str]:
938
+ """System prompts for webtoon roles with story-driven emphasis"""
939
  base_prompts = {
940
  "Korean": {
941
  "planner": """당신은 한국 웹툰 시장을 완벽히 이해하는 웹툰 기획자입니다.
 
952
  "storyboarder": """당신은 웹툰 스토리보드 전문가입니다.
953
  30개 패널로 한 화를 완벽하게 구성합니다.
954
  세로 스크롤에 최적화된 연출을 합니다.
 
955
 
956
+ ⚠️ 가장 중요한 원칙 - 스토리 중심 전개:
957
  1. 반드시 30개 패널을 모두 작성합니다.
958
+ 2. 각 패널은 **스토리 진행의 순간**을 담습니다.
959
+ 3. 단순 인물 샷이 아닌 **사건, 행동, 상황 중심**으로 구성합니다.
960
+ 4. 배경과 환경을 구체적으로 묘사합니다.
961
+ 5. 다양한 샷 타입을 활용하여 역동적으로 구성합니다.
962
+ 6. 캐릭터가 나올 때마다 "캐릭터이름(유명인 닮은 얼굴)" 형식을 유지합니다.""",
963
 
964
  "translator": """You are a professional translator specializing in webtoon and visual content.
965
  Translate Korean webtoon panel descriptions to English while maintaining:
966
+ - Focus on actions and scenes, not just characters
967
  - Visual details and camera angles
968
+ - Environmental descriptions
969
  - Emotional nuances
970
  - Keep celebrity lookalike descriptions consistent"""
971
  },
 
983
 
984
  "storyboarder": """You are a webtoon storyboard specialist.
985
  Perfectly compose one episode with 30 panels.
 
986
 
987
+ ⚠️ Most important principles - Story-Driven Focus:
988
  1. Must write all 30 panels.
989
+ 2. Each panel captures **a moment in story progression**.
990
+ 3. Focus on **events, actions, situations**, not just character shots.
991
+ 4. Describe backgrounds and environments specifically.
992
+ 5. Use varied shot types for dynamic composition.
993
+ 6. Always maintain "CharacterName (celebrity lookalike)" format.""",
994
 
995
  "translator": """You are a professional translator specializing in webtoon and visual content.
996
  Translate Korean webtoon panel descriptions to English while maintaining:
997
+ - Focus on actions and scenes, not just characters
998
  - Visual details and camera angles
999
+ - Environmental descriptions
1000
  - Emotional nuances
1001
  - Keep celebrity lookalike descriptions consistent"""
1002
  }
 
1004
 
1005
  return base_prompts.get(language, base_prompts["Korean"])
1006
 
 
1007
  def process_webtoon_stream(self, query: str, genre: str, language: str,
1008
  session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str, Dict], None, None]:
1009
  """Webtoon planning and storyboard generation process"""
 
1016
  else:
1017
  self.current_session_id = session_id
1018
 
 
1019
  yield "", "", f"🎬 웹툰 기획안 작성 중... (40화 전체 구성 포함) - 장르: {genre}", self.current_session_id, {}
1020
 
1021
  planning_prompt = self.create_planning_prompt(query, genre, language)
 
1026
 
1027
  self.planning_doc = planning_doc
1028
 
 
1029
  characters = self.parse_characters_from_planning(planning_doc)
1030
  character_profiles = self.assign_celebrity_lookalikes(characters)
1031
 
 
1032
  WebtoonDatabase.save_character_profiles(self.current_session_id, character_profiles)
1033
  WebtoonDatabase.save_character_consistency(self.current_session_id, self.character_consistency_map)
1034
 
 
1035
  yield planning_doc, "", "✅ 기획안 완성! (40화 구성 완료)", self.current_session_id, character_profiles
1036
 
 
1037
  yield planning_doc, "", "🎨 1화 스토리보드 작성 중... (30개 패널)", self.current_session_id, character_profiles
1038
 
1039
  storyboard_prompt = self.create_storyboard_prompt(1, planning_doc, genre, language, character_profiles)
 
1042
  "storyboarder", language
1043
  )
1044
 
 
1045
  storyboard = self.parse_storyboard(storyboard_content, 1, character_profiles)
1046
 
 
1047
  WebtoonDatabase.save_storyboard(self.current_session_id, 1, storyboard)
1048
 
 
1049
  yield planning_doc, storyboard_content, "🎉 완성! (기획안 + 1화 스토리보드)", self.current_session_id, character_profiles
1050
 
1051
  except Exception as e:
 
1053
  yield "", "", f"❌ 오류 발생: {e}", self.current_session_id, {}
1054
 
1055
  def parse_storyboard(self, content: str, episode_num: int, character_profiles: Dict[str, CharacterProfile]) -> EpisodeStoryboard:
1056
+ """Parse storyboard text into structured format with scene types"""
1057
  storyboard = EpisodeStoryboard(episode_number=episode_num, title=f"{episode_num}화")
1058
 
1059
  panels = []
 
1064
  for line in lines:
1065
  if '패널' in line or 'Panel' in line:
1066
  if current_panel:
 
1067
  if current_panel.image_prompt and not current_panel.image_prompt_en:
1068
  current_panel.image_prompt_en = self.translate_prompt_to_english(
1069
  current_panel.image_prompt, character_profiles
 
1079
  panel_id=panel_id
1080
  )
1081
  elif current_panel:
1082
+ if ' 타입:' in line or 'Shot type:' in line:
1083
+ shot = line.split(':', 1)[1].strip().lower()
1084
+ if 'establishing' in shot:
1085
+ current_panel.scene_type = "establishing"
1086
+ elif 'wide' in shot:
1087
+ current_panel.scene_type = "wide"
1088
+ elif 'close' in shot and 'extreme' in shot:
1089
+ current_panel.scene_type = "extreme_close_up"
1090
+ elif 'close' in shot:
1091
+ current_panel.scene_type = "close_up"
1092
+ else:
1093
+ current_panel.scene_type = "medium"
1094
+ elif '이미지 프롬프트:' in line or 'Image prompt:' in line:
1095
  prompt = line.split(':', 1)[1].strip()
 
1096
  for char_name, consistency in self.character_consistency_map.items():
1097
  if char_name in prompt and consistency['kr'] not in prompt:
 
1098
  prompt = prompt.replace(char_name, consistency['kr'])
1099
  current_panel.image_prompt = prompt
1100
  elif '대사:' in line or 'Dialogue:' in line:
 
1107
  effects = line.split(':', 1)[1].strip()
1108
  if effects:
1109
  current_panel.sound_effects.append(effects)
 
 
1110
 
1111
  if current_panel:
 
1112
  if current_panel.image_prompt and not current_panel.image_prompt_en:
1113
  current_panel.image_prompt_en = self.translate_prompt_to_english(
1114
  current_panel.image_prompt, character_profiles
 
1118
  storyboard.panels = panels[:30]
1119
  return storyboard
1120
 
1121
+ # Export functions
1122
  def export_planning_to_txt(planning_doc: str, genre: str, title: str = "") -> str:
1123
  """Export only planning document (40 episodes structure) to TXT"""
1124
  content = f"{'=' * 50}\n"
 
1197
  genre_themes = templates.get(genre, templates["로맨스"])
1198
  return random.choice(genre_themes)
1199
 
1200
+ # Parse storyboard panels for display
1201
  def parse_storyboard_panels(storyboard_content, character_profiles=None):
1202
+ """Parse storyboard content into structured panel data"""
1203
+ if not storyboard_content:
1204
+ return []
1205
+
1206
+ panels = []
1207
+ lines = storyboard_content.split('\n')
1208
+ current_panel = None
1209
+ panel_num = 0
1210
+
1211
+ for i, line in enumerate(lines):
1212
+ if any(keyword in line for keyword in ['패널', 'Panel']) and any(char.isdigit() for char in line):
1213
+ if current_panel and current_panel.get('prompt'):
1214
+ panels.append(current_panel)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1215
 
1216
+ numbers = re.findall(r'\d+', line)
1217
+ panel_num = int(numbers[0]) if numbers else panel_num + 1
 
 
 
 
 
 
1218
 
1219
+ current_panel = {
1220
+ 'number': panel_num,
1221
+ 'shot': '',
1222
+ 'prompt': '',
1223
+ 'prompt_en': '',
1224
+ 'dialogue': '',
1225
+ 'narration': '',
1226
+ 'effects': '',
1227
+ 'image_url': None,
1228
+ 'scene_type': 'medium'
1229
  }
1230
+ elif current_panel:
1231
+ if '샷 타입:' in line or 'Shot type:' in line.lower():
1232
+ shot = line.split(':', 1)[1].strip() if ':' in line else ''
1233
+ current_panel['shot'] = shot
1234
+ # Determine scene type
1235
+ if 'establishing' in shot.lower():
1236
+ current_panel['scene_type'] = 'establishing'
1237
+ elif 'wide' in shot.lower():
1238
+ current_panel['scene_type'] = 'wide'
1239
+ elif 'extreme' in shot.lower() and 'close' in shot.lower():
1240
+ current_panel['scene_type'] = 'extreme_close_up'
1241
+ elif 'close' in shot.lower():
1242
+ current_panel['scene_type'] = 'close_up'
1243
+ else:
1244
+ current_panel['scene_type'] = 'medium'
1245
+ elif '이미지 프롬프트:' in line or 'Image prompt:' in line.lower():
1246
+ current_panel['prompt'] = line.split(':', 1)[1].strip() if ':' in line else ''
1247
+ elif '대사:' in line or 'Dialogue:' in line.lower():
1248
+ current_panel['dialogue'] = line.split(':', 1)[1].strip() if ':' in line else ''
1249
+ elif '나레이션:' in line or 'Narration:' in line.lower():
1250
+ current_panel['narration'] = line.split(':', 1)[1].strip() if ':' in line else ''
1251
+ elif '효과음:' in line or 'Sound effect:' in line.lower():
1252
+ current_panel['effects'] = line.split(':', 1)[1].strip() if ':' in line else ''
1253
+
1254
+ if current_panel and current_panel.get('prompt'):
1255
+ panels.append(current_panel)
1256
+
1257
+ return panels[:30]
1258
+
1259
+ # Gradio interface with side-by-side panel layout
1260
+ def create_interface():
1261
+ with gr.Blocks(theme=gr.themes.Soft(), title="K-Webtoon Storyboard Generator", css="""
1262
+ .main-header {
1263
+ text-align: center;
1264
+ margin-bottom: 2rem;
1265
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1266
+ padding: 2rem;
1267
+ border-radius: 15px;
1268
+ color: white;
1269
+ }
1270
+ .header-title {
1271
+ font-size: 3rem;
1272
+ margin-bottom: 1rem;
1273
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
1274
+ }
1275
+ .panel-container {
1276
+ display: grid;
1277
+ grid-template-columns: 1fr 1fr;
1278
+ gap: 20px;
1279
+ margin: 20px 0;
1280
+ }
1281
+ .panel-text-box {
1282
+ border: 2px solid #e0e0e0;
1283
+ border-radius: 10px;
1284
+ padding: 15px;
1285
+ background: #f9f9f9;
1286
+ height: 400px;
1287
+ overflow-y: auto;
1288
+ }
1289
+ .panel-image-box {
1290
+ border: 2px solid #e0e0e0;
1291
+ border-radius: 10px;
1292
+ padding: 15px;
1293
+ background: white;
1294
+ height: 400px;
1295
+ display: flex;
1296
+ align-items: center;
1297
+ justify-content: center;
1298
+ }
1299
+ .panel-header {
1300
+ font-size: 18px;
1301
+ font-weight: bold;
1302
+ color: #764ba2;
1303
+ margin-bottom: 10px;
1304
+ border-bottom: 2px solid #764ba2;
1305
+ padding-bottom: 5px;
1306
+ }
1307
+ .panel-content {
1308
+ font-size: 14px;
1309
+ line-height: 1.8;
1310
+ }
1311
+ .panel-image {
1312
+ max-width: 100%;
1313
+ max-height: 100%;
1314
+ object-fit: contain;
1315
+ border-radius: 8px;
1316
+ }
1317
+ .shot-type {
1318
+ display: inline-block;
1319
+ background: #764ba2;
1320
+ color: white;
1321
+ padding: 2px 8px;
1322
+ border-radius: 4px;
1323
+ font-size: 12px;
1324
+ margin-bottom: 5px;
1325
+ }
1326
+ .panel-dialogue {
1327
+ background: #fff;
1328
+ padding: 8px;
1329
+ border-left: 3px solid #667eea;
1330
+ margin: 5px 0;
1331
+ }
1332
+ .panel-narration {
1333
+ font-style: italic;
1334
+ color: #666;
1335
+ margin: 5px 0;
1336
+ }
1337
+ .panel-effects {
1338
+ color: #ff6b6b;
1339
+ font-weight: bold;
1340
+ margin: 5px 0;
1341
+ }
1342
+ """) as interface:
1343
+ gr.HTML("""
1344
  <div class="main-header">
1345
  <h1 class="header-title">🎨 K-Webtoon Storyboard Generator</h1>
1346
  <p class="header-subtitle">한국형 웹툰 기획 및 스토리보드 자동 생성 시스템</p>
 
1424
  with gr.Tab("🎬 1화 스토리보드 (30패널)"):
1425
  gr.Markdown("""
1426
  ### 📋 1화 스토리보드 - 30개 패널
1427
+ 패널은 스토리 전개에 따라 구성되며, 좌측에 텍스트, 우측에 이미지가 표시됩니다.
1428
  """)
1429
 
1430
  # Control buttons
1431
  with gr.Row():
1432
  apply_edits_btn = gr.Button("✅ 편집 내용 적용", variant="secondary")
1433
+ generate_selected_btn = gr.Button("🎨 선택한 패널 이미지 생성", variant="secondary")
1434
  generate_all_images_btn = gr.Button("🎨 모든 패널 이미지 생성", variant="primary", size="lg")
1435
  download_storyboard_btn = gr.Button("📥 스토리보드 다운로드", variant="secondary")
1436
  clear_images_btn = gr.Button("🗑️ 이미지 초기화", variant="secondary")
 
1441
  # Editable storyboard display
1442
  storyboard_editor = gr.Textbox(
1443
  label="스토리보드 편집기",
1444
+ lines=15,
1445
+ max_lines=30,
1446
  interactive=True,
1447
+ placeholder="스토리보드가 생성되면 여기에 표시됩니다. 자유롭게 편집하세요.",
1448
+ visible=False
1449
  )
1450
 
1451
+ # Panel selector
1452
+ panel_selector = gr.CheckboxGroup(
1453
+ label="이미지 생성할 패널 선택",
1454
+ choices=[f"패널 {i}" for i in range(1, 31)],
1455
+ value=[],
1456
+ visible=False
 
 
 
1457
  )
1458
+
1459
+ # Panels display area with side-by-side layout
1460
+ panels_display = gr.HTML(label="패널 표시", value="<p>스토리보드를 생성하면 여기에 패널이 표시됩니다.</p>")
1461
 
1462
  # Helper functions
1463
  def process_query(query, genre, language, session_id):
 
1479
  html = "<h3>🎭 캐릭터 프로필</h3>"
1480
  for name, profile in profiles.items():
1481
  html += f"""
1482
+ <div style="background: #f0f0f0; padding: 10px; margin: 5px 0; border-radius: 8px;">
1483
  <strong>{name}</strong> - {profile.celebrity_lookalike_kr} 닮은 얼굴 ({profile.celebrity_lookalike_en})
1484
  <br>역할: {profile.role}
1485
  <br>성격: {profile.personality}
 
1488
  """
1489
  return html
1490
 
1491
+ def display_panels_side_by_side(panel_data):
1492
+ """Display panels in side-by-side format"""
1493
+ if not panel_data:
1494
+ return "<p>패널 데이터가 없습니다. 스토리보드를 먼저 생성하세요.</p>"
1495
+
1496
+ html = ""
1497
+ for panel in panel_data:
1498
+ html += f"""
1499
+ <div class="panel-container">
1500
+ <div class="panel-text-box">
1501
+ <div class="panel-header">패널 {panel['number']}</div>
1502
+ <div class="panel-content">
1503
+ <span class="shot-type">{panel.get('shot', 'medium')}</span>
1504
+ <div><strong>장면:</strong> {panel.get('prompt', '')}</div>
1505
+ {f'<div class="panel-dialogue"><strong>대사:</strong> {panel["dialogue"]}</div>' if panel.get('dialogue') else ''}
1506
+ {f'<div class="panel-narration"><strong>나레이션:</strong> {panel["narration"]}</div>' if panel.get('narration') else ''}
1507
+ {f'<div class="panel-effects"><strong>효과음:</strong> {panel["effects"]}</div>' if panel.get('effects') else ''}
1508
+ </div>
1509
+ </div>
1510
+ <div class="panel-image-box">
1511
+ {f'<img src="{panel["image_url"]}" class="panel-image" alt="Panel {panel["number"]}" />' if panel.get('image_url') else '<p style="color: #999;">이미지가 아직 생성되지 않았습니다</p>'}
1512
+ </div>
1513
+ </div>
1514
+ """
1515
+ return html
1516
+
1517
  def apply_edited_storyboard(edited_text, session_id, character_profiles):
1518
  """Parse and apply edited storyboard text"""
1519
  if not edited_text:
1520
+ return [], "<p>스토리보드가 비어있습니다.</p>", gr.update(visible=False), gr.update(visible=False)
1521
 
1522
+ panel_data = parse_storyboard_panels(edited_text, character_profiles)
 
 
 
1523
 
1524
+ if panel_data:
1525
+ html = display_panels_side_by_side(panel_data)
1526
+ panel_choices = [f"패널 {p['number']}" for p in panel_data]
1527
+ return panel_data, html, gr.update(visible=True, choices=panel_choices, value=[]), gr.update(visible=True, value=edited_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1528
 
1529
+ return [], "<p>패널을 파싱할 수 없습니다.</p>", gr.update(visible=False), gr.update(visible=True)
1530
+
1531
+ def generate_selected_panel_images(panel_data, selected_panels, session_id, character_profiles, webtoon_system, progress=gr.Progress()):
1532
+ """Generate images for selected panels only"""
1533
+ if not REPLICATE_API_TOKEN:
1534
+ return display_panels_side_by_side(panel_data), gr.update(visible=True, value="⚠️ Replicate API 토큰이 설정되지 않았습니다.")
1535
+
1536
+ if not panel_data:
1537
+ return display_panels_side_by_side(panel_data), gr.update(visible=True, value="⚠️ 패널 데이터가 없습니다.")
1538
 
1539
+ if not selected_panels:
1540
+ return display_panels_side_by_side(panel_data), gr.update(visible=True, value="⚠️ 생성할 패널을 선택하세요.")
1541
+
1542
+ if not webtoon_system:
1543
+ webtoon_system = WebtoonSystem()
1544
+
1545
+ selected_numbers = [int(p.split()[1]) for p in selected_panels]
1546
+ total = len(selected_numbers)
1547
+ successful = 0
1548
+
1549
+ for i, panel in enumerate(panel_data):
1550
+ if panel['number'] in selected_numbers:
1551
+ idx = selected_numbers.index(panel['number'])
1552
+ progress((idx / total), desc=f"패널 {panel['number']} 생성 중...")
1553
+
1554
+ if panel.get('prompt'):
1555
+ try:
1556
+ # Translate if needed
1557
+ if not panel.get('prompt_en'):
1558
+ panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
1559
+ panel['prompt'], character_profiles
1560
+ )
1561
+
1562
+ # Generate with scene type
1563
+ result = webtoon_system.image_generator.generate_image(
1564
+ panel['prompt_en'],
1565
+ f"ep1_panel{panel['number']}",
1566
+ session_id,
1567
+ scene_type=panel.get('scene_type', 'medium')
1568
+ )
1569
+
1570
+ if result['status'] == 'success':
1571
+ panel['image_url'] = result['image_url']
1572
+ successful += 1
1573
+ except Exception as e:
1574
+ logger.error(f"Error generating panel {panel['number']}: {e}")
1575
+
1576
+ time.sleep(0.5)
1577
+
1578
+ progress(1.0, desc=f"완료! {successful}/{total} 패널 생성 성공")
1579
+ return display_panels_side_by_side(panel_data), gr.update(visible=False)
1580
 
1581
+ def generate_all_panel_images(panel_data, session_id, character_profiles, webtoon_system, progress=gr.Progress()):
1582
+ """Generate images for all panels"""
1583
  if not REPLICATE_API_TOKEN:
1584
+ return display_panels_side_by_side(panel_data), gr.update(visible=True, value="⚠️ Replicate API 토큰이 설정되지 않았습니다.")
1585
 
1586
  if not panel_data:
1587
+ return display_panels_side_by_side(panel_data), gr.update(visible=True, value="⚠️ 패널 데이터가 없습니다.")
1588
 
1589
  if not webtoon_system:
1590
  webtoon_system = WebtoonSystem()
1591
 
 
1592
  total_panels = len(panel_data)
1593
  successful = 0
1594
  failed = 0
 
1598
 
1599
  if panel.get('prompt'):
1600
  try:
1601
+ # Translate if needed
1602
  if not panel.get('prompt_en'):
1603
  panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
1604
  panel['prompt'], character_profiles
1605
  )
1606
 
1607
+ # Generate with scene type
1608
  result = webtoon_system.image_generator.generate_image(
1609
  panel['prompt_en'],
1610
  f"ep1_panel{panel['number']}",
1611
+ session_id,
1612
+ scene_type=panel.get('scene_type', 'medium')
1613
  )
1614
 
1615
  if result['status'] == 'success':
1616
+ panel['image_url'] = result['image_url']
 
 
 
 
 
 
 
1617
  successful += 1
1618
  else:
1619
  failed += 1
 
1624
  time.sleep(0.5) # Rate limiting
1625
 
1626
  progress(1.0, desc=f"완료! 성공: {successful}, 실패: {failed}")
1627
+ return display_panels_side_by_side(panel_data), gr.update(visible=False)
1628
 
1629
+ def clear_all_images(panel_data):
1630
+ for panel in panel_data:
1631
+ panel['image_url'] = None
1632
+ return display_panels_side_by_side(panel_data)
1633
 
1634
  def handle_random_theme(genre, language):
1635
  return generate_random_webtoon_theme(genre, language)
 
1676
  fn=lambda x: x,
1677
  inputs=[storyboard_state],
1678
  outputs=[storyboard_editor]
1679
+ ).then(
1680
+ fn=apply_edited_storyboard,
1681
+ inputs=[storyboard_state, current_session_id, character_profiles_state],
1682
+ outputs=[panel_data_state, panels_display, panel_selector, storyboard_editor]
1683
  )
1684
 
1685
  # Apply edits button
1686
  apply_edits_btn.click(
1687
  fn=apply_edited_storyboard,
1688
  inputs=[storyboard_editor, current_session_id, character_profiles_state],
1689
+ outputs=[panel_data_state, panels_display, panel_selector, storyboard_editor]
1690
  ).then(
1691
  fn=lambda: gr.update(visible=True, value="편집 내용이 적용되었습니다."),
1692
  outputs=[generation_progress]
1693
  )
1694
 
1695
+ # Generate selected images
1696
+ generate_selected_btn.click(
1697
+ fn=lambda: gr.update(visible=True, value="선택한 패널 이미지 생성 시작..."),
1698
+ outputs=[generation_progress]
1699
+ ).then(
1700
+ fn=generate_selected_panel_images,
1701
+ inputs=[panel_data_state, panel_selector, current_session_id, character_profiles_state, webtoon_system],
1702
+ outputs=[panels_display, generation_progress]
1703
+ )
1704
+
1705
  # Generate all images
1706
  generate_all_images_btn.click(
1707
+ fn=lambda: gr.update(visible=True, value="모든 패널 이미지 생성 시작..."),
1708
  outputs=[generation_progress]
1709
  ).then(
1710
+ fn=generate_all_panel_images,
1711
  inputs=[panel_data_state, current_session_id, character_profiles_state, webtoon_system],
1712
+ outputs=[panels_display, generation_progress]
1713
  )
1714
 
1715
  # Clear images
1716
  clear_images_btn.click(
1717
  fn=clear_all_images,
1718
+ inputs=[panel_data_state],
1719
+ outputs=[panels_display]
1720
  )
1721
 
1722
  # Random theme