Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1345,28 +1345,6 @@ def create_interface():
|
|
| 1345 |
padding-bottom: 10px;
|
| 1346 |
}
|
| 1347 |
|
| 1348 |
-
.panel-content {
|
| 1349 |
-
display: grid;
|
| 1350 |
-
grid-template-columns: 1fr 1fr;
|
| 1351 |
-
gap: 20px;
|
| 1352 |
-
}
|
| 1353 |
-
|
| 1354 |
-
.panel-text {
|
| 1355 |
-
padding: 15px;
|
| 1356 |
-
background: #f9f9f9;
|
| 1357 |
-
border-radius: 8px;
|
| 1358 |
-
}
|
| 1359 |
-
|
| 1360 |
-
.panel-image {
|
| 1361 |
-
min-height: 300px;
|
| 1362 |
-
background: #f0f0f0;
|
| 1363 |
-
border-radius: 8px;
|
| 1364 |
-
display: flex;
|
| 1365 |
-
align-items: center;
|
| 1366 |
-
justify-content: center;
|
| 1367 |
-
position: relative;
|
| 1368 |
-
}
|
| 1369 |
-
|
| 1370 |
.episode-structure {
|
| 1371 |
background: #f5f5f5;
|
| 1372 |
padding: 15px;
|
|
@@ -1375,17 +1353,6 @@ def create_interface():
|
|
| 1375 |
max-height: 500px;
|
| 1376 |
overflow-y: auto;
|
| 1377 |
}
|
| 1378 |
-
|
| 1379 |
-
.edit-field {
|
| 1380 |
-
margin: 10px 0;
|
| 1381 |
-
}
|
| 1382 |
-
|
| 1383 |
-
.edit-label {
|
| 1384 |
-
font-weight: bold;
|
| 1385 |
-
color: #333;
|
| 1386 |
-
margin-bottom: 5px;
|
| 1387 |
-
display: block;
|
| 1388 |
-
}
|
| 1389 |
</style>
|
| 1390 |
|
| 1391 |
<div class="main-header">
|
|
@@ -1395,7 +1362,7 @@ def create_interface():
|
|
| 1395 |
</div>
|
| 1396 |
""")
|
| 1397 |
|
| 1398 |
-
# State
|
| 1399 |
current_session_id = gr.State(None)
|
| 1400 |
planning_state = gr.State("")
|
| 1401 |
storyboard_state = gr.State("")
|
|
@@ -1451,7 +1418,7 @@ def create_interface():
|
|
| 1451 |
value="μ₯λ₯΄λ₯Ό μ ννκ³ μ½μ
νΈλ₯Ό μ
λ ₯νμΈμ"
|
| 1452 |
)
|
| 1453 |
|
| 1454 |
-
# Planning output
|
| 1455 |
gr.Markdown("### π μΉν° κΈ°νμ (40ν μ 체 ꡬμ±)")
|
| 1456 |
planning_display = gr.Textbox(
|
| 1457 |
label="κΈ°νμ",
|
|
@@ -1461,10 +1428,8 @@ def create_interface():
|
|
| 1461 |
value="κΈ°νμμ΄ μ¬κΈ°μ νμλ©λλ€ (40ν μ 체 κ΅¬μ± ν¬ν¨)"
|
| 1462 |
)
|
| 1463 |
|
| 1464 |
-
# Character profiles display
|
| 1465 |
character_display = gr.HTML(label="μΊλ¦ν° νλ‘ν")
|
| 1466 |
|
| 1467 |
-
# Download section
|
| 1468 |
with gr.Row():
|
| 1469 |
download_planning_btn = gr.Button("π₯ κΈ°νμ λ€μ΄λ‘λ (40ν ꡬμ±)", variant="secondary")
|
| 1470 |
|
|
@@ -1473,103 +1438,40 @@ def create_interface():
|
|
| 1473 |
with gr.Tab("π¬ 1ν μ€ν 리보λ (30ν¨λ)"):
|
| 1474 |
gr.Markdown("""
|
| 1475 |
### π 1ν μ€ν 리보λ - 30κ° ν¨λ
|
| 1476 |
-
κ° ν¨λμ ν
μ€νΈλ₯Ό
|
| 1477 |
""")
|
| 1478 |
|
| 1479 |
-
#
|
| 1480 |
with gr.Row():
|
| 1481 |
-
|
| 1482 |
-
generate_all_images_btn = gr.Button("π¨ λͺ¨λ ν¨λ μ΄λ―Έμ§ μμ±
|
| 1483 |
download_storyboard_btn = gr.Button("π₯ μ€ν 리보λ λ€μ΄λ‘λ", variant="secondary")
|
| 1484 |
clear_images_btn = gr.Button("ποΈ μ΄λ―Έμ§ μ΄κΈ°ν", variant="secondary")
|
| 1485 |
|
| 1486 |
storyboard_download_file = gr.File(visible=False)
|
|
|
|
| 1487 |
|
| 1488 |
-
#
|
| 1489 |
-
|
| 1490 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1491 |
|
| 1492 |
-
#
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
lines=1,
|
| 1505 |
-
interactive=True,
|
| 1506 |
-
elem_id=f"shot_{i+1}"
|
| 1507 |
-
)
|
| 1508 |
-
|
| 1509 |
-
prompt_input = gr.Textbox(
|
| 1510 |
-
label="μ΄λ―Έμ§ ν둬ννΈ (νκΈ)",
|
| 1511 |
-
lines=3,
|
| 1512 |
-
interactive=True,
|
| 1513 |
-
elem_id=f"prompt_{i+1}"
|
| 1514 |
-
)
|
| 1515 |
-
|
| 1516 |
-
prompt_en_display = gr.Textbox(
|
| 1517 |
-
label="μμ΄ λ²μ (μλ)",
|
| 1518 |
-
lines=3,
|
| 1519 |
-
interactive=False,
|
| 1520 |
-
elem_id=f"prompt_en_{i+1}"
|
| 1521 |
-
)
|
| 1522 |
-
|
| 1523 |
-
dialogue_input = gr.Textbox(
|
| 1524 |
-
label="λμ¬",
|
| 1525 |
-
lines=2,
|
| 1526 |
-
interactive=True,
|
| 1527 |
-
elem_id=f"dialogue_{i+1}"
|
| 1528 |
-
)
|
| 1529 |
-
|
| 1530 |
-
narration_input = gr.Textbox(
|
| 1531 |
-
label="λλ μ΄μ
",
|
| 1532 |
-
lines=2,
|
| 1533 |
-
interactive=True,
|
| 1534 |
-
elem_id=f"narration_{i+1}"
|
| 1535 |
-
)
|
| 1536 |
-
|
| 1537 |
-
effects_input = gr.Textbox(
|
| 1538 |
-
label="ν¨κ³Όμ",
|
| 1539 |
-
lines=1,
|
| 1540 |
-
interactive=True,
|
| 1541 |
-
elem_id=f"effects_{i+1}"
|
| 1542 |
-
)
|
| 1543 |
-
|
| 1544 |
-
with gr.Column():
|
| 1545 |
-
# Image display
|
| 1546 |
-
panel_image = gr.Image(
|
| 1547 |
-
label=f"ν¨λ {i+1} μ΄λ―Έμ§",
|
| 1548 |
-
elem_id=f"image_{i+1}",
|
| 1549 |
-
type="pil"
|
| 1550 |
-
)
|
| 1551 |
-
|
| 1552 |
-
# Individual regeneration button
|
| 1553 |
-
regenerate_btn = gr.Button(
|
| 1554 |
-
f"π ν¨λ {i+1} μ΄λ―Έμ§ μ¬μμ±",
|
| 1555 |
-
variant="secondary",
|
| 1556 |
-
elem_id=f"regen_{i+1}"
|
| 1557 |
-
)
|
| 1558 |
-
|
| 1559 |
-
panel_components.append({
|
| 1560 |
-
'group': panel_group,
|
| 1561 |
-
'shot': shot_input,
|
| 1562 |
-
'prompt': prompt_input,
|
| 1563 |
-
'prompt_en': prompt_en_display,
|
| 1564 |
-
'dialogue': dialogue_input,
|
| 1565 |
-
'narration': narration_input,
|
| 1566 |
-
'effects': effects_input,
|
| 1567 |
-
'image': panel_image,
|
| 1568 |
-
'regenerate': regenerate_btn,
|
| 1569 |
-
'panel_num': i+1
|
| 1570 |
-
})
|
| 1571 |
-
|
| 1572 |
-
# Event handlers
|
| 1573 |
def process_query(query, genre, language, session_id):
|
| 1574 |
system = WebtoonSystem()
|
| 1575 |
planning = ""
|
|
@@ -1583,7 +1485,6 @@ def create_interface():
|
|
| 1583 |
yield planning, storyboard, status, new_session_id, character_profiles, system
|
| 1584 |
|
| 1585 |
def format_character_profiles(profiles: Dict[str, CharacterProfile]) -> str:
|
| 1586 |
-
"""Format character profiles for display"""
|
| 1587 |
if not profiles:
|
| 1588 |
return ""
|
| 1589 |
|
|
@@ -1599,124 +1500,73 @@ def create_interface():
|
|
| 1599 |
"""
|
| 1600 |
return html
|
| 1601 |
|
| 1602 |
-
def
|
| 1603 |
-
"""
|
| 1604 |
-
if not
|
| 1605 |
-
return [
|
| 1606 |
-
|
| 1607 |
-
if "ν¨λ" not in storyboard_content and "Panel" not in storyboard_content:
|
| 1608 |
-
return [gr.update(visible=False) for _ in range(30*9)], []
|
| 1609 |
-
|
| 1610 |
-
panel_data = parse_storyboard_panels(storyboard_content, character_profiles)
|
| 1611 |
-
updates = []
|
| 1612 |
-
|
| 1613 |
-
for i in range(30):
|
| 1614 |
-
if i < len(panel_data):
|
| 1615 |
-
panel = panel_data[i]
|
| 1616 |
-
# Group visibility
|
| 1617 |
-
updates.append(gr.update(visible=True))
|
| 1618 |
-
# Shot type
|
| 1619 |
-
updates.append(gr.update(value=panel.get('shot', '')))
|
| 1620 |
-
# Prompt
|
| 1621 |
-
updates.append(gr.update(value=panel.get('prompt', '')))
|
| 1622 |
-
# Prompt English
|
| 1623 |
-
updates.append(gr.update(value=panel.get('prompt_en', '')))
|
| 1624 |
-
# Dialogue
|
| 1625 |
-
updates.append(gr.update(value=panel.get('dialogue', '')))
|
| 1626 |
-
# Narration
|
| 1627 |
-
updates.append(gr.update(value=panel.get('narration', '')))
|
| 1628 |
-
# Effects
|
| 1629 |
-
updates.append(gr.update(value=panel.get('effects', '')))
|
| 1630 |
-
# Image (initially empty)
|
| 1631 |
-
updates.append(gr.update(value=None))
|
| 1632 |
-
# Regenerate button
|
| 1633 |
-
updates.append(gr.update())
|
| 1634 |
-
else:
|
| 1635 |
-
# Hide unused panels
|
| 1636 |
-
updates.extend([gr.update(visible=False)] + [gr.update()] * 8)
|
| 1637 |
|
| 1638 |
-
|
| 1639 |
-
|
| 1640 |
-
def save_panel_edits(*args):
|
| 1641 |
-
"""Save edited panel data"""
|
| 1642 |
-
# args contains all panel component values
|
| 1643 |
panel_data = []
|
| 1644 |
-
|
|
|
|
| 1645 |
|
| 1646 |
-
for
|
| 1647 |
-
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
-
|
| 1651 |
-
|
| 1652 |
-
|
| 1653 |
-
|
| 1654 |
-
|
| 1655 |
-
'
|
| 1656 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1657 |
'image_url': None
|
| 1658 |
}
|
| 1659 |
-
|
| 1660 |
-
|
| 1661 |
-
|
| 1662 |
-
|
| 1663 |
-
|
| 1664 |
-
|
| 1665 |
-
|
| 1666 |
-
|
| 1667 |
-
|
| 1668 |
-
|
| 1669 |
-
|
| 1670 |
-
webtoon_system = WebtoonSystem()
|
| 1671 |
-
|
| 1672 |
-
panel = next((p for p in panel_data if p['number'] == panel_num), None)
|
| 1673 |
-
|
| 1674 |
-
if not panel or not panel.get('prompt'):
|
| 1675 |
-
return gr.update(value=None)
|
| 1676 |
-
|
| 1677 |
-
# Translate if needed
|
| 1678 |
-
if not panel.get('prompt_en'):
|
| 1679 |
-
panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
|
| 1680 |
-
panel['prompt'], character_profiles
|
| 1681 |
-
)
|
| 1682 |
|
| 1683 |
-
|
| 1684 |
-
|
| 1685 |
-
panel['prompt_en'],
|
| 1686 |
-
f"ep1_panel{panel_num}",
|
| 1687 |
-
session_id
|
| 1688 |
-
)
|
| 1689 |
-
|
| 1690 |
-
if result['status'] == 'success':
|
| 1691 |
-
# Load image from URL
|
| 1692 |
-
import requests
|
| 1693 |
-
from PIL import Image
|
| 1694 |
-
from io import BytesIO
|
| 1695 |
-
|
| 1696 |
-
response = requests.get(result['image_url'])
|
| 1697 |
-
img = Image.open(BytesIO(response.content))
|
| 1698 |
-
return gr.update(value=img)
|
| 1699 |
|
| 1700 |
-
return
|
| 1701 |
|
| 1702 |
-
def
|
| 1703 |
-
"""Generate images for all panels"""
|
| 1704 |
if not REPLICATE_API_TOKEN:
|
| 1705 |
-
return [
|
|
|
|
|
|
|
|
|
|
| 1706 |
|
| 1707 |
if not webtoon_system:
|
| 1708 |
webtoon_system = WebtoonSystem()
|
| 1709 |
|
| 1710 |
-
|
| 1711 |
total_panels = len(panel_data)
|
|
|
|
|
|
|
| 1712 |
|
| 1713 |
-
for i in
|
| 1714 |
-
|
| 1715 |
-
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
| 1719 |
-
# Translate if needed
|
| 1720 |
if not panel.get('prompt_en'):
|
| 1721 |
panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
|
| 1722 |
panel['prompt'], character_profiles
|
|
@@ -1725,39 +1575,38 @@ def create_interface():
|
|
| 1725 |
# Generate image
|
| 1726 |
result = webtoon_system.image_generator.generate_image(
|
| 1727 |
panel['prompt_en'],
|
| 1728 |
-
f"ep1_panel{
|
| 1729 |
session_id
|
| 1730 |
)
|
| 1731 |
|
| 1732 |
if result['status'] == 'success':
|
|
|
|
| 1733 |
import requests
|
| 1734 |
from PIL import Image
|
| 1735 |
from io import BytesIO
|
| 1736 |
|
| 1737 |
response = requests.get(result['image_url'])
|
| 1738 |
img = Image.open(BytesIO(response.content))
|
| 1739 |
-
|
|
|
|
| 1740 |
else:
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
|
|
|
| 1744 |
|
| 1745 |
time.sleep(0.5) # Rate limiting
|
| 1746 |
-
else:
|
| 1747 |
-
updates.append(gr.update())
|
| 1748 |
|
| 1749 |
-
progress(1.0, desc="
|
| 1750 |
-
return
|
| 1751 |
|
| 1752 |
def clear_all_images():
|
| 1753 |
-
|
| 1754 |
-
return [gr.update(value=None) for _ in range(30)]
|
| 1755 |
|
| 1756 |
def handle_random_theme(genre, language):
|
| 1757 |
return generate_random_webtoon_theme(genre, language)
|
| 1758 |
|
| 1759 |
def download_planning(session_id, planning, genre):
|
| 1760 |
-
"""Download planning document"""
|
| 1761 |
try:
|
| 1762 |
title = f"{genre} μΉν°"
|
| 1763 |
content = export_planning_to_txt(planning, genre, title)
|
|
@@ -1770,23 +1619,9 @@ def create_interface():
|
|
| 1770 |
logger.error(f"Download error: {e}")
|
| 1771 |
return None
|
| 1772 |
|
| 1773 |
-
def
|
| 1774 |
-
"""Download storyboard from panel data"""
|
| 1775 |
try:
|
| 1776 |
-
content =
|
| 1777 |
-
content += f"{genre} μΉν° - 1ν μ€ν 리보λ\n"
|
| 1778 |
-
content += f"{'=' * 50}\n\n"
|
| 1779 |
-
|
| 1780 |
-
for panel in panel_data:
|
| 1781 |
-
content += f"\nν¨λ {panel['number']}:\n"
|
| 1782 |
-
content += f"- μ· νμ
: {panel.get('shot', '')}\n"
|
| 1783 |
-
content += f"- μ΄λ―Έμ§ ν둬ννΈ: {panel.get('prompt', '')}\n"
|
| 1784 |
-
if panel.get('dialogue'):
|
| 1785 |
-
content += f"- λμ¬: {panel.get('dialogue')}\n"
|
| 1786 |
-
if panel.get('narration'):
|
| 1787 |
-
content += f"- λλ μ΄μ
: {panel.get('narration')}\n"
|
| 1788 |
-
if panel.get('effects'):
|
| 1789 |
-
content += f"- ν¨κ³Όμ: {panel.get('effects')}\n"
|
| 1790 |
|
| 1791 |
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
| 1792 |
suffix='_storyboard.txt', delete=False) as f:
|
|
@@ -1797,8 +1632,6 @@ def create_interface():
|
|
| 1797 |
return None
|
| 1798 |
|
| 1799 |
# Connect events
|
| 1800 |
-
|
| 1801 |
-
# Main submission
|
| 1802 |
submit_btn.click(
|
| 1803 |
fn=process_query,
|
| 1804 |
inputs=[query_input, genre_select, language_select, current_session_id],
|
|
@@ -1812,36 +1645,19 @@ def create_interface():
|
|
| 1812 |
inputs=[character_profiles_state],
|
| 1813 |
outputs=[character_display]
|
| 1814 |
).then(
|
| 1815 |
-
fn=
|
| 1816 |
-
inputs=[storyboard_state
|
| 1817 |
-
outputs=[
|
| 1818 |
-
# All panel component updates
|
| 1819 |
-
*[comp for panel in panel_components for comp in [
|
| 1820 |
-
panel['group'], panel['shot'], panel['prompt'], panel['prompt_en'],
|
| 1821 |
-
panel['dialogue'], panel['narration'], panel['effects'],
|
| 1822 |
-
panel['image'], panel['regenerate']
|
| 1823 |
-
]],
|
| 1824 |
-
panel_data_state
|
| 1825 |
-
]
|
| 1826 |
)
|
| 1827 |
|
| 1828 |
-
#
|
| 1829 |
-
|
| 1830 |
-
|
| 1831 |
-
|
| 1832 |
-
|
| 1833 |
-
panel['dialogue'], panel['narration'], panel['effects']
|
| 1834 |
-
])
|
| 1835 |
-
|
| 1836 |
-
save_edits_btn.click(
|
| 1837 |
-
fn=save_panel_edits,
|
| 1838 |
-
inputs=all_text_inputs + [panel_data_state, current_session_id, character_profiles_state],
|
| 1839 |
-
outputs=[panel_data_state, edit_status]
|
| 1840 |
).then(
|
| 1841 |
-
fn=lambda: gr.update(visible=
|
| 1842 |
-
|
| 1843 |
-
outputs=[edit_status],
|
| 1844 |
-
_js="() => setTimeout(() => {}, 3000)"
|
| 1845 |
)
|
| 1846 |
|
| 1847 |
# Generate all images
|
|
@@ -1849,24 +1665,15 @@ def create_interface():
|
|
| 1849 |
fn=lambda: gr.update(visible=True, value="μ΄λ―Έμ§ μμ± μμ..."),
|
| 1850 |
outputs=[generation_progress]
|
| 1851 |
).then(
|
| 1852 |
-
fn=
|
| 1853 |
inputs=[panel_data_state, current_session_id, character_profiles_state, webtoon_system],
|
| 1854 |
-
outputs=[
|
| 1855 |
)
|
| 1856 |
|
| 1857 |
-
# Individual panel regeneration
|
| 1858 |
-
for panel in panel_components:
|
| 1859 |
-
panel['regenerate'].click(
|
| 1860 |
-
fn=generate_single_panel,
|
| 1861 |
-
inputs=[gr.State(panel['panel_num']), panel_data_state, current_session_id,
|
| 1862 |
-
character_profiles_state, webtoon_system],
|
| 1863 |
-
outputs=[panel['image']]
|
| 1864 |
-
)
|
| 1865 |
-
|
| 1866 |
# Clear images
|
| 1867 |
clear_images_btn.click(
|
| 1868 |
fn=clear_all_images,
|
| 1869 |
-
outputs=[
|
| 1870 |
)
|
| 1871 |
|
| 1872 |
# Random theme
|
|
@@ -1888,8 +1695,8 @@ def create_interface():
|
|
| 1888 |
)
|
| 1889 |
|
| 1890 |
download_storyboard_btn.click(
|
| 1891 |
-
fn=
|
| 1892 |
-
inputs=[
|
| 1893 |
outputs=[storyboard_download_file]
|
| 1894 |
).then(
|
| 1895 |
fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
|
|
|
|
| 1345 |
padding-bottom: 10px;
|
| 1346 |
}
|
| 1347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
.episode-structure {
|
| 1349 |
background: #f5f5f5;
|
| 1350 |
padding: 15px;
|
|
|
|
| 1353 |
max-height: 500px;
|
| 1354 |
overflow-y: auto;
|
| 1355 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1356 |
</style>
|
| 1357 |
|
| 1358 |
<div class="main-header">
|
|
|
|
| 1362 |
</div>
|
| 1363 |
""")
|
| 1364 |
|
| 1365 |
+
# State variables
|
| 1366 |
current_session_id = gr.State(None)
|
| 1367 |
planning_state = gr.State("")
|
| 1368 |
storyboard_state = gr.State("")
|
|
|
|
| 1418 |
value="μ₯λ₯΄λ₯Ό μ ννκ³ μ½μ
νΈλ₯Ό μ
λ ₯νμΈμ"
|
| 1419 |
)
|
| 1420 |
|
| 1421 |
+
# Planning output
|
| 1422 |
gr.Markdown("### π μΉν° κΈ°νμ (40ν μ 체 ꡬμ±)")
|
| 1423 |
planning_display = gr.Textbox(
|
| 1424 |
label="κΈ°νμ",
|
|
|
|
| 1428 |
value="κΈ°νμμ΄ μ¬κΈ°μ νμλ©λλ€ (40ν μ 체 κ΅¬μ± ν¬ν¨)"
|
| 1429 |
)
|
| 1430 |
|
|
|
|
| 1431 |
character_display = gr.HTML(label="μΊλ¦ν° νλ‘ν")
|
| 1432 |
|
|
|
|
| 1433 |
with gr.Row():
|
| 1434 |
download_planning_btn = gr.Button("π₯ κΈ°νμ λ€μ΄λ‘λ (40ν ꡬμ±)", variant="secondary")
|
| 1435 |
|
|
|
|
| 1438 |
with gr.Tab("π¬ 1ν μ€ν 리보λ (30ν¨λ)"):
|
| 1439 |
gr.Markdown("""
|
| 1440 |
### π 1ν μ€ν 리보λ - 30κ° ν¨λ
|
| 1441 |
+
κ° ν¨λμ ν
μ€νΈλ₯Ό νΈμ§ν μ μμ΅λλ€. νΈμ§ ν 'μ μ©' λ²νΌμ ν΄λ¦νμΈμ.
|
| 1442 |
""")
|
| 1443 |
|
| 1444 |
+
# Control buttons
|
| 1445 |
with gr.Row():
|
| 1446 |
+
apply_edits_btn = gr.Button("β
νΈμ§ λ΄μ© μ μ©", variant="secondary")
|
| 1447 |
+
generate_all_images_btn = gr.Button("π¨ λͺ¨λ ν¨λ μ΄λ―Έμ§ μμ±", variant="primary", size="lg")
|
| 1448 |
download_storyboard_btn = gr.Button("π₯ μ€ν 리보λ λ€μ΄λ‘λ", variant="secondary")
|
| 1449 |
clear_images_btn = gr.Button("ποΈ μ΄λ―Έμ§ μ΄κΈ°ν", variant="secondary")
|
| 1450 |
|
| 1451 |
storyboard_download_file = gr.File(visible=False)
|
| 1452 |
+
generation_progress = gr.Textbox(label="μ§ν μν©", interactive=False, visible=False)
|
| 1453 |
|
| 1454 |
+
# Editable storyboard display
|
| 1455 |
+
storyboard_editor = gr.Textbox(
|
| 1456 |
+
label="μ€ν 리보λ νΈμ§κΈ°",
|
| 1457 |
+
lines=30,
|
| 1458 |
+
max_lines=50,
|
| 1459 |
+
interactive=True,
|
| 1460 |
+
placeholder="μ€ν 리보λκ° μμ±λλ©΄ μ¬κΈ°μ νμλ©λλ€. μμ λ‘κ² νΈμ§νμΈμ."
|
| 1461 |
+
)
|
| 1462 |
|
| 1463 |
+
# Panel images gallery
|
| 1464 |
+
gr.Markdown("### πΌοΈ μμ±λ μ΄λ―Έμ§")
|
| 1465 |
+
images_gallery = gr.Gallery(
|
| 1466 |
+
label="ν¨λ μ΄λ―Έμ§",
|
| 1467 |
+
show_label=False,
|
| 1468 |
+
elem_id="gallery",
|
| 1469 |
+
columns=5,
|
| 1470 |
+
rows=6,
|
| 1471 |
+
height="auto"
|
| 1472 |
+
)
|
| 1473 |
+
|
| 1474 |
+
# Helper functions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1475 |
def process_query(query, genre, language, session_id):
|
| 1476 |
system = WebtoonSystem()
|
| 1477 |
planning = ""
|
|
|
|
| 1485 |
yield planning, storyboard, status, new_session_id, character_profiles, system
|
| 1486 |
|
| 1487 |
def format_character_profiles(profiles: Dict[str, CharacterProfile]) -> str:
|
|
|
|
| 1488 |
if not profiles:
|
| 1489 |
return ""
|
| 1490 |
|
|
|
|
| 1500 |
"""
|
| 1501 |
return html
|
| 1502 |
|
| 1503 |
+
def apply_edited_storyboard(edited_text, session_id, character_profiles):
|
| 1504 |
+
"""Parse and apply edited storyboard text"""
|
| 1505 |
+
if not edited_text:
|
| 1506 |
+
return [], "μ€ν 리보λκ° λΉμ΄μμ΅λλ€."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1507 |
|
| 1508 |
+
# Parse the edited text back into panel data
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1509 |
panel_data = []
|
| 1510 |
+
lines = edited_text.split('\n')
|
| 1511 |
+
current_panel = None
|
| 1512 |
|
| 1513 |
+
for line in lines:
|
| 1514 |
+
if 'ν¨λ' in line and any(char.isdigit() for char in line):
|
| 1515 |
+
if current_panel and current_panel.get('prompt'):
|
| 1516 |
+
panel_data.append(current_panel)
|
| 1517 |
+
|
| 1518 |
+
numbers = re.findall(r'\d+', line)
|
| 1519 |
+
panel_num = int(numbers[0]) if numbers else len(panel_data) + 1
|
| 1520 |
+
|
| 1521 |
+
current_panel = {
|
| 1522 |
+
'number': panel_num,
|
| 1523 |
+
'shot': '',
|
| 1524 |
+
'prompt': '',
|
| 1525 |
+
'prompt_en': '',
|
| 1526 |
+
'dialogue': '',
|
| 1527 |
+
'narration': '',
|
| 1528 |
+
'effects': '',
|
| 1529 |
'image_url': None
|
| 1530 |
}
|
| 1531 |
+
elif current_panel:
|
| 1532 |
+
if 'μ· νμ
:' in line or 'Shot type:' in line.lower():
|
| 1533 |
+
current_panel['shot'] = line.split(':', 1)[1].strip() if ':' in line else ''
|
| 1534 |
+
elif 'μ΄λ―Έμ§ ν둬ννΈ:' in line or 'Image prompt:' in line.lower():
|
| 1535 |
+
current_panel['prompt'] = line.split(':', 1)[1].strip() if ':' in line else ''
|
| 1536 |
+
elif 'λμ¬:' in line or 'Dialogue:' in line.lower():
|
| 1537 |
+
current_panel['dialogue'] = line.split(':', 1)[1].strip() if ':' in line else ''
|
| 1538 |
+
elif 'λλ μ΄μ
:' in line or 'Narration:' in line.lower():
|
| 1539 |
+
current_panel['narration'] = line.split(':', 1)[1].strip() if ':' in line else ''
|
| 1540 |
+
elif 'ν¨κ³Όμ:' in line or 'Sound effect:' in line.lower():
|
| 1541 |
+
current_panel['effects'] = line.split(':', 1)[1].strip() if ':' in line else ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1542 |
|
| 1543 |
+
if current_panel and current_panel.get('prompt'):
|
| 1544 |
+
panel_data.append(current_panel)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1545 |
|
| 1546 |
+
return panel_data[:30], f"β
{len(panel_data)}κ° ν¨λμ΄ μ μ©λμμ΅λλ€."
|
| 1547 |
|
| 1548 |
+
def generate_all_panel_images_from_editor(panel_data, session_id, character_profiles, webtoon_system, progress=gr.Progress()):
|
| 1549 |
+
"""Generate images for all panels from edited data"""
|
| 1550 |
if not REPLICATE_API_TOKEN:
|
| 1551 |
+
return [], gr.update(visible=True, value="β οΈ Replicate API ν ν°μ΄ μ€μ λμ§ μμμ΅λλ€.")
|
| 1552 |
+
|
| 1553 |
+
if not panel_data:
|
| 1554 |
+
return [], gr.update(visible=True, value="β οΈ ν¨λ λ°μ΄ν°κ° μμ΅λλ€.")
|
| 1555 |
|
| 1556 |
if not webtoon_system:
|
| 1557 |
webtoon_system = WebtoonSystem()
|
| 1558 |
|
| 1559 |
+
generated_images = []
|
| 1560 |
total_panels = len(panel_data)
|
| 1561 |
+
successful = 0
|
| 1562 |
+
failed = 0
|
| 1563 |
|
| 1564 |
+
for i, panel in enumerate(panel_data):
|
| 1565 |
+
progress((i / total_panels), desc=f"ν¨λ {panel['number']}/{total_panels} μμ± μ€...")
|
| 1566 |
+
|
| 1567 |
+
if panel.get('prompt'):
|
| 1568 |
+
try:
|
| 1569 |
+
# Translate to English if needed
|
|
|
|
| 1570 |
if not panel.get('prompt_en'):
|
| 1571 |
panel['prompt_en'] = webtoon_system.translate_prompt_to_english(
|
| 1572 |
panel['prompt'], character_profiles
|
|
|
|
| 1575 |
# Generate image
|
| 1576 |
result = webtoon_system.image_generator.generate_image(
|
| 1577 |
panel['prompt_en'],
|
| 1578 |
+
f"ep1_panel{panel['number']}",
|
| 1579 |
session_id
|
| 1580 |
)
|
| 1581 |
|
| 1582 |
if result['status'] == 'success':
|
| 1583 |
+
# Download and convert image
|
| 1584 |
import requests
|
| 1585 |
from PIL import Image
|
| 1586 |
from io import BytesIO
|
| 1587 |
|
| 1588 |
response = requests.get(result['image_url'])
|
| 1589 |
img = Image.open(BytesIO(response.content))
|
| 1590 |
+
generated_images.append((img, f"Panel {panel['number']}"))
|
| 1591 |
+
successful += 1
|
| 1592 |
else:
|
| 1593 |
+
failed += 1
|
| 1594 |
+
except Exception as e:
|
| 1595 |
+
logger.error(f"Error generating panel {panel['number']}: {e}")
|
| 1596 |
+
failed += 1
|
| 1597 |
|
| 1598 |
time.sleep(0.5) # Rate limiting
|
|
|
|
|
|
|
| 1599 |
|
| 1600 |
+
progress(1.0, desc=f"μλ£! μ±κ³΅: {successful}, μ€ν¨: {failed}")
|
| 1601 |
+
return generated_images, gr.update(visible=False)
|
| 1602 |
|
| 1603 |
def clear_all_images():
|
| 1604 |
+
return []
|
|
|
|
| 1605 |
|
| 1606 |
def handle_random_theme(genre, language):
|
| 1607 |
return generate_random_webtoon_theme(genre, language)
|
| 1608 |
|
| 1609 |
def download_planning(session_id, planning, genre):
|
|
|
|
| 1610 |
try:
|
| 1611 |
title = f"{genre} μΉν°"
|
| 1612 |
content = export_planning_to_txt(planning, genre, title)
|
|
|
|
| 1619 |
logger.error(f"Download error: {e}")
|
| 1620 |
return None
|
| 1621 |
|
| 1622 |
+
def download_storyboard_from_editor(edited_text, genre):
|
|
|
|
| 1623 |
try:
|
| 1624 |
+
content = export_storyboard_to_txt(edited_text, genre, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1625 |
|
| 1626 |
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
| 1627 |
suffix='_storyboard.txt', delete=False) as f:
|
|
|
|
| 1632 |
return None
|
| 1633 |
|
| 1634 |
# Connect events
|
|
|
|
|
|
|
| 1635 |
submit_btn.click(
|
| 1636 |
fn=process_query,
|
| 1637 |
inputs=[query_input, genre_select, language_select, current_session_id],
|
|
|
|
| 1645 |
inputs=[character_profiles_state],
|
| 1646 |
outputs=[character_display]
|
| 1647 |
).then(
|
| 1648 |
+
fn=lambda x: x,
|
| 1649 |
+
inputs=[storyboard_state],
|
| 1650 |
+
outputs=[storyboard_editor]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1651 |
)
|
| 1652 |
|
| 1653 |
+
# Apply edits button
|
| 1654 |
+
apply_edits_btn.click(
|
| 1655 |
+
fn=apply_edited_storyboard,
|
| 1656 |
+
inputs=[storyboard_editor, current_session_id, character_profiles_state],
|
| 1657 |
+
outputs=[panel_data_state, generation_progress]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1658 |
).then(
|
| 1659 |
+
fn=lambda: gr.update(visible=True, value="νΈμ§ λ΄μ©μ΄ μ μ©λμμ΅λλ€."),
|
| 1660 |
+
outputs=[generation_progress]
|
|
|
|
|
|
|
| 1661 |
)
|
| 1662 |
|
| 1663 |
# Generate all images
|
|
|
|
| 1665 |
fn=lambda: gr.update(visible=True, value="μ΄λ―Έμ§ μμ± μμ..."),
|
| 1666 |
outputs=[generation_progress]
|
| 1667 |
).then(
|
| 1668 |
+
fn=generate_all_panel_images_from_editor,
|
| 1669 |
inputs=[panel_data_state, current_session_id, character_profiles_state, webtoon_system],
|
| 1670 |
+
outputs=[images_gallery, generation_progress]
|
| 1671 |
)
|
| 1672 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1673 |
# Clear images
|
| 1674 |
clear_images_btn.click(
|
| 1675 |
fn=clear_all_images,
|
| 1676 |
+
outputs=[images_gallery]
|
| 1677 |
)
|
| 1678 |
|
| 1679 |
# Random theme
|
|
|
|
| 1695 |
)
|
| 1696 |
|
| 1697 |
download_storyboard_btn.click(
|
| 1698 |
+
fn=download_storyboard_from_editor,
|
| 1699 |
+
inputs=[storyboard_editor, genre_select],
|
| 1700 |
outputs=[storyboard_download_file]
|
| 1701 |
).then(
|
| 1702 |
fn=lambda x: gr.update(visible=True) if x else gr.update(visible=False),
|