Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
from fastapi import FastAPI | |
from fastapi.responses import HTMLResponse | |
from fastapi.staticfiles import StaticFiles | |
import pathlib, os, uvicorn, base64 | |
BASE = pathlib.Path(__file__).parent | |
app = FastAPI() | |
app.mount("/static", StaticFiles(directory=BASE), name="static") | |
# PDF λλ ν 리 μ€μ | |
PDF_DIR = BASE / "pdf" | |
if not PDF_DIR.exists(): | |
PDF_DIR.mkdir(parents=True) | |
# PDF νμΌ λͺ©λ‘ κ°μ Έμ€κΈ° | |
def get_pdf_files(): | |
pdf_files = [] | |
if PDF_DIR.exists(): | |
pdf_files = [f for f in PDF_DIR.glob("*.pdf")] | |
return pdf_files | |
# PDF μΈλ€μΌ μμ± λ° νλ‘μ νΈ λ°μ΄ν° μ€λΉ | |
def generate_pdf_projects(): | |
projects_data = [] | |
pdf_files = get_pdf_files() | |
for pdf_file in pdf_files: | |
projects_data.append({ | |
"path": str(pdf_file), | |
"name": pdf_file.stem | |
}) | |
return projects_data | |
HTML = """ | |
<!doctype html><html lang="ko"><head> | |
<meta charset="utf-8"><title>FlipBook Space</title> | |
<link rel="stylesheet" href="/static/flipbook.css"> | |
<script src="/static/three.js"></script> | |
<script src="/static/iscroll.js"></script> | |
<script src="/static/mark.js"></script> | |
<script src="/static/mod3d.js"></script> | |
<script src="/static/pdf.js"></script> | |
<script src="/static/flipbook.js"></script> | |
<script src="/static/flipbook.book3.js"></script> | |
<script src="/static/flipbook.scroll.js"></script> | |
<script src="/static/flipbook.swipe.js"></script> | |
<script src="/static/flipbook.webgl.js"></script> | |
<style> | |
body{margin:0;background:#f0f0f0;font-family:sans-serif} | |
header{max-width:960px;margin:0 auto;padding:18px 20px;display:flex;align-items:center} | |
#homeBtn{display:none;width:38px;height:38px;border:none;border-radius:50%;cursor:pointer; | |
background:#0077c2;color:#fff;font-size:20px;margin-right:12px} | |
#homeBtn:hover{background:#005999} | |
h2{margin:0;font-size:1.5rem;font-weight:600} | |
#home,#viewerPage{max-width:960px;margin:0 auto;padding:0 20px 40px} | |
.grid{display:grid;grid-template-columns:repeat(auto-fill,180px);gap:16px;margin-top:24px} | |
.card{ | |
background:#fff url('/static/book2.jpg') no-repeat center center; | |
background-size: 130%; /* λ°°κ²½ μ΄λ―Έμ§ 30% νλ */ | |
border:1px solid #ccc; | |
border-radius:6px; | |
cursor:pointer; | |
box-shadow:0 2px 4px rgba(0,0,0,.12); | |
width: 180px; | |
height: 240px; | |
position: relative; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
} | |
.card img{ | |
width:65%; /* μΈλ€μΌ ν¬κΈ° μ‘°μ */ | |
height:auto; | |
object-fit:contain; | |
position:absolute; /* μ λ μμΉλ‘ λ³κ²½ */ | |
top:50%; /* μλ¨μμ 50% */ | |
left:50%; /* μ’μΈ‘μμ 50% */ | |
transform: translate(-50%, -50%); /* μ μ€μ λ°°μΉ */ | |
border: 1px solid #ddd; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
} | |
.card p{ | |
text-align:center; | |
margin:6px 0; | |
position: absolute; | |
bottom: 10px; | |
left: 50%; | |
transform: translateX(-50%); | |
background: rgba(255, 255, 255, 0.7); | |
padding: 4px 8px; | |
border-radius: 4px; | |
width: 85%; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
max-width: 150px; | |
} | |
button.upload{all:unset;cursor:pointer;border:1px solid #bbb;padding:8px 14px;border-radius:6px;background:#fff;margin:0 8px} | |
#viewer{ | |
width:100%; | |
height:100vh; /* μ 체 νλ©΄ λμ΄λ‘ νμ₯ */ | |
max-width:100%; /* μ΅λ λλΉ μ ν ν΄μ */ | |
margin:0; /* λ§μ§ μ κ±° */ | |
background:#fff; | |
border:none; /* ν λ리 μ κ±° */ | |
position:fixed; /* κ³ μ μμΉ */ | |
top:0; | |
left:0; | |
z-index:1000; /* μ΅μμ νμ */ | |
} | |
</style></head><body> | |
<header> | |
<button id="homeBtn" title="νμΌλ‘">β</button> | |
<h2>My FlipBook Projects</h2> | |
</header> | |
<section id="home"> | |
<div> | |
<label class="upload">π· μ΄λ―Έμ§ <input id="imgInput" type="file" accept="image/*" multiple hidden></label> | |
<label class="upload">π PDF <input id="pdfInput" type="file" accept="application/pdf" hidden></label> | |
</div> | |
<div class="grid" id="grid"></div> | |
</section> | |
<section id="viewerPage" style="display:none"> | |
<div id="viewer"></div> | |
</section> | |
<script> | |
let projects=[], fb=null; | |
const grid=$id('grid'), viewer=$id('viewer'); | |
pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js'; | |
// μλ²μμ 미리 λ‘λλ PDF νλ‘μ νΈ | |
let serverProjects = []; | |
/* π μ€λμ€ unlock β λ΄μ₯ Audio μ κ°μ MP3 κ²½λ‘ μ¬μ© */ | |
['click','touchstart'].forEach(evt=>{ | |
document.addEventListener(evt,function u(){new Audio('static/turnPage2.mp3') | |
.play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});}, | |
{once:true,capture:true}); | |
}); | |
/* ββ μ νΈ ββ */ | |
function $id(id){return document.getElementById(id)} | |
function addCard(i,thumb,title){ | |
const d=document.createElement('div'); | |
d.className='card'; | |
d.onclick=()=>open(i); | |
// μ λͺ© 10κΈμ μ ν λ° λ§μ€μν μ²λ¦¬ | |
const displayTitle = title ? | |
(title.length > 10 ? title.substring(0, 10) + '...' : title) : | |
'νλ‘μ νΈ ' + (i+1); | |
d.innerHTML=`<img src="${thumb}"><p title="${title || 'νλ‘μ νΈ ' + (i+1)}">${displayTitle}</p>`; | |
grid.appendChild(d); | |
} | |
/* ββ μ΄λ―Έμ§ μ λ‘λ ββ */ | |
$id('imgInput').onchange=e=>{ | |
const files=[...e.target.files]; if(!files.length) return; | |
const pages=[],tot=files.length;let done=0; | |
files.forEach((f,i)=>{const r=new FileReader();r.onload=x=>{pages[i]={src:x.target.result,thumb:x.target.result}; | |
if(++done===tot) save(pages);};r.readAsDataURL(f);}); | |
}; | |
/* ββ PDF μ λ‘λ ββ */ | |
$id('pdfInput').onchange=e=>{ | |
const file=e.target.files[0]; if(!file) return; | |
const fr=new FileReader(); | |
fr.onload=v=>{ | |
pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{ | |
const pages=[]; | |
for(let p=1;p<=pdf.numPages;p++){ | |
const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1}); | |
const c=document.createElement('canvas');c.width=vp.width;c.height=vp.height; | |
await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise; | |
pages.push({src:c.toDataURL(),thumb:c.toDataURL()}); | |
} | |
save(pages, file.name.replace('.pdf', '')); | |
}); | |
};fr.readAsArrayBuffer(file); | |
}; | |
/* ββ νλ‘μ νΈ μ μ₯ ββ */ | |
function save(pages, title){ | |
const id=projects.push(pages)-1; | |
addCard(id,pages[0].thumb, title); | |
} | |
/* ββ μλ² PDF λ‘λ ββ */ | |
async function loadServerPDFs() { | |
try { | |
const response = await fetch('/api/pdf-projects'); | |
serverProjects = await response.json(); | |
// μλ² PDF λ‘λ λ° μΈλ€μΌ μμ± | |
for(let i = 0; i < serverProjects.length; i++) { | |
const project = serverProjects[i]; | |
const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`); | |
const data = await response.json(); | |
if(data.thumbnail) { | |
const pages = [{ | |
src: data.thumbnail, | |
thumb: data.thumbnail, | |
path: project.path | |
}]; | |
save(pages, project.name); | |
} | |
} | |
} catch(error) { | |
console.error('μλ² PDF λ‘λ μ€ν¨:', error); | |
} | |
} | |
/* ββ μΉ΄λ β FlipBook ββ */ | |
function open(i){ | |
toggle(false); | |
const pages = projects[i]; | |
// λ‘컬 νλ‘μ νΈ λλ μλ² PDF λ‘λ | |
if(fb){fb.destroy();viewer.innerHTML='';} | |
if(pages[0].path) { | |
// μλ² PDF νμΌ λ‘λ | |
fetch(`/api/pdf-content?path=${encodeURIComponent(pages[0].path)}`) | |
.then(response => { | |
if (!response.ok) { | |
throw new Error('PDF λ‘λ μ€ν¨: ' + response.statusText); | |
} | |
return response.arrayBuffer(); | |
}) | |
.then(pdfData => { | |
// PDF λ°μ΄ν° λ‘λ νμΈ λ‘κΉ | |
console.log('PDF λ°μ΄ν° λ‘λ μλ£:', pdfData.byteLength + ' λ°μ΄νΈ'); | |
return pdfjsLib.getDocument({data: pdfData}).promise; | |
}) | |
.then(async pdf => { | |
console.log('PDF λ¬Έμ λ‘λ μλ£. νμ΄μ§ μ:', pdf.numPages); | |
const pdfPages = []; | |
for(let p = 1; p <= pdf.numPages; p++) { | |
console.log('νμ΄μ§ λ λλ§ μ€:', p + '/' + pdf.numPages); | |
const pg = await pdf.getPage(p); | |
const vp = pg.getViewport({scale: 1}); | |
const c = document.createElement('canvas'); | |
c.width = vp.width; | |
c.height = vp.height; | |
await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise; | |
pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()}); | |
} | |
console.log('λͺ¨λ νμ΄μ§ λ λλ§ μλ£:', pdfPages.length); | |
createFlipBook(pdfPages); | |
}) | |
.catch(error => { | |
console.error('PDF μ²λ¦¬ μ€ μ€λ₯ λ°μ:', error); | |
alert('PDFλ₯Ό λ‘λνλ μ€ μ€λ₯κ° λ°μνμ΅λλ€: ' + error.message); | |
}); | |
} else { | |
// μ λ‘λλ νλ‘μ νΈ λ³΄κΈ° | |
console.log('λ‘컬 μ λ‘λλ νλ‘μ νΈ λ λλ§:', pages.length + 'νμ΄μ§'); | |
createFlipBook(pages); | |
} | |
} | |
function createFlipBook(pages) { | |
console.log('FlipBook μμ± μμ. νμ΄μ§ μ:', pages.length); | |
try { | |
fb = new FlipBook(viewer, { | |
pages: pages, | |
viewMode: 'webgl', | |
autoSize: true, | |
flipDuration: 800, | |
backgroundColor: '#fff', | |
/* π λ΄μ₯ μ¬μ΄λ */ | |
sound: true, | |
assets: {flipMp3: 'static/turnPage2.mp3', hardFlipMp3: 'static/turnPage2.mp3'}, | |
controlsProps: {enableFullscreen: true, thumbnails: true} | |
}); | |
console.log('FlipBook μμ± μλ£'); | |
} catch (error) { | |
console.error('FlipBook μμ± μ€ μ€λ₯ λ°μ:', error); | |
alert('FlipBookμ μμ±νλ μ€ μ€λ₯κ° λ°μνμ΅λλ€: ' + error.message); | |
} | |
} | |
/* ββ λ€λΉκ²μ΄μ ββ */ | |
$id('homeBtn').onclick=()=>toggle(true); | |
function toggle(showHome){ | |
$id('home').style.display=showHome?'block':'none'; | |
$id('viewerPage').style.display=showHome?'none':'block'; | |
$id('homeBtn').style.display=showHome?'none':'inline-block'; | |
} | |
// νμ΄μ§ λ‘λ μ μλ² PDF λ‘λ | |
window.addEventListener('DOMContentLoaded', loadServerPDFs); | |
</script> | |
</body></html> | |
""" | |
# API μλν¬μΈνΈ: PDF νλ‘μ νΈ λͺ©λ‘ | |
async def get_pdf_projects(): | |
return generate_pdf_projects() | |
# API μλν¬μΈνΈ: PDF μΈλ€μΌ μμ± | |
async def get_pdf_thumbnail(path: str): | |
try: | |
import fitz # PyMuPDF | |
# PDF νμΌ μ΄κΈ° | |
doc = fitz.open(path) | |
# 첫 νμ΄μ§ κ°μ Έμ€κΈ° | |
if doc.page_count > 0: | |
page = doc[0] | |
# μΈλ€μΌμ© μ΄λ―Έμ§ λ λλ§ (ν΄μλ μ‘°μ ) | |
pix = page.get_pixmap(matrix=fitz.Matrix(0.5, 0.5)) | |
img_data = pix.tobytes("png") | |
# Base64 μΈμ½λ© | |
b64_img = base64.b64encode(img_data).decode('utf-8') | |
return {"thumbnail": f"data:image/png;base64,{b64_img}"} | |
return {"thumbnail": None} | |
except Exception as e: | |
return {"error": str(e), "thumbnail": None} | |
async def get_pdf_content(path: str): | |
try: | |
# νμΌ μ‘΄μ¬ μ¬λΆ νμΈ | |
pdf_path = pathlib.Path(path) | |
if not pdf_path.exists(): | |
return {"error": f"νμΌμ μ°Ύμ μ μμ΅λλ€: {path}"}, 404 | |
# νμΌ μ½κΈ° | |
with open(path, "rb") as pdf_file: | |
content = pdf_file.read() | |
# μλ΅ ν€λ μ€μ - PDF νμΌμμ λͺ μ | |
headers = { | |
"Content-Type": "application/pdf", | |
"Content-Disposition": f"inline; filename={pdf_path.name}" | |
} | |
# νμΌ μ½ν μΈ λ°ν | |
from fastapi.responses import Response | |
return Response(content=content, headers=headers) | |
except Exception as e: | |
import traceback | |
error_details = traceback.format_exc() | |
print(f"PDF μ½ν μΈ λ‘λ μ€λ₯: {str(e)}\n{error_details}") | |
return {"error": str(e)}, 500 | |
async def root(): | |
return HTML | |
if __name__ == "__main__": | |
uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860))) |