openfree commited on
Commit
fa24ce6
Β·
verified Β·
1 Parent(s): a834421

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -41
app.py CHANGED
@@ -21,7 +21,8 @@ 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')
27
  logger = logging.getLogger(__name__)
@@ -121,17 +122,19 @@ class WebtoonBible:
121
 
122
  @dataclass
123
  class StoryboardPanel:
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:
@@ -354,15 +357,20 @@ class WebtoonDatabase:
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 = {
@@ -381,16 +389,64 @@ class ImageGenerator:
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:
@@ -864,7 +920,7 @@ Write detailed image prompts including celebrity lookalike descriptions.
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
 
870
  panels = []
@@ -877,10 +933,13 @@ Write detailed image prompts including celebrity lookalike descriptions.
877
  if current_panel:
878
  panels.append(current_panel)
879
  panel_number += 1
 
 
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:
@@ -904,6 +963,7 @@ Write detailed image prompts including celebrity lookalike descriptions.
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"""
@@ -1074,22 +1134,39 @@ def create_interface():
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
 
@@ -1272,27 +1349,108 @@ def create_interface():
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 """
 
21
  from PIL import Image
22
  import io as io_module
23
  import base64
24
+ import concurrent.futures
25
+ from threading import Lock
26
  # --- Logging setup ---
27
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
28
  logger = logging.getLogger(__name__)
 
122
 
123
  @dataclass
124
  class StoryboardPanel:
125
+ """Individual storyboard panel with unique ID"""
126
  panel_number: int
127
  scene_type: str # wide, close-up, medium, establishing
128
  image_prompt: str # Image generation prompt with character descriptions
129
+ panel_id: str = "" # Unique panel identifier
130
  dialogue: List[str] = field(default_factory=list)
131
  narration: str = ""
132
  sound_effects: List[str] = field(default_factory=list)
133
  emotion_notes: str = ""
134
  camera_angle: str = ""
135
  background: str = ""
136
+ characters_in_scene: List[str] = field(default_factory=list)
137
+ generated_image_url: str = "" # URL of generated image
138
 
139
  @dataclass
140
  class EpisodeStoryboard:
 
357
 
358
  # --- Image Generation ---
359
  class ImageGenerator:
360
+ """Handle image generation using Replicate API with multi-threading"""
361
 
362
+ def __init__(self):
363
+ self.generation_lock = Lock()
364
+ self.active_generations = {}
365
+
366
+ def generate_image(self, prompt: str, panel_id: str, session_id: str) -> Dict[str, Any]:
367
  """Generate image using Replicate API"""
368
  try:
369
  if not REPLICATE_API_TOKEN:
370
  logger.warning("No Replicate API token, returning placeholder")
371
+ return {"panel_id": panel_id, "status": "error", "message": "No API token"}
372
+
373
+ logger.info(f"Generating image for panel {panel_id}")
374
 
375
  # Run the model
376
  input_params = {
 
389
  image_url = output[0].url() if hasattr(output[0], 'url') else str(output[0])
390
 
391
  # Cache the image
392
+ cache_key = f"{session_id}_{panel_id}"
393
  generated_images_cache[cache_key] = image_url
394
 
395
+ logger.info(f"Successfully generated image for panel {panel_id}")
396
+ return {
397
+ "panel_id": panel_id,
398
+ "status": "success",
399
+ "image_url": image_url,
400
+ "prompt": prompt
401
+ }
402
 
403
+ return {"panel_id": panel_id, "status": "error", "message": "No output from model"}
404
 
405
  except Exception as e:
406
+ logger.error(f"Image generation error for panel {panel_id}: {e}")
407
+ return {"panel_id": panel_id, "status": "error", "message": str(e)}
408
+
409
+ def generate_multiple_images(self, panel_prompts: List[Dict], session_id: str, max_workers: int = 5) -> List[Dict]:
410
+ """Generate multiple images in parallel"""
411
+ results = []
412
+
413
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
414
+ # Submit all tasks
415
+ future_to_panel = {}
416
+ for panel_data in panel_prompts:
417
+ panel_id = panel_data['panel_id']
418
+ prompt = panel_data['prompt']
419
+
420
+ future = executor.submit(
421
+ self.generate_image,
422
+ prompt,
423
+ panel_id,
424
+ session_id
425
+ )
426
+ future_to_panel[future] = panel_data
427
+
428
+ # Collect results as they complete
429
+ for future in concurrent.futures.as_completed(future_to_panel):
430
+ panel_data = future_to_panel[future]
431
+ try:
432
+ result = future.result(timeout=60)
433
+ results.append(result)
434
+ except concurrent.futures.TimeoutError:
435
+ results.append({
436
+ "panel_id": panel_data['panel_id'],
437
+ "status": "error",
438
+ "message": "Generation timeout"
439
+ })
440
+ except Exception as e:
441
+ results.append({
442
+ "panel_id": panel_data['panel_id'],
443
+ "status": "error",
444
+ "message": str(e)
445
+ })
446
+
447
+ # Sort results by panel_id to maintain order
448
+ results.sort(key=lambda x: int(x['panel_id'].split('_panel')[1]))
449
+ return results
450
 
451
  # --- LLM Integration ---
452
  class WebtoonSystem:
 
920
  yield f"❌ 였λ₯˜ λ°œμƒ: {e}", "", "", self.current_session_id, {}
921
 
922
  def parse_storyboard(self, content: str, episode_num: int, character_profiles: Dict[str, CharacterProfile]) -> EpisodeStoryboard:
923
+ """Parse storyboard text into structured format with unique panel IDs"""
924
  storyboard = EpisodeStoryboard(episode_number=episode_num, title=f"{episode_num}ν™”")
925
 
926
  panels = []
 
933
  if current_panel:
934
  panels.append(current_panel)
935
  panel_number += 1
936
+ # Create unique panel ID
937
+ panel_id = f"ep{episode_num}_panel{panel_number}"
938
  current_panel = StoryboardPanel(
939
+ panel_number=panel_number,
940
  scene_type="medium",
941
+ image_prompt="",
942
+ panel_id=panel_id # Add unique panel_id
943
  )
944
  elif current_panel:
945
  if '이미지 ν”„λ‘¬ν”„νŠΈ:' in line or 'Image prompt:' in line:
 
963
  storyboard.panels = panels[:30]
964
  return storyboard
965
 
966
+
967
  # --- Format storyboard for display ---
968
  def format_storyboard_for_display(storyboard_content: str, character_profiles: Dict[str, CharacterProfile], session_id: str) -> str:
969
  """Format storyboard content for panel display with image generation buttons"""
 
1134
  </style>
1135
 
1136
  <script>
1137
+ // κΈ°μ‘΄ generateImage, regenerateImage ν•¨μˆ˜ μ‚­μ œν•˜κ³  μ•„λž˜λ‘œ ꡐ체
1138
+
1139
+ async function regeneratePanel(panelId, sessionId, prompt) {
1140
+ const container = document.querySelector(`#image_${panelId}`);
1141
+ if (!container) {
1142
+ console.error('Container not found for panel:', panelId);
1143
+ return;
1144
+ }
1145
 
1146
+ const parentDiv = container.parentElement;
1147
+ parentDiv.innerHTML = '<p style="text-align: center; padding: 20px;">πŸ”„ μž¬μƒμ„± 쀑...</p>';
1148
 
1149
+ // Gradio의 Python ν•¨μˆ˜λ₯Ό 직접 ν˜ΈμΆœν•˜λŠ” λ°©μ‹μœΌλ‘œ λ³€κ²½
1150
+ // μ‹€μ œλ‘œλŠ” Gradio의 이벀트 μ‹œμŠ€ν…œμ„ 톡해 처리됨
1151
+ console.log('Regenerating panel:', panelId, 'with prompt:', prompt);
1152
 
1153
+ // μž„μ‹œ ν‘œμ‹œ (μ‹€μ œ κ΅¬ν˜„μ‹œ Python λ°±μ—”λ“œμ™€ 연동)
1154
+ setTimeout(() => {
1155
+ parentDiv.innerHTML = `
1156
+ <h4>νŒ¨λ„ ${panelId.split('_panel')[1]}</h4>
1157
+ <p style="color: orange;">⚠️ μž¬μƒμ„± κΈ°λŠ₯ 연동 쀑...</p>
1158
+ <button onclick="regeneratePanel('${panelId}', '${sessionId}', '${prompt}')"
1159
+ style="background: #764ba2; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin-top: 10px;">
1160
+ πŸ”„ μž¬μƒμ„±
1161
+ </button>
1162
+ `;
1163
+ }, 2000);
1164
  }
1165
 
1166
+ // κ°œλ³„ νŒ¨λ„ 이미지 생성 ν•¨μˆ˜
1167
+ function generateSinglePanel(panelNum, sessionId) {
1168
+ console.log('Generating single panel:', panelNum);
1169
+ // Gradio μ΄λ²€νŠΈμ™€ 연동 ν•„μš”
1170
  }
1171
  </script>
1172
 
 
1349
  gr.Warning(f"λ‹€μš΄λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
1350
  return None
1351
 
1352
+ def generate_all_images(session_id, storyboard_content, character_profiles):
1353
+ """Generate images for all panels using multi-threading"""
1354
  if not REPLICATE_API_TOKEN:
1355
  return "<p style='color: red;'>Replicate API 토큰이 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.</p>"
1356
+
1357
+ # Parse storyboard to extract prompts
1358
+ panel_prompts = []
1359
+ lines = storyboard_content.split('\n')
1360
+ current_panel_num = 0
1361
+ current_prompt = ""
1362
+
1363
+ for line in lines:
1364
+ if 'νŒ¨λ„' in line or 'Panel' in line:
1365
+ if current_prompt and current_panel_num > 0:
1366
+ panel_prompts.append({
1367
+ 'panel_id': f"ep1_panel{current_panel_num}",
1368
+ 'panel_num': current_panel_num,
1369
+ 'prompt': current_prompt
1370
+ })
1371
+ current_panel_num += 1
1372
+ current_prompt = ""
1373
+ elif '이미지 ν”„λ‘¬ν”„νŠΈ:' in line or 'Image prompt:' in line:
1374
+ current_prompt = line.split(':', 1)[1].strip()
1375
+
1376
+ # Add last panel if exists
1377
+ if current_prompt and current_panel_num > 0:
1378
+ panel_prompts.append({
1379
+ 'panel_id': f"ep1_panel{current_panel_num}",
1380
+ 'panel_num': current_panel_num,
1381
+ 'prompt': current_prompt
1382
+ })
1383
+
1384
+ # Limit to 30 panels
1385
+ panel_prompts = panel_prompts[:30]
1386
+
1387
+ # Generate images in parallel
1388
+ image_generator = ImageGenerator()
1389
+
1390
+ # Start generation with progress indication
1391
+ image_html = "<div style='padding: 20px;'>"
1392
+ image_html += "<h3>πŸ–ΌοΈ 이미지 생성 쀑...</h3>"
1393
+ image_html += f"<p>총 {len(panel_prompts)}개 νŒ¨λ„ 이미지λ₯Ό λ™μ‹œμ— μƒμ„±ν•©λ‹ˆλ‹€.</p>"
1394
+
1395
+ # Generate images using multi-threading
1396
+ results = image_generator.generate_multiple_images(
1397
+ panel_prompts,
1398
+ session_id,
1399
+ max_workers=5 # Adjust based on API rate limits
1400
+ )
1401
+
1402
+ # Display results with regenerate buttons
1403
  image_html = "<div style='padding: 20px;'>"
1404
  image_html += "<h3>πŸ–ΌοΈ μƒμ„±λœ 이미지</h3>"
1405
+
1406
+ success_count = sum(1 for r in results if r['status'] == 'success')
1407
+ image_html += f"<p>βœ… 성곡: {success_count}/{len(results)} νŒ¨λ„</p>"
1408
+
1409
+ # Display each panel's image with regenerate button
1410
+ for result in results:
1411
+ panel_num = int(result['panel_id'].split('_panel')[1])
1412
+ panel_id = result['panel_id']
1413
+
1414
+ if result['status'] == 'success':
1415
+ image_html += f"""
1416
+ <div class="panel-image-container" style="margin-bottom: 20px; border: 1px solid #ddd; padding: 15px; border-radius: 8px;">
1417
+ <h4>νŒ¨λ„ {panel_num}</h4>
1418
+ <img src="{result['image_url']}" style="width: 100%; max-width: 600px; border-radius: 8px;"
1419
+ id="image_{panel_id}">
1420
+ <p style="font-size: 12px; color: #666; margin-top: 10px;">
1421
+ ν”„λ‘¬ν”„νŠΈ: {result.get('prompt', '')[:100]}...
1422
+ </p>
1423
+ <button onclick="regeneratePanel('{panel_id}', '{session_id}', `{result.get('prompt', '')}`)"
1424
+ style="background: #764ba2; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin-top: 10px;">
1425
+ πŸ”„ μž¬μƒμ„±
1426
+ </button>
1427
+ </div>
1428
+ """
1429
+ else:
1430
+ image_html += f"""
1431
+ <div class="panel-image-container" style="margin-bottom: 20px; background: #fee; padding: 15px; border-radius: 8px;">
1432
+ <h4>νŒ¨λ„ {panel_num}</h4>
1433
+ <p style="color: red;">❌ 생성 μ‹€νŒ¨: {result.get('message', 'Unknown error')}</p>
1434
+ <button onclick="regeneratePanel('{panel_id}', '{session_id}', '')"
1435
+ style="background: #667eea; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;">
1436
+ 🎨 λ‹€μ‹œ 생성
1437
+ </button>
1438
+ </div>
1439
+ """
1440
+
1441
  image_html += "</div>"
1442
  return image_html
1443
+
1444
+ def regenerate_single_panel(panel_id: str, prompt: str, session_id: str) -> Dict:
1445
+ """Regenerate a single panel image"""
1446
+ if not REPLICATE_API_TOKEN:
1447
+ return {"status": "error", "message": "No API token"}
1448
+
1449
+ image_generator = ImageGenerator()
1450
+ result = image_generator.generate_image(prompt, panel_id, session_id)
1451
+ return result
1452
+
1453
+
1454
  def clear_all_images():
1455
  """Clear all generated images"""
1456
  return """