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=( """ """ 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 = "- Properly configured.
" def details_wrapper(text_markdown): text = f"""
Technical details {text_markdown} - GitHub: Byaidu/PDFMathTranslate
- GUI by: Rongxin
- Version: {__version__}
""" 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"- Warning: environmental not found or error ({env_var_name})." + "
- Please make sure that the environment variables are properly configured " + "(guide).
" ) else: value = str(os.environ.get(env_var_name)) envs_status = ( "- Properly configured.
" ) if len(value) < 13: envs_status += ( f"- Env: {os.environ.get(env_var_name)}
" ) else: envs_status += f"- Env: {value[:13]}***
" 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( "- Properly configured.
" ) 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 = ( "- Warning: model not in the list." "
- Please report via " "(guide).
" ) 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('
') 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()