Hritam-Ai commited on
Commit
87ceafd
·
1 Parent(s): bdcfa36

Add PDF parsing app with SmolDocling and Anthropic

Browse files
Files changed (5) hide show
  1. Dockerfile +41 -0
  2. README.md +0 -12
  3. app.py +573 -0
  4. config.py +38 -0
  5. requirements.txt +18 -0
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ software-properties-common \
10
+ git \
11
+ poppler-utils \
12
+ libpoppler-cpp-dev \
13
+ tesseract-ocr \
14
+ tesseract-ocr-eng \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Copy requirements first for better caching
18
+ COPY requirements.txt ./
19
+ RUN pip3 install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy application code
22
+ COPY . .
23
+
24
+ # Create necessary directories with proper permissions
25
+ RUN mkdir -p /tmp/uploads /tmp/processed /app/.cache/huggingface
26
+ RUN chmod -R 777 /tmp /app/.cache
27
+
28
+ # Set environment variables for caching and permissions
29
+ ENV TRANSFORMERS_CACHE=/app/.cache/huggingface
30
+ ENV HF_HOME=/app/.cache/huggingface
31
+ ENV TORCH_HOME=/app/.cache/torch
32
+ ENV HF_DATASETS_CACHE=/app/.cache/huggingface/datasets
33
+
34
+ # Expose FastAPI port
35
+ EXPOSE 7860
36
+
37
+ # Health check for FastAPI
38
+ HEALTHCHECK CMD curl --fail http://localhost:7860/health || exit 1
39
+
40
+ # Run FastAPI application
41
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md DELETED
@@ -1,12 +0,0 @@
1
- ---
2
- title: AIPdfParser
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: pdf parsing using smoldocling
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import asyncio
4
+ from typing import List, Optional
5
+ from pathlib import Path
6
+ import logging
7
+ from io import BytesIO
8
+ import base64
9
+
10
+ from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
11
+ from fastapi.responses import HTMLResponse, JSONResponse
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+ import uvicorn
14
+
15
+ # PDF and image processing
16
+ import PyPDF2
17
+ from pdf2image import convert_from_path, convert_from_bytes
18
+ from PIL import Image
19
+
20
+ # ML and AI
21
+ import torch
22
+ from transformers import AutoProcessor, AutoModelForVision2Seq
23
+
24
+ # Try importing anthropic with error handling
25
+ try:
26
+ import anthropic
27
+ ANTHROPIC_AVAILABLE = True
28
+ except ImportError as e:
29
+ print(f"Warning: Anthropic library not available: {str(e)}")
30
+ ANTHROPIC_AVAILABLE = False
31
+
32
+ # Try importing docling with fallback
33
+ try:
34
+ from docling_core.types.doc import DoclingDocument
35
+ from docling_core.types.doc.document import DocTagsDocument
36
+ DOCLING_AVAILABLE = True
37
+ except ImportError:
38
+ print("Warning: docling_core not available. Using fallback markdown generation.")
39
+ DOCLING_AVAILABLE = False
40
+
41
+ # Environment and configuration
42
+ from dotenv import load_dotenv
43
+ import aiofiles
44
+ from config import config
45
+
46
+ # Load environment variables
47
+ load_dotenv()
48
+
49
+ # Logging setup
50
+ logging.basicConfig(level=logging.INFO)
51
+ logger = logging.getLogger(__name__)
52
+
53
+ app = FastAPI(
54
+ title="PDF Parsing with SmolDocling",
55
+ description="Extract text from PDFs using SmolDocling model and summarize with Anthropic API",
56
+ version="1.0.0"
57
+ )
58
+
59
+ # Add CORS middleware
60
+ app.add_middleware(
61
+ CORSMiddleware,
62
+ allow_origins=["*"],
63
+ allow_credentials=True,
64
+ allow_methods=["*"],
65
+ allow_headers=["*"],
66
+ )
67
+
68
+ # Global variables for model
69
+ processor = None
70
+ model = None
71
+ anthropic_client = None
72
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
73
+
74
+ class PDFProcessor:
75
+ def __init__(self):
76
+ self.max_pages_per_chunk = config.MAX_PAGES_PER_CHUNK
77
+
78
+ async def pdf_to_images(self, pdf_bytes: bytes) -> List[Image.Image]:
79
+ """Convert PDF bytes to list of PIL Images"""
80
+ try:
81
+ # Convert PDF to images
82
+ images = convert_from_bytes(
83
+ pdf_bytes,
84
+ dpi=config.PDF_DPI,
85
+ fmt='RGB'
86
+ )
87
+ logger.info(f"Converted PDF to {len(images)} images")
88
+ return images
89
+ except Exception as e:
90
+ logger.error(f"Error converting PDF to images: {str(e)}")
91
+ raise HTTPException(status_code=400, detail=f"Error converting PDF: {str(e)}")
92
+
93
+ def chunk_images(self, images: List[Image.Image]) -> List[List[Image.Image]]:
94
+ """Chunk images into smaller groups for processing"""
95
+ chunks = []
96
+ for i in range(0, len(images), self.max_pages_per_chunk):
97
+ chunk = images[i:i + self.max_pages_per_chunk]
98
+ chunks.append(chunk)
99
+ logger.info(f"Created {len(chunks)} chunks from {len(images)} images")
100
+ return chunks
101
+
102
+ async def process_image_with_smoldocling(self, image: Image.Image) -> str:
103
+ """Process single image with SmolDocling model"""
104
+ global processor, model
105
+
106
+ if processor is None or model is None:
107
+ logger.warning("SmolDocling model not available, using basic OCR fallback")
108
+ return await self._basic_ocr_fallback(image)
109
+
110
+ try:
111
+ # Prepare the input messages
112
+ messages = [
113
+ {
114
+ "role": "user",
115
+ "content": [
116
+ {"type": "image"},
117
+ {"type": "text", "text": "Convert this page to docling."}
118
+ ]
119
+ },
120
+ ]
121
+
122
+ # Process with the model
123
+ prompt = processor.apply_chat_template(messages, add_generation_prompt=True)
124
+ inputs = processor(text=prompt, images=[image], return_tensors="pt")
125
+ inputs = inputs.to(DEVICE)
126
+
127
+ # Generate output
128
+ with torch.no_grad():
129
+ generated_ids = model.generate(**inputs, max_new_tokens=config.MAX_NEW_TOKENS)
130
+ prompt_length = inputs.input_ids.shape[1]
131
+ trimmed_generated_ids = generated_ids[:, prompt_length:]
132
+ doctags = processor.batch_decode(
133
+ trimmed_generated_ids,
134
+ skip_special_tokens=False,
135
+ )[0].lstrip()
136
+
137
+ # Convert to markdown using docling if available, otherwise return raw doctags
138
+ if DOCLING_AVAILABLE:
139
+ try:
140
+ doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([doctags], [image])
141
+ doc = DoclingDocument.load_from_doctags(doctags_doc, document_name="Page")
142
+ markdown_content = doc.export_to_markdown()
143
+ return markdown_content
144
+ except Exception as e:
145
+ logger.warning(f"Docling conversion failed: {str(e)}, using raw doctags")
146
+ return self._convert_doctags_to_markdown(doctags)
147
+ else:
148
+ # Fallback: convert doctags to basic markdown
149
+ return self._convert_doctags_to_markdown(doctags)
150
+
151
+ except Exception as e:
152
+ logger.error(f"Error processing image with SmolDocling: {str(e)}")
153
+ return f"Error processing page: {str(e)}"
154
+
155
+ async def process_pdf_chunk(self, images: List[Image.Image]) -> str:
156
+ """Process a chunk of images"""
157
+ markdown_parts = []
158
+
159
+ for i, image in enumerate(images):
160
+ try:
161
+ logger.info(f"Processing image {i+1}/{len(images)} in chunk")
162
+ markdown = await self.process_image_with_smoldocling(image)
163
+ markdown_parts.append(f"## Page {i+1}\n\n{markdown}\n\n")
164
+ except Exception as e:
165
+ logger.error(f"Error processing image {i+1}: {str(e)}")
166
+ markdown_parts.append(f"## Page {i+1}\n\nError processing this page: {str(e)}\n\n")
167
+
168
+ return "".join(markdown_parts)
169
+
170
+ def _convert_doctags_to_markdown(self, doctags: str) -> str:
171
+ """Fallback method to convert doctags to basic markdown when docling_core is not available"""
172
+ try:
173
+ # Simple conversion of common doctags to markdown
174
+ lines = doctags.split('\n')
175
+ markdown_lines = []
176
+
177
+ for line in lines:
178
+ line = line.strip()
179
+ if not line:
180
+ markdown_lines.append('')
181
+ continue
182
+
183
+ # Convert common doctags to markdown
184
+ if line.startswith('<title>') and line.endswith('</title>'):
185
+ title = line.replace('<title>', '').replace('</title>', '')
186
+ markdown_lines.append(f'# {title}')
187
+ elif line.startswith('<heading>') and line.endswith('</heading>'):
188
+ heading = line.replace('<heading>', '').replace('</heading>', '')
189
+ markdown_lines.append(f'## {heading}')
190
+ elif line.startswith('<text>') and line.endswith('</text>'):
191
+ text = line.replace('<text>', '').replace('</text>', '')
192
+ markdown_lines.append(text)
193
+ elif line.startswith('<list>') and line.endswith('</list>'):
194
+ item = line.replace('<list>', '').replace('</list>', '')
195
+ markdown_lines.append(f'- {item}')
196
+ elif line.startswith('<table>') and line.endswith('</table>'):
197
+ table = line.replace('<table>', '').replace('</table>', '')
198
+ markdown_lines.append(f'| {table} |')
199
+ elif line.startswith('<formula>') and line.endswith('</formula>'):
200
+ formula = line.replace('<formula>', '').replace('</formula>', '')
201
+ markdown_lines.append(f'$$\n{formula}\n$$')
202
+ elif line.startswith('<code>') and line.endswith('</code>'):
203
+ code = line.replace('<code>', '').replace('</code>', '')
204
+ markdown_lines.append(f'```\n{code}\n```')
205
+ else:
206
+ # Remove any remaining tags and add as text
207
+ import re
208
+ clean_text = re.sub(r'<[^>]+>', '', line)
209
+ if clean_text.strip():
210
+ markdown_lines.append(clean_text)
211
+
212
+ return '\n'.join(markdown_lines)
213
+
214
+ except Exception as e:
215
+ logger.error(f"Error converting doctags to markdown: {str(e)}")
216
+ return f"**Raw DocTags Output:**\n\n```\n{doctags}\n```"
217
+
218
+ async def _basic_ocr_fallback(self, image: Image.Image) -> str:
219
+ """Basic OCR fallback when SmolDocling is not available"""
220
+ try:
221
+ # Try to use pytesseract if available
222
+ try:
223
+ import pytesseract
224
+ text = pytesseract.image_to_string(image)
225
+ return f"# Extracted Text (Basic OCR)\n\n{text}"
226
+ except ImportError:
227
+ pass
228
+
229
+ # If pytesseract is not available, return a placeholder
230
+ return f"""# PDF Page Processed
231
+
232
+ **Note**: SmolDocling model is not available. This page contains content that would normally be extracted using advanced OCR.
233
+
234
+ To get full text extraction capabilities, please:
235
+ 1. Ensure the SmolDocling model loads correctly
236
+ 2. Check that all dependencies are installed
237
+ 3. Try using a GPU-enabled environment for better performance
238
+
239
+ Image dimensions: {image.size[0]} x {image.size[1]} pixels
240
+ """
241
+
242
+ except Exception as e:
243
+ logger.error(f"Basic OCR fallback failed: {str(e)}")
244
+ return f"# Error Processing Page\n\nFailed to process this page: {str(e)}"
245
+
246
+ class SummaryGenerator:
247
+ def __init__(self, api_key: str):
248
+ if not ANTHROPIC_AVAILABLE:
249
+ raise ImportError("Anthropic library is not available")
250
+
251
+ try:
252
+ # Initialize Anthropic client with explicit parameters
253
+ self.client = anthropic.Anthropic(
254
+ api_key=api_key
255
+ )
256
+ logger.info("Anthropic client created successfully")
257
+ except Exception as e:
258
+ logger.error(f"Failed to initialize Anthropic client: {str(e)}")
259
+ raise e
260
+
261
+ async def summarize_text(self, text: str) -> str:
262
+ """Generate summary using Anthropic Claude API"""
263
+ try:
264
+ # If text is too long, chunk it
265
+ max_tokens = config.MAX_TOKENS_PER_CHUNK * 2 # Claude's context window
266
+ if len(text.split()) > max_tokens:
267
+ # Split text into chunks and summarize each, then combine
268
+ chunks = self._chunk_text(text, config.MAX_TOKENS_PER_CHUNK)
269
+ chunk_summaries = []
270
+
271
+ for i, chunk in enumerate(chunks):
272
+ logger.info(f"Summarizing chunk {i+1}/{len(chunks)}")
273
+ summary = await self._summarize_chunk(chunk)
274
+ chunk_summaries.append(summary)
275
+
276
+ # Combine chunk summaries into final summary
277
+ combined_text = "\n\n".join(chunk_summaries)
278
+ final_summary = await self._summarize_chunk(combined_text, is_final=True)
279
+ return final_summary
280
+ else:
281
+ return await self._summarize_chunk(text)
282
+
283
+ except Exception as e:
284
+ logger.error(f"Error generating summary: {str(e)}")
285
+ raise HTTPException(status_code=500, detail=f"Error generating summary: {str(e)}")
286
+
287
+ def _chunk_text(self, text: str, max_tokens: int) -> List[str]:
288
+ """Split text into chunks"""
289
+ words = text.split()
290
+ chunks = []
291
+ current_chunk = []
292
+
293
+ for word in words:
294
+ current_chunk.append(word)
295
+ if len(current_chunk) >= max_tokens:
296
+ chunks.append(" ".join(current_chunk))
297
+ current_chunk = []
298
+
299
+ if current_chunk:
300
+ chunks.append(" ".join(current_chunk))
301
+
302
+ return chunks
303
+
304
+ async def _summarize_chunk(self, text: str, is_final: bool = False) -> str:
305
+ """Summarize a single chunk of text"""
306
+ if is_final:
307
+ prompt = f"""Please provide a comprehensive final summary of this document based on the following chunk summaries:
308
+
309
+ {text}
310
+
311
+ Create a well-structured, detailed summary that captures all the key points, main themes, and important details from the entire document."""
312
+ else:
313
+ prompt = f"""Please provide a detailed summary of the following text, capturing all key points, main themes, and important details:
314
+
315
+ {text}
316
+
317
+ Make sure to preserve important information that might be needed for a final comprehensive summary."""
318
+
319
+ try:
320
+ message = self.client.messages.create(
321
+ model=config.ANTHROPIC_MODEL,
322
+ max_tokens=config.ANTHROPIC_MAX_TOKENS,
323
+ temperature=config.ANTHROPIC_TEMPERATURE,
324
+ messages=[
325
+ {
326
+ "role": "user",
327
+ "content": prompt
328
+ }
329
+ ]
330
+ )
331
+ return message.content[0].text
332
+ except Exception as e:
333
+ logger.error(f"Error calling Anthropic API: {str(e)}")
334
+ return f"Error generating summary: {str(e)}"
335
+
336
+ # Initialize processors
337
+ pdf_processor = PDFProcessor()
338
+ summary_generator = None
339
+
340
+ @app.on_event("startup")
341
+ async def startup_event():
342
+ """Initialize models and clients on startup"""
343
+ global processor, model, summary_generator
344
+
345
+ logger.info("Loading SmolDocling model...")
346
+ try:
347
+ # Load the SmolDocling model
348
+ model_id = config.MODEL_ID
349
+
350
+ # Try loading with different approaches
351
+ try:
352
+ # First try: Standard loading
353
+ logger.info("Attempting to load processor...")
354
+ processor = AutoProcessor.from_pretrained(
355
+ model_id,
356
+ trust_remote_code=True,
357
+ use_fast=False
358
+ )
359
+ logger.info("Processor loaded successfully")
360
+
361
+ except Exception as e:
362
+ logger.warning(f"Standard processor loading failed: {str(e)}")
363
+ # Fallback: Try with explicit trust_remote_code
364
+ try:
365
+ from transformers import AutoTokenizer, AutoImageProcessor
366
+ processor = AutoProcessor.from_pretrained(
367
+ model_id,
368
+ trust_remote_code=True,
369
+ revision="main"
370
+ )
371
+ logger.info("Processor loaded with trust_remote_code=True")
372
+ except Exception as e2:
373
+ logger.error(f"All processor loading attempts failed: {str(e2)}")
374
+ raise e2
375
+
376
+ # Load the model
377
+ logger.info("Loading model...")
378
+ model = AutoModelForVision2Seq.from_pretrained(
379
+ model_id,
380
+ torch_dtype=torch.bfloat16 if DEVICE == "cuda" else torch.float32,
381
+ trust_remote_code=True,
382
+ _attn_implementation="eager", # Use eager attention for better compatibility
383
+ device_map="auto" if DEVICE == "cuda" else None,
384
+ )
385
+
386
+ if DEVICE != "cuda":
387
+ model = model.to(DEVICE)
388
+
389
+ logger.info(f"Model loaded successfully on {DEVICE}")
390
+
391
+ # Initialize Anthropic client
392
+ if config.ANTHROPIC_API_KEY and ANTHROPIC_AVAILABLE:
393
+ try:
394
+ summary_generator = SummaryGenerator(config.ANTHROPIC_API_KEY)
395
+ logger.info("Anthropic client initialized successfully")
396
+ except Exception as e:
397
+ logger.error(f"Failed to initialize Anthropic client: {str(e)}")
398
+ logger.warning("Summary generation will not be available due to Anthropic client error.")
399
+ summary_generator = None
400
+ else:
401
+ if not config.ANTHROPIC_API_KEY:
402
+ logger.warning("ANTHROPIC_API_KEY not found. Summary generation will not be available.")
403
+ if not ANTHROPIC_AVAILABLE:
404
+ logger.warning("Anthropic library not available. Summary generation will not be available.")
405
+ summary_generator = None
406
+
407
+ except Exception as e:
408
+ logger.error(f"Error loading model: {str(e)}")
409
+ logger.error("The application will still work for basic PDF text extraction without the SmolDocling model.")
410
+ # Don't raise the error - let the app start without the model
411
+ processor = None
412
+ model = None
413
+
414
+ @app.get("/")
415
+ async def root():
416
+ """Serve the main HTML interface"""
417
+ html_content = """
418
+ <!DOCTYPE html>
419
+ <html>
420
+ <head>
421
+ <title>PDF Parser with SmolDocling</title>
422
+ <style>
423
+ body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
424
+ .upload-area { border: 2px dashed #ccc; padding: 20px; text-align: center; margin: 20px 0; }
425
+ .result-area { margin-top: 20px; padding: 20px; background-color: #f5f5f5; border-radius: 5px; }
426
+ button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; }
427
+ button:hover { background-color: #0056b3; }
428
+ button:disabled { background-color: #ccc; cursor: not-allowed; }
429
+ .progress { display: none; margin: 10px 0; }
430
+ .error { color: red; }
431
+ .success { color: green; }
432
+ </style>
433
+ </head>
434
+ <body>
435
+ <h1>📄 PDF Parser with SmolDocling</h1>
436
+ <p>Upload a PDF document to extract text and generate a summary using AI.</p>
437
+
438
+ <div class="upload-area">
439
+ <input type="file" id="pdfFile" accept=".pdf" />
440
+ <br><br>
441
+ <button onclick="uploadPDF()" id="uploadBtn">Process PDF</button>
442
+ <div class="progress" id="progress">Processing... Please wait.</div>
443
+ </div>
444
+
445
+ <div class="result-area" id="results" style="display: none;">
446
+ <h3>Results:</h3>
447
+ <div id="resultContent"></div>
448
+ </div>
449
+
450
+ <script>
451
+ async function uploadPDF() {
452
+ const fileInput = document.getElementById('pdfFile');
453
+ const uploadBtn = document.getElementById('uploadBtn');
454
+ const progress = document.getElementById('progress');
455
+ const results = document.getElementById('results');
456
+ const resultContent = document.getElementById('resultContent');
457
+
458
+ if (!fileInput.files.length) {
459
+ alert('Please select a PDF file');
460
+ return;
461
+ }
462
+
463
+ const formData = new FormData();
464
+ formData.append('file', fileInput.files[0]);
465
+
466
+ uploadBtn.disabled = true;
467
+ progress.style.display = 'block';
468
+ results.style.display = 'none';
469
+
470
+ try {
471
+ const response = await fetch('/upload-pdf/', {
472
+ method: 'POST',
473
+ body: formData
474
+ });
475
+
476
+ const result = await response.json();
477
+
478
+ if (response.ok) {
479
+ resultContent.innerHTML = `
480
+ <div class="success">✅ PDF processed successfully!</div>
481
+ <h4>Extracted Text (Markdown):</h4>
482
+ <pre style="white-space: pre-wrap; background: white; padding: 15px; border-radius: 5px; max-height: 400px; overflow-y: auto;">${result.markdown}</pre>
483
+ ${result.summary ? `
484
+ <h4>Summary:</h4>
485
+ <div style="background: white; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff;">${result.summary}</div>
486
+ ` : ''}
487
+ <p><small>Processing time: ${result.processing_time} seconds</small></p>
488
+ `;
489
+ results.style.display = 'block';
490
+ } else {
491
+ resultContent.innerHTML = `<div class="error">❌ Error: ${result.detail}</div>`;
492
+ results.style.display = 'block';
493
+ }
494
+ } catch (error) {
495
+ resultContent.innerHTML = `<div class="error">❌ Error: ${error.message}</div>`;
496
+ results.style.display = 'block';
497
+ } finally {
498
+ uploadBtn.disabled = false;
499
+ progress.style.display = 'none';
500
+ }
501
+ }
502
+ </script>
503
+ </body>
504
+ </html>
505
+ """
506
+ return HTMLResponse(content=html_content)
507
+
508
+ @app.get("/health")
509
+ async def health_check():
510
+ """Health check endpoint"""
511
+ return {"status": "healthy", "model_loaded": model is not None}
512
+
513
+ @app.post("/upload-pdf/")
514
+ async def upload_pdf(file: UploadFile = File(...)):
515
+ """Upload and process PDF file"""
516
+ import time
517
+ start_time = time.time()
518
+
519
+ # Validate file
520
+ if not file.filename.lower().endswith('.pdf'):
521
+ raise HTTPException(status_code=400, detail="Only PDF files are allowed")
522
+
523
+ try:
524
+ # Read PDF content
525
+ pdf_content = await file.read()
526
+ logger.info(f"Received PDF file: {file.filename} ({len(pdf_content)} bytes)")
527
+
528
+ # Convert PDF to images
529
+ images = await pdf_processor.pdf_to_images(pdf_content)
530
+
531
+ # Process in chunks if PDF is large
532
+ image_chunks = pdf_processor.chunk_images(images)
533
+ all_markdown = []
534
+
535
+ for i, chunk in enumerate(image_chunks):
536
+ logger.info(f"Processing chunk {i+1}/{len(image_chunks)} ({len(chunk)} pages)")
537
+ chunk_markdown = await pdf_processor.process_pdf_chunk(chunk)
538
+ all_markdown.append(chunk_markdown)
539
+
540
+ # Combine all markdown
541
+ full_markdown = "\n".join(all_markdown)
542
+
543
+ # Generate summary if Anthropic client is available
544
+ summary = None
545
+ if summary_generator:
546
+ try:
547
+ logger.info("Generating summary...")
548
+ summary = await summary_generator.summarize_text(full_markdown)
549
+ except Exception as e:
550
+ logger.error(f"Summary generation failed: {str(e)}")
551
+ summary = f"Summary generation failed: {str(e)}"
552
+
553
+ processing_time = round(time.time() - start_time, 2)
554
+
555
+ result = {
556
+ "message": "PDF processed successfully",
557
+ "filename": file.filename,
558
+ "total_pages": len(images),
559
+ "chunks_processed": len(image_chunks),
560
+ "markdown": full_markdown,
561
+ "summary": summary,
562
+ "processing_time": processing_time
563
+ }
564
+
565
+ logger.info(f"PDF processing completed in {processing_time} seconds")
566
+ return result
567
+
568
+ except Exception as e:
569
+ logger.error(f"Error processing PDF: {str(e)}")
570
+ raise HTTPException(status_code=500, detail=str(e))
571
+
572
+ if __name__ == "__main__":
573
+ uvicorn.run(app, host="0.0.0.0", port=7860)
config.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional
3
+
4
+ class Config:
5
+ """Application configuration"""
6
+
7
+ # API Keys
8
+ ANTHROPIC_API_KEY: Optional[str] = os.getenv("ANTHROPIC_API_KEY","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")
9
+
10
+ # Model settings
11
+ MODEL_ID: str = "ds4sd/SmolDocling-256M-preview"
12
+ MAX_PAGES_PER_CHUNK: int = int(os.getenv("MAX_PAGES_PER_CHUNK", "10"))
13
+ MAX_NEW_TOKENS: int = 8192
14
+
15
+ # Device settings
16
+ DEVICE: str = os.getenv("MODEL_DEVICE", "auto") # auto, cuda, cpu
17
+
18
+ # Processing settings
19
+ PDF_DPI: int = 200 # DPI for PDF to image conversion
20
+
21
+ # Anthropic settings
22
+ ANTHROPIC_MODEL: str = "claude-3-haiku-20240307"
23
+ ANTHROPIC_MAX_TOKENS: int = 4000
24
+ ANTHROPIC_TEMPERATURE: float = 0.3
25
+
26
+ # Text chunking settings
27
+ MAX_TOKENS_PER_CHUNK: int = 90000 # Half of Claude's context window
28
+
29
+ @classmethod
30
+ def validate(cls) -> bool:
31
+ """Validate configuration"""
32
+ if not cls.ANTHROPIC_API_KEY:
33
+ print("Warning: ANTHROPIC_API_KEY not set. Summary generation will not be available.")
34
+ return False
35
+ return True
36
+
37
+ # Create global config instance
38
+ config = Config()
requirements.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ python-multipart==0.0.6
4
+ python-dotenv==1.0.0
5
+ torch==2.1.1
6
+ transformers>=4.44.0
7
+ accelerate>=0.20.0
8
+ Pillow==10.1.0
9
+ PyPDF2==3.0.1
10
+ pdf2image==1.16.3
11
+ docling-core>=1.0.0
12
+ anthropic>=0.3.0,<1.0.0
13
+ numpy==1.24.3
14
+ requests==2.31.0
15
+ aiofiles==23.2.1
16
+ typing-extensions>=4.5.0
17
+ huggingface-hub>=0.19.0
18
+ pytesseract>=0.3.10