openfree commited on
Commit
4108975
ยท
verified ยท
1 Parent(s): 313fe84

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +421 -274
app.py CHANGED
@@ -17,6 +17,10 @@ from dataclasses import dataclass, field, asdict
17
  from collections import defaultdict
18
  import random
19
  from huggingface_hub import HfApi, upload_file, hf_hub_download
 
 
 
 
20
 
21
  # --- Logging setup ---
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -35,16 +39,19 @@ except ImportError:
35
  DOCX_AVAILABLE = False
36
  logger.warning("python-docx not installed. DOCX export will be disabled.")
37
 
38
- import io # Add io import for DOCX export
39
-
40
  # --- Environment variables and constants ---
41
  FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
 
42
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
43
  API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
44
  MODEL_ID = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
45
  DB_PATH = "webtoon_sessions_v1.db"
46
 
47
- # Target settings for webtoon - UPDATED FOR WEBTOON
 
 
 
 
48
  TARGET_EPISODES = 40 # 40ํ™” ์™„๊ฒฐ
49
  PANELS_PER_EPISODE = 30 # ๊ฐ ํ™”๋‹น 30๊ฐœ ํŒจ๋„
50
  TARGET_PANELS = TARGET_EPISODES * PANELS_PER_EPISODE # ์ด 1200 ํŒจ๋„
@@ -62,24 +69,49 @@ WEBTOON_GENRES = {
62
  "์Šคํฌ์ธ ": "Sports"
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  # --- Environment validation ---
66
  if not FIREWORKS_API_KEY:
67
  logger.error("FIREWORKS_API_KEY not set. Application will not work properly.")
68
  FIREWORKS_API_KEY = "dummy_token_for_testing"
69
 
70
- if not BRAVE_SEARCH_API_KEY:
71
- logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
72
 
73
  # --- Global variables ---
74
  db_lock = threading.Lock()
 
75
 
76
  # --- Data classes ---
 
 
 
 
 
 
 
 
 
 
77
  @dataclass
78
  class WebtoonBible:
79
  """Webtoon story bible for maintaining consistency"""
80
  genre: str = ""
81
  title: str = ""
82
- characters: Dict[str, Dict[str, Any]] = field(default_factory=dict)
83
  settings: Dict[str, str] = field(default_factory=dict)
84
  plot_points: List[Dict[str, Any]] = field(default_factory=list)
85
  episode_hooks: Dict[int, str] = field(default_factory=dict)
@@ -92,13 +124,14 @@ class StoryboardPanel:
92
  """Individual storyboard panel"""
93
  panel_number: int
94
  scene_type: str # wide, close-up, medium, establishing
95
- image_prompt: str # Image generation prompt with gfwm trigger
96
  dialogue: List[str] = field(default_factory=list)
97
  narration: str = ""
98
  sound_effects: List[str] = field(default_factory=list)
99
  emotion_notes: str = ""
100
  camera_angle: str = ""
101
  background: str = ""
 
102
 
103
  @dataclass
104
  class EpisodeStoryboard:
@@ -167,20 +200,6 @@ GENRE_ELEMENTS = {
167
  }
168
  }
169
 
170
- # Panel composition types
171
- PANEL_COMPOSITIONS = {
172
- "establishing": "์ „์ฒด ๋ฐฐ๊ฒฝ๊ณผ ๋ถ„์œ„๊ธฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์›๊ฑฐ๋ฆฌ ์ƒท",
173
- "wide": "์บ๋ฆญํ„ฐ์™€ ๋ฐฐ๊ฒฝ์ด ๋ชจ๋‘ ๋ณด์ด๋Š” ์™€์ด๋“œ ์ƒท",
174
- "medium": "์บ๋ฆญํ„ฐ ์ƒ๋ฐ˜์‹ ์ด ๋ณด์ด๋Š” ๋ฏธ๋””์—„ ์ƒท",
175
- "close_up": "์–ผ๊ตด์ด๋‚˜ ํ‘œ์ •์— ์ง‘์ค‘ํ•˜๋Š” ํด๋กœ์ฆˆ์—…",
176
- "extreme_close_up": "๋ˆˆ, ์ž… ๋“ฑ ํŠน์ • ๋ถ€์œ„ ๊ทน๋‹จ ํด๋กœ์ฆˆ์—…",
177
- "over_shoulder": "์–ด๊นจ ๋„ˆ๋จธ๋กœ ๋ณด๋Š” ์˜ค๋ฒ„์ˆ„๋” ์ƒท",
178
- "dutch_angle": "๊ธด์žฅ๊ฐ์„ ์œ„ํ•œ ๊ธฐ์šธ์–ด์ง„ ์•ต๊ธ€",
179
- "bird_eye": "์œ„์—์„œ ๋‚ด๋ ค๋‹ค๋ณด๋Š” ๋ถ€๊ฐ ์ƒท",
180
- "worm_eye": "์•„๋ž˜์„œ ์˜ฌ๋ ค๋‹ค๋ณด๋Š” ์•™๊ฐ ์ƒท",
181
- "action": "์›€์ง์ž„์„ ๊ฐ•์กฐํ•˜๋Š” ์•ก์…˜ ์ƒท"
182
- }
183
-
184
  # --- Core logic classes ---
185
  class WebtoonTracker:
186
  """Webtoon narrative and storyboard tracker"""
@@ -189,11 +208,17 @@ class WebtoonTracker:
189
  self.episode_storyboards: Dict[int, EpisodeStoryboard] = {}
190
  self.episodes: Dict[int, str] = {}
191
  self.total_panel_count = 0
 
192
 
193
  def set_genre(self, genre: str):
194
  """Set the webtoon genre"""
195
  self.story_bible.genre = genre
196
  self.story_bible.genre_elements = GENRE_ELEMENTS.get(genre, {})
 
 
 
 
 
197
 
198
  def add_storyboard(self, episode_num: int, storyboard: EpisodeStoryboard):
199
  """Add episode storyboard"""
@@ -223,7 +248,8 @@ class WebtoonDatabase:
223
  total_episodes INTEGER DEFAULT 40,
224
  planning_doc TEXT,
225
  story_bible TEXT,
226
- visual_style TEXT
 
227
  )
228
  ''')
229
 
@@ -243,7 +269,7 @@ class WebtoonDatabase:
243
  )
244
  ''')
245
 
246
- # Panels table
247
  cursor.execute('''
248
  CREATE TABLE IF NOT EXISTS panels (
249
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -255,6 +281,7 @@ class WebtoonDatabase:
255
  dialogue TEXT,
256
  narration TEXT,
257
  sound_effects TEXT,
 
258
  created_at TEXT DEFAULT (datetime('now')),
259
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
260
  )
@@ -313,6 +340,57 @@ class WebtoonDatabase:
313
  json.dumps(panel.sound_effects)))
314
 
315
  conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
  # --- LLM Integration ---
318
  class WebtoonSystem:
@@ -323,6 +401,7 @@ class WebtoonSystem:
323
  self.model_id = MODEL_ID
324
  self.tracker = WebtoonTracker()
325
  self.current_session_id = None
 
326
  WebtoonDatabase.init_db()
327
 
328
  def create_headers(self):
@@ -331,10 +410,40 @@ class WebtoonSystem:
331
  "Content-Type": "application/json",
332
  "Authorization": f"Bearer {self.api_key}"
333
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
 
335
  # --- Prompt generation functions ---
336
  def create_planning_prompt(self, query: str, genre: str, language: str) -> str:
337
- """Create initial planning prompt for webtoon"""
338
  genre_info = GENRE_ELEMENTS.get(genre, {})
339
 
340
  lang_prompts = {
@@ -346,26 +455,15 @@ class WebtoonSystem:
346
  **์žฅ๋ฅด:** {genre}
347
  **๋ชฉํ‘œ:** 40ํ™” ์™„๊ฒฐ ์›นํˆฐ
348
 
349
- โš ๏ธ **์ค‘์š”**: ์œ„์— ์ œ์‹œ๋œ ์Šคํ† ๋ฆฌ ์„ค์ •์„ ๋ฐ˜๋“œ์‹œ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ํ”Œ๋กฏ์„ ๊ตฌ์„ฑํ•˜์„ธ์š”.
 
 
350
 
351
  **์žฅ๋ฅด ํ•„์ˆ˜ ์š”์†Œ:**
352
  - ํ•ต์‹ฌ ์š”์†Œ: {', '.join(genre_info.get('key_elements', []))}
353
  - ๋น„์ฃผ์–ผ ์Šคํƒ€์ผ: {', '.join(genre_info.get('visual_styles', []))}
354
  - ์ฃผ์š” ์”ฌ: {', '.join(genre_info.get('typical_scenes', []))}
355
 
356
- **์ „์ฒด ๊ตฌ์„ฑ (40ํ™” ๊ธฐ์ค€):**
357
- 1. **1-5ํ™”**: ์„ธ๊ณ„๊ด€ ์†Œ๊ฐœ, ์ฃผ์ธ๊ณต ๋“ฑ์žฅ, ํ•ต์‹ฌ ๊ฐˆ๋“ฑ ์ œ์‹œ
358
- 2. **6-15ํ™”**: ๊ฐˆ๋“ฑ ์‹ฌํ™”, ์บ๋ฆญํ„ฐ ๊ด€๊ณ„ ๋ฐœ์ „, ์„œ๋ธŒํ”Œ๋กฏ ์ „๊ฐœ
359
- 3. **16-25ํ™”**: ์ค‘๊ฐ„ ํด๋ผ์ด๋งฅ์Šค, ๋ฐ˜์ „, ์ƒˆ๋กœ์šด ์œ„๊ธฐ
360
- 4. **26-35ํ™”**: ์ตœ์ข… ๊ฐˆ๋“ฑ ๊ณ ์กฐ, ๊ฒฐ์ „ ์ค€๋น„
361
- 5. **36-40ํ™”**: ํด๋ผ์ด๋งฅ์Šค, ํ•ด๊ฒฐ, ์—ํ•„๋กœ๊ทธ
362
-
363
- **์›นํˆฐ ํŠนํ™” ์š”์†Œ:**
364
- - ๋งค ํ™” ๋งˆ์ง€๋ง‰ ๊ฐ•๋ ฅํ•œ ํด๋ฆฌํ”„ํ–‰์–ด
365
- - ์„ธ๋กœ ์Šคํฌ๋กค์— ์ ํ•ฉํ•œ ์—ฐ์ถœ
366
- - ์ž„ํŒฉํŠธ ์žˆ๋Š” ๋Œ€์‚ฌ์™€ ์•ก์…˜
367
- - ๋…์ž ๋Œ“๊ธ€์„ ์œ ๋ฐœํ•˜๋Š” ์ „๊ฐœ
368
-
369
  ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”:
370
 
371
  ๐Ÿ“š **์ž‘ํ’ˆ ์ œ๋ชฉ:** [์ž„ํŒฉํŠธ ์žˆ๋Š” ์ œ๋ชฉ]
@@ -375,10 +473,10 @@ class WebtoonSystem:
375
  - ์ƒ‰๊ฐ: [์ฃผ์š” ์ƒ‰์ƒ ํ†ค]
376
  - ์บ๋ฆญํ„ฐ ๋””์ž์ธ ํŠน์ง•: [์ฃผ์ธ๊ณต๋“ค์˜ ๋น„์ฃผ์–ผ ํŠน์ง•]
377
 
378
- ๐Ÿ‘ฅ **์ฃผ์š” ์บ๋ฆญํ„ฐ:**
379
- - ์ฃผ์ธ๊ณต: [์ด๋ฆ„] - [์™ธ๋ชจ ํŠน์ง•, ์„ฑ๊ฒฉ, ๋ชฉํ‘œ]
380
- - ์บ๋ฆญํ„ฐ2: [์ด๋ฆ„] - [์—ญํ• , ํŠน์ง•]
381
- - ์บ๋ฆญํ„ฐ3: [์ด๋ฆ„] - [์—ญํ• , ํŠน์ง•]
382
 
383
  ๐Ÿ“– **์‹œ๋†‰์‹œ์Šค:**
384
  [3-4์ค„๋กœ ์ „์ฒด ์Šคํ† ๋ฆฌ ์š”์•ฝ]
@@ -399,26 +497,15 @@ class WebtoonSystem:
399
  **Genre:** {genre}
400
  **Goal:** 40 episodes webtoon
401
 
402
- โš ๏ธ **IMPORTANT**: You MUST base the plot on the story setting provided above.
 
 
403
 
404
  **Genre Requirements:**
405
  - Key elements: {', '.join(genre_info.get('key_elements', []))}
406
  - Visual styles: {', '.join(genre_info.get('visual_styles', []))}
407
  - Typical scenes: {', '.join(genre_info.get('typical_scenes', []))}
408
 
409
- **Overall Structure (40 episodes):**
410
- 1. **Episodes 1-5**: World introduction, protagonist debut, core conflict
411
- 2. **Episodes 6-15**: Deepening conflict, character development, subplots
412
- 3. **Episodes 16-25**: Mid-climax, plot twist, new crisis
413
- 4. **Episodes 26-35**: Final conflict escalation, preparation for showdown
414
- 5. **Episodes 36-40**: Climax, resolution, epilogue
415
-
416
- **Webtoon-specific elements:**
417
- - Strong cliffhanger at end of each episode
418
- - Vertical scroll-optimized directing
419
- - Impactful dialogue and action
420
- - Comment-inducing development
421
-
422
  Format as follows:
423
 
424
  ๐Ÿ“š **Title:** [Impactful title]
@@ -428,49 +515,53 @@ Format as follows:
428
  - Color tone: [Main color palette]
429
  - Character design: [Visual characteristics]
430
 
431
- ๐Ÿ‘ฅ **Main Characters:**
432
- - Protagonist: [Name] - [Appearance, personality, goal]
433
- - Character2: [Name] - [Role, traits]
434
- - Character3: [Name] - [Role, traits]
435
 
436
  ๐Ÿ“– **Synopsis:**
437
  [3-4 line story summary]
438
 
439
  ๐Ÿ“ **40 Episode Structure:**
440
- Include key events and cliffhangers for each episode.
441
-
442
- Episode 1: [Title] - [Key event] - Cliffhanger: [Shocking ending]
443
- Episode 2: [Title] - [Key event] - Cliffhanger: [Shocking ending]
444
- ...
445
- Episode 40: [Title] - [Key event] - [Grand finale]"""
446
  }
447
 
448
  return lang_prompts.get(language, lang_prompts["Korean"])
449
 
450
  def create_storyboard_prompt(self, episode_num: int, plot_outline: str,
451
- genre: str, language: str) -> str:
452
- """Create prompt for episode 1 storyboard with 30 panels - INCLUDING gfwm trigger"""
453
  genre_info = GENRE_ELEMENTS.get(genre, {})
454
 
 
 
 
 
 
 
455
  lang_prompts = {
456
  "Korean": f"""์›นํˆฐ {episode_num}ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ 30๊ฐœ ํŒจ๋„๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
457
 
458
  **์žฅ๋ฅด:** {genre}
459
  **1ํ™” ๋‚ด์šฉ:** {self._extract_episode_plan(plot_outline, episode_num)}
460
 
 
 
 
 
 
 
461
  **ํŒจ๋„ ๊ตฌ์„ฑ ์ง€์นจ:**
462
  - ์ด 30๊ฐœ ํŒจ๋„๋กœ ๊ตฌ์„ฑ
463
- - ๋‹ค์–‘ํ•œ ์ƒท ์‚ฌ์ด์ฆˆ ํ™œ์šฉ (์™€์ด๋“œ์ƒท, ํด๋กœ์ฆˆ์—…, ๋ฏธ๋””์—„์ƒท ๋“ฑ)
464
  - ์žฅ๋ฅด ํŠน์„ฑ์— ๋งž๋Š” ์—ฐ์ถœ: {', '.join(genre_info.get('panel_types', []))}
465
- - ์„ธ๋กœ ์Šคํฌ๋กค ์›นํˆฐ์— ์ตœ์ ํ™”๋œ ๊ตฌ์„ฑ
466
-
467
- โš ๏ธ **์ค‘์š”**: ๋ชจ๋“  ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋Š” ๋ฐ˜๋“œ์‹œ "gfwm" ํŠธ๋ฆฌ๊ฑฐ์›Œ๋“œ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!
468
 
469
  **๊ฐ ํŒจ๋„๋ณ„๋กœ ๋‹ค์Œ์„ ํฌํ•จํ•˜์—ฌ ์ž‘์„ฑ:**
470
 
471
  ํŒจ๋„ 1:
472
  - ์ƒท ํƒ€์ž…: [establishing/wide/medium/close_up ๋“ฑ]
473
- - ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ: gfwm, [์ƒ์„ธํ•œ ํ•œ๊ธ€ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ]
474
  - ๋Œ€์‚ฌ: [์บ๋ฆญํ„ฐ ๋Œ€์‚ฌ๊ฐ€ ์žˆ๋‹ค๋ฉด]
475
  - ๋‚˜๋ ˆ์ด์…˜: [ํ•ด์„ค์ด ์žˆ๋‹ค๋ฉด]
476
  - ํšจ๊ณผ์Œ: [ํ•„์š”ํ•œ ํšจ๊ณผ์Œ]
@@ -478,48 +569,35 @@ Episode 40: [Title] - [Key event] - [Grand finale]"""
478
 
479
  ...์ด๋Ÿฐ ์‹์œผ๋กœ 30๊ฐœ ํŒจ๋„ ๋ชจ๋‘ ์ž‘์„ฑ
480
 
481
- **์ค‘์š” ์—ฐ์ถœ ํฌ์ธํŠธ:**
482
- 1. ์ฒซ ํŒจ๋„์€ ์ž„ํŒฉํŠธ ์žˆ๋Š” establishing shot
483
- 2. ๊ฐ์ •์„ ์€ ํด๋กœ์ฆˆ์—…์œผ๋กœ ๊ฐ•์กฐ
484
- 3. ์•ก์…˜์€ ๋‹ค์ด๋‚˜๋ฏนํ•œ ์•ต๊ธ€ ํ™œ์šฉ
485
- 4. ๋งˆ์ง€๋ง‰ ํŒจ๋„์€ ๊ฐ•๋ ฅํ•œ ํด๋ฆฌํ”„ํ–‰์–ด
486
- 5. ๋Œ€์‚ฌ์™€ ์ด๋ฏธ์ง€๊ฐ€ ์กฐํ™”๋กญ๊ฒŒ ๊ตฌ์„ฑ
487
-
488
  ๋ฐ˜๋“œ์‹œ 30๊ฐœ ํŒจ๋„์„ ๋ชจ๋‘ ์ž‘์„ฑํ•˜์„ธ์š”.""",
489
 
490
  "English": f"""Create Episode {episode_num} storyboard with 30 panels.
491
 
492
  **Genre:** {genre}
493
- **Episode 1 content:** {self._extract_episode_plan(plot_outline, episode_num)}
 
 
 
 
 
 
494
 
495
  **Panel composition guidelines:**
496
  - Total 30 panels
497
- - Various shot sizes (wide, close-up, medium, etc.)
498
  - Genre-appropriate directing: {', '.join(genre_info.get('panel_types', []))}
499
- - Optimized for vertical scroll webtoon
500
-
501
- โš ๏ธ **IMPORTANT**: All image prompts must start with "gfwm" trigger word!
502
 
503
  **For each panel include:**
504
 
505
  Panel 1:
506
  - Shot type: [establishing/wide/medium/close_up etc]
507
- - Image prompt: gfwm, [Detailed Korean image generation prompt]
508
  - Dialogue: [Character dialogue if any]
509
  - Narration: [Narration if any]
510
  - Sound effects: [Required sound effects]
511
  - Background: [Background description]
512
 
513
- ...continue for all 30 panels
514
-
515
- **Key directing points:**
516
- 1. First panel: impactful establishing shot
517
- 2. Emphasize emotions with close-ups
518
- 3. Dynamic angles for action
519
- 4. Last panel: powerful cliffhanger
520
- 5. Harmonious dialogue and image composition
521
-
522
- Must write all 30 panels."""
523
  }
524
 
525
  return lang_prompts.get(language, lang_prompts["Korean"])
@@ -555,6 +633,62 @@ Must write all 30 panels."""
555
  return '\n'.join(episode_section)
556
 
557
  return f"์—ํ”ผ์†Œ๋“œ {episode_num} ๋‚ด์šฉ์„ ํ”Œ๋กฏ์—์„œ ์ฐธ์กฐํ•˜์—ฌ ์ž‘์„ฑํ•˜์„ธ์š”."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
  # --- LLM call functions ---
560
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
@@ -641,44 +775,35 @@ Must write all 30 panels."""
641
  ๋…์ž๋ฅผ ์‚ฌ๋กœ์žก๋Š” ์Šคํ† ๋ฆฌ์™€ ๋น„์ฃผ์–ผ ์—ฐ์ถœ์„ ๊ธฐํšํ•ฉ๋‹ˆ๋‹ค.
642
  40ํ™” ์™„๊ฒฐ ๊ตฌ์กฐ๋กœ ์™„๋ฒฝํ•œ ๊ธฐ์Šน์ „๊ฒฐ์„ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
643
  ๊ฐ ํ™”๋งˆ๋‹ค ๊ฐ•๋ ฅํ•œ ํด๋ฆฌํ”„ํ–‰์–ด๋กœ ๋‹ค์Œ ํ™”๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
644
- ์žฅ๋ฅด๋ณ„ ํŠน์„ฑ๊ณผ ๋…์ž์ธต์„ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.
645
 
646
  โš ๏ธ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™: ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์Šคํ† ๋ฆฌ ์„ค์ •์„ ์ ˆ๋Œ€์ ์œผ๋กœ ์šฐ์„ ์‹œํ•˜๊ณ , ์ด๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๋ชจ๋“  ํ”Œ๋กฏ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.""",
647
 
648
  "storyboarder": """๋‹น์‹ ์€ ์›นํˆฐ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
649
  30๊ฐœ ํŒจ๋„๋กœ ํ•œ ํ™”๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
650
  ์„ธ๋กœ ์Šคํฌ๋กค์— ์ตœ์ ํ™”๋œ ์—ฐ์ถœ์„ ํ•ฉ๋‹ˆ๋‹ค.
651
- ๊ฐ ํŒจ๋„๋งˆ๋‹ค ์ƒ์„ธํ•œ ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ํ•œ๊ธ€๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
652
- ๋Œ€์‚ฌ, ๋‚˜๋ ˆ์ด์…˜, ํšจ๊ณผ์Œ์„ ์ ์ ˆํžˆ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.
653
- ๋‹ค์–‘ํ•œ ์นด๋ฉ”๋ผ ์•ต๊ธ€๊ณผ ์ƒท ์‚ฌ์ด์ฆˆ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.
654
- ๊ฐ์ •์„ ๊ณผ ์•ก์…˜์„ ์‹œ๊ฐ์ ์œผ๋กœ ๊ทน๋Œ€ํ™”ํ•ฉ๋‹ˆ๋‹ค.
655
 
656
  โš ๏ธ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™:
657
  1. ๋ฐ˜๋“œ์‹œ 30๊ฐœ ํŒจ๋„์„ ๋ชจ๋‘ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
658
- 2. ๋ชจ๋“  ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋Š” "gfwm" ํŠธ๋ฆฌ๊ฑฐ์›Œ๋“œ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
659
- 3. ๊ฐ ํŒจ๋„๋งˆ๋‹ค ์ด๋ฏธ์ง€ ์ƒ์„ฑ์ด ๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์ ์ธ ํ•œ๊ธ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค."""
660
  },
661
  "English": {
662
  "planner": """You perfectly understand the Korean webtoon market.
663
  Design stories and visual direction that captivate readers.
664
  Create perfect story structure in 40 episodes.
665
  Make readers anticipate next episode with strong cliffhangers.
666
- Accurately understand genre characteristics and readership.
667
 
668
  โš ๏ธ Most important principle: Absolutely prioritize the user's story setting and build all plots around it.""",
669
 
670
  "storyboarder": """You are a webtoon storyboard specialist.
671
  Perfectly compose one episode with 30 panels.
672
- Optimize direction for vertical scrolling.
673
- Write detailed image prompts in Korean for each panel.
674
- Properly place dialogue, narration, and sound effects.
675
- Use various camera angles and shot sizes.
676
- Visually maximize emotions and action.
677
 
678
  โš ๏ธ Most important principles:
679
  1. Must write all 30 panels.
680
- 2. All image prompts must start with "gfwm" trigger word.
681
- 3. Provide specific Korean prompts for image generation for each panel."""
682
  }
683
  }
684
 
@@ -686,7 +811,7 @@ Visually maximize emotions and action.
686
 
687
  # --- Main process ---
688
  def process_webtoon_stream(self, query: str, genre: str, language: str,
689
- session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
690
  """Webtoon planning and storyboard generation process"""
691
  try:
692
  if not session_id:
@@ -697,8 +822,8 @@ Visually maximize emotions and action.
697
  else:
698
  self.current_session_id = session_id
699
 
700
- # Phase 1: Generate planning document (40 episodes structure)
701
- yield "๐ŸŽฌ ์›นํˆฐ ๊ธฐํš์•ˆ ์ž‘์„ฑ ์ค‘...", "", f"์žฅ๋ฅด: {genre}", self.current_session_id
702
 
703
  planning_prompt = self.create_planning_prompt(query, genre, language)
704
  planning_doc = self.call_llm_sync(
@@ -708,33 +833,37 @@ Visually maximize emotions and action.
708
 
709
  self.planning_doc = planning_doc
710
 
711
- yield "โœ… ๊ธฐํš์•ˆ ์™„์„ฑ!", planning_doc, "40ํ™” ๊ตฌ์„ฑ ์™„๋ฃŒ", self.current_session_id
 
 
 
 
 
 
 
712
 
713
- # Phase 2: Generate Episode 1 Storyboard (30 panels)
714
- yield "๐ŸŽจ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ž‘์„ฑ ์ค‘...", planning_doc, "30๊ฐœ ํŒจ๋„ ๊ตฌ์„ฑ ์ค‘", self.current_session_id
715
 
716
- storyboard_prompt = self.create_storyboard_prompt(1, planning_doc, genre, language)
717
  storyboard_content = self.call_llm_sync(
718
  [{"role": "user", "content": storyboard_prompt}],
719
  "storyboarder", language
720
  )
721
 
722
  # Parse storyboard into structured format
723
- storyboard = self.parse_storyboard(storyboard_content, 1)
724
 
725
  # Save to database
726
  WebtoonDatabase.save_storyboard(self.current_session_id, 1, storyboard)
727
 
728
- # Format final output
729
- final_output = f"{planning_doc}\n\n{'='*50}\n\n## ๐Ÿ“‹ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ (30 ํŒจ๋„)\n\n{storyboard_content}"
730
-
731
- yield "๐ŸŽ‰ ์™„์„ฑ!", planning_doc, storyboard_content, self.current_session_id
732
 
733
  except Exception as e:
734
  logger.error(f"Webtoon generation error: {e}", exc_info=True)
735
- yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", "", "", self.current_session_id
736
 
737
- def parse_storyboard(self, content: str, episode_num: int) -> EpisodeStoryboard:
738
  """Parse storyboard text into structured format"""
739
  storyboard = EpisodeStoryboard(episode_number=episode_num, title=f"{episode_num}ํ™”")
740
 
@@ -751,15 +880,11 @@ Visually maximize emotions and action.
751
  current_panel = StoryboardPanel(
752
  panel_number=panel_number,
753
  scene_type="medium",
754
- image_prompt="gfwm, " # Initialize with gfwm trigger
755
  )
756
  elif current_panel:
757
  if '์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:' in line or 'Image prompt:' in line:
758
- prompt = line.split(':', 1)[1].strip()
759
- # Ensure gfwm trigger is included
760
- if not prompt.startswith("gfwm"):
761
- prompt = f"gfwm, {prompt}"
762
- current_panel.image_prompt = prompt
763
  elif '๋Œ€์‚ฌ:' in line or 'Dialogue:' in line:
764
  dialogue = line.split(':', 1)[1].strip()
765
  if dialogue:
@@ -776,12 +901,12 @@ Visually maximize emotions and action.
776
  if current_panel:
777
  panels.append(current_panel)
778
 
779
- storyboard.panels = panels[:30] # Limit to 30 panels
780
  return storyboard
781
 
782
  # --- Format storyboard for display ---
783
- def format_storyboard_for_display(storyboard_content: str) -> str:
784
- """Format storyboard content for panel display"""
785
  formatted_panels = []
786
  panel_texts = []
787
  current_panel = {}
@@ -794,10 +919,7 @@ def format_storyboard_for_display(storyboard_content: str) -> str:
794
  panel_texts.append(current_panel)
795
  current_panel = {'number': len(panel_texts) + 1}
796
  elif '์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:' in line or 'Image prompt:' in line:
797
- prompt = line.split(':', 1)[1].strip()
798
- if not prompt.startswith("gfwm"):
799
- prompt = f"gfwm, {prompt}"
800
- current_panel['prompt'] = prompt
801
  elif '๋Œ€์‚ฌ:' in line or 'Dialogue:' in line:
802
  current_panel['dialogue'] = line.split(':', 1)[1].strip()
803
  elif '๋‚˜๋ ˆ์ด์…˜:' in line or 'Narration:' in line:
@@ -811,15 +933,29 @@ def format_storyboard_for_display(storyboard_content: str) -> str:
811
  panel_texts.append(current_panel)
812
 
813
  # Format each panel for display
814
- for panel in panel_texts[:30]: # Ensure only 30 panels
 
815
  panel_html = f"""
816
  <div style="border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; border-radius: 8px; background: #f9f9f9;">
817
- <h4 style="color: #764ba2; margin-top: 0;">๐ŸŽฌ ํŒจ๋„ {panel.get('number', '?')}</h4>
818
  <p><strong>์ƒท ํƒ€์ž…:</strong> {panel.get('shot', 'N/A')}</p>
819
- <p><strong>์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:</strong><br><code style="background: #fff; padding: 5px; display: block; border-radius: 4px;">{panel.get('prompt', 'N/A')}</code></p>
 
 
 
820
  {f"<p><strong>๋Œ€์‚ฌ:</strong> {panel.get('dialogue')}</p>" if panel.get('dialogue') else ""}
821
  {f"<p><strong>๋‚˜๋ ˆ์ด์…˜:</strong> {panel.get('narration')}</p>" if panel.get('narration') else ""}
822
  {f"<p><strong>ํšจ๊ณผ์Œ:</strong> {panel.get('effects')}</p>" if panel.get('effects') else ""}
 
 
 
 
 
 
 
 
 
 
823
  </div>
824
  """
825
  formatted_panels.append(panel_html)
@@ -845,47 +981,6 @@ def export_to_txt(planning_doc: str, storyboard: str, genre: str, title: str = "
845
 
846
  return content
847
 
848
- def export_to_docx(planning_doc: str, storyboard: str, genre: str, title: str = "") -> bytes:
849
- """Export webtoon planning and storyboard to DOCX format"""
850
- if not DOCX_AVAILABLE:
851
- raise Exception("python-docx is not installed")
852
-
853
- doc = Document()
854
-
855
- # Title
856
- doc.add_heading(title if title else f"{genre} ์›นํˆฐ", 0)
857
-
858
- # Info
859
- doc.add_paragraph(f"์žฅ๋ฅด: {genre}")
860
- doc.add_paragraph("์ด 40ํ™” ๊ธฐํš")
861
- doc.add_page_break()
862
-
863
- # Planning document
864
- doc.add_heading("๐Ÿ“š ๊ธฐํš์•ˆ", 1)
865
- for line in planning_doc.split('\n'):
866
- if line.strip():
867
- if line.startswith('#'):
868
- doc.add_heading(line.replace('#', '').strip(), 2)
869
- else:
870
- doc.add_paragraph(line)
871
-
872
- doc.add_page_break()
873
-
874
- # Storyboard
875
- doc.add_heading("๐ŸŽจ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ (30 ํŒจ๋„)", 1)
876
- for line in storyboard.split('\n'):
877
- if line.strip():
878
- if 'ํŒจ๋„' in line or 'Panel' in line:
879
- doc.add_heading(line, 2)
880
- else:
881
- doc.add_paragraph(line)
882
-
883
- # Save to bytes
884
- bytes_io = io.BytesIO()
885
- doc.save(bytes_io)
886
- bytes_io.seek(0)
887
- return bytes_io.getvalue()
888
-
889
  def generate_random_webtoon_theme(genre: str, language: str) -> str:
890
  """Generate random webtoon theme"""
891
  templates = {
@@ -959,48 +1054,45 @@ def create_interface():
959
  text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
960
  }
961
 
962
- .header-subtitle {
963
- font-size: 1.2rem;
964
- margin-bottom: 0.5rem;
965
- }
966
-
967
- .storyboard-container {
968
- display: grid;
969
- grid-template-columns: 1fr 1fr;
970
- gap: 20px;
971
- height: 800px;
972
- overflow: hidden;
973
- }
974
-
975
- .panel-text {
976
- overflow-y: auto;
977
- padding: 20px;
978
- background: #f5f5f5;
979
- border-radius: 10px;
980
- }
981
-
982
- .panel-images {
983
- overflow-y: auto;
984
- padding: 20px;
985
- background: #fafafa;
986
- border-radius: 10px;
987
- border: 2px dashed #ddd;
988
  }
989
 
990
- .panel-placeholder {
991
- height: 400px;
992
  background: #fff;
993
- border: 1px solid #e0e0e0;
994
  border-radius: 8px;
995
  margin-bottom: 20px;
996
  display: flex;
997
  align-items: center;
998
  justify-content: center;
999
- color: #999;
1000
- font-size: 14px;
1001
  }
1002
  </style>
1003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1004
  <div class="main-header">
1005
  <h1 class="header-title">๐ŸŽจ K-Webtoon Storyboard Generator</h1>
1006
  <p class="header-subtitle">ํ•œ๊ตญํ˜• ์›นํˆฐ ๊ธฐํš ๋ฐ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ž๋™ ์ƒ์„ฑ ์‹œ์Šคํ…œ</p>
@@ -1011,6 +1103,7 @@ def create_interface():
1011
  current_session_id = gr.State(None)
1012
  planning_state = gr.State("")
1013
  storyboard_state = gr.State("")
 
1014
 
1015
  with gr.Tab("๐Ÿ“š ๊ธฐํš์•ˆ ์ž‘์„ฑ"):
1016
  with gr.Group():
@@ -1043,9 +1136,10 @@ def create_interface():
1043
  )
1044
 
1045
  gr.Markdown("""
1046
- **๐Ÿ”ฅ ํŠธ๋ฆฌ๊ฑฐ์›Œ๋“œ: gfwm**
1047
 
1048
- ๋ชจ๋“  ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ์— ์ž๋™ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.
 
1049
  """)
1050
 
1051
  status_text = gr.Textbox(
@@ -1057,9 +1151,12 @@ def create_interface():
1057
  # Planning output
1058
  planning_display = gr.Markdown("*๊ธฐํš์•ˆ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค*")
1059
 
 
 
 
1060
  with gr.Row():
1061
  download_format = gr.Radio(
1062
- choices=["TXT", "DOCX"],
1063
  value="TXT",
1064
  label="๋‹ค์šด๋กœ๋“œ ํ˜•์‹"
1065
  )
@@ -1070,7 +1167,7 @@ def create_interface():
1070
  with gr.Tab("๐ŸŽฌ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ"):
1071
  gr.Markdown("""
1072
  ### ๐Ÿ“‹ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ (30 ํŒจ๋„)
1073
- ์ขŒ์ธก: ํ…์ŠคํŠธ ๋ฐ ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ | ์šฐ์ธก: ์ด๋ฏธ์ง€ ๊ณต๊ฐ„ (์ค€๋น„์ค‘)
1074
  """)
1075
 
1076
  with gr.Row():
@@ -1081,18 +1178,20 @@ def create_interface():
1081
  )
1082
 
1083
  with gr.Column():
1084
- gr.HTML("""
1085
- <div style="padding: 20px; background: #fafafa; border: 2px dashed #ddd; border-radius: 10px; min-height: 800px;">
1086
- <p style="color: #999; text-align: center;">๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ณต๊ฐ„</p>
1087
- <p style="color: #aaa; text-align: center; font-size: 12px;">๊ฐ ํŒจ๋„์˜ ์ด๋ฏธ์ง€๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค</p>
1088
-
1089
- <!-- Panel placeholders -->
1090
- <div class="panel-placeholder">ํŒจ๋„ 1 ์ด๋ฏธ์ง€</div>
1091
- <div class="panel-placeholder">ํŒจ๋„ 2 ์ด๋ฏธ์ง€</div>
1092
- <div class="panel-placeholder">ํŒจ๋„ 3 ์ด๋ฏธ์ง€</div>
1093
- <p style="color: #ccc; text-align: center;">... 30๊ฐœ ํŒจ๋„๊นŒ์ง€</p>
1094
- </div>
1095
- """)
 
 
1096
 
1097
  with gr.Tab("๐Ÿ“š ์‚ฌ์šฉ ๊ฐ€์ด๋“œ"):
1098
  gr.Markdown("""
@@ -1102,26 +1201,20 @@ def create_interface():
1102
  - ์žฅ๋ฅด ์„ ํƒ
1103
  - ์Šคํ† ๋ฆฌ ์ฝ˜์…‰ํŠธ ์ž…๋ ฅ
1104
  - "๊ธฐํš ์‹œ์ž‘" ํด๋ฆญ
1105
- - 40ํ™” ์ „์ฒด ๊ตฌ์„ฑ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
1106
 
1107
- 2. **1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ ํƒญ**์—์„œ:
1108
- - 30๊ฐœ ํŒจ๋„์˜ ์ƒ์„ธ ์Šคํ† ๋ฆฌ๋ณด๋“œ ํ™•์ธ
1109
- - ๊ฐ ํŒจ๋„๋ณ„ ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ (gfwm ํŠธ๋ฆฌ๊ฑฐ ํฌํ•จ)
1110
- - ๋Œ€์‚ฌ, ๋‚˜๋ ˆ์ด์…˜, ํšจ๊ณผ์Œ ์ •๋ณด
1111
 
1112
- ### ํŠธ๋ฆฌ๊ฑฐ์›Œ๋“œ: gfwm
1113
- ๋ชจ๋“  ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ์— ์ž๋™์œผ๋กœ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
 
 
1114
 
1115
- ### ์žฅ๋ฅด๋ณ„ ํŠน์ง•
1116
- - **๋กœ๋งจ์Šค**: ๊ฐ์ •์„ , ๊ด€๊ณ„ ์ค‘์‹ฌ
1117
- - **๋กœํŒ**: ํšŒ๊ท€/๋น™์˜ ํŒํƒ€์ง€
1118
- - **ํŒํƒ€์ง€**: ๋ชจํ—˜๊ณผ ์„ฑ์žฅ
1119
- - **ํ˜„ํŒ**: ํ˜„๋Œ€ ๋ฐฐ๊ฒฝ ๋Šฅ๋ ฅ๋ฌผ
1120
- - **๋ฌดํ˜‘**: ๋ฌด๊ณต๊ณผ ๋ณต์ˆ˜
1121
- - **์Šค๋ฆด๋Ÿฌ**: ์„œ์ŠคํŽœ์Šค์™€ ๋ฐ˜์ „
1122
- - **์ผ์ƒ**: ๊ณต๊ฐ๊ณผ ํž๋ง
1123
- - **๊ฐœ๊ทธ**: ์›ƒ์Œ๊ณผ ํŒจ๋Ÿฌ๋””
1124
- - **์Šคํฌ์ธ **: ์—ด์ •๊ณผ ์„ฑ์žฅ
1125
  """)
1126
 
1127
  # Event handlers
@@ -1129,18 +1222,36 @@ def create_interface():
1129
  system = WebtoonSystem()
1130
  planning = ""
1131
  storyboard = ""
 
1132
 
1133
- for planning_content, storyboard_content, status, new_session_id in system.process_webtoon_stream(query, genre, language, session_id):
1134
  planning = planning_content
1135
  storyboard = storyboard_content
1136
- yield planning, storyboard, status, new_session_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1137
 
1138
- def update_storyboard_display(storyboard_content):
1139
  """Update storyboard display with formatted panels"""
1140
  if not storyboard_content or storyboard_content == "":
1141
  return "<p style='color: #999; text-align: center; padding: 50px;'>๊ธฐํš์•ˆ ์ž‘์„ฑ ํ›„ ์Šคํ† ๋ฆฌ๋ณด๋“œ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค</p>"
1142
 
1143
- return format_storyboard_for_display(storyboard_content)
1144
 
1145
  def handle_random_theme(genre, language):
1146
  return generate_random_webtoon_theme(genre, language)
@@ -1149,44 +1260,64 @@ def create_interface():
1149
  """Handle download request"""
1150
  try:
1151
  title = f"{genre} ์›นํˆฐ"
 
1152
 
1153
- if download_format == "TXT":
1154
- content = export_to_txt(planning, storyboard, genre, title)
1155
-
1156
- with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
1157
- suffix='.txt', delete=False) as f:
1158
- f.write(content)
1159
- return f.name
1160
-
1161
- elif download_format == "DOCX":
1162
- if not DOCX_AVAILABLE:
1163
- gr.Warning("DOCX export requires python-docx library")
1164
- return None
1165
-
1166
- content = export_to_docx(planning, storyboard, genre, title)
1167
-
1168
- with tempfile.NamedTemporaryFile(mode='wb', suffix='.docx',
1169
- delete=False) as f:
1170
- f.write(content)
1171
- return f.name
1172
 
1173
  except Exception as e:
1174
  logger.error(f"Download error: {e}")
1175
  gr.Warning(f"๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
1176
  return None
1177
 
1178
- # Connect events
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1179
  submit_btn.click(
1180
  fn=process_query,
1181
  inputs=[query_input, genre_select, language_select, current_session_id],
1182
- outputs=[planning_state, storyboard_state, status_text, current_session_id]
1183
  ).then(
1184
  fn=lambda x: x,
1185
  inputs=[planning_state],
1186
  outputs=[planning_display]
 
 
 
 
1187
  ).then(
1188
  fn=update_storyboard_display,
1189
- inputs=[storyboard_state],
1190
  outputs=[storyboard_text_display]
1191
  )
1192
 
@@ -1206,6 +1337,18 @@ def create_interface():
1206
  outputs=[download_file]
1207
  )
1208
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  # Examples
1210
  gr.Examples(
1211
  examples=[
@@ -1234,7 +1377,11 @@ if __name__ == "__main__":
1234
  logger.info(f"Model: {MODEL_ID}")
1235
  logger.info(f"Target: {TARGET_EPISODES} episodes, {PANELS_PER_EPISODE} panels per episode")
1236
  logger.info("Genres: " + ", ".join(WEBTOON_GENRES.keys()))
1237
- logger.info("Trigger word: gfwm (automatically added to all image prompts)")
 
 
 
 
1238
 
1239
  logger.info("=" * 60)
1240
 
@@ -1249,4 +1396,4 @@ if __name__ == "__main__":
1249
  server_name="0.0.0.0",
1250
  server_port=7860,
1251
  share=False
1252
- )
 
17
  from collections import defaultdict
18
  import random
19
  from huggingface_hub import HfApi, upload_file, hf_hub_download
20
+ import replicate
21
+ from PIL import Image
22
+ import io as io_module
23
+ import base64
24
 
25
  # --- Logging setup ---
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
39
  DOCX_AVAILABLE = False
40
  logger.warning("python-docx not installed. DOCX export will be disabled.")
41
 
 
 
42
  # --- Environment variables and constants ---
43
  FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
44
+ REPLICATE_API_TOKEN = os.getenv("REPLICATE_API_TOKEN", "")
45
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
46
  API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
47
  MODEL_ID = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
48
  DB_PATH = "webtoon_sessions_v1.db"
49
 
50
+ # Initialize Replicate client if token exists
51
+ if REPLICATE_API_TOKEN:
52
+ os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN
53
+
54
+ # Target settings for webtoon
55
  TARGET_EPISODES = 40 # 40ํ™” ์™„๊ฒฐ
56
  PANELS_PER_EPISODE = 30 # ๊ฐ ํ™”๋‹น 30๊ฐœ ํŒจ๋„
57
  TARGET_PANELS = TARGET_EPISODES * PANELS_PER_EPISODE # ์ด 1200 ํŒจ๋„
 
69
  "์Šคํฌ์ธ ": "Sports"
70
  }
71
 
72
+ # Celebrity face references for character design
73
+ CELEBRITY_FACES = {
74
+ "male": [
75
+ "ํ†ฐ ํฌ๋ฃจ์ฆˆ", "๋ธŒ๋ž˜๋“œ ํ”ผํŠธ", "๋ ˆ์˜ค๋‚˜๋ฅด๋„ ๋””์นดํ”„๋ฆฌ์˜ค", "๋ผ์ด์–ธ ๊ณ ์Šฌ๋ง",
76
+ "ํฌ๋ฆฌ์Šค ํ—ด์Šค์›Œ์Šค", "๋กœ๋ฒ„ํŠธ ๋‹ค์šฐ๋‹ˆ ์ฃผ๋‹ˆ์–ด", "ํฌ๋ฆฌ์Šค ์—๋ฐ˜์Šค", "ํ†ฐ ํžˆ๋“ค์Šคํ„ด",
77
+ "๋ฒ ๋„ค๋”•ํŠธ ์ปด๋ฒ„๋ฐฐ์น˜", "ํ‚ค์•„๋ˆ„ ๋ฆฌ๋ธŒ์Šค", "์ด๋ณ‘ํ—Œ", "๊ณต์œ ", "๋ฐ•์„œ์ค€", "์†ก์ค‘๊ธฐ"
78
+ ],
79
+ "female": [
80
+ "์Šค์นผ๋ › ์š”ํ•œ์Šจ", "์— ๋งˆ ์™“์Šจ", "์ œ๋‹ˆํผ ๋กœ๋ Œ์Šค", "๊ฐค ๊ฐ€๋—",
81
+ "๋งˆ๊ณ  ๋กœ๋น„", "์— ๋งˆ ์Šคํ†ค", "์•ค ํ•ด์„œ์›จ์ด", "๋‚˜ํƒˆ๋ฆฌ ํฌํŠธ๋งŒ",
82
+ "์ „์ง€ํ˜„", "์†กํ˜œ๊ต", "๊น€ํƒœ๋ฆฌ", "์•„์ด์œ ", "์ˆ˜์ง€", "ํ•œ์†Œํฌ"
83
+ ]
84
+ }
85
+
86
  # --- Environment validation ---
87
  if not FIREWORKS_API_KEY:
88
  logger.error("FIREWORKS_API_KEY not set. Application will not work properly.")
89
  FIREWORKS_API_KEY = "dummy_token_for_testing"
90
 
91
+ if not REPLICATE_API_TOKEN:
92
+ logger.warning("REPLICATE_API_TOKEN not set. Image generation will be disabled.")
93
 
94
  # --- Global variables ---
95
  db_lock = threading.Lock()
96
+ generated_images_cache = {} # Cache for generated images
97
 
98
  # --- Data classes ---
99
+ @dataclass
100
+ class CharacterProfile:
101
+ """Character profile with celebrity lookalike"""
102
+ name: str
103
+ role: str
104
+ personality: str
105
+ appearance: str
106
+ celebrity_lookalike: str
107
+ gender: str
108
+
109
  @dataclass
110
  class WebtoonBible:
111
  """Webtoon story bible for maintaining consistency"""
112
  genre: str = ""
113
  title: str = ""
114
+ characters: Dict[str, CharacterProfile] = field(default_factory=dict)
115
  settings: Dict[str, str] = field(default_factory=dict)
116
  plot_points: List[Dict[str, Any]] = field(default_factory=list)
117
  episode_hooks: Dict[int, str] = field(default_factory=dict)
 
124
  """Individual storyboard panel"""
125
  panel_number: int
126
  scene_type: str # wide, close-up, medium, establishing
127
+ image_prompt: str # Image generation prompt with character descriptions
128
  dialogue: List[str] = field(default_factory=list)
129
  narration: str = ""
130
  sound_effects: List[str] = field(default_factory=list)
131
  emotion_notes: str = ""
132
  camera_angle: str = ""
133
  background: str = ""
134
+ characters_in_scene: List[str] = field(default_factory=list) # Character names in this panel
135
 
136
  @dataclass
137
  class EpisodeStoryboard:
 
200
  }
201
  }
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  # --- Core logic classes ---
204
  class WebtoonTracker:
205
  """Webtoon narrative and storyboard tracker"""
 
208
  self.episode_storyboards: Dict[int, EpisodeStoryboard] = {}
209
  self.episodes: Dict[int, str] = {}
210
  self.total_panel_count = 0
211
+ self.character_profiles: Dict[str, CharacterProfile] = {}
212
 
213
  def set_genre(self, genre: str):
214
  """Set the webtoon genre"""
215
  self.story_bible.genre = genre
216
  self.story_bible.genre_elements = GENRE_ELEMENTS.get(genre, {})
217
+
218
+ def add_character(self, character: CharacterProfile):
219
+ """Add character with celebrity lookalike"""
220
+ self.character_profiles[character.name] = character
221
+ self.story_bible.characters[character.name] = character
222
 
223
  def add_storyboard(self, episode_num: int, storyboard: EpisodeStoryboard):
224
  """Add episode storyboard"""
 
248
  total_episodes INTEGER DEFAULT 40,
249
  planning_doc TEXT,
250
  story_bible TEXT,
251
+ visual_style TEXT,
252
+ character_profiles TEXT
253
  )
254
  ''')
255
 
 
269
  )
270
  ''')
271
 
272
+ # Panels table with image data
273
  cursor.execute('''
274
  CREATE TABLE IF NOT EXISTS panels (
275
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
281
  dialogue TEXT,
282
  narration TEXT,
283
  sound_effects TEXT,
284
+ generated_image TEXT,
285
  created_at TEXT DEFAULT (datetime('now')),
286
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
287
  )
 
340
  json.dumps(panel.sound_effects)))
341
 
342
  conn.commit()
343
+
344
+ @staticmethod
345
+ def save_character_profiles(session_id: str, profiles: Dict[str, CharacterProfile]):
346
+ with WebtoonDatabase.get_db() as conn:
347
+ cursor = conn.cursor()
348
+ profiles_json = json.dumps({name: asdict(profile) for name, profile in profiles.items()})
349
+ cursor.execute(
350
+ "UPDATE sessions SET character_profiles = ? WHERE session_id = ?",
351
+ (profiles_json, session_id)
352
+ )
353
+ conn.commit()
354
+
355
+ # --- Image Generation ---
356
+ class ImageGenerator:
357
+ """Handle image generation using Replicate API"""
358
+
359
+ @staticmethod
360
+ def generate_image(prompt: str, panel_num: int, session_id: str) -> Optional[str]:
361
+ """Generate image using Replicate API"""
362
+ try:
363
+ if not REPLICATE_API_TOKEN:
364
+ logger.warning("No Replicate API token, returning placeholder")
365
+ return None
366
+
367
+ # Run the model
368
+ input_params = {
369
+ "prompt": prompt,
370
+ "num_inference_steps": 25,
371
+ "guidance_scale": 7.5
372
+ }
373
+
374
+ output = replicate.run(
375
+ "qwen/qwen-image",
376
+ input=input_params
377
+ )
378
+
379
+ # Get the image URL
380
+ if output and len(output) > 0:
381
+ image_url = output[0].url() if hasattr(output[0], 'url') else str(output[0])
382
+
383
+ # Cache the image
384
+ cache_key = f"{session_id}_{panel_num}"
385
+ generated_images_cache[cache_key] = image_url
386
+
387
+ return image_url
388
+
389
+ return None
390
+
391
+ except Exception as e:
392
+ logger.error(f"Image generation error: {e}")
393
+ return None
394
 
395
  # --- LLM Integration ---
396
  class WebtoonSystem:
 
401
  self.model_id = MODEL_ID
402
  self.tracker = WebtoonTracker()
403
  self.current_session_id = None
404
+ self.image_generator = ImageGenerator()
405
  WebtoonDatabase.init_db()
406
 
407
  def create_headers(self):
 
410
  "Content-Type": "application/json",
411
  "Authorization": f"Bearer {self.api_key}"
412
  }
413
+
414
+ def assign_celebrity_lookalikes(self, characters: List[Dict]) -> Dict[str, CharacterProfile]:
415
+ """Assign celebrity lookalikes to characters"""
416
+ profiles = {}
417
+ used_celebrities = []
418
+
419
+ for char in characters:
420
+ gender = char.get('gender', 'male')
421
+ available_celebrities = [c for c in CELEBRITY_FACES.get(gender, [])
422
+ if c not in used_celebrities]
423
+
424
+ if not available_celebrities:
425
+ available_celebrities = CELEBRITY_FACES.get(gender, [])
426
+
427
+ celebrity = random.choice(available_celebrities)
428
+ used_celebrities.append(celebrity)
429
+
430
+ profile = CharacterProfile(
431
+ name=char.get('name', ''),
432
+ role=char.get('role', ''),
433
+ personality=char.get('personality', ''),
434
+ appearance=char.get('appearance', ''),
435
+ celebrity_lookalike=celebrity,
436
+ gender=gender
437
+ )
438
+
439
+ profiles[profile.name] = profile
440
+ self.tracker.add_character(profile)
441
+
442
+ return profiles
443
 
444
  # --- Prompt generation functions ---
445
  def create_planning_prompt(self, query: str, genre: str, language: str) -> str:
446
+ """Create initial planning prompt for webtoon with character profiles"""
447
  genre_info = GENRE_ELEMENTS.get(genre, {})
448
 
449
  lang_prompts = {
 
455
  **์žฅ๋ฅด:** {genre}
456
  **๋ชฉํ‘œ:** 40ํ™” ์™„๊ฒฐ ์›นํˆฐ
457
 
458
+ โš ๏ธ **์ค‘์š”**:
459
+ 1. ์œ„์— ์ œ์‹œ๋œ ์Šคํ† ๋ฆฌ ์„ค์ •์„ ๋ฐ˜๋“œ์‹œ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ํ”Œ๋กฏ์„ ๊ตฌ์„ฑํ•˜์„ธ์š”.
460
+ 2. ๊ฐ ์บ๋ฆญํ„ฐ์˜ ์„ฑ๋ณ„(gender)์„ ๋ช…ํ™•ํžˆ ์ง€์ •ํ•˜์„ธ์š” (male/female).
461
 
462
  **์žฅ๋ฅด ํ•„์ˆ˜ ์š”์†Œ:**
463
  - ํ•ต์‹ฌ ์š”์†Œ: {', '.join(genre_info.get('key_elements', []))}
464
  - ๋น„์ฃผ์–ผ ์Šคํƒ€์ผ: {', '.join(genre_info.get('visual_styles', []))}
465
  - ์ฃผ์š” ์”ฌ: {', '.join(genre_info.get('typical_scenes', []))}
466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”:
468
 
469
  ๐Ÿ“š **์ž‘ํ’ˆ ์ œ๋ชฉ:** [์ž„ํŒฉํŠธ ์žˆ๋Š” ์ œ๋ชฉ]
 
473
  - ์ƒ‰๊ฐ: [์ฃผ์š” ์ƒ‰์ƒ ํ†ค]
474
  - ์บ๋ฆญํ„ฐ ๋””์ž์ธ ํŠน์ง•: [์ฃผ์ธ๊ณต๋“ค์˜ ๋น„์ฃผ์–ผ ํŠน์ง•]
475
 
476
+ ๐Ÿ‘ฅ **์ฃผ์š” ์บ๋ฆญํ„ฐ:** (๊ฐ ์บ๋ฆญํ„ฐ๋งˆ๋‹ค ์„ฑ๋ณ„์„ ๋ฐ˜๋“œ์‹œ ๋ช…์‹œ!)
477
+ - ์ฃผ์ธ๊ณต: [์ด๋ฆ„] - ์„ฑ๋ณ„: [male/female] - [์™ธ๋ชจ ํŠน์ง•, ์„ฑ๊ฒฉ, ๋ชฉํ‘œ]
478
+ - ์บ๋ฆญํ„ฐ2: [์ด๋ฆ„] - ์„ฑ๋ณ„: [male/female] - [์—ญํ• , ํŠน์ง•]
479
+ - ์บ๋ฆญํ„ฐ3: [์ด๋ฆ„] - ์„ฑ๋ณ„: [male/female] - [์—ญํ• , ํŠน์ง•]
480
 
481
  ๐Ÿ“– **์‹œ๋†‰์‹œ์Šค:**
482
  [3-4์ค„๋กœ ์ „์ฒด ์Šคํ† ๋ฆฌ ์š”์•ฝ]
 
497
  **Genre:** {genre}
498
  **Goal:** 40 episodes webtoon
499
 
500
+ โš ๏ธ **IMPORTANT**:
501
+ 1. You MUST base the plot on the story setting provided above.
502
+ 2. Clearly specify each character's gender (male/female).
503
 
504
  **Genre Requirements:**
505
  - Key elements: {', '.join(genre_info.get('key_elements', []))}
506
  - Visual styles: {', '.join(genre_info.get('visual_styles', []))}
507
  - Typical scenes: {', '.join(genre_info.get('typical_scenes', []))}
508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  Format as follows:
510
 
511
  ๐Ÿ“š **Title:** [Impactful title]
 
515
  - Color tone: [Main color palette]
516
  - Character design: [Visual characteristics]
517
 
518
+ ๐Ÿ‘ฅ **Main Characters:** (Must specify gender for each!)
519
+ - Protagonist: [Name] - Gender: [male/female] - [Appearance, personality, goal]
520
+ - Character2: [Name] - Gender: [male/female] - [Role, traits]
521
+ - Character3: [Name] - Gender: [male/female] - [Role, traits]
522
 
523
  ๐Ÿ“– **Synopsis:**
524
  [3-4 line story summary]
525
 
526
  ๐Ÿ“ **40 Episode Structure:**
527
+ Include key events and cliffhangers for each episode."""
 
 
 
 
 
528
  }
529
 
530
  return lang_prompts.get(language, lang_prompts["Korean"])
531
 
532
  def create_storyboard_prompt(self, episode_num: int, plot_outline: str,
533
+ genre: str, language: str, character_profiles: Dict[str, CharacterProfile]) -> str:
534
+ """Create prompt for episode storyboard with character descriptions"""
535
  genre_info = GENRE_ELEMENTS.get(genre, {})
536
 
537
+ # Create character description string
538
+ char_descriptions = "\n".join([
539
+ f"- {name}: {profile.celebrity_lookalike} ๋‹ฎ์€ ์–ผ๊ตด์˜ {profile.gender}"
540
+ for name, profile in character_profiles.items()
541
+ ])
542
+
543
  lang_prompts = {
544
  "Korean": f"""์›นํˆฐ {episode_num}ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ 30๊ฐœ ํŒจ๋„๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
545
 
546
  **์žฅ๋ฅด:** {genre}
547
  **1ํ™” ๋‚ด์šฉ:** {self._extract_episode_plan(plot_outline, episode_num)}
548
 
549
+ **์บ๋ฆญํ„ฐ ์–ผ๊ตด ์„ค์ •:**
550
+ {char_descriptions}
551
+
552
+ โš ๏ธ **์ค‘์š”**: ์บ๋ฆญํ„ฐ๊ฐ€ ๋“ฑ์žฅํ•  ๋•Œ๋งˆ๋‹ค "์บ๋ฆญํ„ฐ์ด๋ฆ„(์œ ๋ช…์ธ ๋‹ฎ์€ ์–ผ๊ตด์˜ ์„ฑ๋ณ„)" ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”!
553
+ ์˜ˆ์‹œ: "ํ™๊ธธ๋™(ํ†ฐ ํฌ๋ฃจ์ฆˆ ๋‹ฎ์€ ์–ผ๊ตด์˜ ๋‚จ์ž)์ด ๊ฑฐ๋ฆฌ๋ฅผ ๊ฑท๊ณ  ์žˆ๋‹ค"
554
+
555
  **ํŒจ๋„ ๊ตฌ์„ฑ ์ง€์นจ:**
556
  - ์ด 30๊ฐœ ํŒจ๋„๋กœ ๊ตฌ์„ฑ
557
+ - ๋‹ค์–‘ํ•œ ์ƒท ์‚ฌ์ด์ฆˆ ํ™œ์šฉ
558
  - ์žฅ๋ฅด ํŠน์„ฑ์— ๋งž๋Š” ์—ฐ์ถœ: {', '.join(genre_info.get('panel_types', []))}
 
 
 
559
 
560
  **๊ฐ ํŒจ๋„๋ณ„๋กœ ๋‹ค์Œ์„ ํฌํ•จํ•˜์—ฌ ์ž‘์„ฑ:**
561
 
562
  ํŒจ๋„ 1:
563
  - ์ƒท ํƒ€์ž…: [establishing/wide/medium/close_up ๋“ฑ]
564
+ - ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ: [์บ๋ฆญํ„ฐ ์„ค๋ช… ํฌํ•จํ•œ ์ƒ์„ธํ•œ ํ•œ๊ธ€ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ”„๋กฌํ”„ํŠธ]
565
  - ๋Œ€์‚ฌ: [์บ๋ฆญํ„ฐ ๋Œ€์‚ฌ๊ฐ€ ์žˆ๋‹ค๋ฉด]
566
  - ๋‚˜๋ ˆ์ด์…˜: [ํ•ด์„ค์ด ์žˆ๋‹ค๋ฉด]
567
  - ํšจ๊ณผ์Œ: [ํ•„์š”ํ•œ ํšจ๊ณผ์Œ]
 
569
 
570
  ...์ด๋Ÿฐ ์‹์œผ๋กœ 30๊ฐœ ํŒจ๋„ ๋ชจ๋‘ ์ž‘์„ฑ
571
 
 
 
 
 
 
 
 
572
  ๋ฐ˜๋“œ์‹œ 30๊ฐœ ํŒจ๋„์„ ๋ชจ๋‘ ์ž‘์„ฑํ•˜์„ธ์š”.""",
573
 
574
  "English": f"""Create Episode {episode_num} storyboard with 30 panels.
575
 
576
  **Genre:** {genre}
577
+ **Episode content:** {self._extract_episode_plan(plot_outline, episode_num)}
578
+
579
+ **Character Face Settings:**
580
+ {char_descriptions}
581
+
582
+ โš ๏ธ **IMPORTANT**: Always describe characters as "CharacterName (celebrity lookalike face gender)"!
583
+ Example: "John (Tom Cruise lookalike male) walking down the street"
584
 
585
  **Panel composition guidelines:**
586
  - Total 30 panels
587
+ - Various shot sizes
588
  - Genre-appropriate directing: {', '.join(genre_info.get('panel_types', []))}
 
 
 
589
 
590
  **For each panel include:**
591
 
592
  Panel 1:
593
  - Shot type: [establishing/wide/medium/close_up etc]
594
+ - Image prompt: [Detailed prompt with character descriptions]
595
  - Dialogue: [Character dialogue if any]
596
  - Narration: [Narration if any]
597
  - Sound effects: [Required sound effects]
598
  - Background: [Background description]
599
 
600
+ ...continue for all 30 panels"""
 
 
 
 
 
 
 
 
 
601
  }
602
 
603
  return lang_prompts.get(language, lang_prompts["Korean"])
 
633
  return '\n'.join(episode_section)
634
 
635
  return f"์—ํ”ผ์†Œ๋“œ {episode_num} ๋‚ด์šฉ์„ ํ”Œ๋กฏ์—์„œ ์ฐธ์กฐํ•˜์—ฌ ์ž‘์„ฑํ•˜์„ธ์š”."
636
+
637
+ def parse_characters_from_planning(self, planning_doc: str) -> List[Dict]:
638
+ """Parse character information from planning document"""
639
+ characters = []
640
+ lines = planning_doc.split('\n')
641
+
642
+ in_character_section = False
643
+ current_char = {}
644
+
645
+ for line in lines:
646
+ if '์ฃผ์š” ์บ๋ฆญํ„ฐ' in line or 'Main Characters' in line:
647
+ in_character_section = True
648
+ continue
649
+ elif in_character_section and ('์‹œ๋†‰์‹œ์Šค' in line or 'Synopsis' in line):
650
+ if current_char:
651
+ characters.append(current_char)
652
+ break
653
+ elif in_character_section and line.strip():
654
+ # Parse character line
655
+ if '์„ฑ๋ณ„:' in line or 'Gender:' in line:
656
+ if current_char:
657
+ characters.append(current_char)
658
+
659
+ parts = line.split('-')
660
+ if len(parts) >= 2:
661
+ name = parts[0].strip().replace('์ฃผ์ธ๊ณต:', '').replace('์บ๋ฆญํ„ฐ', '').strip()
662
+
663
+ # Extract gender
664
+ gender = 'male' # default
665
+ if 'female' in line.lower() or '์—ฌ' in line:
666
+ gender = 'female'
667
+ elif 'male' in line.lower() or '๋‚จ' in line:
668
+ gender = 'male'
669
+
670
+ current_char = {
671
+ 'name': name,
672
+ 'gender': gender,
673
+ 'role': parts[1].strip() if len(parts) > 1 else '',
674
+ 'personality': parts[2].strip() if len(parts) > 2 else '',
675
+ 'appearance': parts[3].strip() if len(parts) > 3 else ''
676
+ }
677
+
678
+ if current_char and current_char not in characters:
679
+ characters.append(current_char)
680
+
681
+ # Ensure at least 3 characters
682
+ while len(characters) < 3:
683
+ characters.append({
684
+ 'name': f'์บ๋ฆญํ„ฐ{len(characters)+1}',
685
+ 'gender': 'male' if len(characters) % 2 == 0 else 'female',
686
+ 'role': '์กฐ์—ฐ',
687
+ 'personality': '์ผ๋ฐ˜์ ',
688
+ 'appearance': 'ํ‰๋ฒ”ํ•œ ์™ธ๋ชจ'
689
+ })
690
+
691
+ return characters
692
 
693
  # --- LLM call functions ---
694
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
 
775
  ๋…์ž๋ฅผ ์‚ฌ๋กœ์žก๋Š” ์Šคํ† ๋ฆฌ์™€ ๋น„์ฃผ์–ผ ์—ฐ์ถœ์„ ๊ธฐํšํ•ฉ๋‹ˆ๋‹ค.
776
  40ํ™” ์™„๊ฒฐ ๊ตฌ์กฐ๋กœ ์™„๋ฒฝํ•œ ๊ธฐ์Šน์ „๊ฒฐ์„ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
777
  ๊ฐ ํ™”๋งˆ๋‹ค ๊ฐ•๋ ฅํ•œ ํด๋ฆฌํ”„ํ–‰์–ด๋กœ ๋‹ค์Œ ํ™”๋ฅผ ๊ธฐ๋Œ€ํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
778
+ ์บ๋ฆญํ„ฐ์˜ ์„ฑ๋ณ„์„ ๋ช…ํ™•ํžˆ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
779
 
780
  โš ๏ธ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™: ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์Šคํ† ๋ฆฌ ์„ค์ •์„ ์ ˆ๋Œ€์ ์œผ๋กœ ์šฐ์„ ์‹œํ•˜๊ณ , ์ด๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๋ชจ๋“  ํ”Œ๋กฏ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.""",
781
 
782
  "storyboarder": """๋‹น์‹ ์€ ์›นํˆฐ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
783
  30๊ฐœ ํŒจ๋„๋กœ ํ•œ ํ™”๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
784
  ์„ธ๋กœ ์Šคํฌ๋กค์— ์ตœ์ ํ™”๋œ ์—ฐ์ถœ์„ ํ•ฉ๋‹ˆ๋‹ค.
785
+ ๊ฐ ํŒจ๋„๋งˆ๋‹ค ์บ๋ฆญํ„ฐ์˜ ์œ ๋ช…์ธ ๋‹ฎ์€๊ผด ์„ค์ •์„ ํฌํ•จํ•œ ์ƒ์„ธํ•œ ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
 
 
 
786
 
787
  โš ๏ธ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์›์น™:
788
  1. ๋ฐ˜๋“œ์‹œ 30๊ฐœ ํŒจ๋„์„ ๋ชจ๋‘ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
789
+ 2. ์บ๋ฆญํ„ฐ๊ฐ€ ๋“ฑ์žฅํ•  ๋•Œ๋งˆ๋‹ค "์บ๋ฆญํ„ฐ์ด๋ฆ„(์œ ๋ช…์ธ ๋‹ฎ์€ ์–ผ๊ตด์˜ ์„ฑ๋ณ„)" ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค."""
 
790
  },
791
  "English": {
792
  "planner": """You perfectly understand the Korean webtoon market.
793
  Design stories and visual direction that captivate readers.
794
  Create perfect story structure in 40 episodes.
795
  Make readers anticipate next episode with strong cliffhangers.
796
+ Clearly specify character genders.
797
 
798
  โš ๏ธ Most important principle: Absolutely prioritize the user's story setting and build all plots around it.""",
799
 
800
  "storyboarder": """You are a webtoon storyboard specialist.
801
  Perfectly compose one episode with 30 panels.
802
+ Write detailed image prompts including celebrity lookalike descriptions.
 
 
 
 
803
 
804
  โš ๏ธ Most important principles:
805
  1. Must write all 30 panels.
806
+ 2. Always describe characters as "CharacterName (celebrity lookalike face gender)"."""
 
807
  }
808
  }
809
 
 
811
 
812
  # --- Main process ---
813
  def process_webtoon_stream(self, query: str, genre: str, language: str,
814
+ session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str, Dict], None, None]:
815
  """Webtoon planning and storyboard generation process"""
816
  try:
817
  if not session_id:
 
822
  else:
823
  self.current_session_id = session_id
824
 
825
+ # Phase 1: Generate planning document
826
+ yield "๐ŸŽฌ ์›นํˆฐ ๊ธฐํš์•ˆ ์ž‘์„ฑ ์ค‘...", "", f"์žฅ๋ฅด: {genre}", self.current_session_id, {}
827
 
828
  planning_prompt = self.create_planning_prompt(query, genre, language)
829
  planning_doc = self.call_llm_sync(
 
833
 
834
  self.planning_doc = planning_doc
835
 
836
+ # Parse characters and assign celebrity lookalikes
837
+ characters = self.parse_characters_from_planning(planning_doc)
838
+ character_profiles = self.assign_celebrity_lookalikes(characters)
839
+
840
+ # Save character profiles
841
+ WebtoonDatabase.save_character_profiles(self.current_session_id, character_profiles)
842
+
843
+ yield "โœ… ๊ธฐํš์•ˆ ์™„์„ฑ!", planning_doc, "40ํ™” ๊ตฌ์„ฑ ์™„๋ฃŒ", self.current_session_id, character_profiles
844
 
845
+ # Phase 2: Generate Episode 1 Storyboard
846
+ yield "๐ŸŽจ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ž‘์„ฑ ์ค‘...", planning_doc, "30๊ฐœ ํŒจ๋„ ๊ตฌ์„ฑ ์ค‘", self.current_session_id, character_profiles
847
 
848
+ storyboard_prompt = self.create_storyboard_prompt(1, planning_doc, genre, language, character_profiles)
849
  storyboard_content = self.call_llm_sync(
850
  [{"role": "user", "content": storyboard_prompt}],
851
  "storyboarder", language
852
  )
853
 
854
  # Parse storyboard into structured format
855
+ storyboard = self.parse_storyboard(storyboard_content, 1, character_profiles)
856
 
857
  # Save to database
858
  WebtoonDatabase.save_storyboard(self.current_session_id, 1, storyboard)
859
 
860
+ yield "๐ŸŽ‰ ์™„์„ฑ!", planning_doc, storyboard_content, self.current_session_id, character_profiles
 
 
 
861
 
862
  except Exception as e:
863
  logger.error(f"Webtoon generation error: {e}", exc_info=True)
864
+ yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", "", "", self.current_session_id, {}
865
 
866
+ def parse_storyboard(self, content: str, episode_num: int, character_profiles: Dict[str, CharacterProfile]) -> EpisodeStoryboard:
867
  """Parse storyboard text into structured format"""
868
  storyboard = EpisodeStoryboard(episode_number=episode_num, title=f"{episode_num}ํ™”")
869
 
 
880
  current_panel = StoryboardPanel(
881
  panel_number=panel_number,
882
  scene_type="medium",
883
+ image_prompt=""
884
  )
885
  elif current_panel:
886
  if '์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:' in line or 'Image prompt:' in line:
887
+ current_panel.image_prompt = line.split(':', 1)[1].strip()
 
 
 
 
888
  elif '๋Œ€์‚ฌ:' in line or 'Dialogue:' in line:
889
  dialogue = line.split(':', 1)[1].strip()
890
  if dialogue:
 
901
  if current_panel:
902
  panels.append(current_panel)
903
 
904
+ storyboard.panels = panels[:30]
905
  return storyboard
906
 
907
  # --- Format storyboard for display ---
908
+ def format_storyboard_for_display(storyboard_content: str, character_profiles: Dict[str, CharacterProfile], session_id: str) -> str:
909
+ """Format storyboard content for panel display with image generation buttons"""
910
  formatted_panels = []
911
  panel_texts = []
912
  current_panel = {}
 
919
  panel_texts.append(current_panel)
920
  current_panel = {'number': len(panel_texts) + 1}
921
  elif '์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:' in line or 'Image prompt:' in line:
922
+ current_panel['prompt'] = line.split(':', 1)[1].strip()
 
 
 
923
  elif '๋Œ€์‚ฌ:' in line or 'Dialogue:' in line:
924
  current_panel['dialogue'] = line.split(':', 1)[1].strip()
925
  elif '๋‚˜๋ ˆ์ด์…˜:' in line or 'Narration:' in line:
 
933
  panel_texts.append(current_panel)
934
 
935
  # Format each panel for display
936
+ for panel in panel_texts[:30]:
937
+ panel_num = panel.get('number', '?')
938
  panel_html = f"""
939
  <div style="border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; border-radius: 8px; background: #f9f9f9;">
940
+ <h4 style="color: #764ba2; margin-top: 0;">๐ŸŽฌ ํŒจ๋„ {panel_num}</h4>
941
  <p><strong>์ƒท ํƒ€์ž…:</strong> {panel.get('shot', 'N/A')}</p>
942
+ <p><strong>์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ:</strong><br>
943
+ <code style="background: #fff; padding: 8px; display: block; border-radius: 4px; font-size: 12px;">
944
+ {panel.get('prompt', 'N/A')}
945
+ </code></p>
946
  {f"<p><strong>๋Œ€์‚ฌ:</strong> {panel.get('dialogue')}</p>" if panel.get('dialogue') else ""}
947
  {f"<p><strong>๋‚˜๋ ˆ์ด์…˜:</strong> {panel.get('narration')}</p>" if panel.get('narration') else ""}
948
  {f"<p><strong>ํšจ๊ณผ์Œ:</strong> {panel.get('effects')}</p>" if panel.get('effects') else ""}
949
+ <div id="panel_{panel_num}_buttons" style="margin-top: 10px;">
950
+ <button onclick="generateImage({panel_num}, '{session_id}')"
951
+ style="background: #667eea; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;">
952
+ ๐ŸŽจ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
953
+ </button>
954
+ <button onclick="regenerateImage({panel_num}, '{session_id}')"
955
+ style="background: #764ba2; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px;">
956
+ ๐Ÿ”„ ๋‹ค์‹œ ์ƒ์„ฑ
957
+ </button>
958
+ </div>
959
  </div>
960
  """
961
  formatted_panels.append(panel_html)
 
981
 
982
  return content
983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  def generate_random_webtoon_theme(genre: str, language: str) -> str:
985
  """Generate random webtoon theme"""
986
  templates = {
 
1054
  text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
1055
  }
1056
 
1057
+ .character-profile {
1058
+ background: #f0f0f0;
1059
+ padding: 10px;
1060
+ margin: 5px 0;
1061
+ border-radius: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1062
  }
1063
 
1064
+ .panel-image-container {
1065
+ min-height: 400px;
1066
  background: #fff;
1067
+ border: 2px dashed #ddd;
1068
  border-radius: 8px;
1069
  margin-bottom: 20px;
1070
  display: flex;
1071
  align-items: center;
1072
  justify-content: center;
 
 
1073
  }
1074
  </style>
1075
 
1076
+ <script>
1077
+ async function generateImage(panelNum, sessionId) {
1078
+ const promptElement = document.querySelector(`#panel_${panelNum}_prompt`);
1079
+ if (!promptElement) return;
1080
+
1081
+ const prompt = promptElement.textContent;
1082
+ const imageContainer = document.querySelector(`#panel_${panelNum}_image`);
1083
+
1084
+ imageContainer.innerHTML = '<p>์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘...</p>';
1085
+
1086
+ // Call image generation API
1087
+ // This would be connected to your backend
1088
+ console.log('Generating image for panel', panelNum, 'with prompt:', prompt);
1089
+ }
1090
+
1091
+ async function regenerateImage(panelNum, sessionId) {
1092
+ generateImage(panelNum, sessionId);
1093
+ }
1094
+ </script>
1095
+
1096
  <div class="main-header">
1097
  <h1 class="header-title">๐ŸŽจ K-Webtoon Storyboard Generator</h1>
1098
  <p class="header-subtitle">ํ•œ๊ตญํ˜• ์›นํˆฐ ๊ธฐํš ๋ฐ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ž๋™ ์ƒ์„ฑ ์‹œ์Šคํ…œ</p>
 
1103
  current_session_id = gr.State(None)
1104
  planning_state = gr.State("")
1105
  storyboard_state = gr.State("")
1106
+ character_profiles_state = gr.State({})
1107
 
1108
  with gr.Tab("๐Ÿ“š ๊ธฐํš์•ˆ ์ž‘์„ฑ"):
1109
  with gr.Group():
 
1136
  )
1137
 
1138
  gr.Markdown("""
1139
+ **๐ŸŽญ ์บ๋ฆญํ„ฐ ์–ผ๊ตด ์„ค์ •**
1140
 
1141
+ ๊ฐ ์บ๋ฆญํ„ฐ์— ์œ ๋ช…์ธ ๋‹ฎ์€๊ผด์ด
1142
+ ์ž๋™์œผ๋กœ ๋ฐฐ์ •๋ฉ๋‹ˆ๋‹ค.
1143
  """)
1144
 
1145
  status_text = gr.Textbox(
 
1151
  # Planning output
1152
  planning_display = gr.Markdown("*๊ธฐํš์•ˆ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค*")
1153
 
1154
+ # Character profiles display
1155
+ character_display = gr.HTML(label="์บ๋ฆญํ„ฐ ํ”„๋กœํ•„")
1156
+
1157
  with gr.Row():
1158
  download_format = gr.Radio(
1159
+ choices=["TXT"],
1160
  value="TXT",
1161
  label="๋‹ค์šด๋กœ๋“œ ํ˜•์‹"
1162
  )
 
1167
  with gr.Tab("๐ŸŽฌ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ"):
1168
  gr.Markdown("""
1169
  ### ๐Ÿ“‹ 1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ (30 ํŒจ๋„)
1170
+ ์ขŒ์ธก: ํ…์ŠคํŠธ ๋ฐ ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ | ์šฐ์ธก: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€
1171
  """)
1172
 
1173
  with gr.Row():
 
1178
  )
1179
 
1180
  with gr.Column():
1181
+ image_display_area = gr.HTML(
1182
+ value="""
1183
+ <div style="padding: 20px; background: #fafafa; border: 2px dashed #ddd; border-radius: 10px; min-height: 800px;">
1184
+ <p style="color: #999; text-align: center;">๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ณต๊ฐ„</p>
1185
+ <p style="color: #aaa; text-align: center; font-size: 12px;">๊ฐ ํŒจ๋„์˜ ์ƒ์„ฑ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์ด๋ฏธ์ง€๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค</p>
1186
+ </div>
1187
+ """,
1188
+ label="์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€"
1189
+ )
1190
+
1191
+ # Image generation controls
1192
+ with gr.Row():
1193
+ generate_all_btn = gr.Button("๐ŸŽจ ๋ชจ๋“  ์ด๋ฏธ์ง€ ์ƒ์„ฑ", variant="primary")
1194
+ clear_images_btn = gr.Button("๐Ÿ—‘๏ธ ์ด๋ฏธ์ง€ ์ดˆ๊ธฐํ™”", variant="secondary")
1195
 
1196
  with gr.Tab("๐Ÿ“š ์‚ฌ์šฉ ๊ฐ€์ด๋“œ"):
1197
  gr.Markdown("""
 
1201
  - ์žฅ๋ฅด ์„ ํƒ
1202
  - ์Šคํ† ๋ฆฌ ์ฝ˜์…‰ํŠธ ์ž…๋ ฅ
1203
  - "๊ธฐํš ์‹œ์ž‘" ํด๋ฆญ
1204
+ - 40ํ™” ์ „์ฒด ๊ตฌ์„ฑ๊ณผ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค
1205
 
1206
+ 2. **์บ๋ฆญํ„ฐ ์–ผ๊ตด ์„ค์ •**:
1207
+ - ๊ฐ ์บ๋ฆญํ„ฐ์— ์œ ๋ช…์ธ ๋‹ฎ์€๊ผด์ด ์ž๋™ ๋ฐฐ์ •๋ฉ๋‹ˆ๋‹ค
1208
+ - ์ด๋ฏธ์ง€ ํ”„๋กฌํ”„ํŠธ์— "์บ๋ฆญํ„ฐ๋ช…(์œ ๋ช…์ธ ๋‹ฎ์€ ์–ผ๊ตด)" ํ˜•์‹์œผ๋กœ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค
 
1209
 
1210
+ 3. **1ํ™” ์Šคํ† ๋ฆฌ๋ณด๋“œ ํƒญ**์—์„œ:
1211
+ - 30๊ฐœ ํŒจ๋„์˜ ์ƒ์„ธ ์Šคํ† ๋ฆฌ๋ณด๋“œ ํ™•์ธ
1212
+ - ๊ฐ ํŒจ๋„๋ณ„ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ
1213
+ - Replicate API๋ฅผ ํ†ตํ•œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
1214
 
1215
+ ### API ์„ค์ •
1216
+ - REPLICATE_API_TOKEN ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ•„์š”
1217
+ - ์ด๋ฏธ์ง€ ์ƒ์„ฑ์€ qwen/qwen-image ๋ชจ๋ธ ์‚ฌ์šฉ
 
 
 
 
 
 
 
1218
  """)
1219
 
1220
  # Event handlers
 
1222
  system = WebtoonSystem()
1223
  planning = ""
1224
  storyboard = ""
1225
+ character_profiles = {}
1226
 
1227
+ for planning_content, storyboard_content, status, new_session_id, profiles in system.process_webtoon_stream(query, genre, language, session_id):
1228
  planning = planning_content
1229
  storyboard = storyboard_content
1230
+ character_profiles = profiles
1231
+ yield planning, storyboard, status, new_session_id, character_profiles
1232
+
1233
+ def format_character_profiles(profiles: Dict[str, CharacterProfile]) -> str:
1234
+ """Format character profiles for display"""
1235
+ if not profiles:
1236
+ return ""
1237
+
1238
+ html = "<h3>๐ŸŽญ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„</h3>"
1239
+ for name, profile in profiles.items():
1240
+ html += f"""
1241
+ <div class="character-profile">
1242
+ <strong>{name}</strong> - {profile.celebrity_lookalike} ๋‹ฎ์€ ์–ผ๊ตด
1243
+ <br>์—ญํ• : {profile.role}
1244
+ <br>์„ฑ๊ฒฉ: {profile.personality}
1245
+ </div>
1246
+ """
1247
+ return html
1248
 
1249
+ def update_storyboard_display(storyboard_content, character_profiles, session_id):
1250
  """Update storyboard display with formatted panels"""
1251
  if not storyboard_content or storyboard_content == "":
1252
  return "<p style='color: #999; text-align: center; padding: 50px;'>๊ธฐํš์•ˆ ์ž‘์„ฑ ํ›„ ์Šคํ† ๋ฆฌ๋ณด๋“œ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค</p>"
1253
 
1254
+ return format_storyboard_for_display(storyboard_content, character_profiles, session_id)
1255
 
1256
  def handle_random_theme(genre, language):
1257
  return generate_random_webtoon_theme(genre, language)
 
1260
  """Handle download request"""
1261
  try:
1262
  title = f"{genre} ์›นํˆฐ"
1263
+ content = export_to_txt(planning, storyboard, genre, title)
1264
 
1265
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
1266
+ suffix='.txt', delete=False) as f:
1267
+ f.write(content)
1268
+ return f.name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1269
 
1270
  except Exception as e:
1271
  logger.error(f"Download error: {e}")
1272
  gr.Warning(f"๋‹ค์šด๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
1273
  return None
1274
 
1275
+ def generate_all_images(session_id, storyboard_content):
1276
+ """Generate images for all panels"""
1277
+ if not REPLICATE_API_TOKEN:
1278
+ return "<p style='color: red;'>Replicate API ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.</p>"
1279
+
1280
+ # Parse panels and generate images
1281
+ image_html = "<div style='padding: 20px;'>"
1282
+ image_html += "<h3>๐Ÿ–ผ๏ธ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€</h3>"
1283
+
1284
+ # This would iterate through panels and generate images
1285
+ # For now, returning placeholder
1286
+ for i in range(1, 31):
1287
+ image_html += f"""
1288
+ <div class="panel-image-container" id="panel_{i}_image">
1289
+ <p style='color: #999;'>ํŒจ๋„ {i} ์ด๋ฏธ์ง€ (์ƒ์„ฑ ๋Œ€๊ธฐ)</p>
1290
+ </div>
1291
+ """
1292
+
1293
+ image_html += "</div>"
1294
+ return image_html
1295
+
1296
+ def clear_all_images():
1297
+ """Clear all generated images"""
1298
+ return """
1299
+ <div style="padding: 20px; background: #fafafa; border: 2px dashed #ddd; border-radius: 10px; min-height: 800px;">
1300
+ <p style="color: #999; text-align: center;">๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ณต๊ฐ„</p>
1301
+ <p style="color: #aaa; text-align: center; font-size: 12px;">์ด๋ฏธ์ง€๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค</p>
1302
+ </div>
1303
+ """
1304
+
1305
+ # Connect events
1306
  submit_btn.click(
1307
  fn=process_query,
1308
  inputs=[query_input, genre_select, language_select, current_session_id],
1309
+ outputs=[planning_state, storyboard_state, status_text, current_session_id, character_profiles_state]
1310
  ).then(
1311
  fn=lambda x: x,
1312
  inputs=[planning_state],
1313
  outputs=[planning_display]
1314
+ ).then(
1315
+ fn=format_character_profiles,
1316
+ inputs=[character_profiles_state],
1317
+ outputs=[character_display]
1318
  ).then(
1319
  fn=update_storyboard_display,
1320
+ inputs=[storyboard_state, character_profiles_state, current_session_id],
1321
  outputs=[storyboard_text_display]
1322
  )
1323
 
 
1337
  outputs=[download_file]
1338
  )
1339
 
1340
+ generate_all_btn.click(
1341
+ fn=generate_all_images,
1342
+ inputs=[current_session_id, storyboard_state],
1343
+ outputs=[image_display_area]
1344
+ )
1345
+
1346
+ clear_images_btn.click(
1347
+ fn=clear_all_images,
1348
+ inputs=[],
1349
+ outputs=[image_display_area]
1350
+ )
1351
+
1352
  # Examples
1353
  gr.Examples(
1354
  examples=[
 
1377
  logger.info(f"Model: {MODEL_ID}")
1378
  logger.info(f"Target: {TARGET_EPISODES} episodes, {PANELS_PER_EPISODE} panels per episode")
1379
  logger.info("Genres: " + ", ".join(WEBTOON_GENRES.keys()))
1380
+
1381
+ if REPLICATE_API_TOKEN:
1382
+ logger.info("Replicate API: Configured โœ“")
1383
+ else:
1384
+ logger.warning("Replicate API: Not configured (Image generation disabled)")
1385
 
1386
  logger.info("=" * 60)
1387
 
 
1396
  server_name="0.0.0.0",
1397
  server_port=7860,
1398
  share=False
1399
+ )