AI-BOOK / app.py
ginipick's picture
Update app.py
8d9dabe verified
raw
history blame
16.2 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
3D Flipbook Viewer (Gradio) – μ΅œμ’… μˆ˜μ • 버전
μ΅œμ’… μˆ˜μ •: 2025-05-18
- f-string κ΅¬λ¬Έμ—μ„œ JS의 { }λ₯Ό {{ }}둜 μ΄μŠ€μΌ€μ΄ν”„ 처리
- Python 3.9 μ΄ν•˜ ν˜Έν™˜(typing.Optional, typing.List)
- Gradioκ°€ μƒμ„±ν•œ μž„μ‹œ 파일 β†’ temp/uploads ν΄λ”λ‘œ 볡사 ν›„ 처리
- /public 폴더에 μ΅œμ’… HTML 생성 (정적 μ„œλΉ™ 별도 μ„€μ • ν•„μš”)
"""
import os
import shutil
import uuid
import json
import logging
import traceback
from pathlib import Path
from typing import Optional, List, Dict
import gradio as gr
from PIL import Image
import fitz # PyMuPDF
# ────────────────────────────
# λ‘œκΉ… μ„€μ •
# ────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
filename="app.log",
filemode="a",
)
logging.info("πŸš€ Flipbook app started")
# ────────────────────────────
# 폴더 경둜 μ„€μ •
# ────────────────────────────
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")
# 폴더 생성
for d in [TEMP_DIR, UPLOAD_DIR, OUTPUT_DIR, THUMBS_DIR, HTML_DIR]:
os.makedirs(d, exist_ok=True)
# ────────────────────────────
# μœ ν‹Έ ν•¨μˆ˜: 이미지 썸넀일
# ────────────────────────────
def create_thumbnail(src: str, dst: str, size=(300, 300)) -> Optional[str]:
"""원본 이미지λ₯Ό μΈλ„€μΌλ‘œ μ €μž₯ (이미지 μ—΄κΈ° μ‹€νŒ¨ μ‹œ None 리턴)"""
try:
with Image.open(src) as im:
im.thumbnail(size, Image.LANCZOS)
im.save(dst)
return dst
except Exception as e:
logging.error("Thumbnail error: %s", e)
return None
# ────────────────────────────
# PDF 처리 ν•¨μˆ˜: PDF β†’ 이미지
# ────────────────────────────
def process_pdf(pdf_path: str, session_id: str) -> List[Dict]:
"""PDF νŒŒμΌμ„ νŽ˜μ΄μ§€ 별 PNG둜 λ³€ν™˜ν•˜κ³ , νŽ˜μ΄μ§€ 정보 리슀트 λ°˜ν™˜"""
pages_info = []
out_dir = os.path.join(OUTPUT_DIR, session_id)
th_dir = os.path.join(THUMBS_DIR, session_id)
os.makedirs(out_dir, exist_ok=True)
os.makedirs(th_dir, exist_ok=True)
try:
pdf_doc = fitz.open(pdf_path)
for idx, page in enumerate(pdf_doc):
# 해상도(1.5λ°° 정도) - ν•„μš”μ‹œ 쑰절
mat = fitz.Matrix(1.5, 1.5)
pix = page.get_pixmap(matrix=mat)
img_path = os.path.join(out_dir, f"page_{idx+1}.png")
pix.save(img_path)
thumb_path = os.path.join(th_dir, f"thumb_{idx+1}.png")
create_thumbnail(img_path, thumb_path)
# 첫 νŽ˜μ΄μ§€ μ˜ˆμ‹œλ‘œ μ˜€λ²„λ ˆμ΄ HTML
html_overlay = (
"""
<div style="position:absolute;top:50px;left:50px;
background:rgba(255,255,255,.7);padding:10px;
border-radius:5px;">
<div style="font-size:18px;font-weight:bold;color:#333;">
μΈν„°λž™ν‹°λΈŒ ν”Œλ¦½λΆ 예제
</div>
<div style="margin-top:5px;color:#666;">
이 νŽ˜μ΄μ§€λŠ” μΈν„°λž™ν‹°λΈŒ 컨텐츠 κΈ°λŠ₯을 λ³΄μ—¬μ€λ‹ˆλ‹€.
</div>
</div>
"""
if idx == 0 else None
)
pages_info.append(
{
"src": f"./temp/output/{session_id}/page_{idx+1}.png",
"thumb": f"./temp/output/thumbs/{session_id}/thumb_{idx+1}.png",
"title": f"νŽ˜μ΄μ§€ {idx+1}",
"htmlContent": html_overlay,
}
)
logging.info("PDF page %d β†’ %s", idx + 1, img_path)
return pages_info
except Exception as e:
logging.error("process_pdf() failed: %s", e)
return []
# ────────────────────────────
# 이미지 처리 ν•¨μˆ˜
# ────────────────────────────
def process_images(img_paths: List[str], session_id: str) -> List[Dict]:
"""이미지듀을 temp/output으둜 볡사, 썸넀일 생성, νŽ˜μ΄μ§€ 정보 λ°˜ν™˜"""
pages_info = []
out_dir = os.path.join(OUTPUT_DIR, session_id)
th_dir = os.path.join(THUMBS_DIR, session_id)
os.makedirs(out_dir, exist_ok=True)
os.makedirs(th_dir, exist_ok=True)
for i, src in enumerate(img_paths):
try:
dst = os.path.join(out_dir, f"image_{i+1}.png")
shutil.copy(src, dst)
thumb = os.path.join(th_dir, f"thumb_{i+1}.png")
create_thumbnail(dst, thumb)
# νŽ˜μ΄μ§€λ³„ μ˜€λ²„λ ˆμ΄ μ˜ˆμ‹œ
if i == 0:
html_overlay = """
<div style="position:absolute;top:50px;left:50px;
background:rgba(255,255,255,.7);padding:10px;
border-radius:5px;">
<div style="font-size:18px;font-weight:bold;color:#333;">
이미지 가러리
</div>
<div style="margin-top:5px;color:#666;">
가러리의 첫 번째 μ΄λ―Έμ§€μž…λ‹ˆλ‹€.
</div>
</div>
"""
elif i == 1:
html_overlay = """
<div style="position:absolute;top:50px;left:50px;
background:rgba(255,255,255,.7);padding:10px;
border-radius:5px;">
<div style="font-size:18px;font-weight:bold;color:#333;">
두 번째 이미지
</div>
<div style="margin-top:5px;color:#666;">
νŽ˜μ΄μ§€ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•΄ λ„˜κ²¨λ³΄μ„Έμš”.
</div>
</div>
"""
else:
html_overlay = 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_overlay,
}
)
logging.info("Image %d copied β†’ %s", i + 1, dst)
except Exception as e:
logging.error("process_images() error (%s): %s", src, e)
return pages_info
# ────────────────────────────
# ν”Œλ¦½λΆ HTML 생성
# ────────────────────────────
def generate_flipbook_html(
pages_info: List[Dict],
session_id: str,
view_mode: str,
skin: str
) -> str:
"""
3D Flipbook 용 HTML 파일 생성 ν›„, HTML 링크(λ²„νŠΌ) 블둝을 λ°˜ν™˜
- f-string μ•ˆμ—μ„œ JS의 { }λŠ” {{ }}둜 μ΄μŠ€μΌ€μ΄ν”„
"""
# htmlContent=None 제거
for p in pages_info:
if p.get("htmlContent") is None:
p.pop("htmlContent", None)
pages_json = json.dumps(pages_info, ensure_ascii=False)
html_file = f"flipbook_{session_id}.html"
html_path = os.path.join(HTML_DIR, html_file)
html = f"""
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>3D Flipbook</title>
<!-- 3D Flipbook κ΄€λ ¨ CSS/JS -->
<link rel="stylesheet" href="/public/libs/flipbook/css/flipbook.style.css">
<script src="/public/libs/flipbook/js/flipbook.min.js"></script>
<script src="/public/libs/flipbook/js/flipbook.webgl.min.js"></script>
<style>
html,body{{margin:0;height:100%;overflow:hidden}}
#flipbook-container{{position:absolute;inset:0}}
.loading{{position:absolute;top:50%;left:50%;
transform:translate(-50%,-50%);text-align:center;font-family:sans-serif}}
.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(0)}}100%{{transform:rotate(360deg)}}}}
</style>
</head>
<body>
<div id="flipbook-container"></div>
<div id="loading" class="loading">
<div class="spinner"></div>
<div>ν”Œλ¦½λΆ λ‘œλ”© 쀑...</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {{
const hide = () => {{
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,
thumbnailsOnStart: true,
btnThumbs: {{ enabled: true }},
btnPrint: {{ enabled: true }},
btnDownloadPages: {{ enabled: true }},
btnDownloadPdf: {{ enabled: true }},
btnShare: {{ enabled: true }},
btnSound: {{ enabled: true }},
btnExpand: {{ enabled: true }}
}};
new FlipBook(document.getElementById('flipbook-container'), options);
setTimeout(hide, 1000);
}} catch(e) {{
console.error(e);
alert('ν”Œλ¦½λΆ μ΄ˆκΈ°ν™” 였λ₯˜:' + e.message);
}}
}});
</script>
</body>
</html>
"""
Path(html_path).write_text(html, encoding="utf-8")
public_url = f"/public/flipbooks/{html_file}"
# μ‚¬μš©μžμ—κ²Œ λŒλ €μ€„ HTML 블둝
return f"""
<div style="text-align:center;padding:20px;background:#f9f9f9;border-radius:5px">
<h2 style="margin:0;color:#333">ν”Œλ¦½λΆμ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€!</h2>
<p style="margin:15px 0">λ²„νŠΌμ„ 눌러 μƒˆ μ°½μ—μ„œ ν™•μΈν•˜μ„Έμš”.</p>
<a href="{public_url}" target="_blank"
style="display:inline-block;background:#4caf50;color:#fff;
padding:12px 24px;border-radius:4px;font-weight:bold;font-size:16px">
ν”Œλ¦½λΆ μ—΄κΈ°
</a>
</div>
"""
# ────────────────────────────
# PDF μ—…λ‘œλ“œ 콜백
# ────────────────────────────
def create_flipbook_from_pdf(
pdf_file: Optional[gr.File],
view_mode: str = "2d",
skin: str = "light"
):
session_id = str(uuid.uuid4())
debug: List[str] = []
if not pdf_file:
return (
"<div style='color:red;padding:20px;'>PDF νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”.</div>",
"No file",
)
try:
# Gradio μž„μ‹œ 경둜
uploaded_temp_path = pdf_file.name
# temp/uploads 폴더에 볡사
filename_only = os.path.basename(uploaded_temp_path)
pdf_path = os.path.join(UPLOAD_DIR, filename_only)
shutil.copyfile(uploaded_temp_path, pdf_path)
debug.append(f"Copied PDF to: {pdf_path}")
# PDF 처리
pages_info = process_pdf(pdf_path, session_id)
debug.append(f"Extracted pages: {len(pages_info)}")
if not pages_info:
raise RuntimeError("PDF 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
# ν”Œλ¦½λΆ HTML
html_block = generate_flipbook_html(pages_info, session_id, view_mode, skin)
return html_block, "\n".join(debug)
except Exception as e:
tb = traceback.format_exc()
logging.error(tb)
debug.extend(["❌ ERROR ↓↓↓", tb])
return (
f"<div style='color:red;padding:20px;'>였λ₯˜: {e}</div>",
"\n".join(debug),
)
# ────────────────────────────
# 이미지 μ—…λ‘œλ“œ 콜백
# ────────────────────────────
def create_flipbook_from_images(
images: Optional[List[gr.File]],
view_mode: str = "2d",
skin: str = "light"
):
session_id = str(uuid.uuid4())
debug: List[str] = []
if not images:
return (
"<div style='color:red;padding:20px;'>이미지λ₯Ό ν•˜λ‚˜ 이상 μ—…λ‘œλ“œν•˜μ„Έμš”.</div>",
"No images",
)
try:
# μž„μ‹œ 이미지 κ²½λ‘œλ“€
img_paths = []
for fobj in images:
uploaded_temp_path = fobj.name
filename_only = os.path.basename(uploaded_temp_path)
local_img_path = os.path.join(UPLOAD_DIR, filename_only)
shutil.copyfile(uploaded_temp_path, local_img_path)
img_paths.append(local_img_path)
debug.append(f"Images: {img_paths}")
# 이미지 처리
pages_info = process_images(img_paths, session_id)
debug.append(f"Processed: {len(pages_info)}")
if not pages_info:
raise RuntimeError("이미지 처리 μ‹€νŒ¨")
# ν”Œλ¦½λΆ HTML
html_block = generate_flipbook_html(pages_info, session_id, view_mode, skin)
return html_block, "\n".join(debug)
except Exception as e:
tb = traceback.format_exc()
logging.error(tb)
debug.extend(["❌ ERROR ↓↓↓", tb])
return (
f"<div style='color:red;padding:20px;'>였λ₯˜: {e}</div>",
"\n".join(debug),
)
# ────────────────────────────
# Gradio UI
# ────────────────────────────
with gr.Blocks(title="3D Flipbook Viewer") as demo:
gr.Markdown("# 3D Flipbook Viewer\nPDF λ˜λŠ” 이미지λ₯Ό μ—…λ‘œλ“œν•΄ μΈν„°λž™ν‹°λΈŒ ν”Œλ¦½λΆμ„ λ§Œλ“œμ„Έμš”.")
with gr.Tabs():
# PDF νƒ­
with gr.TabItem("PDF μ—…λ‘œλ“œ"):
pdf_file = gr.File(label="PDF 파일", file_types=[".pdf"])
with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
pdf_view = gr.Radio(["webgl", "3d", "2d", "swipe"], value="2d", label="λ·° λͺ¨λ“œ")
pdf_skin = gr.Radio(["light", "dark", "gradient"], value="light", label="μŠ€ν‚¨")
pdf_btn = gr.Button("PDF β†’ ν”Œλ¦½λΆ", variant="primary")
pdf_out = gr.HTML()
pdf_dbg = gr.Textbox(label="디버그", lines=10)
pdf_btn.click(
create_flipbook_from_pdf,
inputs=[pdf_file, pdf_view, pdf_skin],
outputs=[pdf_out, pdf_dbg],
)
# 이미지 νƒ­
with gr.TabItem("이미지 μ—…λ‘œλ“œ"):
imgs = gr.File(label="이미지 νŒŒμΌλ“€", file_types=["image"], file_count="multiple")
with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
img_view = gr.Radio(["webgl", "3d", "2d", "swipe"], value="2d", label="λ·° λͺ¨λ“œ")
img_skin = gr.Radio(["light", "dark", "gradient"], value="light", label="μŠ€ν‚¨")
img_btn = gr.Button("이미지 β†’ ν”Œλ¦½λΆ", variant="primary")
img_out = gr.HTML()
img_dbg = gr.Textbox(label="디버그", lines=10)
img_btn.click(
create_flipbook_from_images,
inputs=[imgs, img_view, img_skin],
outputs=[img_out, img_dbg],
)
gr.Markdown(
"### μ‚¬μš©λ²•\n"
"1. PDF λ˜λŠ” 이미지 탭을 μ„ νƒν•˜κ³  νŒŒμΌμ„ μ—…λ‘œλ“œν•©λ‹ˆλ‹€.\n"
"2. ν•„μš”ν•˜λ©΄ λ·° λͺ¨λ“œ/μŠ€ν‚¨μ„ λ°”κΏ‰λ‹ˆλ‹€.\n"
"3. β€˜ν”Œλ¦½λΆβ€™ λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ κ²°κ³Όκ°€ μ•„λž˜ λœΉλ‹ˆλ‹€."
)
# ────────────────────────────
# μ‹€ν–‰
# ────────────────────────────
if __name__ == "__main__":
# share=True λ“± μ˜΅μ…˜μ„ λ„£μ–΄ 배포/곡유 κ°€λŠ₯
demo.launch(debug=True)