Spaces:
Running
Running
Update app.py
Browse files
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)
|
|
|
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 |
-
|
360 |
-
|
|
|
|
|
|
|
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
|
|
|
|
|
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}_{
|
385 |
generated_images_cache[cache_key] = image_url
|
386 |
|
387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
|
389 |
-
return
|
390 |
|
391 |
except Exception as e:
|
392 |
-
logger.error(f"Image generation error: {e}")
|
393 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
1078 |
-
|
1079 |
-
|
|
|
|
|
|
|
|
|
|
|
1080 |
|
1081 |
-
const
|
1082 |
-
|
1083 |
|
1084 |
-
|
|
|
|
|
1085 |
|
1086 |
-
//
|
1087 |
-
|
1088 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1089 |
}
|
1090 |
|
1091 |
-
|
1092 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1281 |
image_html = "<div style='padding: 20px;'>"
|
1282 |
image_html += "<h3>πΌοΈ μμ±λ μ΄λ―Έμ§</h3>"
|
1283 |
-
|
1284 |
-
|
1285 |
-
|
1286 |
-
|
1287 |
-
|
1288 |
-
|
1289 |
-
|
1290 |
-
|
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 """
|