Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
#!/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) | |