omgy commited on
Commit
fdddc6f
·
verified ·
1 Parent(s): ecf1d8d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -23
app.py CHANGED
@@ -6,13 +6,17 @@ import json
6
  import traceback
7
  import unicodedata
8
  import re
 
9
  from fastapi import FastAPI, File, UploadFile, Form
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from fastapi.responses import JSONResponse, StreamingResponse
 
12
  import fitz # pymupdf
13
  from docx import Document
14
- from docx.shared import Pt, Inches
15
  from docx.enum.text import WD_ALIGN_PARAGRAPH
 
 
16
  import requests
17
 
18
  app = FastAPI(title="Document Enhancer v5")
@@ -214,14 +218,15 @@ def extract_text_and_layout(file_bytes: bytes, kind: str):
214
  def enhance_text_with_gemini(text: str, doc_type: str = "auto", user_prompt: str = "") -> str:
215
  """
216
  Send prompt to Gemini to produce JSON document layout for ANY document type.
 
217
  For very long documents, uses a summarization strategy.
218
  Returns the raw string response (may include fences), caller should clean it.
219
 
220
  doc_type: "auto", "resume", "letter", "report", "article", "essay", etc.
221
  user_prompt: Custom instructions from user
222
  """
223
- if not GEMINI_API_KEY:
224
- return json.dumps({"error": "GEMINI_API_KEY not set"})
225
 
226
  # For very long documents, use a more concise output strategy
227
  is_long_doc = len(text) > 10000
@@ -294,24 +299,61 @@ IMPORTANT:
294
  }
295
  }
296
 
297
- try:
298
- res = requests.post(GEMINI_URL + f"?key={GEMINI_API_KEY}", json=payload, timeout=240)
299
- res.raise_for_status()
300
- resp_json = res.json()
301
- except Exception as e:
302
- return json.dumps({"error": f"gemini request failed: {str(e)}"})
303
-
304
- try:
305
- content = resp_json["candidates"][0]["content"]["parts"][0]["text"]
306
-
307
- # Check if response looks truncated
308
- if not content.rstrip().endswith("}") and not content.rstrip().endswith("]"):
309
- # Try to repair JSON by closing it
310
- content = repair_truncated_json(content)
311
-
312
- return content
313
- except Exception:
314
- return json.dumps({"error": resp_json})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
 
317
  # -------------------------
@@ -642,6 +684,93 @@ async def route_full(
642
  return JSONResponse({"error": str(e)}, status_code=500)
643
 
644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
  # -------------------------
646
  # Root
647
  # -------------------------
@@ -655,6 +784,7 @@ def root():
655
  "endpoints": {
656
  "/extract": "Extract plain text from document",
657
  "/enhance": "Full pipeline: extract + AI enhancement + DOCX (RECOMMENDED)",
 
658
  "/preview": "Preview JSON layout without downloading",
659
  "/design": "Same as /enhance (legacy)",
660
  "/full": "Same as /enhance (legacy)"
@@ -670,6 +800,7 @@ def root():
670
  def health():
671
  return {
672
  "status": "healthy",
673
- "gemini_configured": bool(GEMINI_API_KEY),
674
- "version": "5.0 - Universal with Prompt Support"
 
675
  }
 
6
  import traceback
7
  import unicodedata
8
  import re
9
+ import base64
10
  from fastapi import FastAPI, File, UploadFile, Form
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import JSONResponse, StreamingResponse
13
+ from typing import Optional
14
  import fitz # pymupdf
15
  from docx import Document
16
+ from docx.shared import Pt, Inches, RGBColor
17
  from docx.enum.text import WD_ALIGN_PARAGRAPH
18
+ from docx.oxml.ns import qn
19
+ from PIL import Image
20
  import requests
21
 
22
  app = FastAPI(title="Document Enhancer v5")
 
218
  def enhance_text_with_gemini(text: str, doc_type: str = "auto", user_prompt: str = "") -> str:
219
  """
220
  Send prompt to Gemini to produce JSON document layout for ANY document type.
221
+ Tries multiple API keys if one fails (rate limit, quota exceeded, etc.).
222
  For very long documents, uses a summarization strategy.
223
  Returns the raw string response (may include fences), caller should clean it.
224
 
225
  doc_type: "auto", "resume", "letter", "report", "article", "essay", etc.
226
  user_prompt: Custom instructions from user
227
  """
228
+ if not GEMINI_API_KEYS:
229
+ return json.dumps({"error": "No GEMINI_API_KEY configured"})
230
 
231
  # For very long documents, use a more concise output strategy
232
  is_long_doc = len(text) > 10000
 
299
  }
300
  }
301
 
302
+ # Try each API key in sequence until one works
303
+ last_error = None
304
+ for idx, api_key in enumerate(GEMINI_API_KEYS):
305
+ try:
306
+ print(f"Trying Gemini API key #{idx + 1}/{len(GEMINI_API_KEYS)}...")
307
+
308
+ res = requests.post(
309
+ GEMINI_URL + f"?key={api_key}",
310
+ json=payload,
311
+ timeout=240
312
+ )
313
+
314
+ # Check for rate limit or quota errors
315
+ if res.status_code == 429:
316
+ print(f"API key #{idx + 1} rate limited, trying next...")
317
+ last_error = "Rate limit exceeded"
318
+ continue
319
+
320
+ if res.status_code == 403:
321
+ print(f"API key #{idx + 1} quota exceeded or invalid, trying next...")
322
+ last_error = "Quota exceeded or invalid API key"
323
+ continue
324
+
325
+ res.raise_for_status()
326
+ resp_json = res.json()
327
+
328
+ # Extract content
329
+ content = resp_json["candidates"][0]["content"]["parts"][0]["text"]
330
+
331
+ # Check if response looks truncated
332
+ if not content.rstrip().endswith("}") and not content.rstrip().endswith("]"):
333
+ content = repair_truncated_json(content)
334
+
335
+ print(f"✓ API key #{idx + 1} succeeded!")
336
+ return content
337
+
338
+ except requests.exceptions.Timeout:
339
+ print(f"API key #{idx + 1} timed out, trying next...")
340
+ last_error = "Request timed out"
341
+ continue
342
+
343
+ except requests.exceptions.RequestException as e:
344
+ print(f"API key #{idx + 1} failed: {str(e)}, trying next...")
345
+ last_error = str(e)
346
+ continue
347
+
348
+ except Exception as e:
349
+ print(f"API key #{idx + 1} error: {str(e)}, trying next...")
350
+ last_error = str(e)
351
+ continue
352
+
353
+ # All API keys failed
354
+ return json.dumps({
355
+ "error": f"All {len(GEMINI_API_KEYS)} API keys failed. Last error: {last_error}"
356
+ })
357
 
358
 
359
  # -------------------------
 
684
  return JSONResponse({"error": str(e)}, status_code=500)
685
 
686
 
687
+ @app.post("/add-signature")
688
+ async def route_add_signature(
689
+ file: UploadFile = File(...),
690
+ signature: str = Form(...), # Base64 encoded image
691
+ position: str = Form("bottom-right"), # bottom-right, bottom-center, bottom-left, custom
692
+ signer_name: Optional[str] = Form(None)
693
+ ):
694
+ """
695
+ Add a signature image to an existing DOCX file.
696
+
697
+ Parameters:
698
+ - file: DOCX file to sign
699
+ - signature: Base64 encoded signature image (PNG/JPEG)
700
+ - position: Where to place signature (bottom-right, bottom-center, bottom-left)
701
+ - signer_name: Optional name to display under signature
702
+ """
703
+ try:
704
+ # Read the DOCX file
705
+ docx_bytes = await file.read()
706
+ doc = Document(io.BytesIO(docx_bytes))
707
+
708
+ # Decode signature image from base64
709
+ try:
710
+ # Remove data URI prefix if present (data:image/png;base64,...)
711
+ if ',' in signature:
712
+ signature = signature.split(',', 1)[1]
713
+
714
+ signature_bytes = base64.b64decode(signature)
715
+ signature_image = io.BytesIO(signature_bytes)
716
+
717
+ # Validate it's a valid image
718
+ img = Image.open(signature_image)
719
+ signature_image.seek(0) # Reset stream position
720
+
721
+ except Exception as e:
722
+ return JSONResponse({"error": f"Invalid signature image: {str(e)}"}, status_code=400)
723
+
724
+ # Get the last section (last page)
725
+ last_section = doc.sections[-1]
726
+
727
+ # Add signature at the end of document
728
+ # Add some spacing before signature
729
+ doc.add_paragraph()
730
+
731
+ # Create signature paragraph
732
+ sig_paragraph = doc.add_paragraph()
733
+
734
+ # Set alignment based on position
735
+ if position == "bottom-center":
736
+ sig_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
737
+ elif position == "bottom-left":
738
+ sig_paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
739
+ else: # bottom-right (default)
740
+ sig_paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
741
+
742
+ # Add signature image (resize to reasonable size)
743
+ run = sig_paragraph.add_run()
744
+ run.add_picture(signature_image, width=Inches(1.5))
745
+
746
+ # Add signer name if provided
747
+ if signer_name and signer_name.strip():
748
+ name_paragraph = doc.add_paragraph()
749
+ name_paragraph.alignment = sig_paragraph.alignment
750
+ name_run = name_paragraph.add_run(signer_name.strip())
751
+ name_run.font.size = Pt(10)
752
+ name_run.font.italic = True
753
+
754
+ # Save to bytes
755
+ output = io.BytesIO()
756
+ doc.save(output)
757
+ output.seek(0)
758
+
759
+ safe_filename = sanitize_filename(f"Signed_{file.filename or 'document.docx'}")
760
+
761
+ return StreamingResponse(
762
+ output,
763
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
764
+ headers={
765
+ "Content-Disposition": f'attachment; filename="{safe_filename}"'
766
+ },
767
+ )
768
+
769
+ except Exception as e:
770
+ traceback.print_exc()
771
+ return JSONResponse({"error": f"Failed to add signature: {str(e)}"}, status_code=500)
772
+
773
+
774
  # -------------------------
775
  # Root
776
  # -------------------------
 
784
  "endpoints": {
785
  "/extract": "Extract plain text from document",
786
  "/enhance": "Full pipeline: extract + AI enhancement + DOCX (RECOMMENDED)",
787
+ "/add-signature": "Add signature to existing DOCX file",
788
  "/preview": "Preview JSON layout without downloading",
789
  "/design": "Same as /enhance (legacy)",
790
  "/full": "Same as /enhance (legacy)"
 
800
  def health():
801
  return {
802
  "status": "healthy",
803
+ "api_keys_configured": len(GEMINI_API_KEYS),
804
+ "api_keys_available": GEMINI_API_KEYS[:1] if GEMINI_API_KEYS else [], # Only show first key for security
805
+ "version": "5.0 - Universal with Prompt Support + Multi-API-Key Fallback"
806
  }