AI-BOOK / app.py
ginipick's picture
Update app.py
08e6637 verified
raw
history blame
15.4 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
3D Flipbook Viewer (Gradio) – 전체 μ†ŒμŠ€
μ΅œμ’… μˆ˜μ •: 2025-05-18
"""
# ────────────────────────────
# κΈ°λ³Έ λͺ¨λ“ˆ
# ────────────────────────────
import os
import shutil
import uuid
import json
import logging
import traceback
from pathlib import Path
# μ™ΈλΆ€ 라이브러리
import gradio as gr
from PIL import Image
import fitz # PyMuPDF
# ────────────────────────────
# λ‘œκΉ… μ„€μ •
# ────────────────────────────
logging.basicConfig(
level=logging.INFO, # ν•„μš”ν•˜λ©΄ DEBUG
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)) -> 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 β†’ 이미지
# ────────────────────────────
def process_pdf(pdf_path: str, session_id: str) -> list[dict]:
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):
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # 2Γ— 해상도
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_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]:
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(src, 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:
# None 값은 JSON 직렬화 전에 제거
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>
<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}"
# μ‚¬μš©μžμ—κ²Œ λŒλ €μ€„ 링크 덩어리
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: gr.File | None, view_mode="2d", skin="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:
pdf_path = pdf_file.name
debug.append(f"PDF path: {pdf_path}")
pages_info = process_pdf(pdf_path, session_id)
debug.append(f"Extracted pages: {len(pages_info)}")
if not pages_info:
raise RuntimeError("PDF 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
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: list[gr.File] | None, view_mode="2d", skin="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 = [f.name for f in images]
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_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__\":
demo.launch(debug=True) # share=True ν•„μš” μ‹œ μΆ”κ°€