Spaces:
Running
Running
| import os | |
| import shutil | |
| from pathlib import Path | |
| from pdf2zh import __version__ | |
| from pdf2zh.pdf2zh import extract_text | |
| import gradio as gr | |
| import numpy as np | |
| import pymupdf | |
| import tqdm | |
| import requests | |
| # Map service names to pdf2zh service options | |
| service_map = { | |
| "Google": "google", | |
| "DeepL": "deepl", | |
| "DeepLX": "deeplx", | |
| "Ollama": "ollama", | |
| "OpenAI": "openai", | |
| "Azure": "azure", | |
| } | |
| lang_map = { | |
| "Chinese": "zh", | |
| "English": "en", | |
| "French": "fr", | |
| "German": "de", | |
| "Japanese": "ja", | |
| "Korean": "ko", | |
| "Russian": "ru", | |
| "Spanish": "es", | |
| "Italian": "it", | |
| } | |
| page_map = { | |
| "All": None, | |
| "First": [0], | |
| "First 5 pages": list(range(0, 5)), | |
| } | |
| flag_demo = False | |
| if os.environ.get("PDF2ZH_DEMO"): | |
| flag_demo = True | |
| service_map = { | |
| "Google": "google", | |
| } | |
| page_map = { | |
| "First": [0], | |
| "First 20 pages": list(range(0, 20)), | |
| } | |
| client_key = os.environ.get("PDF2ZH_CLIENT_KEY") | |
| server_key = os.environ.get("PDF2ZH_SERVER_KEY") | |
| def verify_recaptcha(response): | |
| recaptcha_url = "https://www.google.com/recaptcha/api/siteverify" | |
| print("reCAPTCHA", server_key, response) | |
| data = {"secret": server_key, "response": response} | |
| result = requests.post(recaptcha_url, data=data).json() | |
| print("reCAPTCHA", result.get("success")) | |
| return result.get("success") | |
| def pdf_preview(file): | |
| doc = pymupdf.open(file) | |
| page = doc[0] | |
| pix = page.get_pixmap() | |
| image = np.frombuffer(pix.samples, np.uint8).reshape(pix.height, pix.width, 3) | |
| return image | |
| def upload_file(file, service, progress=gr.Progress()): | |
| """Handle file upload, validation, and initial preview.""" | |
| if not file or not os.path.exists(file): | |
| return None, None | |
| try: | |
| # Convert first page for preview | |
| preview_image = pdf_preview(file) | |
| return file, preview_image | |
| except Exception as e: | |
| print(f"Error converting PDF: {e}") | |
| return None, None | |
| def translate( | |
| file_path, | |
| service, | |
| model_id, | |
| lang, | |
| page_range, | |
| recaptcha_response, | |
| progress=gr.Progress(), | |
| ): | |
| """Translate PDF content using selected service.""" | |
| if not file_path: | |
| raise gr.Error("No input") | |
| if flag_demo and not verify_recaptcha(recaptcha_response): | |
| raise gr.Error("reCAPTCHA fail") | |
| progress(0, desc="Starting translation...") | |
| output = Path("pdf2zh_files") | |
| output.mkdir(parents=True, exist_ok=True) | |
| filename = os.path.splitext(os.path.basename(file_path))[0] | |
| file_en = output / f"{filename}.pdf" | |
| file_zh = output / f"{filename}-zh.pdf" | |
| file_dual = output / f"{filename}-dual.pdf" | |
| shutil.copyfile(file_path, file_en) | |
| selected_service = service_map.get(service, "google") | |
| selected_page = page_map.get(page_range, [0]) | |
| lang_to = lang_map.get(lang, "zh") | |
| if selected_service == "google": | |
| lang_to = "zh-CN" if lang_to == "zh" else lang_to | |
| print(f"Files before translation: {os.listdir(output)}") | |
| def progress_bar(t: tqdm.tqdm): | |
| progress(t.n / t.total, desc="Translating...") | |
| param = { | |
| "files": [file_en], | |
| "pages": selected_page, | |
| "lang_in": "auto", | |
| "lang_out": lang_to, | |
| "service": f"{selected_service}:{model_id}", | |
| "output": output, | |
| "thread": 4, | |
| "callback": progress_bar, | |
| } | |
| print(param) | |
| extract_text(**param) | |
| print(f"Files after translation: {os.listdir(output)}") | |
| if not file_zh.exists() or not file_dual.exists(): | |
| raise gr.Error("No output") | |
| try: | |
| translated_preview = pdf_preview(str(file_zh)) | |
| except Exception: | |
| raise gr.Error("No preview") | |
| progress(1.0, desc="Translation complete!") | |
| return ( | |
| str(file_zh), | |
| translated_preview, | |
| str(file_dual), | |
| gr.update(visible=True), | |
| gr.update(visible=True), | |
| gr.update(visible=True), | |
| ) | |
| # Global setup | |
| custom_blue = gr.themes.Color( | |
| c50="#E8F3FF", | |
| c100="#BEDAFF", | |
| c200="#94BFFF", | |
| c300="#6AA1FF", | |
| c400="#4080FF", | |
| c500="#165DFF", # Primary color | |
| c600="#0E42D2", | |
| c700="#0A2BA6", | |
| c800="#061D79", | |
| c900="#03114D", | |
| c950="#020B33", | |
| ) | |
| with gr.Blocks( | |
| title="PDFMathTranslate - PDF Translation with preserved formats", | |
| theme=gr.themes.Default( | |
| primary_hue=custom_blue, spacing_size="md", radius_size="lg" | |
| ), | |
| css=""" | |
| .secondary-text {color: #999 !important;} | |
| footer {visibility: hidden} | |
| .env-warning {color: #dd5500 !important;} | |
| .env-success {color: #559900 !important;} | |
| /* Add dashed border to input-file class */ | |
| .input-file { | |
| border: 1.2px dashed #165DFF !important; | |
| border-radius: 6px !important; | |
| # background-color: #ffffff !important; | |
| transition: background-color 0.4s ease-out; | |
| } | |
| .input-file:hover { | |
| border: 1.2px dashed #165DFF !important; | |
| border-radius: 6px !important; | |
| color: #165DFF !important; | |
| background-color: #E8F3FF !important; | |
| transition: background-color 0.2s ease-in; | |
| } | |
| .progress-bar-wrap { | |
| border-radius: 8px !important; | |
| } | |
| .progress-bar { | |
| border-radius: 8px !important; | |
| } | |
| # .input-file label { | |
| # color: #165DFF !important; | |
| # border: 1.2px dashed #165DFF !important; | |
| # border-left: none !important; | |
| # border-top: none !important; | |
| # } | |
| # .input-file .wrap { | |
| # color: #165DFF !important; | |
| # } | |
| # .input-file .or { | |
| # color: #165DFF !important; | |
| # } | |
| """, | |
| head=( | |
| """ | |
| <script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer></script> | |
| <script type="text/javascript"> | |
| var onVerify = function(token) { | |
| el=document.getElementById('verify').getElementsByTagName('textarea')[0]; | |
| el.value=token; | |
| el.dispatchEvent(new Event('input')); | |
| }; | |
| </script> | |
| """ | |
| if flag_demo | |
| else "" | |
| ), | |
| ) as demo: | |
| gr.Markdown( | |
| "# [PDFMathTranslate @ GitHub](https://github.com/Byaidu/PDFMathTranslate)" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("## File | < 5 MB" if flag_demo else "## File") | |
| file_input = gr.File( | |
| label="Document", | |
| file_count="single", | |
| file_types=[".pdf"], | |
| type="filepath", | |
| elem_classes=["input-file"], | |
| ) | |
| gr.Markdown("## Option") | |
| service = gr.Dropdown( | |
| label="Service", | |
| info="Which translation service to use. Some require keys", | |
| choices=service_map.keys(), | |
| value="Google", | |
| ) | |
| lang_to = gr.Dropdown( | |
| label="Translate to", | |
| info="Which language to translate to (optional)", | |
| choices=lang_map.keys(), | |
| value="Chinese", | |
| ) | |
| page_range = gr.Radio( | |
| choices=page_map.keys(), | |
| label="Pages", | |
| info="Translate the full document or just few pages (optional)", | |
| value=list(page_map.keys())[0], | |
| ) | |
| model_id = gr.Textbox( | |
| label="Model ID", | |
| info="Please enter the identifier of the model you wish to use (e.g., gemma2). " | |
| "This identifier will be used to specify the particular model for translation.", | |
| # value="gemma2", | |
| visible=False, # hide by default | |
| ) | |
| envs_status = "<span class='env-success'>- Properly configured.</span><br>" | |
| def details_wrapper(text_markdown): | |
| text = f""" | |
| <details> | |
| <summary>Technical details</summary> | |
| {text_markdown} | |
| - GitHub: <a href="https://github.com/Byaidu/PDFMathTranslate">Byaidu/PDFMathTranslate</a><br> | |
| - GUI by: <a href="https://github.com/reycn">Rongxin</a><br> | |
| - Version: {__version__} | |
| </details>""" | |
| return text | |
| def env_var_checker(env_var_name: str) -> str: | |
| if ( | |
| not os.environ.get(env_var_name) | |
| or os.environ.get(env_var_name) == "" | |
| ): | |
| envs_status = ( | |
| f"<span class='env-warning'>- Warning: environmental not found or error ({env_var_name})." | |
| + "</span><br>- Please make sure that the environment variables are properly configured " | |
| + "(<a href='https://github.com/Byaidu/PDFMathTranslate'>guide</a>).<br>" | |
| ) | |
| else: | |
| value = str(os.environ.get(env_var_name)) | |
| envs_status = ( | |
| "<span class='env-success'>- Properly configured.</span><br>" | |
| ) | |
| if len(value) < 13: | |
| envs_status += ( | |
| f"- Env: <code>{os.environ.get(env_var_name)}</code><br>" | |
| ) | |
| else: | |
| envs_status += f"- Env: <code>{value[:13]}***</code><br>" | |
| return details_wrapper(envs_status) | |
| def on_select_service(value, evt: gr.EventData): | |
| # hide model id by default | |
| model_visibility = gr.update(visible=False) | |
| # add a text description | |
| if value == "Google": | |
| envs_status = details_wrapper( | |
| "<span class='env-success'>- Properly configured.</span><br>" | |
| ) | |
| elif value == "DeepL": | |
| envs_status = env_var_checker("DEEPL_AUTH_KEY") | |
| elif value == "DeepLX": | |
| envs_status = env_var_checker("DEEPLX_AUTH_KEY") | |
| elif value == "Azure": | |
| envs_status = env_var_checker("AZURE_APIKEY") | |
| elif value == "OpenAI": | |
| model_visibility = gr.update( | |
| visible=True, value="gpt-4o" | |
| ) # show model id when service is selected | |
| envs_status = env_var_checker("OPENAI_API_KEY") | |
| elif value == "Ollama": | |
| model_visibility = gr.update( | |
| visible=True, value="gemma2" | |
| ) # show model id when service is selected | |
| envs_status = env_var_checker("OLLAMA_HOST") | |
| else: | |
| envs_status = ( | |
| "<span class='env-warning'>- Warning: model not in the list." | |
| "</span><br>- Please report via " | |
| "(<a href='https://github.com/Byaidu/PDFMathTranslate'>guide</a>).<br>" | |
| ) | |
| return envs_status, model_visibility | |
| output_title = gr.Markdown("## Translated", visible=False) | |
| output_file = gr.File(label="Download Translation", visible=False) | |
| output_file_dual = gr.File( | |
| label="Download Translation (Dual)", visible=False | |
| ) | |
| recaptcha_response = gr.Textbox( | |
| label="reCAPTCHA Response", elem_id="verify", visible=False | |
| ) | |
| recaptcha_box = gr.HTML('<div id="recaptcha-box"></div>') | |
| translate_btn = gr.Button("Translate", variant="primary") | |
| tech_details_tog = gr.Markdown( | |
| details_wrapper(envs_status), | |
| elem_classes=["secondary-text"], | |
| ) | |
| service.select(on_select_service, service, [tech_details_tog, model_id]) | |
| with gr.Column(scale=2): | |
| gr.Markdown("## Preview") | |
| preview = gr.Image(label="Document Preview", visible=True) | |
| # Event handlers | |
| file_input.upload( | |
| upload_file, | |
| inputs=[file_input, service], | |
| outputs=[file_input, preview], | |
| js=( | |
| f""" | |
| (a,b)=>{{ | |
| try{{ | |
| grecaptcha.render('recaptcha-box',{{ | |
| 'sitekey':'{client_key}', | |
| 'callback':'onVerify' | |
| }}); | |
| }}catch(error){{}} | |
| return [a]; | |
| }} | |
| """ | |
| if flag_demo | |
| else "" | |
| ), | |
| ) | |
| translate_btn.click( | |
| translate, | |
| inputs=[file_input, service, model_id, lang_to, page_range, recaptcha_response], | |
| outputs=[ | |
| output_file, | |
| preview, | |
| output_file_dual, | |
| output_file, | |
| output_file_dual, | |
| output_title, | |
| ], | |
| ).then(lambda: None, js="()=>{grecaptcha.reset()}" if flag_demo else "") | |
| def setup_gui(share=False): | |
| if flag_demo: | |
| demo.launch(server_name="0.0.0.0", max_file_size="5mb", inbrowser=True) | |
| else: | |
| try: | |
| demo.launch(server_name="0.0.0.0", debug=True, inbrowser=True, share=share) | |
| except Exception: | |
| print( | |
| "Error launching GUI using 0.0.0.0.\nThis may be caused by global mode of proxy software." | |
| ) | |
| try: | |
| demo.launch( | |
| server_name="127.0.0.1", debug=True, inbrowser=True, share=share | |
| ) | |
| except Exception: | |
| print( | |
| "Error launching GUI using 127.0.0.1.\nThis may be caused by global mode of proxy software." | |
| ) | |
| demo.launch(debug=True, inbrowser=True, share=True) | |
| # For auto-reloading while developing | |
| if __name__ == "__main__": | |
| setup_gui() | |