h-xml commited on
Commit
92a0b42
·
verified ·
1 Parent(s): 24112b0

Upload 6 files

Browse files
Files changed (6) hide show
  1. .env.example +1 -0
  2. __init__.py +1 -0
  3. app.py +319 -0
  4. modal_deploy.py +316 -0
  5. requirements.txt +24 -0
  6. server.py +511 -0
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ BABELDOCS_MODAL_URL=https://your-workspace.modal.run
__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # A simple MCP server for document translation with layout preservation
app.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BabelDocs x Agentic AI MCP - Gradio Application
3
+
4
+ PDF Translation with Google Drive Integration.
5
+ Accepts public GDrive links or local file uploads.
6
+
7
+ For Anthropic Hackathon - Track 1: Building MCP
8
+
9
+ Usage:
10
+ python app.py
11
+ """
12
+
13
+ import os
14
+ import re
15
+ import base64
16
+ import tempfile
17
+ import httpx
18
+ import gradio as gr
19
+ from pathlib import Path
20
+ from datetime import datetime
21
+ from dotenv import load_dotenv
22
+
23
+ load_dotenv()
24
+
25
+ # Modal endpoint configuration
26
+ # Set BABELDOCS_MODAL_URL as HuggingFace Space secret for production
27
+ MODAL_BASE_URL = os.getenv("BABELDOCS_MODAL_URL")
28
+ if not MODAL_BASE_URL:
29
+ raise ValueError("BABELDOCS_MODAL_URL environment variable required. Set it as a HuggingFace Space secret.")
30
+ MODAL_TRANSLATE_URL = f"{MODAL_BASE_URL}-babeldocstranslator-api.modal.run"
31
+ MODAL_HEALTH_URL = f"{MODAL_BASE_URL}-babeldocstranslator-health.modal.run"
32
+
33
+ # Max pages limit (test phase)
34
+ MAX_PAGES = 20
35
+
36
+ # Supported languages
37
+ LANGUAGES = {
38
+ "fr": "French",
39
+ "en": "English",
40
+ "es": "Spanish",
41
+ "de": "German",
42
+ "it": "Italian",
43
+ "pt": "Portuguese",
44
+ "zh": "Chinese",
45
+ "ja": "Japanese",
46
+ "ko": "Korean",
47
+ "ru": "Russian",
48
+ "ar": "Arabic",
49
+ }
50
+
51
+
52
+ def log_message(logs: list, message: str) -> list:
53
+ """Add timestamped message to logs."""
54
+ timestamp = datetime.now().strftime("%H:%M:%S")
55
+ logs.append(f"[{timestamp}] {message}")
56
+ return logs
57
+
58
+
59
+ def extract_gdrive_file_id(url: str) -> str | None:
60
+ """Extract file ID from Google Drive URL."""
61
+ patterns = [
62
+ r"/file/d/([a-zA-Z0-9_-]+)",
63
+ r"id=([a-zA-Z0-9_-]+)",
64
+ r"/d/([a-zA-Z0-9_-]+)",
65
+ ]
66
+ for pattern in patterns:
67
+ match = re.search(pattern, url)
68
+ if match:
69
+ return match.group(1)
70
+ return None
71
+
72
+
73
+ async def download_gdrive_public(url: str) -> tuple[bytes, str]:
74
+ """Download file from public Google Drive link.
75
+
76
+ Returns (file_bytes, filename).
77
+ """
78
+ file_id = extract_gdrive_file_id(url)
79
+ if not file_id:
80
+ raise ValueError("Invalid Google Drive URL")
81
+
82
+ # Direct download URL
83
+ download_url = f"https://drive.google.com/uc?export=download&id={file_id}"
84
+
85
+ async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
86
+ response = await client.get(download_url)
87
+ response.raise_for_status()
88
+
89
+ # Try to get filename from Content-Disposition header
90
+ content_disp = response.headers.get("Content-Disposition", "")
91
+ filename_match = re.search(r'filename="?([^";\n]+)"?', content_disp)
92
+ if filename_match:
93
+ filename = filename_match.group(1)
94
+ else:
95
+ filename = f"gdrive_{file_id}.pdf"
96
+
97
+ return response.content, filename
98
+
99
+
100
+ async def translate_pdf_modal(
101
+ pdf_file,
102
+ gdrive_url: str,
103
+ target_lang: str,
104
+ progress=gr.Progress()
105
+ ) -> tuple:
106
+ """Translate PDF using Modal cloud."""
107
+ logs = []
108
+
109
+ # Validate input
110
+ if not pdf_file and not gdrive_url:
111
+ return None, None, "Please upload a PDF or provide a Google Drive link", "", "\n".join(logs)
112
+
113
+ if pdf_file and gdrive_url:
114
+ return None, None, "Please use either file upload OR Google Drive link, not both", "", "\n".join(logs)
115
+
116
+ try:
117
+ logs = log_message(logs, "Starting translation...")
118
+
119
+ # Get PDF bytes and filename
120
+ if gdrive_url:
121
+ logs = log_message(logs, f"Downloading from Google Drive...")
122
+ progress(0.05, desc="Downloading from Google Drive...")
123
+ pdf_bytes, source_filename = await download_gdrive_public(gdrive_url.strip())
124
+ logs = log_message(logs, f"Downloaded: {source_filename}")
125
+ else:
126
+ pdf_path = Path(pdf_file)
127
+ pdf_bytes = pdf_path.read_bytes()
128
+ source_filename = pdf_path.name
129
+
130
+ pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8")
131
+
132
+ logs = log_message(logs, f"Input: {source_filename}")
133
+ logs = log_message(logs, f"Size: {len(pdf_bytes) / 1024:.1f} KB")
134
+ logs = log_message(logs, f"Target: {LANGUAGES.get(target_lang, target_lang)}")
135
+
136
+ progress(0.1, desc="Uploading to Modal...")
137
+
138
+ payload = {
139
+ "pdf_base64": pdf_base64,
140
+ "target_lang": target_lang,
141
+ }
142
+
143
+ logs = log_message(logs, "Translating on Modal cloud...")
144
+ logs = log_message(logs, "(This may take several minutes)")
145
+
146
+ progress(0.2, desc="Translating...")
147
+ start_time = datetime.now()
148
+
149
+ async with httpx.AsyncClient(timeout=900.0, follow_redirects=True) as client:
150
+ response = await client.post(MODAL_TRANSLATE_URL, json=payload)
151
+ response.raise_for_status()
152
+ result = response.json()
153
+
154
+ duration = (datetime.now() - start_time).total_seconds()
155
+ progress(0.8, desc="Processing result...")
156
+
157
+ if not result.get("success"):
158
+ error_msg = result.get("message", "Unknown error")
159
+ logs = log_message(logs, f"ERROR: {error_msg}")
160
+ return None, None, "Translation failed", "", "\n".join(logs)
161
+
162
+ # Process mono_img PDF
163
+ mono_img_path = None
164
+ mono_img_base64 = result.get("mono_img_pdf_base64")
165
+ if mono_img_base64:
166
+ mono_img_bytes = base64.b64decode(mono_img_base64)
167
+ stem = Path(source_filename).stem
168
+ mono_img_filename = f"{stem}_translated.{target_lang}.pdf"
169
+ mono_img_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
170
+ mono_img_file.write(mono_img_bytes)
171
+ mono_img_file.close()
172
+ mono_img_path = mono_img_file.name
173
+ logs = log_message(logs, f"Mono: {mono_img_filename} ({len(mono_img_bytes) / 1024:.1f} KB)")
174
+
175
+ # Process dual_img PDF
176
+ dual_img_path = None
177
+ dual_img_base64 = result.get("dual_img_pdf_base64")
178
+ if dual_img_base64:
179
+ dual_img_bytes = base64.b64decode(dual_img_base64)
180
+ stem = Path(source_filename).stem
181
+ dual_img_filename = f"{stem}_translated.{target_lang}.dual.pdf"
182
+ dual_img_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
183
+ dual_img_file.write(dual_img_bytes)
184
+ dual_img_file.close()
185
+ dual_img_path = dual_img_file.name
186
+ logs = log_message(logs, f"Dual: {dual_img_filename} ({len(dual_img_bytes) / 1024:.1f} KB)")
187
+
188
+ if not mono_img_path and not dual_img_path:
189
+ logs = log_message(logs, "ERROR: No output PDF in response")
190
+ return None, None, "Translation failed", "", "\n".join(logs)
191
+
192
+ logs = log_message(logs, f"Duration: {duration:.1f} seconds")
193
+
194
+ stats_msg = f"""**Translation completed!**
195
+
196
+ - **Duration:** {duration:.1f} seconds
197
+ - **Target:** {LANGUAGES.get(target_lang, target_lang)}"""
198
+
199
+ progress(1.0, desc="Done!")
200
+
201
+ return mono_img_path, dual_img_path, "Translation successful!", stats_msg, "\n".join(logs)
202
+
203
+ except httpx.TimeoutException:
204
+ logs = log_message(logs, "ERROR: Translation timed out")
205
+ return None, None, "Translation timed out", "", "\n".join(logs)
206
+ except httpx.HTTPStatusError as e:
207
+ logs = log_message(logs, f"ERROR: HTTP {e.response.status_code}")
208
+ return None, None, f"HTTP error: {e.response.status_code}", "", "\n".join(logs)
209
+ except Exception as e:
210
+ logs = log_message(logs, f"ERROR: {str(e)}")
211
+ return None, None, f"Error: {str(e)}", "", "\n".join(logs)
212
+
213
+
214
+ # Gradio Interface
215
+ with gr.Blocks(title="BabelDocs x Agentic AI MCP") as demo:
216
+
217
+ gr.Markdown("""
218
+ # BabelDocs x Agentic AI MCP - PDF Translation with Google Drive Integration
219
+
220
+ **Translate PDFs directly from Google Drive and save back automatically**
221
+
222
+ ---
223
+
224
+ ## Key Feature: Full Google Drive Workflow in CLAUDE Desktop MCP
225
+
226
+ ```
227
+ "Translate my Q3 report to French and save it to Translations folder"
228
+
229
+ Claude searches → downloads → translates → uploads → done!
230
+ ```
231
+
232
+ ---
233
+ """)
234
+
235
+ with gr.Row():
236
+ with gr.Column(scale=1):
237
+ gr.Markdown("### Input")
238
+
239
+ gdrive_url = gr.Textbox(
240
+ label="Google Drive Link (public)",
241
+ placeholder="https://drive.google.com/file/d/... or leave empty",
242
+ info="Paste a public GDrive link, OR upload a local file below",
243
+ )
244
+
245
+ gr.Markdown("**OR**")
246
+
247
+ pdf_input = gr.File(
248
+ label="Upload PDF",
249
+ file_types=[".pdf"],
250
+ type="filepath",
251
+ )
252
+
253
+ target_lang = gr.Dropdown(
254
+ choices=list(LANGUAGES.keys()),
255
+ value="fr",
256
+ label="Target Language",
257
+ )
258
+
259
+ translate_btn = gr.Button(
260
+ "Translate PDF",
261
+ variant="primary",
262
+ size="lg",
263
+ )
264
+
265
+ with gr.Column(scale=1):
266
+ gr.Markdown("### Result")
267
+
268
+ status_output = gr.Textbox(
269
+ label="Status",
270
+ interactive=False,
271
+ )
272
+
273
+ stats_output = gr.Markdown(label="Statistics")
274
+
275
+ gr.Markdown("**Downloads:**")
276
+ with gr.Row():
277
+ mono_img_output = gr.File(label="Mono (translated + images)")
278
+ dual_img_output = gr.File(label="Dual (bilingual + images)")
279
+
280
+ logs_output = gr.Textbox(
281
+ label="Logs",
282
+ interactive=False,
283
+ lines=10,
284
+ max_lines=15,
285
+ )
286
+
287
+ gr.Markdown("""
288
+ ---
289
+
290
+ ### How it works
291
+
292
+ ```
293
+ 1. Upload PDF or paste GDrive link
294
+
295
+ 2. Send to Modal cloud (serverless)
296
+
297
+ 3. BabelDOC with Agentic AI translates text + images, preserves layout
298
+
299
+ 4. Download translated PDF
300
+ ```
301
+
302
+ ---
303
+
304
+ **Built with:** BabelDOC, Modal, Nebius AI, Gradio | **Hackathon:** Anthropic MCP Track 1
305
+ """)
306
+
307
+ translate_btn.click(
308
+ fn=translate_pdf_modal,
309
+ inputs=[pdf_input, gdrive_url, target_lang],
310
+ outputs=[mono_img_output, dual_img_output, status_output, stats_output, logs_output],
311
+ )
312
+
313
+
314
+ if __name__ == "__main__":
315
+ demo.launch(
316
+ server_name="127.0.0.1",
317
+ server_port=7860,
318
+ share=False,
319
+ )
modal_deploy.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BabelDOC with Agentic AI - Modal Deployment
3
+
4
+ PDF translation API with layout preservation.
5
+ 20-page limit during test phase.
6
+
7
+ Setup:
8
+ modal secret create babeldocs-secrets \
9
+ NEBIUS_API_KEY=your_key \
10
+ NEBIUS_API_BASE=https://api.tokenfactory.nebius.com/v1/ \
11
+ NEBIUS_TRANSLATION_MODEL=openai/gpt-oss-120b
12
+
13
+ Deploy:
14
+ modal deploy modal_deploy.py
15
+ """
16
+
17
+ import modal
18
+ import os
19
+ from pathlib import Path
20
+
21
+ THIS_DIR = Path(__file__).parent.resolve()
22
+ BABELDOC_DIR = THIS_DIR.parent / "BabelDOC"
23
+
24
+ # Max pages allowed (test phase limit)
25
+ MAX_PAGES = 20
26
+
27
+ # Modal app - custom name for hackathon
28
+ app = modal.App("mcp1stann-babeldocs")
29
+
30
+ # Image with uv and BabelDOC installed
31
+ babeldocs_image = (
32
+ modal.Image.debian_slim(python_version="3.11")
33
+ .apt_install(
34
+ "git",
35
+ "libgl1-mesa-glx",
36
+ "libglib2.0-0",
37
+ "libsm6",
38
+ "libxext6",
39
+ "libxrender-dev",
40
+ "libgomp1",
41
+ "curl",
42
+ "libspatialindex-dev", # For rtree
43
+ "libharfbuzz-dev", # For uharfbuzz
44
+ "libfreetype6-dev", # For freetype-py
45
+ "libopencv-dev", # For opencv dependencies
46
+ "libzstd-dev", # For pyzstd
47
+ )
48
+ .pip_install("uv")
49
+ .env({
50
+ "PYTHONIOENCODING": "utf-8",
51
+ "PYTHONUNBUFFERED": "1",
52
+ "UV_SYSTEM_PYTHON": "1",
53
+ })
54
+ .pip_install("fastapi[standard]")
55
+ .add_local_dir(
56
+ str(BABELDOC_DIR),
57
+ remote_path="/app/BabelDOC",
58
+ copy=True,
59
+ )
60
+ .run_commands(
61
+ "cd /app/BabelDOC && uv pip install -e . --python python3.11",
62
+ )
63
+ )
64
+
65
+ # Volume for caching models and fonts
66
+ cache_volume = modal.Volume.from_name("babeldocs-cache", create_if_missing=True)
67
+ CACHE_PATH = "/cache"
68
+
69
+
70
+ @app.cls(
71
+ image=babeldocs_image,
72
+ timeout=900, # 15 minutes
73
+ memory=8192,
74
+ cpu=4,
75
+ volumes={CACHE_PATH: cache_volume},
76
+ secrets=[modal.Secret.from_name("babeldocs-secrets")],
77
+ scaledown_window=300, # Keep warm for 5 minutes
78
+ )
79
+ class BabelDocsTranslator:
80
+ """Class-based translator for BabelDOC (based on working SVG generator pattern)."""
81
+
82
+ def _count_pdf_pages(self, pdf_bytes: bytes) -> int:
83
+ """Count pages in PDF using PyMuPDF."""
84
+ try:
85
+ import fitz # PyMuPDF
86
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
87
+ count = len(doc)
88
+ doc.close()
89
+ return count
90
+ except Exception:
91
+ return -1 # Unknown
92
+
93
+ def _translate_internal(
94
+ self,
95
+ pdf_base64: str,
96
+ target_lang: str = "fr",
97
+ pages: str = "",
98
+ no_dual: bool = False,
99
+ no_mono: bool = False,
100
+ ) -> dict:
101
+ """BabelDOC with Agentic AI - Internal translation."""
102
+ import base64
103
+ import subprocess
104
+ import tempfile
105
+ from pathlib import Path
106
+ from datetime import datetime
107
+
108
+ try:
109
+ if not pdf_base64:
110
+ return {"success": False, "message": "No PDF provided"}
111
+
112
+ pdf_bytes = base64.b64decode(pdf_base64)
113
+
114
+ # Check page limit (test phase)
115
+ page_count = self._count_pdf_pages(pdf_bytes)
116
+ if page_count > MAX_PAGES:
117
+ return {
118
+ "success": False,
119
+ "message": f"PDF has {page_count} pages. Maximum allowed: {MAX_PAGES} pages (test phase limit)."
120
+ }
121
+
122
+ with tempfile.TemporaryDirectory() as tmpdir:
123
+ input_path = Path(tmpdir) / "input.pdf"
124
+ output_dir = Path(tmpdir) / "output"
125
+ output_dir.mkdir()
126
+
127
+ input_path.write_bytes(pdf_bytes)
128
+
129
+ cmd = [
130
+ "babeldoc",
131
+ "--files", str(input_path),
132
+ "--output", str(output_dir),
133
+ "--lang-out", target_lang,
134
+ "--openai",
135
+ "--openai-model", os.getenv("NEBIUS_TRANSLATION_MODEL", "openai/gpt-oss-120b"),
136
+ "--openai-base-url", os.getenv("NEBIUS_API_BASE", "https://api.tokenfactory.nebius.com/v1/"),
137
+ "--openai-api-key", os.getenv("NEBIUS_API_KEY", ""),
138
+ "--no-watermark",
139
+ "--translate-table-text",
140
+ "--enhance-compatibility",
141
+ # Enable image translation (orchestration PASS 2) with vision model
142
+ "--vision-model", os.getenv("NEBIUS_VISION_MODEL", "Qwen/Qwen2.5-VL-72B-Instruct"),
143
+ ]
144
+
145
+ if pages:
146
+ cmd.extend(["--pages", pages])
147
+ cmd.append("--only-include-translated-page")
148
+
149
+ if no_dual:
150
+ cmd.append("--no-dual")
151
+
152
+ if no_mono:
153
+ cmd.append("--no-mono")
154
+
155
+ start_time = datetime.now()
156
+
157
+ result = subprocess.run(
158
+ cmd,
159
+ capture_output=True,
160
+ text=True,
161
+ encoding="utf-8",
162
+ errors="replace",
163
+ cwd="/app/BabelDOC",
164
+ env={
165
+ **os.environ,
166
+ "HF_HOME": CACHE_PATH,
167
+ },
168
+ )
169
+
170
+ duration = (datetime.now() - start_time).total_seconds()
171
+
172
+ if result.returncode != 0:
173
+ return {
174
+ "success": False,
175
+ "message": "Translation failed",
176
+ "stderr": result.stderr[:1000] if result.stderr else "",
177
+ "stdout": result.stdout[:500] if result.stdout else "",
178
+ }
179
+
180
+ # Find all 4 types of PDFs:
181
+ # Format: name.no_watermark.{lang}.{mono|dual}.pdf
182
+ # Format: name.no_watermark.{lang}.{mono|dual}.images_translated.pdf
183
+
184
+ # Get all PDFs in output directory
185
+ all_pdfs = list(output_dir.glob("*.pdf"))
186
+
187
+ # Categorize by type
188
+ mono_matches = [p for p in all_pdfs if f".{target_lang}.mono.pdf" in p.name and "images_translated" not in p.name]
189
+ mono_img_matches = [p for p in all_pdfs if f".{target_lang}.mono.images_translated.pdf" in p.name]
190
+ dual_matches = [p for p in all_pdfs if f".{target_lang}.dual.pdf" in p.name and "images_translated" not in p.name]
191
+ dual_img_matches = [p for p in all_pdfs if f".{target_lang}.dual.images_translated.pdf" in p.name]
192
+
193
+ mono_pdf = mono_matches[0] if mono_matches else None
194
+ mono_img_pdf = mono_img_matches[0] if mono_img_matches else None
195
+ dual_pdf = dual_matches[0] if dual_matches else None
196
+ dual_img_pdf = dual_img_matches[0] if dual_img_matches else None
197
+
198
+ if not any([mono_pdf, mono_img_pdf, dual_pdf, dual_img_pdf]):
199
+ # Fallback to any PDF
200
+ if not all_pdfs:
201
+ return {"success": False, "message": "No output PDF generated"}
202
+ mono_pdf = all_pdfs[0]
203
+
204
+ result_data = {
205
+ "success": True,
206
+ "stats": {
207
+ "duration_seconds": round(duration, 2),
208
+ }
209
+ }
210
+
211
+ # Add mono PDF (without image translation)
212
+ if mono_pdf and not no_mono:
213
+ mono_bytes = mono_pdf.read_bytes()
214
+ result_data["mono_pdf_base64"] = base64.b64encode(mono_bytes).decode("utf-8")
215
+ result_data["mono_filename"] = mono_pdf.name
216
+ result_data["stats"]["mono_size_bytes"] = len(mono_bytes)
217
+
218
+ # Add mono PDF with image translation
219
+ if mono_img_pdf and not no_mono:
220
+ mono_img_bytes = mono_img_pdf.read_bytes()
221
+ result_data["mono_img_pdf_base64"] = base64.b64encode(mono_img_bytes).decode("utf-8")
222
+ result_data["mono_img_filename"] = mono_img_pdf.name
223
+ result_data["stats"]["mono_img_size_bytes"] = len(mono_img_bytes)
224
+
225
+ # Add dual PDF (without image translation)
226
+ if dual_pdf and not no_dual:
227
+ dual_bytes = dual_pdf.read_bytes()
228
+ result_data["dual_pdf_base64"] = base64.b64encode(dual_bytes).decode("utf-8")
229
+ result_data["dual_filename"] = dual_pdf.name
230
+ result_data["stats"]["dual_size_bytes"] = len(dual_bytes)
231
+
232
+ # Add dual PDF with image translation
233
+ if dual_img_pdf and not no_dual:
234
+ dual_img_bytes = dual_img_pdf.read_bytes()
235
+ result_data["dual_img_pdf_base64"] = base64.b64encode(dual_img_bytes).decode("utf-8")
236
+ result_data["dual_img_filename"] = dual_img_pdf.name
237
+ result_data["stats"]["dual_img_size_bytes"] = len(dual_img_bytes)
238
+
239
+ return result_data
240
+
241
+ except Exception as e:
242
+ return {"success": False, "message": f"Error: {str(e)}"}
243
+
244
+ @modal.method()
245
+ def translate(
246
+ self,
247
+ pdf_base64: str,
248
+ target_lang: str = "fr",
249
+ pages: str = "",
250
+ no_dual: bool = False,
251
+ no_mono: bool = False,
252
+ ) -> dict:
253
+ """Translate method (callable via Modal)."""
254
+ return self._translate_internal(pdf_base64, target_lang, pages, no_dual, no_mono)
255
+
256
+ @modal.fastapi_endpoint(method="POST")
257
+ def api(self, request: dict) -> dict:
258
+ """
259
+ FastAPI endpoint POST for PDF translation.
260
+
261
+ Request body:
262
+ {
263
+ "pdf_base64": "base64_encoded_pdf",
264
+ "target_lang": "fr",
265
+ "pages": "1,2,3" (optional),
266
+ "no_dual": false,
267
+ "no_mono": false
268
+ }
269
+ """
270
+ pdf_base64 = request.get("pdf_base64", "")
271
+ target_lang = request.get("target_lang", "fr")
272
+ pages = request.get("pages", "")
273
+ no_dual = request.get("no_dual", False)
274
+ no_mono = request.get("no_mono", False)
275
+
276
+ return self._translate_internal(pdf_base64, target_lang, pages, no_dual, no_mono)
277
+
278
+ @modal.fastapi_endpoint(method="GET")
279
+ def health(self) -> dict:
280
+ """Health check endpoint."""
281
+ return {
282
+ "status": "healthy",
283
+ "service": "BabelDOC with Agentic AI",
284
+ "version": "1.0.0",
285
+ "max_pages": MAX_PAGES,
286
+ }
287
+
288
+ @modal.fastapi_endpoint(method="GET")
289
+ def languages(self) -> dict:
290
+ """Get supported languages."""
291
+ return {
292
+ "languages": {
293
+ "fr": "French",
294
+ "en": "English",
295
+ "es": "Spanish",
296
+ "de": "German",
297
+ "it": "Italian",
298
+ "pt": "Portuguese",
299
+ "zh": "Chinese",
300
+ "ja": "Japanese",
301
+ "ko": "Korean",
302
+ "ru": "Russian",
303
+ "ar": "Arabic",
304
+ }
305
+ }
306
+
307
+
308
+ @app.local_entrypoint()
309
+ def main():
310
+ """BabelDOC with Agentic AI - Local test."""
311
+ print("BabelDOC with Agentic AI - Modal Deployment")
312
+ print("=" * 45)
313
+ print(f"Max pages: {MAX_PAGES} (test phase)")
314
+ print()
315
+ print("Deploy: modal deploy modal_deploy.py")
316
+ print("Test: modal serve modal_deploy.py")
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BabelDocs MCP Server Dependencies
2
+
3
+ # MCP Framework
4
+ fastmcp>=0.1.0
5
+
6
+ # Web Framework
7
+ gradio>=4.44.0
8
+ uvicorn>=0.30.0
9
+
10
+ # PDF Processing
11
+ pymupdf>=1.24.0
12
+
13
+ # HTTP Client
14
+ httpx>=0.25.0
15
+
16
+ # Environment
17
+ python-dotenv>=1.0.0
18
+
19
+ # AI APIs
20
+ anthropic>=0.39.0
21
+ openai>=1.0.0
22
+
23
+ # Modal deployment
24
+ modal>=0.64.0
server.py ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ BabelDOC with Agentic AI - MCP Server
3
+ PDF Translation with Layout Preservation + Google Drive Integration
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import json
9
+ import base64
10
+ import httpx
11
+ from pathlib import Path
12
+ from typing import Optional, Tuple, List
13
+ from datetime import datetime
14
+
15
+ from fastmcp import FastMCP
16
+
17
+ # Google Drive OAuth
18
+ try:
19
+ from google.oauth2.credentials import Credentials
20
+ from google_auth_oauthlib.flow import InstalledAppFlow
21
+ from google.auth.transport.requests import Request
22
+ from googleapiclient.discovery import build
23
+ from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
24
+ import io
25
+ GOOGLE_AVAILABLE = True
26
+ except ImportError:
27
+ GOOGLE_AVAILABLE = False
28
+
29
+ # Constants
30
+ MAX_PAGES = 20 # Test phase limit
31
+ GRADIO_URL = "http://127.0.0.1:7860"
32
+ OUTPUT_DIR = Path.home() / "Downloads" / "BabelDocs"
33
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
34
+
35
+ # Google Drive OAuth config
36
+ GDRIVE_SCOPES = ['https://www.googleapis.com/auth/drive']
37
+ GDRIVE_OAUTH_PATH = Path(os.getenv(
38
+ "GDRIVE_OAUTH_CREDENTIALS",
39
+ Path.home() / "Downloads" / "gcp-oauth.keys.json"
40
+ ))
41
+ GDRIVE_TOKEN_PATH = OUTPUT_DIR / "gdrive_token.json"
42
+
43
+ # Modal endpoints (set BABELDOCS_MODAL_URL env var)
44
+ MODAL_BASE_URL = os.getenv("BABELDOCS_MODAL_URL")
45
+ if not MODAL_BASE_URL:
46
+ raise ValueError("BABELDOCS_MODAL_URL environment variable is required")
47
+ MODAL_TRANSLATE_URL = f"{MODAL_BASE_URL}-babeldocstranslator-api.modal.run"
48
+ MODAL_HEALTH_URL = f"{MODAL_BASE_URL}-babeldocstranslator-health.modal.run"
49
+
50
+ SUPPORTED_LANGUAGES = {
51
+ "en": "English", "fr": "French", "es": "Spanish", "de": "German",
52
+ "it": "Italian", "pt": "Portuguese", "zh": "Chinese", "ja": "Japanese",
53
+ "ko": "Korean", "ru": "Russian", "ar": "Arabic",
54
+ }
55
+
56
+
57
+ # === Helper Functions ===
58
+
59
+ async def _warmup_modal():
60
+ """Wake up Modal container."""
61
+ try:
62
+ async with httpx.AsyncClient(timeout=10.0) as client:
63
+ await client.get(MODAL_HEALTH_URL)
64
+ except:
65
+ pass
66
+
67
+
68
+ def _count_pdf_pages(pdf_bytes: bytes) -> int:
69
+ """Count pages in PDF."""
70
+ try:
71
+ import fitz
72
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
73
+ count = len(doc)
74
+ doc.close()
75
+ return count
76
+ except ImportError:
77
+ content = pdf_bytes.decode('latin-1', errors='ignore')
78
+ return content.count('/Type /Page') - content.count('/Type /Pages')
79
+
80
+
81
+ def _extract_gdrive_file_id(url: str) -> Optional[str]:
82
+ """Extract file ID from Google Drive URL."""
83
+ patterns = [
84
+ r'/file/d/([a-zA-Z0-9_-]+)',
85
+ r'id=([a-zA-Z0-9_-]+)',
86
+ r'/open\?id=([a-zA-Z0-9_-]+)',
87
+ r'^([a-zA-Z0-9_-]{25,})$',
88
+ ]
89
+ for pattern in patterns:
90
+ match = re.search(pattern, url)
91
+ if match:
92
+ return match.group(1)
93
+ return None
94
+
95
+
96
+ def _get_gdrive_credentials():
97
+ """Get or refresh Google Drive credentials."""
98
+ if not GOOGLE_AVAILABLE:
99
+ return None, "Google libraries not installed"
100
+ if not GDRIVE_OAUTH_PATH.exists():
101
+ return None, f"OAuth credentials not found at {GDRIVE_OAUTH_PATH}"
102
+
103
+ creds = None
104
+ if GDRIVE_TOKEN_PATH.exists():
105
+ try:
106
+ creds = Credentials.from_authorized_user_file(str(GDRIVE_TOKEN_PATH), GDRIVE_SCOPES)
107
+ except:
108
+ pass
109
+
110
+ if not creds or not creds.valid:
111
+ if creds and creds.expired and creds.refresh_token:
112
+ try:
113
+ creds.refresh(Request())
114
+ except:
115
+ creds = None
116
+
117
+ if not creds:
118
+ try:
119
+ flow = InstalledAppFlow.from_client_secrets_file(str(GDRIVE_OAUTH_PATH), GDRIVE_SCOPES)
120
+ for port in [8101, 8102, 8103, 0]:
121
+ try:
122
+ creds = flow.run_local_server(port=port, open_browser=True, bind_addr="127.0.0.1")
123
+ break
124
+ except OSError:
125
+ if port == 0:
126
+ raise
127
+ except Exception as e:
128
+ return None, f"OAuth failed: {str(e)}"
129
+
130
+ with open(GDRIVE_TOKEN_PATH, 'w') as token:
131
+ token.write(creds.to_json())
132
+
133
+ return creds, None
134
+
135
+
136
+ def _upload_to_gdrive(file_path: str, folder_id: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]:
137
+ """Upload file to Google Drive. Returns (file_id, error)."""
138
+ creds, error = _get_gdrive_credentials()
139
+ if error:
140
+ return None, error
141
+
142
+ try:
143
+ service = build('drive', 'v3', credentials=creds)
144
+ file_metadata = {'name': Path(file_path).name}
145
+ if folder_id:
146
+ file_metadata['parents'] = [folder_id]
147
+
148
+ media = MediaFileUpload(file_path, mimetype='application/pdf', resumable=True)
149
+ file = service.files().create(body=file_metadata, media_body=media, fields='id, webViewLink').execute()
150
+ return file.get('id'), None
151
+ except Exception as e:
152
+ return None, f"Upload failed: {str(e)}"
153
+
154
+
155
+ def _list_gdrive_folders() -> Tuple[Optional[List[dict]], Optional[str]]:
156
+ """List folders in Google Drive."""
157
+ creds, error = _get_gdrive_credentials()
158
+ if error:
159
+ return None, error
160
+
161
+ try:
162
+ service = build('drive', 'v3', credentials=creds)
163
+ results = service.files().list(
164
+ q="mimeType='application/vnd.google-apps.folder' and trashed=false",
165
+ fields='files(id, name)', pageSize=50
166
+ ).execute()
167
+ return results.get('files', []), None
168
+ except Exception as e:
169
+ return None, f"Failed to list folders: {str(e)}"
170
+
171
+
172
+ def _list_gdrive_files(folder_id: Optional[str] = None, file_type: Optional[str] = None) -> Tuple[Optional[List[dict]], Optional[str]]:
173
+ """List files in Google Drive."""
174
+ creds, error = _get_gdrive_credentials()
175
+ if error:
176
+ return None, error
177
+
178
+ try:
179
+ service = build('drive', 'v3', credentials=creds)
180
+ query_parts = ["trashed=false"]
181
+ if folder_id:
182
+ query_parts.append(f"'{folder_id}' in parents")
183
+ if file_type == "pdf":
184
+ query_parts.append("mimeType='application/pdf'")
185
+ elif file_type == "folder":
186
+ query_parts.append("mimeType='application/vnd.google-apps.folder'")
187
+
188
+ results = service.files().list(
189
+ q=" and ".join(query_parts),
190
+ fields='files(id, name, mimeType, size, webViewLink)',
191
+ pageSize=100, orderBy='modifiedTime desc'
192
+ ).execute()
193
+ return results.get('files', []), None
194
+ except Exception as e:
195
+ return None, f"Failed to list files: {str(e)}"
196
+
197
+
198
+ def _search_gdrive_files(query: str, file_type: Optional[str] = None) -> Tuple[Optional[List[dict]], Optional[str]]:
199
+ """Search files in Google Drive by name."""
200
+ creds, error = _get_gdrive_credentials()
201
+ if error:
202
+ return None, error
203
+
204
+ try:
205
+ service = build('drive', 'v3', credentials=creds)
206
+ query_parts = [f"name contains '{query}'", "trashed=false"]
207
+ if file_type == "pdf":
208
+ query_parts.append("mimeType='application/pdf'")
209
+
210
+ results = service.files().list(
211
+ q=" and ".join(query_parts),
212
+ fields='files(id, name, mimeType, size, webViewLink)',
213
+ pageSize=50, orderBy='modifiedTime desc'
214
+ ).execute()
215
+ return results.get('files', []), None
216
+ except Exception as e:
217
+ return None, f"Search failed: {str(e)}"
218
+
219
+
220
+ def _download_gdrive_file(file_id: str, destination: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]:
221
+ """Download file from Google Drive."""
222
+ creds, error = _get_gdrive_credentials()
223
+ if error:
224
+ return None, error
225
+
226
+ try:
227
+ service = build('drive', 'v3', credentials=creds)
228
+ file_metadata = service.files().get(fileId=file_id, fields='name').execute()
229
+ filename = file_metadata.get('name', f'download_{file_id}')
230
+
231
+ dest_path = Path(destination) / filename if destination else OUTPUT_DIR / filename
232
+
233
+ request = service.files().get_media(fileId=file_id)
234
+ file_handle = io.BytesIO()
235
+ downloader = MediaIoBaseDownload(file_handle, request)
236
+ done = False
237
+ while not done:
238
+ _, done = downloader.next_chunk()
239
+
240
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
241
+ dest_path.write_bytes(file_handle.getvalue())
242
+ return str(dest_path), None
243
+ except Exception as e:
244
+ return None, f"Download failed: {str(e)}"
245
+
246
+
247
+ async def _get_pdf_bytes(source: str) -> Tuple[bytes, str, Optional[str]]:
248
+ """Get PDF bytes from local file or Google Drive URL. Returns (bytes, source_name, error)."""
249
+ # Google Drive URL
250
+ if "drive.google.com" in source or "docs.google.com" in source:
251
+ file_id = _extract_gdrive_file_id(source)
252
+ if not file_id:
253
+ return b"", "", "Invalid Google Drive URL"
254
+
255
+ local_path, error = _download_gdrive_file(file_id)
256
+ if error:
257
+ return b"", "", error
258
+
259
+ pdf_bytes = Path(local_path).read_bytes()
260
+ return pdf_bytes, f"Google Drive: {Path(local_path).name}", None
261
+
262
+ # Just file ID
263
+ if re.match(r'^[a-zA-Z0-9_-]{25,}$', source):
264
+ local_path, error = _download_gdrive_file(source)
265
+ if error:
266
+ return b"", "", error
267
+
268
+ pdf_bytes = Path(local_path).read_bytes()
269
+ return pdf_bytes, f"Google Drive: {Path(local_path).name}", None
270
+
271
+ # Local file
272
+ pdf_path = Path(source)
273
+ if not pdf_path.exists():
274
+ return b"", "", f"File not found: {source}"
275
+ if pdf_path.suffix.lower() != ".pdf":
276
+ return b"", "", "File must be a PDF"
277
+
278
+ return pdf_path.read_bytes(), pdf_path.name, None
279
+
280
+
281
+ # === MCP Server ===
282
+
283
+ mcp = FastMCP(
284
+ name="babeldocs",
285
+ instructions=f"""PDF translation with layout preservation + Google Drive integration.
286
+
287
+ Max {MAX_PAGES} pages. For larger PDFs use Gradio at {GRADIO_URL}
288
+
289
+ WORKFLOW:
290
+ 1. search_gdrive("filename") - Find PDF
291
+ 2. download_from_gdrive(file_id) - Download
292
+ 3. translate_pdf(path, "fr") - Translate
293
+ 4. upload_to_gdrive(path, folder_id) - Upload
294
+
295
+ Or all-in-one: translate_and_upload(source, "fr", folder_id)
296
+
297
+ Output: {OUTPUT_DIR}
298
+ """
299
+ )
300
+
301
+
302
+ @mcp.tool()
303
+ async def translate_pdf(source: str, target_lang: str = "fr") -> dict:
304
+ """Translate PDF with layout preservation. Returns single translated file."""
305
+ await _warmup_modal()
306
+
307
+ try:
308
+ pdf_bytes, source_name, error = await _get_pdf_bytes(source)
309
+ if error:
310
+ return {"success": False, "message": error}
311
+
312
+ page_count = _count_pdf_pages(pdf_bytes)
313
+ if page_count > MAX_PAGES:
314
+ return {
315
+ "success": False,
316
+ "message": f"PDF has {page_count} pages (max {MAX_PAGES}). Use Gradio: {GRADIO_URL}"
317
+ }
318
+
319
+ if target_lang not in SUPPORTED_LANGUAGES:
320
+ return {"success": False, "message": f"Unsupported language: {target_lang}"}
321
+
322
+ # Call Modal
323
+ payload = {
324
+ "pdf_base64": base64.b64encode(pdf_bytes).decode("utf-8"),
325
+ "target_lang": target_lang,
326
+ "no_dual": True,
327
+ "no_mono": False,
328
+ }
329
+
330
+ async with httpx.AsyncClient(timeout=900.0, follow_redirects=True) as client:
331
+ response = await client.post(MODAL_TRANSLATE_URL, json=payload)
332
+ response.raise_for_status()
333
+ result = response.json()
334
+
335
+ if not result.get("success"):
336
+ return {"success": False, "message": result.get("message", "Translation failed")}
337
+
338
+ # Get mono_img (priority) or mono
339
+ pdf_data = result.get("mono_img_pdf_base64") or result.get("mono_pdf_base64")
340
+ if not pdf_data:
341
+ return {"success": False, "message": "No output PDF generated"}
342
+
343
+ # Build output filename
344
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
345
+ if source_name.startswith("Google Drive:"):
346
+ output_filename = f"translated_{timestamp}.{target_lang}.pdf"
347
+ else:
348
+ original_name = Path(source_name).stem
349
+ output_filename = f"{original_name}_translated.{target_lang}.pdf"
350
+
351
+ output_path = OUTPUT_DIR / output_filename
352
+ output_path.write_bytes(base64.b64decode(pdf_data))
353
+
354
+ return {
355
+ "success": True,
356
+ "message": f"Translated to {SUPPORTED_LANGUAGES[target_lang]}",
357
+ "source": source_name,
358
+ "page_count": page_count,
359
+ "output_file": str(output_path),
360
+ "filename": output_filename,
361
+ "stats": result.get("stats", {}),
362
+ }
363
+
364
+ except httpx.TimeoutException:
365
+ return {"success": False, "message": "Translation timed out (max 15 min)"}
366
+ except Exception as e:
367
+ return {"success": False, "message": f"Error: {str(e)}"}
368
+
369
+
370
+ @mcp.tool()
371
+ async def translate_and_upload(source: str, target_lang: str = "fr", folder_id: Optional[str] = None) -> dict:
372
+ """Translate PDF and upload to Google Drive."""
373
+ result = await translate_pdf(source, target_lang)
374
+ if not result.get("success"):
375
+ return result
376
+
377
+ file_id, error = _upload_to_gdrive(result["output_file"], folder_id)
378
+ if error:
379
+ return {"success": False, "message": error, "local_file": result["output_file"]}
380
+
381
+ return {
382
+ "success": True,
383
+ "message": f"Translated and uploaded to Google Drive",
384
+ "source": result.get("source"),
385
+ "page_count": result.get("page_count"),
386
+ "gdrive_id": file_id,
387
+ "gdrive_link": f"https://drive.google.com/file/d/{file_id}/view",
388
+ "local_file": result["output_file"],
389
+ }
390
+
391
+
392
+ @mcp.tool()
393
+ async def check_pdf(source: str) -> dict:
394
+ """Check if PDF can be translated (page count)."""
395
+ await _warmup_modal()
396
+
397
+ try:
398
+ pdf_bytes, source_name, error = await _get_pdf_bytes(source)
399
+ if error:
400
+ return {"success": False, "message": error}
401
+
402
+ page_count = _count_pdf_pages(pdf_bytes)
403
+ can_translate = page_count <= MAX_PAGES
404
+
405
+ return {
406
+ "success": True,
407
+ "source": source_name,
408
+ "pages": page_count,
409
+ "size_mb": round(len(pdf_bytes) / (1024 * 1024), 2),
410
+ "can_translate": can_translate,
411
+ "message": f"Ready ({page_count} pages)" if can_translate else f"Too large ({page_count} > {MAX_PAGES})"
412
+ }
413
+ except Exception as e:
414
+ return {"success": False, "message": f"Error: {str(e)}"}
415
+
416
+
417
+ @mcp.tool()
418
+ async def get_supported_languages() -> dict:
419
+ """Get supported languages."""
420
+ return {"languages": SUPPORTED_LANGUAGES, "default": "fr"}
421
+
422
+
423
+ @mcp.tool()
424
+ async def upload_to_gdrive(file_path: str, folder_id: Optional[str] = None) -> dict:
425
+ """Upload file to Google Drive."""
426
+ if not GOOGLE_AVAILABLE:
427
+ return {"success": False, "message": "Google libraries not installed"}
428
+
429
+ path = Path(file_path)
430
+ if not path.exists():
431
+ return {"success": False, "message": f"File not found: {file_path}"}
432
+
433
+ file_id, error = _upload_to_gdrive(file_path, folder_id)
434
+ if error:
435
+ return {"success": False, "message": error}
436
+
437
+ return {
438
+ "success": True,
439
+ "message": f"Uploaded {path.name}",
440
+ "file_id": file_id,
441
+ "web_link": f"https://drive.google.com/file/d/{file_id}/view",
442
+ }
443
+
444
+
445
+ @mcp.tool()
446
+ async def list_gdrive_folders() -> dict:
447
+ """List Google Drive folders."""
448
+ if not GOOGLE_AVAILABLE:
449
+ return {"success": False, "message": "Google libraries not installed"}
450
+
451
+ folders, error = _list_gdrive_folders()
452
+ if error:
453
+ return {"success": False, "message": error}
454
+
455
+ return {"success": True, "folders": folders, "count": len(folders)}
456
+
457
+
458
+ @mcp.tool()
459
+ async def list_gdrive_files(folder_id: Optional[str] = None, file_type: Optional[str] = None) -> dict:
460
+ """List files in Google Drive."""
461
+ if not GOOGLE_AVAILABLE:
462
+ return {"success": False, "message": "Google libraries not installed"}
463
+
464
+ files, error = _list_gdrive_files(folder_id, file_type)
465
+ if error:
466
+ return {"success": False, "message": error}
467
+
468
+ for f in files:
469
+ if f.get('size'):
470
+ f['size_mb'] = round(int(f['size']) / (1024 * 1024), 2)
471
+
472
+ return {"success": True, "files": files, "count": len(files)}
473
+
474
+
475
+ @mcp.tool()
476
+ async def search_gdrive(query: str, file_type: Optional[str] = None) -> dict:
477
+ """Search Google Drive by filename."""
478
+ if not GOOGLE_AVAILABLE:
479
+ return {"success": False, "message": "Google libraries not installed"}
480
+
481
+ files, error = _search_gdrive_files(query, file_type)
482
+ if error:
483
+ return {"success": False, "message": error}
484
+
485
+ for f in files:
486
+ if f.get('size'):
487
+ f['size_mb'] = round(int(f['size']) / (1024 * 1024), 2)
488
+
489
+ return {"success": True, "query": query, "files": files, "count": len(files)}
490
+
491
+
492
+ @mcp.tool()
493
+ async def download_from_gdrive(file_id: str) -> dict:
494
+ """Download file from Google Drive."""
495
+ if not GOOGLE_AVAILABLE:
496
+ return {"success": False, "message": "Google libraries not installed"}
497
+
498
+ local_path, error = _download_gdrive_file(file_id)
499
+ if error:
500
+ return {"success": False, "message": error}
501
+
502
+ return {
503
+ "success": True,
504
+ "message": f"Downloaded to {local_path}",
505
+ "local_path": local_path,
506
+ "filename": Path(local_path).name,
507
+ }
508
+
509
+
510
+ if __name__ == "__main__":
511
+ mcp.run()