|
|
"""
|
|
|
BabelDocs x Agentic AI MCP - Gradio Application
|
|
|
|
|
|
PDF Translation with Google Drive Integration.
|
|
|
Accepts public GDrive links or local file uploads.
|
|
|
|
|
|
For Anthropic Hackathon - Track 1: Building MCP
|
|
|
|
|
|
Usage:
|
|
|
python app.py
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
import re
|
|
|
import base64
|
|
|
import tempfile
|
|
|
import httpx
|
|
|
import gradio as gr
|
|
|
from pathlib import Path
|
|
|
from datetime import datetime
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
MODAL_BASE_URL = os.getenv("BABELDOCS_MODAL_URL")
|
|
|
if not MODAL_BASE_URL:
|
|
|
raise ValueError("BABELDOCS_MODAL_URL environment variable required. Set it as a HuggingFace Space secret.")
|
|
|
MODAL_TRANSLATE_URL = f"{MODAL_BASE_URL}-babeldocstranslator-api.modal.run"
|
|
|
MODAL_HEALTH_URL = f"{MODAL_BASE_URL}-babeldocstranslator-health.modal.run"
|
|
|
|
|
|
|
|
|
MAX_PAGES = 20
|
|
|
|
|
|
|
|
|
LANGUAGES = {
|
|
|
"fr": "French",
|
|
|
"en": "English",
|
|
|
"es": "Spanish",
|
|
|
"de": "German",
|
|
|
"it": "Italian",
|
|
|
"pt": "Portuguese",
|
|
|
"zh": "Chinese",
|
|
|
"ja": "Japanese",
|
|
|
"ko": "Korean",
|
|
|
"ru": "Russian",
|
|
|
"ar": "Arabic",
|
|
|
}
|
|
|
|
|
|
|
|
|
def log_message(logs: list, message: str) -> list:
|
|
|
"""Add timestamped message to logs."""
|
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
|
logs.append(f"[{timestamp}] {message}")
|
|
|
return logs
|
|
|
|
|
|
|
|
|
def extract_gdrive_file_id(url: str) -> str | None:
|
|
|
"""Extract file ID from Google Drive URL."""
|
|
|
patterns = [
|
|
|
r"/file/d/([a-zA-Z0-9_-]+)",
|
|
|
r"id=([a-zA-Z0-9_-]+)",
|
|
|
r"/d/([a-zA-Z0-9_-]+)",
|
|
|
]
|
|
|
for pattern in patterns:
|
|
|
match = re.search(pattern, url)
|
|
|
if match:
|
|
|
return match.group(1)
|
|
|
return None
|
|
|
|
|
|
|
|
|
async def download_gdrive_public(url: str) -> tuple[bytes, str]:
|
|
|
"""Download file from public Google Drive link.
|
|
|
|
|
|
Returns (file_bytes, filename).
|
|
|
"""
|
|
|
file_id = extract_gdrive_file_id(url)
|
|
|
if not file_id:
|
|
|
raise ValueError("Invalid Google Drive URL")
|
|
|
|
|
|
|
|
|
download_url = f"https://drive.google.com/uc?export=download&id={file_id}"
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
|
|
|
response = await client.get(download_url)
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
content_disp = response.headers.get("Content-Disposition", "")
|
|
|
filename_match = re.search(r'filename="?([^";\n]+)"?', content_disp)
|
|
|
if filename_match:
|
|
|
filename = filename_match.group(1)
|
|
|
else:
|
|
|
filename = f"gdrive_{file_id}.pdf"
|
|
|
|
|
|
return response.content, filename
|
|
|
|
|
|
|
|
|
async def translate_pdf_modal(
|
|
|
pdf_file,
|
|
|
gdrive_url: str,
|
|
|
target_lang: str,
|
|
|
progress=gr.Progress()
|
|
|
) -> tuple:
|
|
|
"""Translate PDF using Modal cloud."""
|
|
|
logs = []
|
|
|
|
|
|
|
|
|
if not pdf_file and not gdrive_url:
|
|
|
return None, None, "Please upload a PDF or provide a Google Drive link", "", "\n".join(logs)
|
|
|
|
|
|
if pdf_file and gdrive_url:
|
|
|
return None, None, "Please use either file upload OR Google Drive link, not both", "", "\n".join(logs)
|
|
|
|
|
|
try:
|
|
|
logs = log_message(logs, "Starting translation...")
|
|
|
|
|
|
|
|
|
if gdrive_url:
|
|
|
logs = log_message(logs, f"Downloading from Google Drive...")
|
|
|
progress(0.05, desc="Downloading from Google Drive...")
|
|
|
pdf_bytes, source_filename = await download_gdrive_public(gdrive_url.strip())
|
|
|
logs = log_message(logs, f"Downloaded: {source_filename}")
|
|
|
else:
|
|
|
pdf_path = Path(pdf_file)
|
|
|
pdf_bytes = pdf_path.read_bytes()
|
|
|
source_filename = pdf_path.name
|
|
|
|
|
|
pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8")
|
|
|
|
|
|
logs = log_message(logs, f"Input: {source_filename}")
|
|
|
logs = log_message(logs, f"Size: {len(pdf_bytes) / 1024:.1f} KB")
|
|
|
logs = log_message(logs, f"Target: {LANGUAGES.get(target_lang, target_lang)}")
|
|
|
|
|
|
progress(0.1, desc="Uploading to Modal...")
|
|
|
|
|
|
payload = {
|
|
|
"pdf_base64": pdf_base64,
|
|
|
"target_lang": target_lang,
|
|
|
}
|
|
|
|
|
|
logs = log_message(logs, "Translating on Modal cloud...")
|
|
|
logs = log_message(logs, "(This may take several minutes)")
|
|
|
|
|
|
progress(0.2, desc="Translating...")
|
|
|
start_time = datetime.now()
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=900.0, follow_redirects=True) as client:
|
|
|
response = await client.post(MODAL_TRANSLATE_URL, json=payload)
|
|
|
response.raise_for_status()
|
|
|
result = response.json()
|
|
|
|
|
|
duration = (datetime.now() - start_time).total_seconds()
|
|
|
progress(0.8, desc="Processing result...")
|
|
|
|
|
|
if not result.get("success"):
|
|
|
error_msg = result.get("message", "Unknown error")
|
|
|
logs = log_message(logs, f"ERROR: {error_msg}")
|
|
|
return None, None, "Translation failed", "", "\n".join(logs)
|
|
|
|
|
|
|
|
|
mono_img_path = None
|
|
|
mono_img_base64 = result.get("mono_img_pdf_base64")
|
|
|
if mono_img_base64:
|
|
|
mono_img_bytes = base64.b64decode(mono_img_base64)
|
|
|
stem = Path(source_filename).stem
|
|
|
mono_img_filename = f"{stem}_translated.{target_lang}.pdf"
|
|
|
mono_img_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
|
|
mono_img_file.write(mono_img_bytes)
|
|
|
mono_img_file.close()
|
|
|
mono_img_path = mono_img_file.name
|
|
|
logs = log_message(logs, f"Mono: {mono_img_filename} ({len(mono_img_bytes) / 1024:.1f} KB)")
|
|
|
|
|
|
|
|
|
dual_img_path = None
|
|
|
dual_img_base64 = result.get("dual_img_pdf_base64")
|
|
|
if dual_img_base64:
|
|
|
dual_img_bytes = base64.b64decode(dual_img_base64)
|
|
|
stem = Path(source_filename).stem
|
|
|
dual_img_filename = f"{stem}_translated.{target_lang}.dual.pdf"
|
|
|
dual_img_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
|
|
dual_img_file.write(dual_img_bytes)
|
|
|
dual_img_file.close()
|
|
|
dual_img_path = dual_img_file.name
|
|
|
logs = log_message(logs, f"Dual: {dual_img_filename} ({len(dual_img_bytes) / 1024:.1f} KB)")
|
|
|
|
|
|
if not mono_img_path and not dual_img_path:
|
|
|
logs = log_message(logs, "ERROR: No output PDF in response")
|
|
|
return None, None, "Translation failed", "", "\n".join(logs)
|
|
|
|
|
|
logs = log_message(logs, f"Duration: {duration:.1f} seconds")
|
|
|
|
|
|
stats_msg = f"""**Translation completed!**
|
|
|
|
|
|
- **Duration:** {duration:.1f} seconds
|
|
|
- **Target:** {LANGUAGES.get(target_lang, target_lang)}"""
|
|
|
|
|
|
progress(1.0, desc="Done!")
|
|
|
|
|
|
return mono_img_path, dual_img_path, "Translation successful!", stats_msg, "\n".join(logs)
|
|
|
|
|
|
except httpx.TimeoutException:
|
|
|
logs = log_message(logs, "ERROR: Translation timed out")
|
|
|
return None, None, "Translation timed out", "", "\n".join(logs)
|
|
|
except httpx.HTTPStatusError as e:
|
|
|
logs = log_message(logs, f"ERROR: HTTP {e.response.status_code}")
|
|
|
return None, None, f"HTTP error: {e.response.status_code}", "", "\n".join(logs)
|
|
|
except Exception as e:
|
|
|
logs = log_message(logs, f"ERROR: {str(e)}")
|
|
|
return None, None, f"Error: {str(e)}", "", "\n".join(logs)
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="BabelDocs x Agentic AI MCP") as demo:
|
|
|
|
|
|
gr.Markdown("""
|
|
|
# BabelDocs x Agentic AI MCP - PDF Translation with Google Drive Integration
|
|
|
|
|
|
**Translate PDFs directly from Google Drive and save back automatically**
|
|
|
|
|
|
---
|
|
|
|
|
|
## Key Feature: Full Google Drive Workflow in CLAUDE Desktop MCP
|
|
|
|
|
|
```
|
|
|
"Translate my Q3 report to French and save it to Translations folder"
|
|
|
↓
|
|
|
Claude searches → downloads → translates → uploads → done!
|
|
|
```
|
|
|
|
|
|
---
|
|
|
""")
|
|
|
|
|
|
with gr.Row():
|
|
|
with gr.Column(scale=1):
|
|
|
gr.Markdown("### Input")
|
|
|
|
|
|
gdrive_url = gr.Textbox(
|
|
|
label="Google Drive Link (public)",
|
|
|
placeholder="https://drive.google.com/file/d/... or leave empty",
|
|
|
info="Paste a public GDrive link, OR upload a local file below",
|
|
|
)
|
|
|
|
|
|
gr.Markdown("**OR**")
|
|
|
|
|
|
pdf_input = gr.File(
|
|
|
label="Upload PDF",
|
|
|
file_types=[".pdf"],
|
|
|
type="filepath",
|
|
|
)
|
|
|
|
|
|
target_lang = gr.Dropdown(
|
|
|
choices=list(LANGUAGES.keys()),
|
|
|
value="fr",
|
|
|
label="Target Language",
|
|
|
)
|
|
|
|
|
|
translate_btn = gr.Button(
|
|
|
"Translate PDF",
|
|
|
variant="primary",
|
|
|
size="lg",
|
|
|
)
|
|
|
|
|
|
with gr.Column(scale=1):
|
|
|
gr.Markdown("### Result")
|
|
|
|
|
|
status_output = gr.Textbox(
|
|
|
label="Status",
|
|
|
interactive=False,
|
|
|
)
|
|
|
|
|
|
stats_output = gr.Markdown(label="Statistics")
|
|
|
|
|
|
gr.Markdown("**Downloads:**")
|
|
|
with gr.Row():
|
|
|
mono_img_output = gr.File(label="Mono (translated + images)")
|
|
|
dual_img_output = gr.File(label="Dual (bilingual + images)")
|
|
|
|
|
|
logs_output = gr.Textbox(
|
|
|
label="Logs",
|
|
|
interactive=False,
|
|
|
lines=10,
|
|
|
max_lines=15,
|
|
|
)
|
|
|
|
|
|
gr.Markdown("""
|
|
|
---
|
|
|
|
|
|
### How it works
|
|
|
|
|
|
```
|
|
|
1. Upload PDF or paste GDrive link
|
|
|
↓
|
|
|
2. Send to Modal cloud (serverless)
|
|
|
↓
|
|
|
3. BabelDOC with Agentic AI translates text + images, preserves layout
|
|
|
↓
|
|
|
4. Download translated PDF
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
**Built with:** BabelDOC, Modal, Nebius AI, Gradio | **Hackathon:** Anthropic MCP Track 1
|
|
|
""")
|
|
|
|
|
|
translate_btn.click(
|
|
|
fn=translate_pdf_modal,
|
|
|
inputs=[pdf_input, gdrive_url, target_lang],
|
|
|
outputs=[mono_img_output, dual_img_output, status_output, stats_output, logs_output],
|
|
|
)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
demo.launch(
|
|
|
server_name="127.0.0.1",
|
|
|
server_port=7860,
|
|
|
share=False,
|
|
|
)
|
|
|
|