csrabbit/Bepub3
0
1import os2import re3import uuid4import unicodedata5import zipfile6import markdown7import shutil8import gradio as gr9import time10import threading11from PyPDF2 import PdfReader, PdfWriter12from typhoon_ocr import ocr_document13from concurrent.futures import ThreadPoolExecutor14 15# --- Global State ---16is_cancelled = False17rate_lock = threading.Lock()18last_request_time = 019 20def cancel_process():21 global is_cancelled22 is_cancelled = True23 return "🛑 ระบบกำลังหยุดทำงานและสรุปไฟล์เท่าที่ทำได้..."24 25def final_build(content, base_name, user_id):26 if not content.strip(): return None, None, "⚠️ ไม่มีเนื้อหา"27 clean = re.sub(r'\n\*\s*\d+\s*\n|\*\s*\d+\s*\*|\'', '', content)28 clean = re.sub(r'\n\s*\n', '\n\n', clean)29 sections = clean.split('--- Page Break ---')30 build_dir = f"build_{user_id}"31 if os.path.exists(build_dir): shutil.rmtree(build_dir)32 os.makedirs(f'{build_dir}/OEBPS', exist_ok=True)33 os.makedirs(f'{build_dir}/META-INF', exist_ok=True)34 with open(f'{build_dir}/mimetype', 'w') as f: f.write('application/epub+zip')35 with open(f'{build_dir}/META-INF/container.xml', 'w') as f:36 f.write('<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>')37 38 manifest, spine = "", ""39 for i, sec in enumerate(sections):40 if not sec.strip(): continue41 html = markdown.markdown(sec.strip())42 f_name = f'p{i}.xhtml'43 with open(f'{build_dir}/OEBPS/{f_name}', 'w', encoding='utf-8') as f:44 f.write(f'<html><body>{html}</body></html>')45 manifest += f'<item id="p{i}" href="{f_name}" media-type="application/xhtml+xml"/>\n'46 spine += f'<itemref idref="p{i}"/>\n'47 48 with open(f'{build_dir}/OEBPS/content.opf', 'w', encoding='utf-8') as f:49 f.write(f'<?xml version="1.0"?><package xmlns="http://www.idpf.org/2007/opf" version="2.0"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>{base_name}</dc:title><dc:language>th</dc:language></metadata><manifest>{manifest}<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/></manifest><spine toc="ncx">{spine}</spine></package>')50 51 epub_name = f"{base_name}_{user_id}.epub"52 txt_name = f"{base_name}_{user_id}.txt"53 with open(txt_name, "w", encoding="utf-8-sig") as f: f.write(content)54 55 with zipfile.ZipFile(epub_name, 'w', zipfile.ZIP_DEFLATED) as z:56 for r, d, fs in os.walk(build_dir):57 for file in fs:58 file_path = os.path.join(r, file)59 z.write(file_path, os.path.relpath(file_path, build_dir))60 61 shutil.rmtree(build_dir)62 return txt_name, epub_name, "✅ เสร็จสมบูรณ์"63 64def process_and_build_epub(api_key, pdf_file, start_page, end_page, progress=gr.Progress()):65 global is_cancelled66 is_cancelled = False67 user_id = str(uuid.uuid4())[:8]68 if not api_key or not pdf_file: return None, None, "❌ กรุณาใส่ API Key"69 70 os.environ["TYPHOON_OCR_API_KEY"] = api_key71 reader = PdfReader(pdf_file.name)72 total_pages = len(reader.pages)73 s_p = int(max(1, start_page))74 e_p = int(min(total_pages, end_page if end_page > 0 else total_pages))75 target_indices = list(range(s_p - 1, e_p))76 base_name = os.path.basename(pdf_file.name).rsplit('.', 1)[0]77 78 # ฟังก์ชันย่อยสำหรับรัน OCR รายหน้า (จะถูกเรียกแบบขนาน)79 def ocr_worker(idx):80 global last_request_time81 if is_cancelled: return ""82 83 writer = PdfWriter()84 writer.add_page(reader.pages[idx])85 temp_pdf = f"temp_{user_id}_{idx}.pdf"86 with open(temp_pdf, "wb") as f: writer.write(f)87 88 # Traffic Control ทุก 4 วินาที89 with rate_lock:90 current_time = time.time()91 # คำนวณว่าต้องรออีกกี่วินาทีเพื่อให้ห่างจาก Request ล่าสุด 4 วินาที92 wait_time = max(0, 4 - (current_time - last_request_time))93 time.sleep(wait_time)94 last_request_time = time.time()95 96 try:97 page_res = ocr_document(temp_pdf)98 return unicodedata.normalize('NFC', str(page_res)) if page_res else ""99 except Exception as e:100 return f"⚠️ [Error: {str(e)}]"101 finally:102 if os.path.exists(temp_pdf): os.remove(temp_pdf)103 104 try:105 results = []106 with ThreadPoolExecutor(max_workers=5) as executor:107 # ใช้ progress.tqdm เพื่อแสดงความคืบหน้าบน UI108 for res in progress.tqdm(executor.map(ocr_worker, target_indices), total=len(target_indices), desc="⚡ กำลังประมวลผล"):109 if is_cancelled: break110 results.append(res)111 112 full_text_content = "\n\n--- Page Break ---\n\n".join([r for r in results if r])113 return final_build(full_text_content, base_name, user_id)114 115 except Exception as e:116 return None, None, f"❌ Error: {str(e)}"117 118# --- Frontend Layout ---119css = """120.duplicate-btn {121 height: 60px !important; 122 font-size: 1.2em !important;123 border: 2px solid orange !important;124}125"""126 127with gr.Blocks(title="Bepub v2.1.0 Parallel", css=css) as demo:128 gr.Markdown("## Bepub v2.1.0")129 gr.DuplicateButton(130 value="👯 Duplicate Space to Run Privately", 131 variant="secondary",132 elem_classes="duplicate-btn"133 )134 135 with gr.Row():136 # ฝั่งซ้าย: Input137 with gr.Column(scale=1):138 file_input = gr.File(label="1. อัปโหลด PDF", file_types=[".pdf"])139 140 with gr.Group():141 api_input = gr.Textbox(142 label="2. Typhoon API Key", 143 type="password", 144 placeholder="ใส่ API Key ที่นี่..."145 )146 gr.Markdown(147 '🔑 <a href="https://playground.opentyphoon.ai" target="_blank" rel="noopener noreferrer">คลิกเพื่อรับ Typhoon API Key</a>',148 )149 150 with gr.Row():151 start_page = gr.Number(label="เริ่มหน้า", value=1, precision=0)152 end_page = gr.Number(label="ถึงหน้า", value=1000, precision=0)153 154 with gr.Row():155 btn_start = gr.Button("🚀 เริ่มแปลงไฟล์", variant="primary")156 btn_stop = gr.Button("🛑 หยุดทำงาน", variant="stop")157 158 # ฝั่งขวา: Output (ปรับความสูงที่นี่)159 with gr.Column(scale=1):160 # ปรับ lines=8 เพื่อให้ความสูงรวมของกล่องสถานะใกล้เคียงกับกล่อง File Upload ด้านซ้าย161 status_msg = gr.Textbox(162 label="สถานะ", 163 interactive=False, 164 placeholder="รอการเริ่มทำงาน..."165 )166 167 # กล่องดาวน์โหลดจะใช้พื้นที่ที่เหลือด้านล่าง168 with gr.Row():169 epub_output = gr.File(label="ไฟล์ EPUB")170 txt_output = gr.File(label="ไฟล์ TXT")171 172 # ส่วนการเชื่อมต่อ Function (คงเดิม)173 btn_start.click(174 fn=process_and_build_epub,175 inputs=[api_input, file_input, start_page, end_page],176 outputs=[txt_output, epub_output, status_msg]177 )178 btn_stop.click(fn=cancel_process, outputs=status_msg)179 180 gr.HTML("<hr>")181 with gr.Accordion("📖 วิธีใช้งานและข้อแนะนำ (คลิกเพื่อเปิด)", open=False):182 gr.Markdown("""183 ### 🛠 ขั้นตอนการใช้งาน184 1. (สำคัญ) กด Duplicate Space มาที่ account ตัวเอง เพื่อให้ทำงานโดยไม่ต้องรอคิวคนอื่น185 2. ไปที่ [Typhoon Playground](https://playground.opentyphoon.ai/settings/api-key) คัดลอก Key186 3. Upload ไฟล์ PDF187 4. ระบุหน้าเริ่มต้นและหน้าสิ้นสุด (ระบบตั้งไว้ที่ 1-1000 เป็นค่าเริ่มต้น)188 5. สามารถกดดาวน์โหลดได้ทั้งแบบ **.epub** และ **.txt**189 190 ### ⚠️ ข้อควรรู้191 * ความเร็วขึ้นอยู่กับความหนาแน่นของตัวอักษรใน PDF192 * ไฟล์จะถูกประมวลผลชั่วคราวและถูกลบออก ระบบไม่มีการเก็บข้อมูลส่วนตัวของคุณ193 * หากไฟล์ใหญ่เกินไป แนะนำให้แบ่งทำทีละส่วน (เช่น ครั้งละ 100 หน้า)194 * สามารถยกเลิกกลางคันได้ ระบบจะนำไฟล์เท่าที่ทำเสร็จมาให้ดาวน์โหลด195 """)196 197if __name__ == "__main__":198 demo.queue(default_concurrency_limit=2).launch(199 ssr_mode=False,200 theme=gr.themes.Soft(201 primary_hue="orange", 202 font=[gr.themes.GoogleFont("Noto Sans Thai"), "sans-serif"]203 )204 )