AI-BOOK / app.py
ginipick's picture
Update app.py
82e2b16 verified
raw
history blame
16.8 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
from typing import Optional, List, Dict
# μ™ΈλΆ€ 라이브러리
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)) -> 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 β†’ 이미지
# ────────────────────────────
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]:
"""μ—…λ‘œλ“œλœ 이미지λ₯Ό 볡사/썸넀일 생성 ν›„ νŽ˜μ΄μ§€ μ •λ³΄λ‘œ 리턴"""
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 파일둜 λ§Œλ“€κ³  링크 λ°˜ν™˜"""
# htmlContentκ°€ 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>
<!-- 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}"
# μ‚¬μš©μžμ—κ²Œ λŒλ €μ€„ λ²„νŠΌ ν˜•νƒœ 링크
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κ°€ λ„˜κ²¨μ€€ μž„μ‹œ PDF 경둜
uploaded_temp_path = pdf_file.name
# μ„œλ²„ λ‚΄ μž„μ‹œ μ—…λ‘œλ“œ 폴더에 μ•ˆμ „ν•˜κ²Œ 볡사
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:
# Gradioκ°€ λ„˜κ²¨μ€€ μž„μ‹œ 이미지 κ²½λ‘œλ“€
img_paths = []
for fobj in images:
# μ•ˆμ „ν•˜κ²Œ temp 폴더에 볡사
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)