Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
#!/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 νμ μ μΆκ° | |