Taha493 commited on
Commit
e9ea814
·
verified ·
1 Parent(s): 7b30e61

Create server.py

Browse files
Files changed (1) hide show
  1. server.py +582 -0
server.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BabelDOC FastAPI Server - Production Ready with Real-Time Progress"""
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ import uuid
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+
17
+ from babeldoc.format.pdf.high_level import async_translate, init
18
+ from babeldoc.format.pdf.translation_config import TranslationConfig
19
+ from babeldoc.progress_monitor import ProgressMonitor
20
+ from babeldoc.translator.translator import OpenAITranslator
21
+
22
+ # Configure logging
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
26
+ )
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Suppress verbose logs
30
+ logging.getLogger("httpx").setLevel("CRITICAL")
31
+ logging.getLogger("openai").setLevel("CRITICAL")
32
+
33
+ # Initialize FastAPI app
34
+ app = FastAPI(
35
+ title="TransDOC Translation API",
36
+ description="Intelligent PDF Translation with Layout Preservation",
37
+ version="1.0.0"
38
+ )
39
+
40
+ # Configure CORS
41
+ app.add_middleware(
42
+ CORSMiddleware,
43
+ allow_origins=["*"], # Change in production
44
+ allow_credentials=True,
45
+ allow_methods=["*"],
46
+ allow_headers=["*"],
47
+ )
48
+
49
+ # Serve frontend static files
50
+ try:
51
+ app.mount("/static", StaticFiles(directory="frontend"), name="static")
52
+ except RuntimeError:
53
+ logger.warning("Frontend directory not found, skipping static file serving")
54
+
55
+ # Temporary directory for file processing
56
+ TEMP_DIR = Path(tempfile.gettempdir()) / "babeldoc_api"
57
+ TEMP_DIR.mkdir(exist_ok=True)
58
+
59
+ # Store active translation sessions
60
+ translation_sessions = {}
61
+
62
+ # Language code mapping
63
+ LANGUAGE_CODES = {
64
+ 'en': 'en',
65
+ 'ar': 'en-ar',
66
+ 'es': 'es',
67
+ 'fr': 'fr',
68
+ 'de': 'de',
69
+ 'zh': 'zh',
70
+ 'ja': 'ja',
71
+ 'ko': 'ko',
72
+ 'pt': 'pt',
73
+ 'ru': 'ru',
74
+ 'it': 'it',
75
+ }
76
+
77
+ # Human-readable stage names
78
+ STAGE_DISPLAY_NAMES = {
79
+ 'Create IL': '📄 Parsing PDF Structure',
80
+ 'Detect Scanned File': '🔍 Detecting Scanned Content',
81
+ 'Layout': '📐 Analyzing Page Layout',
82
+ 'Table': '📊 Processing Tables',
83
+ 'Paragraph': '📝 Finding Paragraphs',
84
+ 'Styles And Formulas': '🔢 Processing Formulas & Styles',
85
+ 'AutomaticTermExtractor': '📚 Extracting Terms',
86
+ 'Translate': '🌐 Translating Content',
87
+ 'Typesetting': '✍️ Typesetting Document',
88
+ 'Font Mapper': '🔤 Mapping Fonts',
89
+ 'PDF Creater': '📑 Generating PDF',
90
+ 'Subset Font': '⚙️ Subsetting Fonts',
91
+ 'Save PDF': '💾 Saving Document',
92
+ }
93
+
94
+
95
+ def get_display_stage_name(stage: str) -> str:
96
+ """Convert internal stage name to human-readable display name"""
97
+ # Check direct match first
98
+ if stage in STAGE_DISPLAY_NAMES:
99
+ return STAGE_DISPLAY_NAMES[stage]
100
+
101
+ # Check if stage contains any known key
102
+ for key, value in STAGE_DISPLAY_NAMES.items():
103
+ if key.lower() in stage.lower():
104
+ return value
105
+
106
+ return f"⏳ {stage}"
107
+
108
+
109
+ @app.on_event("startup")
110
+ async def startup_event():
111
+ logger.info("Initializing TransDOC...")
112
+ try:
113
+ init()
114
+ logger.info("TransDOC initialized successfully")
115
+ except Exception as e:
116
+ logger.error(f"Failed to initialize BabelDOC: {e}")
117
+
118
+
119
+ @app.get("/")
120
+ @app.head("/")
121
+ async def root():
122
+ """Serve the frontend HTML"""
123
+ try:
124
+ with open("frontend/index.html", "r", encoding="utf-8") as f:
125
+ return HTMLResponse(content=f.read())
126
+ except FileNotFoundError:
127
+ return JSONResponse({
128
+ "name": "BabelDOC API",
129
+ "version": "1.0.0",
130
+ "status": "running",
131
+ "endpoints": {
132
+ "health": "/health",
133
+ "languages": "/languages",
134
+ "translate": "/translate",
135
+ "translate_stream": "/translate/stream"
136
+ }
137
+ })
138
+
139
+
140
+ @app.get("/health")
141
+ async def health_check():
142
+ """Health check endpoint"""
143
+ return {
144
+ "status": "healthy",
145
+ "service": "babeldoc-api",
146
+ "version": "1.0.0"
147
+ }
148
+
149
+
150
+ @app.get("/languages")
151
+ async def get_supported_languages():
152
+ """Get list of supported languages"""
153
+ return {
154
+ "supported_languages": {
155
+ "en": "English",
156
+ "ar": "Arabic",
157
+ "es": "Spanish",
158
+ "fr": "French",
159
+ "de": "German",
160
+ "zh": "Chinese",
161
+ "ja": "Japanese",
162
+ "ko": "Korean",
163
+ "pt": "Portuguese",
164
+ "ru": "Russian",
165
+ "it": "Italian",
166
+ },
167
+ "count": len(LANGUAGE_CODES)
168
+ }
169
+
170
+
171
+ @app.post("/translate/start")
172
+ async def start_translation(
173
+ file: UploadFile = File(...),
174
+ source_lang: str = Form(...),
175
+ target_lang: str = Form(...),
176
+ model: Optional[str] = Form("gpt-4o-mini"),
177
+ ):
178
+ """
179
+ Start a translation job and return a session ID for progress tracking.
180
+ """
181
+ # Validate file type
182
+ if not file.filename.lower().endswith('.pdf'):
183
+ raise HTTPException(status_code=400, detail="Only PDF files are supported")
184
+
185
+ # Validate languages
186
+ if source_lang not in LANGUAGE_CODES:
187
+ raise HTTPException(status_code=400, detail=f"Unsupported source language: {source_lang}")
188
+
189
+ if target_lang not in LANGUAGE_CODES:
190
+ raise HTTPException(status_code=400, detail=f"Unsupported target language: {target_lang}")
191
+
192
+ if source_lang == target_lang:
193
+ raise HTTPException(status_code=400, detail="Source and target languages must be different")
194
+
195
+ # Create session
196
+ session_id = str(uuid.uuid4())
197
+ session_dir = TEMP_DIR / session_id
198
+ session_dir.mkdir(exist_ok=True)
199
+
200
+ input_path = session_dir / file.filename
201
+ output_directory = session_dir / "output"
202
+ output_directory.mkdir(exist_ok=True)
203
+
204
+ # Save uploaded file
205
+ with open(input_path, "wb") as buffer:
206
+ shutil.copyfileobj(file.file, buffer)
207
+
208
+ # Store session info
209
+ translation_sessions[session_id] = {
210
+ "status": "pending",
211
+ "input_path": str(input_path),
212
+ "output_directory": str(output_directory),
213
+ "source_lang": source_lang,
214
+ "target_lang": target_lang,
215
+ "model": model,
216
+ "filename": file.filename,
217
+ "progress": 0,
218
+ "stage": "Initializing",
219
+ "result": None,
220
+ "error": None,
221
+ }
222
+
223
+ logger.info(f"Translation session created: {session_id}")
224
+
225
+ return {"session_id": session_id}
226
+
227
+
228
+ @app.get("/translate/progress/{session_id}")
229
+ async def get_translation_progress(session_id: str):
230
+ """
231
+ Stream translation progress events using Server-Sent Events (SSE).
232
+ """
233
+ if session_id not in translation_sessions:
234
+ raise HTTPException(status_code=404, detail="Session not found")
235
+
236
+ session = translation_sessions[session_id]
237
+
238
+ async def event_generator():
239
+ """Generate SSE events for translation progress"""
240
+ try:
241
+ # Verify API key
242
+ openai_api_key = os.getenv("OPENAI_API_KEY")
243
+ if not openai_api_key:
244
+ yield f"data: {json.dumps({'type': 'error', 'error': 'OPENAI_API_KEY not configured'})}\n\n"
245
+ return
246
+
247
+ # Update session status
248
+ session["status"] = "processing"
249
+
250
+ # Send initial event
251
+ yield f"data: {json.dumps({'type': 'init', 'message': 'Starting translation...'})}\n\n"
252
+
253
+ # Create translator
254
+ translator = OpenAITranslator(
255
+ lang_in=LANGUAGE_CODES[session["source_lang"]],
256
+ lang_out=LANGUAGE_CODES[session["target_lang"]],
257
+ model=session["model"],
258
+ api_key=openai_api_key,
259
+ ignore_cache=True
260
+ )
261
+
262
+ # Configure translation
263
+ config = TranslationConfig(
264
+ translator=translator,
265
+ input_file=session["input_path"],
266
+ lang_in=LANGUAGE_CODES[session["source_lang"]],
267
+ lang_out=LANGUAGE_CODES[session["target_lang"]],
268
+ output_dir=session["output_directory"],
269
+ doc_layout_model=None,
270
+ pages=None,
271
+ skip_clean=False,
272
+ )
273
+
274
+ # Process translation and stream progress
275
+ translate_result = None
276
+ async for event in async_translate(config):
277
+ event_type = event.get("type", "unknown")
278
+
279
+ if event_type == "stage_summary":
280
+ # Send stage information
281
+ stages = event.get("stages", [])
282
+ stage_info = [
283
+ {
284
+ "name": s["name"],
285
+ "display_name": get_display_stage_name(s["name"]),
286
+ "percent": round(s["percent"] * 100, 1)
287
+ }
288
+ for s in stages
289
+ ]
290
+ yield f"data: {json.dumps({'type': 'stage_summary', 'stages': stage_info})}\n\n"
291
+
292
+ elif event_type == "progress_start":
293
+ stage = event.get("stage", "Unknown")
294
+ display_name = get_display_stage_name(stage)
295
+ session["stage"] = display_name
296
+
297
+ progress_event = {
298
+ "type": "progress_start",
299
+ "stage": stage,
300
+ "display_name": display_name,
301
+ "stage_progress": 0,
302
+ "stage_current": event.get("stage_current", 0),
303
+ "stage_total": event.get("stage_total", 0),
304
+ "overall_progress": event.get("overall_progress", 0),
305
+ "part_index": event.get("part_index", 1),
306
+ "total_parts": event.get("total_parts", 1),
307
+ }
308
+ yield f"data: {json.dumps(progress_event)}\n\n"
309
+
310
+ elif event_type == "progress_update":
311
+ stage = event.get("stage", "Unknown")
312
+ display_name = get_display_stage_name(stage)
313
+ overall_progress = event.get("overall_progress", 0)
314
+
315
+ session["stage"] = display_name
316
+ session["progress"] = overall_progress
317
+
318
+ progress_event = {
319
+ "type": "progress_update",
320
+ "stage": stage,
321
+ "display_name": display_name,
322
+ "stage_progress": round(event.get("stage_progress", 0), 1),
323
+ "stage_current": event.get("stage_current", 0),
324
+ "stage_total": event.get("stage_total", 0),
325
+ "overall_progress": round(overall_progress, 1),
326
+ "part_index": event.get("part_index", 1),
327
+ "total_parts": event.get("total_parts", 1),
328
+ }
329
+ yield f"data: {json.dumps(progress_event)}\n\n"
330
+
331
+ elif event_type == "progress_end":
332
+ stage = event.get("stage", "Unknown")
333
+ display_name = get_display_stage_name(stage)
334
+ overall_progress = event.get("overall_progress", 0)
335
+
336
+ session["progress"] = overall_progress
337
+
338
+ progress_event = {
339
+ "type": "progress_end",
340
+ "stage": stage,
341
+ "display_name": display_name,
342
+ "stage_progress": 100,
343
+ "stage_current": event.get("stage_total", 0),
344
+ "stage_total": event.get("stage_total", 0),
345
+ "overall_progress": round(overall_progress, 1),
346
+ "part_index": event.get("part_index", 1),
347
+ "total_parts": event.get("total_parts", 1),
348
+ }
349
+ yield f"data: {json.dumps(progress_event)}\n\n"
350
+
351
+ elif event_type == "finish":
352
+ translate_result = event.get("translate_result")
353
+ session["status"] = "completed"
354
+ session["progress"] = 100
355
+ session["result"] = translate_result
356
+
357
+ # Find output file
358
+ output_pdf = None
359
+ output_directory = Path(session["output_directory"])
360
+
361
+ if translate_result:
362
+ if hasattr(translate_result, 'mono_pdf_path') and translate_result.mono_pdf_path:
363
+ output_pdf = translate_result.mono_pdf_path
364
+ elif hasattr(translate_result, 'no_watermark_mono_pdf_path') and translate_result.no_watermark_mono_pdf_path:
365
+ output_pdf = translate_result.no_watermark_mono_pdf_path
366
+
367
+ if not output_pdf or not Path(output_pdf).exists():
368
+ pdf_files = list(output_directory.glob("*.pdf"))
369
+ if pdf_files:
370
+ output_pdf = str(pdf_files[0])
371
+
372
+ if output_pdf:
373
+ session["output_path"] = str(output_pdf)
374
+ yield f"data: {json.dumps({'type': 'finish', 'overall_progress': 100, 'message': 'Translation completed!'})}\n\n"
375
+ else:
376
+ yield f"data: {json.dumps({'type': 'error', 'error': 'Translation completed but output file not found'})}\n\n"
377
+
378
+ break
379
+
380
+ elif event_type == "error":
381
+ error_msg = str(event.get("error", "Unknown error"))
382
+ session["status"] = "error"
383
+ session["error"] = error_msg
384
+ yield f"data: {json.dumps({'type': 'error', 'error': error_msg})}\n\n"
385
+ break
386
+
387
+ # Small delay to prevent overwhelming the client
388
+ await asyncio.sleep(0.05)
389
+
390
+ except Exception as e:
391
+ logger.error(f"Translation error: {str(e)}", exc_info=True)
392
+ session["status"] = "error"
393
+ session["error"] = str(e)
394
+ yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
395
+
396
+ return StreamingResponse(
397
+ event_generator(),
398
+ media_type="text/event-stream",
399
+ headers={
400
+ "Cache-Control": "no-cache",
401
+ "Connection": "keep-alive",
402
+ "X-Accel-Buffering": "no",
403
+ }
404
+ )
405
+
406
+
407
+ @app.get("/translate/download/{session_id}")
408
+ async def download_translation(session_id: str):
409
+ """
410
+ Download the translated PDF file.
411
+ """
412
+ if session_id not in translation_sessions:
413
+ raise HTTPException(status_code=404, detail="Session not found")
414
+
415
+ session = translation_sessions[session_id]
416
+
417
+ if session["status"] != "completed":
418
+ raise HTTPException(status_code=400, detail="Translation not completed yet")
419
+
420
+ output_path = session.get("output_path")
421
+ if not output_path or not Path(output_path).exists():
422
+ raise HTTPException(status_code=404, detail="Output file not found")
423
+
424
+ output_filename = f"translated_{session['filename']}"
425
+
426
+ return FileResponse(
427
+ path=output_path,
428
+ filename=output_filename,
429
+ media_type="application/pdf",
430
+ headers={
431
+ "Content-Disposition": f"attachment; filename={output_filename}"
432
+ }
433
+ )
434
+
435
+
436
+ @app.post("/translate")
437
+ async def translate_document(
438
+ file: UploadFile = File(...),
439
+ source_lang: str = Form(...),
440
+ target_lang: str = Form(...),
441
+ model: Optional[str] = Form("gpt-4o-mini"),
442
+ ):
443
+ """
444
+ Translate a PDF document (non-streaming, for backwards compatibility).
445
+ """
446
+ # Validate file type
447
+ if not file.filename.lower().endswith('.pdf'):
448
+ raise HTTPException(status_code=400, detail="Only PDF files are supported")
449
+
450
+ # Validate languages
451
+ if source_lang not in LANGUAGE_CODES:
452
+ raise HTTPException(status_code=400, detail=f"Unsupported source language: {source_lang}")
453
+
454
+ if target_lang not in LANGUAGE_CODES:
455
+ raise HTTPException(status_code=400, detail=f"Unsupported target language: {target_lang}")
456
+
457
+ if source_lang == target_lang:
458
+ raise HTTPException(status_code=400, detail="Source and target languages must be different")
459
+
460
+ # Create session directory
461
+ session_id = f"session_{os.urandom(8).hex()}"
462
+ session_dir = TEMP_DIR / session_id
463
+ session_dir.mkdir(exist_ok=True)
464
+
465
+ input_path = session_dir / file.filename
466
+ output_directory = session_dir / "output"
467
+ output_directory.mkdir(exist_ok=True)
468
+
469
+ try:
470
+ # Save uploaded file
471
+ logger.info(f"Processing translation: {file.filename}")
472
+ logger.info(f"Language pair: {source_lang} -> {target_lang}")
473
+ logger.info(f"Model: {model}")
474
+
475
+ with open(input_path, "wb") as buffer:
476
+ shutil.copyfileobj(file.file, buffer)
477
+
478
+ # Verify API key
479
+ openai_api_key = os.getenv("OPENAI_API_KEY")
480
+ if not openai_api_key:
481
+ raise HTTPException(status_code=500, detail="OPENAI_API_KEY not configured on server")
482
+
483
+ # Create translator
484
+ translator = OpenAITranslator(
485
+ lang_in=LANGUAGE_CODES[source_lang],
486
+ lang_out=LANGUAGE_CODES[target_lang],
487
+ model=model,
488
+ api_key=openai_api_key,
489
+ ignore_cache=True
490
+ )
491
+
492
+ # Configure translation
493
+ config = TranslationConfig(
494
+ translator=translator,
495
+ input_file=str(input_path),
496
+ lang_in=LANGUAGE_CODES[source_lang],
497
+ lang_out=LANGUAGE_CODES[target_lang],
498
+ output_dir=str(output_directory),
499
+ doc_layout_model=None,
500
+ pages=None,
501
+ skip_clean=False,
502
+ )
503
+
504
+ # Perform translation
505
+ logger.info("Starting translation process...")
506
+
507
+ translate_result = None
508
+ async for event in async_translate(config):
509
+ if event["type"] == "progress_update":
510
+ logger.debug(
511
+ f"Progress: {event['stage']} - "
512
+ f"{event['stage_current']}/{event['stage_total']} "
513
+ f"(Overall: {event['overall_progress']}%)"
514
+ )
515
+ elif event["type"] == "finish":
516
+ translate_result = event["translate_result"]
517
+ logger.info("Translation completed successfully")
518
+ break
519
+ elif event["type"] == "error":
520
+ error_msg = event.get("error", "Unknown error")
521
+ logger.error(f"Translation error: {error_msg}")
522
+ raise HTTPException(status_code=500, detail=f"Translation failed: {error_msg}")
523
+
524
+ if translate_result is None:
525
+ raise HTTPException(status_code=500, detail="Translation completed but no result returned")
526
+
527
+ # Find the output PDF
528
+ output_pdf = None
529
+
530
+ if hasattr(translate_result, 'mono_pdf_path') and translate_result.mono_pdf_path:
531
+ output_pdf = translate_result.mono_pdf_path
532
+ elif hasattr(translate_result, 'no_watermark_mono_pdf_path') and translate_result.no_watermark_mono_pdf_path:
533
+ output_pdf = translate_result.no_watermark_mono_pdf_path
534
+
535
+ if not output_pdf or not Path(output_pdf).exists():
536
+ pdf_files = list(output_directory.glob("*.pdf"))
537
+ if pdf_files:
538
+ output_pdf = pdf_files[0]
539
+
540
+ if not output_pdf:
541
+ raise HTTPException(status_code=500, detail="Translation completed but output file not found")
542
+
543
+ if isinstance(output_pdf, str):
544
+ output_pdf = Path(output_pdf)
545
+
546
+ if not output_pdf.exists():
547
+ raise HTTPException(status_code=500, detail=f"Translation completed but output file does not exist")
548
+
549
+ logger.info(f"Translation successful: {output_pdf}")
550
+
551
+ output_filename = f"translated_{file.filename}"
552
+
553
+ return FileResponse(
554
+ path=str(output_pdf),
555
+ filename=output_filename,
556
+ media_type="application/pdf",
557
+ headers={
558
+ "Content-Disposition": f"attachment; filename={output_filename}"
559
+ }
560
+ )
561
+
562
+ except HTTPException:
563
+ raise
564
+ except Exception as e:
565
+ logger.error(f"Translation error: {str(e)}", exc_info=True)
566
+ raise HTTPException(status_code=500, detail=f"Translation failed: {str(e)}")
567
+
568
+
569
+ if __name__ == "__main__":
570
+ import uvicorn
571
+
572
+ port = int(os.getenv("PORT", 7860))
573
+
574
+ logger.info(f"Starting TransDOC API server on port {port}")
575
+
576
+ uvicorn.run(
577
+ "server:app",
578
+ host="0.0.0.0",
579
+ port=port,
580
+ log_level="info",
581
+ reload=False
582
+ )