AI-BOOK / app.py
ginipick's picture
Update app.py
898cc4c verified
raw
history blame
18.8 kB
import os
import gradio as gr
import shutil
import uuid
from pathlib import Path
import json
import logging
import traceback
from PIL import Image
import fitz # PyMuPDF for PDF handling
# ────────────────────────────────
# Logging μ„€μ •
# ────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
filename="app.log", # μ‹€ν–‰ 디렉터리에 app.log 파일 μ €μž₯
filemode="a",
)
logging.info("πŸš€ Flipbook app started")
# Constants
TEMP_DIR = "temp"
UPLOAD_DIR = os.path.join(TEMP_DIR, "uploads")
OUTPUT_DIR = os.path.join(TEMP_DIR, "output")
THUMBS_DIR = os.path.join(OUTPUT_DIR, "thumbs")
HTML_DIR = os.path.join("public", "flipbooks") # Directory accessible via web
# Ensure directories exist
for dir_path in [TEMP_DIR, UPLOAD_DIR, OUTPUT_DIR, THUMBS_DIR, HTML_DIR]:
os.makedirs(dir_path, exist_ok=True)
def create_thumbnail(image_path, output_path, size=(300, 300)):
"""Create a thumbnail from an image."""
try:
with Image.open(image_path) as img:
img.thumbnail(size, Image.LANCZOS)
img.save(output_path)
return output_path
except Exception as e:
logging.error("Error creating thumbnail: %s", e)
return None
def process_pdf(pdf_path, session_id):
"""Extract pages from a PDF and save as images with thumbnails."""
pages_info = []
output_folder = os.path.join(OUTPUT_DIR, session_id)
thumbs_folder = os.path.join(THUMBS_DIR, session_id)
os.makedirs(output_folder, exist_ok=True)
os.makedirs(thumbs_folder, exist_ok=True)
try:
pdf_document = fitz.open(pdf_path)
for page_num, page in enumerate(pdf_document):
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
image_path = os.path.join(output_folder, f"page_{page_num + 1}.png")
pix.save(image_path)
thumb_path = os.path.join(thumbs_folder, f"thumb_{page_num + 1}.png")
create_thumbnail(image_path, thumb_path)
html_content = """
<div style="position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;">
<div style="color: #333; font-size: 18px; font-weight: bold;">μΈν„°λž™ν‹°λΈŒ ν”Œλ¦½λΆ 예제</div>
<div style="color: #666; margin-top: 5px;">이 νŽ˜μ΄μ§€λŠ” μΈν„°λž™ν‹°λΈŒ 컨텐츠 κΈ°λŠ₯을 λ³΄μ—¬μ€λ‹ˆλ‹€.</div>
</div>
""" if page_num == 0 else None
pages_info.append({
"src": f"./temp/output/{session_id}/page_{page_num + 1}.png",
"thumb": f"./temp/output/thumbs/{session_id}/thumb_{page_num + 1}.png",
"title": f"νŽ˜μ΄μ§€ {page_num + 1}",
"htmlContent": html_content,
})
logging.info("Processed PDF page %d: %s", page_num + 1, image_path)
return pages_info
except Exception as e:
logging.error("Error processing PDF: %s", e)
return []
def process_images(image_paths, session_id):
"""Process uploaded images and create thumbnails."""
pages_info = []
output_folder = os.path.join(OUTPUT_DIR, session_id)
thumbs_folder = os.path.join(THUMBS_DIR, session_id)
os.makedirs(output_folder, exist_ok=True)
os.makedirs(thumbs_folder, exist_ok=True)
for i, img_path in enumerate(image_paths):
try:
dest_path = os.path.join(output_folder, f"image_{i + 1}.png")
shutil.copy(img_path, dest_path)
thumb_path = os.path.join(thumbs_folder, f"thumb_{i + 1}.png")
create_thumbnail(img_path, thumb_path)
if i == 0:
html_content = """
<div style=\"position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;\">
<div style=\"color: #333; font-size: 18px; font-weight: bold;\">이미지 가러리</div>
<div style=\"color: #666; margin-top: 5px;\">가러리의 첫 번째 μ΄λ―Έμ§€μž…λ‹ˆλ‹€.</div>
</div>
"""
elif i == 1:
html_content = """
<div style=\"position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;\">
<div style=\"color: #333; font-size: 18px; font-weight: bold;\">두 번째 이미지</div>
<div style=\"color: #666; margin-top: 5px;\">νŽ˜μ΄μ§€λ₯Ό λ„˜κΈ°κ±°λ‚˜ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•˜μ—¬ 이미지λ₯Ό 탐색할 수 μžˆμŠ΅λ‹ˆλ‹€.</div>
</div>
"""
else:
html_content = None
pages_info.append({
"src": f"./temp/output/{session_id}/image_{i + 1}.png",
"thumb": f"./temp/output/thumbs/{session_id}/thumb_{i + 1}.png",
"title": f"이미지 {i + 1}",
"htmlContent": html_content,
})
logging.info("Processed image %d: %s", i + 1, dest_path)
except Exception as e:
logging.error("Error processing image %s: %s", img_path, e)
return pages_info
def create_flipbook_from_pdf(pdf_file, view_mode="2d", skin="light"):
"""Create a flipbook from an uploaded PDF."""
session_id = str(uuid.uuid4())
debug_info = []
if not pdf_file:
return (
"<div style='color:red;padding:20px;'>PDF νŒŒμΌμ„ μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.</div>",
"No file uploaded",
)
try:
pdf_path = pdf_file.name
debug_info.append(f"PDF path: {pdf_path}")
# 1) PDF β†’ 이미지
pages_info = process_pdf(pdf_path, session_id)
debug_info.append(f"Number of pages: {len(pages_info)}")
if not pages_info:
raise RuntimeError("PDF 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
# 2) HTML 생성
iframe_html = generate_flipbook_html(pages_info, session_id, view_mode, skin)
return iframe_html, "\n".join(debug_info)
except Exception as e:
tb = traceback.format_exc()
logging.error(tb)
debug_info.append("❌ ERROR ↓↓↓")
debug_info.append(tb)
return (
f"<div style='color:red;padding:20px;'>였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}</div>",
"\n".join(debug_info),
)
def create_flipbook_from_images(images, view_mode="2d", skin="light"):
"""Create a flipbook from uploaded images."""
session_id = str(uuid.uuid4())
debug_info = []
if not images:
return (
"<div style='color:red;padding:20px;'>μ΅œμ†Œ ν•œ 개 μ΄μƒμ˜ 이미지λ₯Ό μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.</div>",
"No images uploaded",
)
try:
image_paths = [img.name for img in images]
debug_info.append(f"Image paths: {image_paths}")
pages_info = process_images(image_paths, session_id)
debug_info.append(f"Number of images processed: {len(pages_info)}")
if not pages_info:
raise RuntimeError("이미지 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
iframe_html = generate_flipbook_html(pages_info, session_id, view_mode, skin)
return iframe_html, "\n".join(debug_info)
except Exception as e:
tb = traceback.format_exc()
logging.error(tb)
debug_info.append("❌ ERROR ↓↓↓")
debug_info.append(tb)
return (
f"<div style='color:red;padding:20px;'>였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}</div>",
"\n".join(debug_info),
)
def generate_flipbook_html(pages_info, session_id, view_mode, skin):
"""Generate a standalone HTML file for the flipbook and return link HTML."""
for page in pages_info:
if page.get("htmlContent") is None:
page.pop("htmlContent", None)
if page.get("items") is None:
page.pop("items", None)
pages_json = json.dumps(pages_info)
html_filename = f"flipbook_{session_id}.html"
html_path = os.path.join(HTML_DIR, html_filename)
html_content = f"""
<!DOCTYPE html>
<html lang=\"ko\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>3D ν”Œλ¦½λΆ</title>
<link rel=\"stylesheet\" type=\"text/css\" href=\"../flipbook.css\">
<style>
body, html {{ margin: 0; padding: 0; height: 100%; overflow: hidden; }}
#flipbook-container {{ width: 100%; height: 100%; position: absolute; top: 0; left: 0; }}
.loading {{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; font-family: Arial, sans-serif; }}
.loading .spinner {{ width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; }}
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
</style>
<script src=\"../flipbook.js\"></script>
<script src=\"../flipbook.webgl.js\"></script>
<script src=\"../flipbook.swipe.js\"></script>
<script src=\"../flipbook.scroll.js\"></script>
<script src=\"../flipbook.book3.js\"></script>
</head>
<body>
<div id=\"flipbook-container\"></div>
<div id=\"loading\" class=\"loading\">
<div class=\"spinner\"></div>
<div>ν”Œλ¦½λΆ λ‘œλ”© 쀑...</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {{
function hideLoading() {{ document.getElementById('loading').style.display = 'none'; }}
try {{
const options = {{
pages: {pages_json},
viewMode: '{view_mode}',
skin: '{skin}',
responsiveView: true,
singlePageMode: false,
singlePageModeIfMobile: true,
pageFlipDuration: 1,
sound: true,
backgroundMusic: false,
thumbnailsOnStart: true,
btnThumbs: {{ enabled: true }},
btnPrint: {{ enabled: true }},
btnDownloadPages: {{ enabled: true }},
btnDownloadPdf: {{ enabled: true }},
btnShare: {{ enabled: true }},
btnSound: {{ enabled: true }},
btnExpand: {{ enabled: true }},
rightToLeft: false,
autoplayOnStart: false,
autoplayInterval: 3000,
}};
const container = document.getElementById('flipbook-container');
if (container) {{
new FlipBook(container, options);
setTimeout(hideLoading, 1000);
}} else {{
console.error('Flipbook container not found');
alert('였λ₯˜: ν”Œλ¦½λΆ μ»¨ν…Œμ΄λ„ˆλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.');
}}
}} catch (error) {{
console.error('Error initializing flipbook:', error);
alert('ν”Œλ¦½λΆ μ΄ˆκΈ°ν™” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ' + error.message);
document.getElementById('loading').innerHTML = '<div>였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.</div>';
}}
}});
</script>
</body>
</html>
"""
with open(html_path, "w", encoding="utf-8") as f:
f.write(html_content)
public_url = f"/public/flipbooks/{html_filename}"
link_html = f"""
<div style=\"text-align:center; padding:20px; background-color:#f9f9f9; border-radius:5px; margin-bottom:20px;\">
<h2 style=\"margin-top:0; color:#333;\">ν”Œλ¦½λΆμ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€!</h2>
<p style=\"margin-bottom:20px;\">μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ ν”Œλ¦½λΆμ„ μƒˆ μ°½μ—μ„œ μ—΄μ–΄λ³΄μ„Έμš”.</p>
<a href=\"{public_url}\" target=\"_blank\" style=\"display:inline-block; background-color:#4CAF50; color:white; padding
<div style="text-align:center; padding:20px; background-color:#f9f9f9; border-radius:5px; margin-bottom:20px;">
<h2 style="margin-top:0; color:#333;">ν”Œλ¦½λΆμ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€!</h2>
<p style="margin-bottom:20px;">μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ ν”Œλ¦½λΆμ„ μƒˆ μ°½μ—μ„œ μ—΄μ–΄λ³΄μ„Έμš”.</p>
<a href="{public_url}" target="_blank" style="display:inline-block; background-color:#4CAF50; color:white; padding:12px 24px; text-decoration:none; border-radius:4px; font-weight:bold; font-size:16px;">ν”Œλ¦½λΆ μ—΄κΈ°</a>
</div>
<div style="margin-top:20px; padding:15px; background-color:#f5f5f5; border-radius:5px; line-height:1.5;">
<h3 style="margin-top:0; color:#333;">μ‚¬μš© 팁:</h3>
<ul style="margin:10px 0; padding-left:20px;">
<li>νŽ˜μ΄μ§€ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•˜μ—¬ λ„˜κΈΈ 수 μžˆμŠ΅λ‹ˆλ‹€.</li>
<li>ν•˜λ‹¨ νˆ΄λ°”μ˜ μ•„μ΄μ½˜μ„ μ‚¬μš©ν•˜μ—¬ λ‹€μ–‘ν•œ κΈ°λŠ₯을 ν™œμš©ν•˜μ„Έμš”.</li>
<li>전체화면 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ 더 큰 ν™”λ©΄μœΌλ‘œ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.</li>
</ul>
<div style="margin-top:10px; padding:10px; background-color:#e8f4fd; border-left:4px solid #2196F3; border-radius:2px;">
<strong>μ°Έκ³ :</strong> ν”Œλ¦½λΆμ€ 2D λͺ¨λ“œμ—μ„œ κ°€μž₯ μ•ˆμ •μ μœΌλ‘œ μž‘λ™ν•©λ‹ˆλ‹€.
</div>
</div>
<div style="margin-top:15px; background-color:#f5f5f5; border-radius:5px; padding:10px;">
<details>
<summary style="cursor:pointer; color:#2196F3; font-weight:bold;">기술적 세뢀사항 (개발자용)</summary>
<div style="margin-top:10px;">
<p>μ„Έμ…˜ ID: {session_id}</p>
<p>HTML 파일 경둜: {html_path}</p>
<p>νŽ˜μ΄μ§€ 수: {len(pages_info)}</p>
<p>λ·° λͺ¨λ“œ: {view_mode}</p>
<p>μŠ€ν‚¨: {skin}</p>
</div>
</details>
</div>
"""
return link_html
# Define the Gradio interface
with gr.Blocks(title="3D Flipbook Viewer") as demo:
gr.Markdown("# 3D Flipbook Viewer")
gr.Markdown("""
## 3D ν”Œλ¦½λΆ λ·°μ–΄
PDF νŒŒμΌμ΄λ‚˜ μ—¬λŸ¬ 이미지λ₯Ό μ—…λ‘œλ“œν•˜μ—¬ μΈν„°λž™ν‹°λΈŒ 3D ν”Œλ¦½λΆμ„ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.
### νŠΉμ§•:
- νŽ˜μ΄μ§€ λ„˜κΉ€ νš¨κ³Όμ™€ ν•¨κ»˜ μΈν„°λž™ν‹°λΈŒν•œ κΈ°λŠ₯ 제곡
- 첫 νŽ˜μ΄μ§€μ—λŠ” μ˜ˆμ‹œλ‘œ μΈν„°λž™ν‹°λΈŒ μš”μ†Œκ°€ 포함됨
- νˆ΄λ°”λ₯Ό μ‚¬μš©ν•˜κ±°λ‚˜ νŽ˜μ΄μ§€ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•˜μ—¬ 탐색
- 썸넀일 보기둜 λΉ λ₯Έ 탐색 κ°€λŠ₯
- 전체 ν™”λ©΄μœΌλ‘œ μ „ν™˜ν•˜μ—¬ 더 λ‚˜μ€ 보기 κ²½ν—˜
""")
with gr.Tabs():
with gr.TabItem("PDF μ—…λ‘œλ“œ"):
pdf_file = gr.File(label="PDF 파일 μ—…λ‘œλ“œ", file_types=[".pdf"])
with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
pdf_view_mode = gr.Radio(
choices=["webgl", "3d", "2d", "swipe"],
value="2d", # Changed default to 2d for better compatibility
label="λ·° λͺ¨λ“œ",
info="WebGL: 졜고 ν’ˆμ§ˆ, 2D: κ°€μž₯ μ•ˆμ •μ , 3D: 쀑간, Swipe: λͺ¨λ°”μΌμš©"
)
pdf_skin = gr.Radio(
choices=["light", "dark", "gradient"],
value="light",
label="μŠ€ν‚¨",
info="light: 밝은 ν…Œλ§ˆ, dark: μ–΄λ‘μš΄ ν…Œλ§ˆ, gradient: κ·ΈλΌλ°μ΄μ…˜ ν…Œλ§ˆ"
)
pdf_create_btn = gr.Button("PDFμ—μ„œ ν”Œλ¦½λΆ λ§Œλ“€κΈ°", variant="primary", size="lg")
pdf_debug = gr.Textbox(label="디버그 정보", visible=False)
pdf_output = gr.HTML(label="ν”Œλ¦½λΆ κ²°κ³Όλ¬Ό")
# Set up PDF event handler
pdf_create_btn.click(
fn=create_flipbook_from_pdf,
inputs=[pdf_file, pdf_view_mode, pdf_skin],
outputs=[pdf_output, pdf_debug]
)
with gr.TabItem("이미지 μ—…λ‘œλ“œ"):
images = gr.File(label="이미지 파일 μ—…λ‘œλ“œ", file_types=["image"], file_count="multiple")
with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
img_view_mode = gr.Radio(
choices=["webgl", "3d", "2d", "swipe"],
value="2d", # Changed default to 2d for better compatibility
label="λ·° λͺ¨λ“œ",
info="WebGL: 졜고 ν’ˆμ§ˆ, 2D: κ°€μž₯ μ•ˆμ •μ , 3D: 쀑간, Swipe: λͺ¨λ°”μΌμš©"
)
img_skin = gr.Radio(
choices=["light", "dark", "gradient"],
value="light",
label="μŠ€ν‚¨",
info="light: 밝은 ν…Œλ§ˆ, dark: μ–΄λ‘μš΄ ν…Œλ§ˆ, gradient: κ·ΈλΌλ°μ΄μ…˜ ν…Œλ§ˆ"
)
img_create_btn = gr.Button("μ΄λ―Έμ§€μ—μ„œ ν”Œλ¦½λΆ λ§Œλ“€κΈ°", variant="primary", size="lg")
img_debug = gr.Textbox(label="디버그 정보", visible=False)
img_output = gr.HTML(label="ν”Œλ¦½λΆ κ²°κ³Όλ¬Ό")
# Set up image event handler
img_create_btn.click(
fn=create_flipbook_from_images,
inputs=[images, img_view_mode, img_skin],
outputs=[img_output, img_debug]
)
gr.Markdown("""
### μ‚¬μš©λ²•:
1. 컨텐츠 μœ ν˜•μ— 따라 탭을 μ„ νƒν•˜μ„Έμš” (PDF λ˜λŠ” 이미지)
2. νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”
3. ν•„μš”μ— 따라 κ³ κΈ‰ μ„€μ •μ—μ„œ λ·° λͺ¨λ“œμ™€ μŠ€ν‚¨μ„ μ‘°μ •ν•˜μ„Έμš”
4. ν”Œλ¦½λΆ λ§Œλ“€κΈ° λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”
5. 좜λ ₯ μ˜μ—­μ—μ„œ ν”Œλ¦½λΆκ³Ό μƒν˜Έμž‘μš©ν•˜μ„Έμš”
### μ°Έκ³ :
- 처음 νŽ˜μ΄μ§€μ—λŠ” μ˜ˆμ‹œλ‘œ μΈν„°λž™ν‹°λΈŒ μš”μ†Œμ™€ 링크가 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€
- μ΅œμƒμ˜ κ²°κ³Όλ₯Ό μœ„ν•΄ μ„ λͺ…ν•œ ν…μŠ€νŠΈμ™€ 이미지가 μžˆλŠ” PDFλ₯Ό μ‚¬μš©ν•˜μ„Έμš”
- μ§€μ›λ˜λŠ” 이미지 ν˜•μ‹: JPG, PNG, GIF λ“±
- ν”Œλ¦½λΆμ΄ 보이지 μ•ŠλŠ” 경우, 2D λͺ¨λ“œλ₯Ό μ„ νƒν•˜κ³  λ‹€μ‹œ μ‹œλ„ν•΄λ³΄μ„Έμš”
""")
# Launch the app
if __name__ == "__main__":
demo.launch() # Remove share=True as it's not supported in Spaces