cstr commited on
Commit
1715de1
Β·
verified Β·
1 Parent(s): ae95fcb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +381 -0
app.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gradio interface for Ultimate Document Translator
4
+ Designed for Hugging Face Spaces deployment
5
+ """
6
+
7
+ import gradio as gr
8
+ import asyncio
9
+ import os
10
+ import sys
11
+ import logging
12
+ import subprocess
13
+ import shutil
14
+ from pathlib import Path
15
+ from typing import Optional, Tuple
16
+ import tempfile
17
+
18
+ # Setup logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(levelname)s - %(message)s'
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Import from translator.py
26
+ from translator import (
27
+ UltimateDocumentTranslator,
28
+ TranslationMode,
29
+ TranslationBackend,
30
+ AlignerBackend
31
+ )
32
+
33
+ # ============================================================================
34
+ # ENVIRONMENT SETUP
35
+ # ============================================================================
36
+
37
+ def check_and_setup_environment():
38
+ """
39
+ Verify and setup required tools for Hugging Face Spaces.
40
+ Returns status messages.
41
+ """
42
+ status_messages = []
43
+
44
+ # 1. Check CTranslate2
45
+ try:
46
+ import ctranslate2
47
+ status_messages.append("βœ“ CTranslate2 installed")
48
+ except ImportError:
49
+ status_messages.append("⚠ CTranslate2 not found - installing...")
50
+ try:
51
+ subprocess.run([sys.executable, "-m", "pip", "install", "ctranslate2"], check=True)
52
+ status_messages.append("βœ“ CTranslate2 installed successfully")
53
+ except Exception as e:
54
+ status_messages.append(f"βœ— CTranslate2 installation failed: {e}")
55
+
56
+ # 2. Check fast_align (optional, complex to build on HF Spaces)
57
+ fast_align_path = shutil.which("fast_align")
58
+ if fast_align_path:
59
+ status_messages.append(f"βœ“ fast_align found at {fast_align_path}")
60
+ else:
61
+ status_messages.append("β„Ή fast_align not available (optional - will use other aligners)")
62
+ # Note: Building fast_align on HF Spaces is challenging due to build dependencies
63
+ # We'll rely on the Python-based aligners instead
64
+
65
+ # 3. Check for API keys (optional)
66
+ if os.getenv("OPENAI_API_KEY"):
67
+ status_messages.append("βœ“ OpenAI API key detected")
68
+ if os.getenv("ANTHROPIC_API_KEY"):
69
+ status_messages.append("βœ“ Anthropic API key detected")
70
+ if not os.getenv("OPENAI_API_KEY") and not os.getenv("ANTHROPIC_API_KEY"):
71
+ status_messages.append("β„Ή No LLM API keys found (LLM modes will be unavailable)")
72
+
73
+ return "\n".join(status_messages)
74
+
75
+ # Run setup on startup
76
+ SETUP_STATUS = check_and_setup_environment()
77
+ logger.info(f"Setup complete:\n{SETUP_STATUS}")
78
+
79
+ # ============================================================================
80
+ # TRANSLATION FUNCTION
81
+ # ============================================================================
82
+
83
+ async def translate_document_async(
84
+ input_file,
85
+ source_lang: str,
86
+ target_lang: str,
87
+ mode: str,
88
+ nmt_backend: str,
89
+ nllb_size: str,
90
+ aligner: str,
91
+ llm_provider: Optional[str],
92
+ progress=gr.Progress()
93
+ ) -> Tuple[Optional[str], str]:
94
+ """
95
+ Asynchronous document translation with progress tracking.
96
+
97
+ Returns:
98
+ Tuple of (output_file_path, log_messages)
99
+ """
100
+
101
+ if input_file is None:
102
+ return None, "❌ Error: No file uploaded"
103
+
104
+ # Create temp directory for processing
105
+ temp_dir = Path(tempfile.mkdtemp())
106
+
107
+ try:
108
+ # Setup paths
109
+ input_path = Path(input_file.name)
110
+ output_filename = f"{input_path.stem}_translated_{source_lang}_{target_lang}.docx"
111
+ output_path = temp_dir / output_filename
112
+
113
+ # Validate file type
114
+ if not input_path.suffix.lower() == '.docx':
115
+ return None, "❌ Error: Only .docx files are supported"
116
+
117
+ # Map UI selections to enums
118
+ mode_map = {
119
+ 'NMT Only': TranslationMode.NMT_ONLY,
120
+ 'LLM with Alignment': TranslationMode.LLM_WITH_ALIGN,
121
+ 'LLM without Alignment': TranslationMode.LLM_WITHOUT_ALIGN,
122
+ 'Hybrid (Recommended)': TranslationMode.HYBRID
123
+ }
124
+
125
+ # Setup logging capture
126
+ log_messages = []
127
+
128
+ class LogCapture(logging.Handler):
129
+ def emit(self, record):
130
+ log_messages.append(self.format(record))
131
+
132
+ log_handler = LogCapture()
133
+ log_handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
134
+ logging.getLogger().addHandler(log_handler)
135
+
136
+ progress(0.1, desc="Initializing translator...")
137
+
138
+ # Initialize translator
139
+ translator = UltimateDocumentTranslator(
140
+ src_lang=source_lang,
141
+ tgt_lang=target_lang,
142
+ mode=mode_map[mode],
143
+ nmt_backend=nmt_backend.lower() if nmt_backend != "Auto" else "auto",
144
+ llm_provider=llm_provider.lower() if llm_provider and llm_provider != "None" else None,
145
+ aligner=aligner.lower() if aligner != "Auto" else "auto",
146
+ nllb_model_size=nllb_size
147
+ )
148
+
149
+ progress(0.2, desc="Processing document...")
150
+
151
+ # Translate
152
+ await translator.translate_document(input_path, output_path)
153
+
154
+ progress(1.0, desc="Translation complete!")
155
+
156
+ # Cleanup log handler
157
+ logging.getLogger().removeHandler(log_handler)
158
+
159
+ # Format log output
160
+ log_output = "\n".join(log_messages[-50:]) # Last 50 messages
161
+ success_msg = f"""
162
+ βœ… Translation Complete!
163
+
164
+ πŸ“„ Input: {input_path.name}
165
+ πŸ“„ Output: {output_filename}
166
+ 🌍 Direction: {source_lang.upper()} β†’ {target_lang.upper()}
167
+ βš™οΈ Mode: {mode}
168
+ πŸ”§ Backend: {nmt_backend}
169
+
170
+ Recent Logs:
171
+ {log_output}
172
+ """
173
+
174
+ return str(output_path), success_msg
175
+
176
+ except Exception as e:
177
+ error_msg = f"❌ Translation Error:\n{str(e)}\n\nPlease check your settings and try again."
178
+ logger.error(f"Translation failed: {e}", exc_info=True)
179
+ return None, error_msg
180
+
181
+ def translate_document_sync(*args, **kwargs):
182
+ """Synchronous wrapper for Gradio"""
183
+ return asyncio.run(translate_document_async(*args, **kwargs))
184
+
185
+ # ============================================================================
186
+ # GRADIO INTERFACE
187
+ # ============================================================================
188
+
189
+ def create_interface():
190
+ """Create the Gradio interface"""
191
+
192
+ # Language options (common pairs)
193
+ languages = {
194
+ "English": "en",
195
+ "German": "de",
196
+ "French": "fr",
197
+ "Spanish": "es",
198
+ "Italian": "it",
199
+ "Portuguese": "pt",
200
+ "Russian": "ru",
201
+ "Chinese": "zh",
202
+ "Japanese": "ja",
203
+ "Korean": "ko",
204
+ "Arabic": "ar",
205
+ "Hindi": "hi",
206
+ "Dutch": "nl",
207
+ "Polish": "pl",
208
+ "Turkish": "tr",
209
+ "Czech": "cs",
210
+ "Ukrainian": "uk",
211
+ "Vietnamese": "vi"
212
+ }
213
+
214
+ with gr.Blocks(title="Document Translator", theme=gr.themes.Soft()) as demo:
215
+ gr.Markdown("""
216
+ # 🌍 Ultimate Document Translator
217
+
218
+ Translate Word documents while preserving formatting, footnotes, and styling.
219
+
220
+ **Features:**
221
+ - Multiple neural translation backends (NLLB, Madlad, Opus-MT, WMT21)
222
+ - Word-level alignment for format preservation
223
+ - Support for footnotes, tables, headers/footers
224
+ - Optional LLM enhancement (OpenAI/Anthropic)
225
+
226
+ **Recommended Settings:**
227
+ - Mode: Hybrid (best quality)
228
+ - Backend: NLLB (fastest, good quality)
229
+ - Size: 600M (good balance)
230
+ """)
231
+
232
+ gr.Markdown(f"### System Status\n```\n{SETUP_STATUS}\n```")
233
+
234
+ with gr.Row():
235
+ with gr.Column(scale=1):
236
+ gr.Markdown("### πŸ“€ Input")
237
+
238
+ input_file = gr.File(
239
+ label="Upload Document (.docx)",
240
+ file_types=[".docx"],
241
+ type="filepath"
242
+ )
243
+
244
+ with gr.Row():
245
+ source_lang = gr.Dropdown(
246
+ choices=list(languages.keys()),
247
+ value="English",
248
+ label="Source Language"
249
+ )
250
+ target_lang = gr.Dropdown(
251
+ choices=list(languages.keys()),
252
+ value="German",
253
+ label="Target Language"
254
+ )
255
+
256
+ gr.Markdown("### βš™οΈ Settings")
257
+
258
+ mode = gr.Dropdown(
259
+ choices=[
260
+ "Hybrid (Recommended)",
261
+ "NMT Only",
262
+ "LLM with Alignment",
263
+ "LLM without Alignment"
264
+ ],
265
+ value="Hybrid (Recommended)",
266
+ label="Translation Mode",
267
+ info="Hybrid uses NMT with optional LLM enhancement"
268
+ )
269
+
270
+ nmt_backend = gr.Dropdown(
271
+ choices=["NLLB", "Madlad", "Opus", "CT2", "Auto"],
272
+ value="NLLB",
273
+ label="NMT Backend",
274
+ info="NLLB: Fast & balanced | Madlad: Academic | Opus: Specialized pairs"
275
+ )
276
+
277
+ nllb_size = gr.Dropdown(
278
+ choices=["600M", "1.3B", "3.3B"],
279
+ value="600M",
280
+ label="NLLB Model Size",
281
+ info="600M recommended for Hugging Face Spaces (limited RAM)"
282
+ )
283
+
284
+ aligner = gr.Dropdown(
285
+ choices=["Auto", "Awesome", "SimAlign", "Lindat", "Heuristic"],
286
+ value="Auto",
287
+ label="Word Aligner",
288
+ info="Auto will select best available aligner"
289
+ )
290
+
291
+ llm_provider = gr.Dropdown(
292
+ choices=["None", "OpenAI", "Anthropic", "Ollama"],
293
+ value="None",
294
+ label="LLM Provider (Optional)",
295
+ info="Requires API key in environment variables"
296
+ )
297
+
298
+ translate_btn = gr.Button("πŸš€ Translate Document", variant="primary", size="lg")
299
+
300
+ with gr.Column(scale=1):
301
+ gr.Markdown("### πŸ“₯ Output")
302
+
303
+ output_file = gr.File(
304
+ label="Translated Document",
305
+ interactive=False
306
+ )
307
+
308
+ log_output = gr.Textbox(
309
+ label="Translation Log",
310
+ lines=20,
311
+ max_lines=30,
312
+ interactive=False
313
+ )
314
+
315
+ gr.Markdown("""
316
+ ### πŸ“– Tips
317
+
318
+ - **For best quality**: Use "Hybrid" mode with NLLB backend
319
+ - **For speed**: Use "NMT Only" with NLLB 600M
320
+ - **For academic texts**: Try Madlad backend
321
+ - **For specific language pairs**: Opus-MT (if available)
322
+ - **LLM modes**: Require API keys set as environment variables
323
+
324
+ ### ⚠️ Limitations
325
+
326
+ - Only .docx format supported (not .doc)
327
+ - Large documents may take several minutes
328
+ - Complex formatting may require manual review
329
+ - LLM modes are slower and require API access
330
+ """)
331
+
332
+ # Wire up the translation
333
+ translate_btn.click(
334
+ fn=lambda *args: translate_document_sync(*args),
335
+ inputs=[
336
+ input_file,
337
+ gr.State(lambda x: languages.get(x, "en"), inputs=[source_lang]),
338
+ gr.State(lambda x: languages.get(x, "de"), inputs=[target_lang]),
339
+ mode,
340
+ nmt_backend,
341
+ nllb_size,
342
+ aligner,
343
+ llm_provider
344
+ ],
345
+ outputs=[output_file, log_output]
346
+ )
347
+
348
+ # Fix the actual event handler
349
+ def handle_translate(input_f, src_lang_name, tgt_lang_name, *rest):
350
+ src_code = languages.get(src_lang_name, "en")
351
+ tgt_code = languages.get(tgt_lang_name, "de")
352
+ return translate_document_sync(input_f, src_code, tgt_code, *rest)
353
+
354
+ translate_btn.click(
355
+ fn=handle_translate,
356
+ inputs=[
357
+ input_file,
358
+ source_lang,
359
+ target_lang,
360
+ mode,
361
+ nmt_backend,
362
+ nllb_size,
363
+ aligner,
364
+ llm_provider
365
+ ],
366
+ outputs=[output_file, log_output]
367
+ )
368
+
369
+ return demo
370
+
371
+ # ============================================================================
372
+ # MAIN
373
+ # ============================================================================
374
+
375
+ if __name__ == "__main__":
376
+ demo = create_interface()
377
+ demo.launch(
378
+ server_name="0.0.0.0",
379
+ server_port=7860,
380
+ share=False
381
+ )