smyk07 commited on
Commit
540bc36
·
1 Parent(s): d6716ee

Initial commit

Browse files
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soumyak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,15 +1,41 @@
 
 
 
 
 
 
1
  ---
2
- title: VisionMark
3
- emoji: 📊
4
- colorFrom: green
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.22.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- short_description: Convert Anything to Markdown file. (.md for life)
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VisionMark
2
+
3
+ VisionMark is a local-first document conversion pipeline that transforms various file formats into clean, structured Markdown optimized for Large Language Model (LLM) context windows.
4
+
5
+ While other tools rely on cloud APIs and external services, VisionMark is built to run entirely on your local machine. It uses a hybrid approach: rule-based parsing for standard text formats and native, local execution of the Qwen2.5-VL vision model for complex, scanned, or visually-heavy documents. This ensures your data remains private and you incur zero API costs.
6
+
7
  ---
8
+
9
+ ## Core Features
10
+
11
+ - **Fully Local Execution:** Runs Qwen2.5-VL directly via Hugging Face Transformers. No third-party API keys or cloud services required.
12
+ - **Format Agnostic:** Native support for PDFs, Office documents (DOCX, PPTX, XLSX), images, code files, and Jupyter Notebooks.
13
+ - **Hybrid Pipeline:** Automatically routes text-heavy documents through fast rule-based parsers and uses the vision model for complex layouts or images.
14
+ - **Smart PDF Batching:** Dynamically adjusts batch sizes when processing large PDFs to manage VRAM and token limits efficiently.
15
+ - **Granular Control:** Offers CLI and UI options to control generation temperature, token limits, and concurrent processing threads.
16
+
 
 
17
  ---
18
 
19
+
20
+ ## Supported Formats
21
+ Category | Formats |
22
+ |------------|----------------------------------|
23
+ | Documents | PDF, DOCX, PPTX, XLSX |
24
+ | Images | PNG, JPG, JPEG, BMP |
25
+ | Code | Python, R, JavaScript, C++, etc.|
26
+ | Notebooks | Jupyter Notebooks (.ipynb) |
27
+ | Markdown | MD, RMD |
28
+ | Text | TXT |
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ git clone https://github.com/mkdir-smyk/VisionMark.git
36
+ cd VisionMark
37
+
38
+ python -m venv venv
39
+ source venv/bin/activate
40
+
41
+ pip install -r requirements.txt
app.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from ui.gradio_app import create_ui
2
+
3
+ demo = create_ui()
4
+
5
+ if __name__ == "__main__":
6
+ demo.launch()
main.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ from pathlib import Path
4
+ from processors.base import BaseDocumentProcessor, DocumentType
5
+ from processors.vision.vision_processor import VisionDocumentProcessor
6
+
7
+ def process_file(file_path, output_dir, force_vision=False, max_concurrent=2, images_per_batch=1,
8
+ temperature=0.0, max_tokens=None, dynamic_batching=True, max_tokens_per_batch=4000,
9
+ api_key=None, base_url=None, model=None):
10
+ """Process a single file"""
11
+ try:
12
+ print(f"Processing: {file_path}")
13
+
14
+ # Determine document type first
15
+ doc_type = DocumentType.from_file_extension(file_path)
16
+
17
+ # Select appropriate processor based on type and options
18
+ if force_vision and doc_type == DocumentType.PDF:
19
+ print(f"Using Vision processor for PDF: {file_path}")
20
+ processor = VisionDocumentProcessor(
21
+ api_key=api_key,
22
+ base_url=base_url,
23
+ model=model,
24
+ temperature=temperature,
25
+ max_tokens=max_tokens
26
+ )
27
+ elif doc_type == DocumentType.IMAGE:
28
+ print(f"Using Vision processor for image: {file_path}")
29
+ processor = VisionDocumentProcessor(
30
+ api_key=api_key,
31
+ base_url=base_url,
32
+ model=model,
33
+ temperature=temperature,
34
+ max_tokens=max_tokens
35
+ )
36
+ else:
37
+ # Use standard document detection
38
+ processor = BaseDocumentProcessor.get_processor(file_path)
39
+
40
+ print(f"Using {processor.__class__.__name__} for {file_path}")
41
+
42
+ # Process document - with max_concurrent and images_per_batch for PDFs
43
+ if isinstance(processor, VisionDocumentProcessor) and doc_type == DocumentType.PDF:
44
+ document = processor.process(
45
+ file_path,
46
+ max_concurrent=max_concurrent,
47
+ images_per_batch=images_per_batch,
48
+ dynamic_batching=dynamic_batching,
49
+ max_tokens_per_batch=max_tokens_per_batch
50
+ )
51
+ print(f"Processed PDF with {max_concurrent} concurrent workers, dynamic batching: {dynamic_batching}")
52
+ else:
53
+ document = processor.process(file_path)
54
+
55
+ # Generate output filename
56
+ base_name = Path(file_path).stem
57
+ output_base = os.path.join(output_dir, base_name)
58
+
59
+ md_path = f"{output_base}.md"
60
+ with open(md_path, "w", encoding="utf-8") as f:
61
+ f.write(document.to_markdown())
62
+ print(f"Markdown saved to: {md_path}")
63
+
64
+ except Exception as e:
65
+ print(f"Error processing {file_path}: {str(e)}")
66
+ import traceback
67
+ traceback.print_exc()
68
+
69
+ def process_directory(input_dir, output_dir, force_vision=False, max_concurrent=2, images_per_batch=1,
70
+ temperature=0.0, max_tokens=None, dynamic_batching=True, max_tokens_per_batch=4000,
71
+ api_key=None, base_url=None, model=None):
72
+ """Process all files in directory"""
73
+ for root, _, files in os.walk(input_dir):
74
+ for file in files:
75
+ file_path = os.path.join(root, file)
76
+ process_file(file_path, output_dir, force_vision, max_concurrent, images_per_batch,
77
+ temperature, max_tokens, dynamic_batching, max_tokens_per_batch,
78
+ api_key, base_url, model)
79
+
80
+ def main():
81
+ parser = argparse.ArgumentParser(description="Convert documents to structured markdown")
82
+
83
+ parser.add_argument("input", nargs="?", help="Input file or directory path")
84
+ parser.add_argument("--output", "-o", help="Output directory path", default="output")
85
+ parser.add_argument("--ui", action="store_true", help="Launch graphical user interface")
86
+ parser.add_argument("--force-vision", action="store_true",
87
+ help="Force using vision model for PDFs instead of text extraction")
88
+ parser.add_argument("--max-concurrent", type=int, default=2,
89
+ help="Maximum number of concurrent workers for PDF page processing (default: 2)")
90
+ parser.add_argument("--api-key", help="API key for Qwen vision processor")
91
+ parser.add_argument("--base-url", help="Base URL for API")
92
+ parser.add_argument("--model", help="Model name to use")
93
+ parser.add_argument("--images-per-batch", type=int, default=1,
94
+ help="Maximum number of PDF pages to process in a single API call (default: 1, 2+ enables multi-image processing)")
95
+ parser.add_argument("--temperature", type=float, default=0.0,
96
+ help="Temperature for vision model generation (0.0-1.0, lower is more deterministic)")
97
+ parser.add_argument("--max-tokens", type=int,
98
+ help="Maximum tokens for vision model generation (default uses model's limit)")
99
+ parser.add_argument("--dynamic-batching", action="store_true", default=True,
100
+ help="Automatically determine optimal batch size based on image complexity. The optimal batch size will not exceed the number set by --images-per-batch.")
101
+ parser.add_argument("--no-dynamic-batching", action="store_false", dest="dynamic_batching",
102
+ help="Disable dynamic batching and use fixed images-per-batch")
103
+ parser.add_argument("--max-tokens-per-batch", type=int, default=4000,
104
+ help="Maximum tokens per batch when using dynamic batching (default: 4000)")
105
+
106
+ args = parser.parse_args()
107
+
108
+ # Launch UI if requested
109
+ if args.ui:
110
+ from ui.app import create_ui
111
+ app = create_ui()
112
+ # Allow access to any path
113
+ app.launch(allowed_paths=["/"])
114
+ return
115
+
116
+ # If no input is provided and not in UI mode, print help
117
+ if not args.input:
118
+ parser.print_help()
119
+ return
120
+
121
+ # Ensure output directory exists
122
+ os.makedirs(args.output, exist_ok=True)
123
+
124
+ if os.path.isdir(args.input):
125
+ # Process directory
126
+ process_directory(
127
+ args.input, args.output, args.force_vision, args.max_concurrent, args.images_per_batch,
128
+ args.temperature, args.max_tokens, args.dynamic_batching, args.max_tokens_per_batch,
129
+ args.api_key, args.base_url, args.model
130
+ )
131
+ else:
132
+ # Process single file
133
+ process_file(
134
+ args.input, args.output, args.force_vision, args.max_concurrent, args.images_per_batch,
135
+ args.temperature, args.max_tokens, args.dynamic_batching, args.max_tokens_per_batch,
136
+ args.api_key, args.base_url, args.model
137
+ )
138
+
139
+ if __name__ == "__main__":
140
+ main()
processors/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VisionMark document processors package.
3
+ """
4
+
5
+ from processors.base import (
6
+ BaseDocumentProcessor,
7
+ DocumentType,
8
+ DocumentElement,
9
+ DocumentSection,
10
+ StructuredDocument
11
+ )
12
+
13
+ __all__ = [
14
+ 'BaseDocumentProcessor',
15
+ 'DocumentType',
16
+ 'DocumentElement',
17
+ 'DocumentSection',
18
+ 'StructuredDocument'
19
+ ]
processors/base.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, List, Optional, Union, Any
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ import os
6
+ import json
7
+
8
+ class DocumentType(Enum):
9
+ """Supported document types"""
10
+ PDF = "pdf"
11
+ IMAGE = "image" # jpg, png, etc.
12
+ WORD = "docx"
13
+ POWERPOINT = "pptx"
14
+ JUPYTER = "ipynb"
15
+ PYTHON = "py"
16
+ R_SCRIPT = "r"
17
+ R_MARKDOWN = "rmd"
18
+ MARKDOWN = "md"
19
+ TEXT = "txt"
20
+ EXCEL = "excel" # xlsx, xls files
21
+ UNKNOWN = "unknown"
22
+
23
+ @classmethod
24
+ def from_file_extension(cls, file_path: str) -> "DocumentType":
25
+ """Determine document type from file extension"""
26
+ ext = os.path.splitext(file_path.lower())[1][1:]
27
+
28
+ if ext in ("jpg", "jpeg", "png", "bmp", "tiff"):
29
+ return cls.IMAGE
30
+ elif ext == "pdf":
31
+ return cls.PDF
32
+ elif ext == "docx":
33
+ return cls.WORD
34
+ elif ext == "pptx":
35
+ return cls.POWERPOINT
36
+ elif ext == "ipynb":
37
+ return cls.JUPYTER
38
+ elif ext == "py":
39
+ return cls.PYTHON
40
+ elif ext == "r":
41
+ return cls.R_SCRIPT
42
+ elif ext == "rmd":
43
+ return cls.R_MARKDOWN
44
+ elif ext == "md":
45
+ return cls.MARKDOWN
46
+ elif ext == "txt":
47
+ return cls.TEXT
48
+ elif ext in ("xlsx", "xls", "xlsm"):
49
+ return cls.EXCEL
50
+ else:
51
+ return cls.UNKNOWN
52
+
53
+ class DocumentElement:
54
+ """Base class for document elements"""
55
+ def __init__(
56
+ self,
57
+ content: str,
58
+ element_type: str,
59
+ position: Optional[Dict[str, Any]] = None,
60
+ metadata: Optional[Dict[str, Any]] = None
61
+ ):
62
+ self.content = content
63
+ self.element_type = element_type
64
+ self.position = position or {}
65
+ self.metadata = metadata or {}
66
+
67
+ def to_dict(self) -> Dict[str, Any]:
68
+ """Convert to dictionary representation"""
69
+ return {
70
+ "content": self.content,
71
+ "type": self.element_type,
72
+ "position": self.position,
73
+ "metadata": self.metadata
74
+ }
75
+
76
+ def to_markdown(self) -> str:
77
+ """Convert to markdown representation"""
78
+ if self.element_type == "heading":
79
+ level = self.metadata.get("level", 1)
80
+ return f"{'#' * level} {self.content}\n\n"
81
+ elif self.element_type == "paragraph":
82
+ return f"{self.content}\n\n"
83
+ elif self.element_type == "code":
84
+ lang = self.metadata.get("language", "")
85
+ return f"```{lang}\n{self.content}\n```\n\n"
86
+ elif self.element_type == "list_item":
87
+ return f"- {self.content}\n"
88
+ elif self.element_type == "image":
89
+ alt = self.metadata.get("alt", "Image")
90
+ src = self.metadata.get("src", "")
91
+ return f"![{alt}]({src})\n\n"
92
+ elif self.element_type == "table":
93
+ # Basic table rendering - in real implementation this would be more complex
94
+ return f"**Table**: {self.content}\n\n"
95
+ else:
96
+ return f"{self.content}\n\n"
97
+
98
+ class DocumentSection:
99
+ """Section of a document containing multiple elements"""
100
+ def __init__(
101
+ self,
102
+ title: Optional[str] = None,
103
+ elements: Optional[List[DocumentElement]] = None,
104
+ level: int = 1,
105
+ metadata: Optional[Dict[str, Any]] = None
106
+ ):
107
+ self.title = title
108
+ self.elements = elements or []
109
+ self.level = level
110
+ self.metadata = metadata or {}
111
+
112
+ def add_element(self, element: DocumentElement) -> None:
113
+ """Add an element to the section"""
114
+ self.elements.append(element)
115
+
116
+ def to_dict(self) -> Dict[str, Any]:
117
+ """Convert to dictionary representation"""
118
+ return {
119
+ "title": self.title,
120
+ "level": self.level,
121
+ "elements": [element.to_dict() for element in self.elements],
122
+ "metadata": self.metadata
123
+ }
124
+
125
+ def to_markdown(self) -> str:
126
+ """Convert to markdown representation"""
127
+ result = ""
128
+ if self.title:
129
+ result += f"{'#' * self.level} {self.title}\n\n"
130
+
131
+ for element in self.elements:
132
+ result += element.to_markdown()
133
+
134
+ return result
135
+
136
+ class StructuredDocument:
137
+ """Complete structured document"""
138
+ def __init__(
139
+ self,
140
+ title: Optional[str] = None,
141
+ source_file: Optional[str] = None,
142
+ doc_type: Optional[DocumentType] = None,
143
+ sections: Optional[List[DocumentSection]] = None,
144
+ metadata: Optional[Dict[str, Any]] = None
145
+ ):
146
+ self.title = title
147
+ self.source_file = source_file
148
+ self.doc_type = doc_type or DocumentType.UNKNOWN
149
+ self.sections = sections or []
150
+ self.metadata = metadata or {}
151
+
152
+ def add_section(self, section: DocumentSection) -> None:
153
+ """Add a section to the document"""
154
+ self.sections.append(section)
155
+
156
+ def to_dict(self) -> Dict[str, Any]:
157
+ """Convert to dictionary representation"""
158
+ return {
159
+ "title": self.title,
160
+ "source_file": self.source_file,
161
+ "doc_type": self.doc_type.value if self.doc_type else None,
162
+ "sections": [section.to_dict() for section in self.sections],
163
+ "metadata": self.metadata
164
+ }
165
+
166
+ def to_markdown(self) -> str:
167
+ """Convert document to markdown format"""
168
+ # If we have direct markdown in metadata, use that
169
+ if "markdown" in self.metadata:
170
+ return self.metadata["markdown"]
171
+
172
+ # Otherwise, build markdown from structure
173
+ parts = []
174
+
175
+ # Add document title
176
+ if self.title:
177
+ parts.append(f"# {self.title}\n\n")
178
+
179
+ # Add each section
180
+ for section in self.sections:
181
+ # Add section title with appropriate heading level
182
+ if section.title:
183
+ level = section.level if section.level else 1
184
+ heading = "#" * (level + 1) # +1 because document title is h1
185
+ parts.append(f"{heading} {section.title}\n\n")
186
+
187
+ # Add elements
188
+ for element in section.elements:
189
+ if element.element_type == "markdown":
190
+ # For markdown elements, just add the content directly
191
+ parts.append(f"{element.content}\n\n")
192
+ else:
193
+ # For other elements, convert to markdown
194
+ if element.element_type == "paragraph":
195
+ parts.append(f"{element.content}\n\n")
196
+ elif element.element_type == "heading":
197
+ level = element.metadata.get("level", 2)
198
+ heading = "#" * (level + 1) # +1 because doc title is h1
199
+ parts.append(f"{heading} {element.content}\n\n")
200
+ elif element.element_type == "list":
201
+ for item in element.content.split("\n"):
202
+ parts.append(f"- {item.strip()}\n")
203
+ parts.append("\n")
204
+ elif element.element_type == "code":
205
+ lang = element.metadata.get("language", "")
206
+ parts.append(f"```{lang}\n{element.content}\n```\n\n")
207
+ else:
208
+ # Default behavior for unknown element types
209
+ parts.append(f"{element.content}\n\n")
210
+
211
+ return "".join(parts)
212
+
213
+ def save_markdown(self, output_path: str) -> None:
214
+ """Save as markdown file"""
215
+ with open(output_path, "w", encoding="utf-8") as f:
216
+ f.write(self.to_markdown())
217
+
218
+ def get_direct_markdown(self) -> str:
219
+ """
220
+ Get direct markdown if available, otherwise generate from structure
221
+
222
+ Returns:
223
+ str: Markdown representation of the document
224
+ """
225
+ if "direct_markdown" in self.metadata:
226
+ return self.metadata["direct_markdown"]
227
+ else:
228
+ return self.to_markdown()
229
+
230
+ class BaseDocumentProcessor(ABC):
231
+ """Abstract base class for document processors"""
232
+
233
+ def __init__(self):
234
+ """Initialize the document processor"""
235
+ pass
236
+
237
+ @abstractmethod
238
+ def process(self, file_path: str) -> StructuredDocument:
239
+ """
240
+ Process a document and return structured content
241
+
242
+ Args:
243
+ file_path: Path to the document file
244
+
245
+ Returns:
246
+ StructuredDocument: The processed document
247
+ """
248
+ pass
249
+
250
+ @classmethod
251
+ def get_processor(cls, file_path: str, force_vision: bool = False) -> "BaseDocumentProcessor":
252
+ """Factory method to get appropriate processor for a file"""
253
+ doc_type = DocumentType.from_file_extension(file_path)
254
+
255
+ # Import here to avoid circular imports
256
+ from processors.text.pdf_processor import PDFProcessor
257
+ from processors.vision.vision_processor import VisionDocumentProcessor
258
+ from processors.text.docx_processor import DocxProcessor
259
+ from processors.text.pptx_processor import PowerPointProcessor
260
+ from processors.text.ipynb_processor import JupyterNotebookProcessor
261
+ from processors.text.code_processor import CodeFileProcessor
262
+ from processors.text.markdown_processor import MarkdownProcessor
263
+ from processors.text.text_processor import TextProcessor
264
+ from processors.text.excel_processor import ExcelProcessor
265
+
266
+ # Use vision processor if forced (applies to PDF)
267
+ if force_vision:
268
+ return VisionDocumentProcessor()
269
+
270
+ # Return appropriate processor based on document type
271
+ if doc_type == DocumentType.PDF:
272
+ return PDFProcessor()
273
+ elif doc_type == DocumentType.WORD:
274
+ return DocxProcessor()
275
+ elif doc_type == DocumentType.POWERPOINT:
276
+ return PowerPointProcessor()
277
+ elif doc_type == DocumentType.JUPYTER:
278
+ return JupyterNotebookProcessor()
279
+ elif doc_type == DocumentType.IMAGE:
280
+ return VisionDocumentProcessor()
281
+ elif doc_type in (DocumentType.PYTHON, DocumentType.R_SCRIPT):
282
+ return CodeFileProcessor()
283
+ elif doc_type in (DocumentType.MARKDOWN, DocumentType.R_MARKDOWN):
284
+ return MarkdownProcessor()
285
+ elif doc_type == DocumentType.EXCEL:
286
+ return ExcelProcessor()
287
+ elif doc_type == DocumentType.TEXT:
288
+ return TextProcessor()
289
+ else:
290
+ # Default to text processor for unknown types
291
+ return TextProcessor()
processors/text/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.text.pdf_processor import PDFProcessor
2
+ from processors.text.docx_processor import DocxProcessor
3
+ from processors.text.pptx_processor import PowerPointProcessor
4
+ from processors.text.ipynb_processor import JupyterNotebookProcessor
5
+ from processors.text.code_processor import CodeFileProcessor
6
+ from processors.text.markdown_processor import MarkdownProcessor
7
+ from processors.text.text_processor import TextProcessor
8
+ from processors.text.excel_processor import ExcelProcessor
9
+
10
+ __all__ = [
11
+ 'PDFProcessor',
12
+ 'DocxProcessor',
13
+ 'PowerPointProcessor',
14
+ 'JupyterNotebookProcessor',
15
+ 'CodeFileProcessor',
16
+ 'MarkdownProcessor',
17
+ 'TextProcessor',
18
+ 'ExcelProcessor',
19
+ ]
processors/text/code_processor.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+
4
+ class CodeFileProcessor(BaseDocumentProcessor):
5
+ """Processor for code files (Python, R, etc.)"""
6
+
7
+ # Language mapping for syntax highlighting
8
+ LANGUAGE_MAP = {
9
+ "py": "python",
10
+ "r": "r",
11
+ "js": "javascript",
12
+ "ts": "typescript",
13
+ "sh": "bash",
14
+ "cpp": "cpp",
15
+ "c": "c",
16
+ "java": "java",
17
+ "go": "go",
18
+ "rb": "ruby",
19
+ "php": "php",
20
+ }
21
+
22
+ def process(self, file_path: str) -> StructuredDocument:
23
+ """Process code file"""
24
+ file_ext = Path(file_path).suffix.lower()[1:]
25
+ doc_type = DocumentType.from_file_extension(file_path)
26
+
27
+ document = StructuredDocument(
28
+ title=Path(file_path).stem,
29
+ source_file=file_path,
30
+ doc_type=doc_type
31
+ )
32
+
33
+ try:
34
+ # Read code file
35
+ with open(file_path, 'r', encoding='utf-8') as f:
36
+ code_content = f.read()
37
+
38
+ # Get language for syntax highlighting
39
+ language = self.LANGUAGE_MAP.get(file_ext, file_ext)
40
+
41
+ # Create markdown representation
42
+ markdown = f"# {Path(file_path).name}\n\n"
43
+ markdown += f"```{language}\n{code_content}\n```\n"
44
+
45
+ # Create document structure
46
+ main_section = DocumentSection(title=Path(file_path).name)
47
+ main_section.add_element(DocumentElement(
48
+ content=code_content,
49
+ element_type="code",
50
+ metadata={"language": language}
51
+ ))
52
+ document.add_section(main_section)
53
+
54
+ # Store markdown
55
+ document.metadata["markdown"] = markdown
56
+
57
+ except Exception as e:
58
+ print(f"Error processing code file: {e}")
59
+ error_section = DocumentSection(title="Error")
60
+ error_section.add_element(DocumentElement(
61
+ content=f"Failed to process code file: {str(e)}",
62
+ element_type="paragraph"
63
+ ))
64
+ document.add_section(error_section)
65
+
66
+ return document
processors/text/docx_processor.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+ import mammoth
4
+ import re
5
+ import docx
6
+ from docx2python import docx2python
7
+ import pandas as pd
8
+ from io import StringIO
9
+
10
+ class DocxProcessor(BaseDocumentProcessor):
11
+ """Processor for DOCX documents with enhanced table support"""
12
+
13
+ def process(self, file_path: str) -> StructuredDocument:
14
+ """Process DOCX document with improved table handling"""
15
+ document = StructuredDocument(
16
+ title=Path(file_path).stem,
17
+ source_file=file_path,
18
+ doc_type=DocumentType.WORD
19
+ )
20
+
21
+ try:
22
+ # First approach: Try to extract tables using python-docx directly
23
+ doc = docx.Document(file_path)
24
+ has_tables = len(doc.tables) > 0
25
+
26
+ # Convert DOCX to Markdown using mammoth for general content
27
+ with open(file_path, "rb") as docx_file:
28
+ result = mammoth.convert_to_markdown(docx_file)
29
+ markdown_content = result.value
30
+
31
+ # If document has tables, use our direct table extraction
32
+ if (has_tables):
33
+ markdown_content = self._handle_tables_directly(doc, markdown_content)
34
+ else:
35
+ # As a fallback, try docx2python for table extraction
36
+ try:
37
+ doc_data = docx2python(file_path)
38
+ if hasattr(doc_data, 'tables') and doc_data.tables:
39
+ markdown_content = self._process_tables(file_path, markdown_content, doc_data.tables)
40
+ except Exception as table_error:
41
+ print(f"Warning: Unable to process tables with docx2python: {table_error}")
42
+
43
+ # Create a single section with the markdown content
44
+ section = DocumentSection(title="Document Content")
45
+ section.add_element(DocumentElement(
46
+ content=markdown_content,
47
+ element_type="markdown"
48
+ ))
49
+ document.add_section(section)
50
+
51
+ # Store raw markdown in metadata
52
+ document.metadata["markdown"] = markdown_content
53
+
54
+ # Extract any messages/warnings
55
+ if result.messages:
56
+ document.metadata["conversion_messages"] = [msg.message for msg in result.messages]
57
+
58
+ except Exception as e:
59
+ print(f"Error extracting content from DOCX: {e}")
60
+ # Create error section
61
+ error_section = DocumentSection(title="Error")
62
+ error_section.add_element(DocumentElement(
63
+ content=f"Failed to process document: {str(e)}",
64
+ element_type="paragraph"
65
+ ))
66
+ document.add_section(error_section)
67
+
68
+ return document
69
+
70
+ def _handle_tables_directly(self, doc, content):
71
+ """
72
+ Extract tables directly from python-docx and insert them as proper markdown tables
73
+
74
+ Args:
75
+ doc: python-docx Document object
76
+ content: Markdown content from mammoth
77
+
78
+ Returns:
79
+ str: Markdown content with properly formatted tables
80
+ """
81
+ if not doc.tables:
82
+ return content
83
+
84
+ # Split content into paragraphs
85
+ paragraphs = content.split('\n\n')
86
+ result_paragraphs = []
87
+ table_markers = []
88
+
89
+ # Find potential table markers
90
+ for i, para in enumerate(paragraphs):
91
+ if 'table' in para.lower():
92
+ table_markers.append(i)
93
+
94
+ # If no explicit markers, look for potential places by identifying short consecutive lines
95
+ if not table_markers:
96
+ for i in range(len(paragraphs) - 1):
97
+ lines = paragraphs[i].split('\n')
98
+ # Look for potential table headers (short lines)
99
+ if all(len(line) < 30 for line in lines) and len(lines) > 1:
100
+ table_markers.append(i)
101
+
102
+ # Process each paragraph, inserting tables at appropriate places
103
+ i = 0
104
+ tables_inserted = 0
105
+
106
+ while i < len(paragraphs):
107
+ if i in table_markers and tables_inserted < len(doc.tables):
108
+ # Add the current paragraph (table caption or header)
109
+ result_paragraphs.append(paragraphs[i])
110
+
111
+ # Convert the table to markdown and add it
112
+ table_md = self._table_to_markdown(doc.tables[tables_inserted])
113
+ result_paragraphs.append(table_md)
114
+
115
+ # Skip any paragraphs that might be part of the table in the original content
116
+ next_text = False
117
+ j = i + 1
118
+ while j < len(paragraphs) and not next_text:
119
+ # Check if paragraph j looks like normal text
120
+ if len(paragraphs[j].split('\n')) <= 1 and len(paragraphs[j]) > 30:
121
+ next_text = True
122
+ j += 1
123
+
124
+ if next_text:
125
+ i = j - 1 # Go back to the text paragraph
126
+ else:
127
+ i = j
128
+
129
+ tables_inserted += 1
130
+ else:
131
+ # Regular paragraph, just add it
132
+ result_paragraphs.append(paragraphs[i])
133
+ i += 1
134
+
135
+ # If we still have tables left, add them at the end
136
+ for j in range(tables_inserted, len(doc.tables)):
137
+ table_md = self._table_to_markdown(doc.tables[j])
138
+ result_paragraphs.append("Additional table found:")
139
+ result_paragraphs.append(table_md)
140
+
141
+ return '\n\n'.join(result_paragraphs)
142
+
143
+ def _table_to_markdown(self, table):
144
+ """
145
+ Convert python-docx table to markdown table format
146
+
147
+ Args:
148
+ table: python-docx Table object
149
+
150
+ Returns:
151
+ str: Markdown table
152
+ """
153
+ if not table.rows:
154
+ return ""
155
+
156
+ markdown_rows = []
157
+
158
+ # Get cell text for each row (preserve empty cells)
159
+ all_rows = []
160
+ for row in table.rows:
161
+ row_cells = []
162
+ for cell in row.cells:
163
+ # Remove newlines and extra spaces from cell text
164
+ cell_text = cell.text.strip().replace('\n', ' ')
165
+ cell_text = re.sub(r'\s+', ' ', cell_text)
166
+ row_cells.append(cell_text if cell_text else " ")
167
+ all_rows.append(row_cells)
168
+
169
+ # Determine the number of columns based on the row with the most cells
170
+ num_cols = max(len(row) for row in all_rows)
171
+
172
+ # Ensure all rows have the same number of columns
173
+ for row in all_rows:
174
+ while len(row) < num_cols:
175
+ row.append(" ")
176
+
177
+ # Generate header row
178
+ header_row = "| " + " | ".join(all_rows[0]) + " |"
179
+ markdown_rows.append(header_row)
180
+
181
+ # Generate separator row
182
+ separator_row = "| " + " | ".join(["---"] * num_cols) + " |"
183
+ markdown_rows.append(separator_row)
184
+
185
+ # Generate data rows
186
+ for row in all_rows[1:]:
187
+ data_row = "| " + " | ".join(row) + " |"
188
+ markdown_rows.append(data_row)
189
+
190
+ return "\n".join(markdown_rows)
191
+
192
+ def _process_tables(self, file_path: str, content: str, tables_data) -> str:
193
+ """
194
+ Process tables extracted by docx2python and replace mammoth's table output
195
+ with better markdown tables
196
+
197
+ Args:
198
+ file_path: Path to the DOCX file
199
+ content: Markdown content from mammoth
200
+ tables_data: Tables extracted by docx2python
201
+
202
+ Returns:
203
+ str: Markdown content with improved tables
204
+ """
205
+ if not tables_data:
206
+ # Direct table detection in the content if docx2python didn't find tables
207
+ return self._detect_and_format_tables_from_content(content, file_path)
208
+
209
+ try:
210
+ # Try to use python-docx to get additional table information
211
+ doc = docx.Document(file_path)
212
+
213
+ # Find table markers in mammoth output
214
+ table_pattern = r'\|[^\n]+\|\n\|[^\n]+\|'
215
+ table_matches = list(re.finditer(table_pattern, content))
216
+
217
+ # If no tables found with the standard pattern, try a broader pattern
218
+ if not table_matches:
219
+ # This broader pattern looks for possible table content by finding consecutive lines
220
+ # with keyword patterns that might indicate a flattened table
221
+ broader_pattern = r'(?:[\w\s]+\n){2,}(?:[\w\s]+)'
222
+ table_matches = list(re.finditer(broader_pattern, content))
223
+
224
+ # If we still don't find tables with regex patterns, use doc.tables directly
225
+ if (not table_matches or not doc.tables) and doc.tables:
226
+ return self._reconstruct_tables_from_python_docx(content, doc.tables)
227
+
228
+ # Start with the original content
229
+ result = content
230
+
231
+ # If tables found in docx but not in mammoth output, reconstruct them completely
232
+ if doc.tables and not table_matches:
233
+ return self._reconstruct_tables_from_python_docx(content, doc.tables)
234
+
235
+ # Standard processing when we have matching tables
236
+ offset = 0
237
+
238
+ # Process each table
239
+ for i, table_data in enumerate(tables_data):
240
+ if i >= len(doc.tables):
241
+ break
242
+
243
+ # Get table from python-docx
244
+ table = doc.tables[i]
245
+
246
+ # Find the table in the content
247
+ if i < len(table_matches):
248
+ table_match = table_matches[i]
249
+ start_pos = table_match.start() + offset
250
+ end_pos = table_match.end() + offset
251
+ else:
252
+ # If we don't have an exact match, place the table at the end
253
+ start_pos = len(result)
254
+ end_pos = start_pos
255
+
256
+ # Convert table to pandas DataFrame
257
+ df = self._table_to_dataframe(table_data)
258
+
259
+ # Convert DataFrame to markdown table
260
+ markdown_table = self._df_to_markdown(df)
261
+
262
+ # Replace table in content
263
+ result = result[:start_pos] + markdown_table + result[end_pos:]
264
+
265
+ # Update offset for next replacement
266
+ offset += len(markdown_table) - (end_pos - start_pos)
267
+
268
+ return result
269
+
270
+ except Exception as e:
271
+ print(f"Error processing tables: {e}")
272
+ # Fallback to direct content processing
273
+ return self._detect_and_format_tables_from_content(content, file_path)
274
+
275
+ def _detect_and_format_tables_from_content(self, content: str, file_path: str) -> str:
276
+ """
277
+ Detect and format tables directly from the content when docx2python fails
278
+
279
+ Args:
280
+ content: Markdown content
281
+ file_path: Path to the DOCX file
282
+
283
+ Returns:
284
+ str: Content with improved tables
285
+ """
286
+ try:
287
+ # Try to open with python-docx directly
288
+ doc = docx.Document(file_path)
289
+ if not doc.tables:
290
+ return content
291
+
292
+ return self._reconstruct_tables_from_python_docx(content, doc.tables)
293
+ except:
294
+ # Another fallback approach - try to detect tables based on text patterns
295
+ # Look for lines that might be table headers (consecutive short lines followed by a series of values)
296
+ lines = content.split('\n')
297
+ result = []
298
+ i = 0
299
+ while i < len(lines):
300
+ line = lines[i]
301
+ # Check if this line might be the start of a table (contains "table" keyword)
302
+ if "table" in line.lower() and i + 2 < len(lines):
303
+ potential_headers = []
304
+ j = i + 1
305
+ # Collect potential header lines (short lines with no punctuation)
306
+ while j < len(lines) and len(lines[j].strip()) < 30 and not re.search(r'[.,:;]', lines[j]) and lines[j].strip():
307
+ potential_headers.append(lines[j].strip())
308
+ j += 1
309
+
310
+ # If we found potential headers, format as a table
311
+ if len(potential_headers) >= 2:
312
+ result.append(line) # Add the table description line
313
+ result.append("") # Empty line before table
314
+
315
+ # Create markdown table headers
316
+ result.append("| " + " | ".join(potential_headers) + " |")
317
+ result.append("| " + " | ".join(["---"] * len(potential_headers)) + " |")
318
+
319
+ # Try to detect table rows
320
+ k = j
321
+ row_data = []
322
+ while k < len(lines) and len(row_data) < 20: # Limit to 20 rows to avoid false positives
323
+ if not lines[k].strip():
324
+ k += 1
325
+ continue
326
+
327
+ if len(lines[k].strip()) < 30:
328
+ row_data.append(lines[k].strip())
329
+ else:
330
+ break # Stop if we hit a long line (paragraph)
331
+ k += 1
332
+
333
+ # Create rows based on headers (split data into rows)
334
+ rows = []
335
+ row = []
336
+ for item in row_data:
337
+ row.append(item)
338
+ if len(row) == len(potential_headers):
339
+ rows.append("| " + " | ".join(row) + " |")
340
+ row = []
341
+
342
+ # If we have a partial row at the end, add it with empty cells
343
+ if row:
344
+ while len(row) < len(potential_headers):
345
+ row.append("")
346
+ rows.append("| " + " | ".join(row) + " |")
347
+
348
+ # Add all rows to result
349
+ result.extend(rows)
350
+ result.append("") # Empty line after table
351
+
352
+ i = k # Skip processed lines
353
+ else:
354
+ result.append(line)
355
+ i += 1
356
+ else:
357
+ result.append(line)
358
+ i += 1
359
+
360
+ return "\n".join(result)
361
+
362
+ def _reconstruct_tables_from_python_docx(self, content: str, tables) -> str:
363
+ """
364
+ Completely reconstruct tables using python-docx
365
+
366
+ Args:
367
+ content: Original markdown content
368
+ tables: Tables from python-docx
369
+
370
+ Returns:
371
+ str: Content with properly formatted tables
372
+ """
373
+ # First, find places in the content where tables might go
374
+ # Look for lines mentioning "table" for table captions/descriptions
375
+ table_markers = []
376
+ lines = content.split('\n')
377
+
378
+ for i, line in enumerate(lines):
379
+ if re.search(r'table|Table', line):
380
+ table_markers.append(i)
381
+
382
+ # If we found markers for all tables, use them to insert tables
383
+ if table_markers and len(table_markers) >= len(tables):
384
+ for i, table in enumerate(tables):
385
+ if i >= len(table_markers):
386
+ break
387
+
388
+ marker_pos = table_markers[i]
389
+
390
+ # Create markdown table from this table
391
+ markdown_table = self._convert_python_docx_table(table)
392
+
393
+ # Insert after the marker line
394
+ lines.insert(marker_pos + 1, "")
395
+ lines.insert(marker_pos + 2, markdown_table)
396
+ lines.insert(marker_pos + 3, "")
397
+
398
+ # Update remaining marker positions
399
+ for j in range(i + 1, len(table_markers)):
400
+ table_markers[j] += 3
401
+
402
+ return "\n".join(lines)
403
+ else:
404
+ # If we can't find markers for all tables, append tables at the end
405
+ result = content
406
+
407
+ for table in tables:
408
+ markdown_table = self._convert_python_docx_table(table)
409
+ result += "\n\n" + markdown_table + "\n"
410
+
411
+ return result
412
+
413
+ def _convert_python_docx_table(self, table) -> str:
414
+ """
415
+ Convert a python-docx table to markdown
416
+
417
+ Args:
418
+ table: python-docx table object
419
+
420
+ Returns:
421
+ str: Markdown table
422
+ """
423
+ # Extract the table data
424
+ table_data = []
425
+
426
+ for row in table.rows:
427
+ row_data = []
428
+ for cell in row.cells:
429
+ # Get text and clean it
430
+ text = cell.text.strip().replace('\n', ' ')
431
+ row_data.append(text)
432
+ table_data.append(row_data)
433
+
434
+ # If no data, return empty string
435
+ if not table_data:
436
+ return ""
437
+
438
+ # Get the max columns
439
+ max_cols = max(len(row) for row in table_data)
440
+
441
+ # Ensure all rows have the same number of columns
442
+ for row in table_data:
443
+ while len(row) < max_cols:
444
+ row.append("")
445
+
446
+ # Create header row
447
+ header = "| " + " | ".join(table_data[0]) + " |"
448
+
449
+ # Create separator row
450
+ separator = "| " + " | ".join(["---"] * max_cols) + " |"
451
+
452
+ # Create data rows
453
+ rows = []
454
+ for row in table_data[1:]:
455
+ rows.append("| " + " | ".join(row) + " |")
456
+
457
+ # Combine all parts
458
+ return header + "\n" + separator + "\n" + "\n".join(rows)
459
+
460
+ def _df_to_markdown(self, df: pd.DataFrame) -> str:
461
+ """Convert pandas DataFrame to markdown table"""
462
+ if df.empty:
463
+ return ''
464
+
465
+ # Use pandas built-in to_markdown when possible
466
+ try:
467
+ return df.to_markdown(index=False)
468
+ except Exception as e:
469
+ # Fallback to manual conversion
470
+ return self._complex_df_to_markdown(df)
processors/text/excel_processor.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+ import pandas as pd
4
+ import os
5
+
6
+ class ExcelProcessor(BaseDocumentProcessor):
7
+ """Processor for Excel files (xlsx, xls, xlsm)"""
8
+
9
+ def process(self, file_path: str) -> StructuredDocument:
10
+ """
11
+ Process Excel file and convert to markdown
12
+
13
+ Args:
14
+ file_path: Path to Excel file
15
+
16
+ Returns:
17
+ StructuredDocument: The processed document
18
+ """
19
+ document = StructuredDocument(
20
+ title=Path(file_path).stem,
21
+ source_file=file_path,
22
+ doc_type=DocumentType.EXCEL
23
+ )
24
+
25
+ try:
26
+ # Get all sheet names
27
+ xl = pd.ExcelFile(file_path)
28
+ sheet_names = xl.sheet_names
29
+
30
+ # Process all sheets and create complete markdown
31
+ all_markdown = []
32
+
33
+ # Add document title - make it more descriptive
34
+ # Clean up the filename to remove temporary path prefixes
35
+ filename = Path(file_path).stem
36
+ # Remove temporary filename prefixes like med_temp_XXX_
37
+ if 'med_temp_' in filename:
38
+ clean_filename = filename.split('_', 3)[-1] if len(filename.split('_')) > 3 else filename
39
+ else:
40
+ clean_filename = filename
41
+
42
+ all_markdown.append(f"# Excel Document: {clean_filename}\n\n")
43
+
44
+ # Track sheet data for document metadata
45
+ sheets_data = {}
46
+
47
+ # Process each sheet
48
+ for sheet_name in sheet_names:
49
+ # Read the sheet into a DataFrame
50
+ df = pd.read_excel(file_path, sheet_name=sheet_name)
51
+
52
+ # Skip empty sheets
53
+ if df.empty:
54
+ continue
55
+
56
+ # Create markdown for this sheet with better heading
57
+ sheet_markdown = f"## Sheet: {sheet_name}"
58
+
59
+ # Add row and column count for better context
60
+ row_count = len(df)
61
+ col_count = len(df.columns)
62
+ sheet_markdown += f" ({row_count} rows × {col_count} columns)\n\n"
63
+
64
+ # Convert DataFrame to markdown table
65
+ table_markdown = df.to_markdown(index=False)
66
+ if table_markdown:
67
+ sheet_markdown += table_markdown + "\n\n"
68
+ else:
69
+ # Fallback for complex tables
70
+ sheet_markdown += self._complex_df_to_markdown(df) + "\n\n"
71
+
72
+ # Add sheet data to metadata
73
+ sheets_data[sheet_name] = {
74
+ "rows": row_count,
75
+ "columns": col_count,
76
+ "column_names": df.columns.tolist()
77
+ }
78
+
79
+ # Create section for this sheet with improved title
80
+ section = DocumentSection(title=f"Sheet: {sheet_name} ({row_count} rows × {col_count} columns)", level=2)
81
+ section.metadata["sheet_name"] = sheet_name
82
+ section.metadata["rows"] = row_count
83
+ section.metadata["columns"] = col_count
84
+
85
+ # Add the table as a markdown element
86
+ section.add_element(DocumentElement(
87
+ content=table_markdown,
88
+ element_type="markdown"
89
+ ))
90
+
91
+ # Add section to document
92
+ document.add_section(section)
93
+
94
+ # Add to combined markdown
95
+ all_markdown.append(sheet_markdown)
96
+
97
+ # Store the combined markdown in metadata
98
+ document.metadata["markdown"] = "\n".join(all_markdown)
99
+ document.metadata["sheets"] = sheets_data
100
+ document.metadata["sheet_count"] = len(sheet_names)
101
+
102
+ except Exception as e:
103
+ print(f"Error processing Excel file: {str(e)}")
104
+ error_section = DocumentSection(title="Error")
105
+ error_section.add_element(DocumentElement(
106
+ content=f"Failed to process Excel file: {str(e)}",
107
+ element_type="paragraph"
108
+ ))
109
+ document.add_section(error_section)
110
+ document.metadata["error"] = str(e)
111
+
112
+ return document
113
+
114
+ def _complex_df_to_markdown(self, df: pd.DataFrame) -> str:
115
+ """
116
+ Convert complex DataFrames to markdown tables
117
+ Handles cases where the standard to_markdown might fail
118
+
119
+ Args:
120
+ df: DataFrame to convert
121
+
122
+ Returns:
123
+ str: Markdown table representation
124
+ """
125
+ # Create header row
126
+ header = "| " + " | ".join(str(col) for col in df.columns) + " |"
127
+
128
+ # Create separator row
129
+ separator = "| " + " | ".join(["---"] * len(df.columns)) + " |"
130
+
131
+ # Create data rows
132
+ rows = []
133
+ for _, row in df.iterrows():
134
+ row_str = "| " + " | ".join(str(val) if not pd.isna(val) else "" for val in row) + " |"
135
+ rows.append(row_str)
136
+
137
+ # Combine all parts
138
+ return header + "\n" + separator + "\n" + "\n".join(rows)
processors/text/ipynb_processor.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+ import json
4
+ import nbformat
5
+
6
+ class JupyterNotebookProcessor(BaseDocumentProcessor):
7
+ """Processor for Jupyter Notebooks"""
8
+
9
+ def process(self, file_path: str) -> StructuredDocument:
10
+ """Process Jupyter Notebook"""
11
+ document = StructuredDocument(
12
+ title=Path(file_path).stem,
13
+ source_file=file_path,
14
+ doc_type=DocumentType.JUPYTER
15
+ )
16
+
17
+ try:
18
+ # Parse the notebook
19
+ notebook = nbformat.read(file_path, as_version=4)
20
+
21
+ # Create a section for each cell
22
+ markdown_parts = []
23
+
24
+ # Add notebook title if available
25
+ if 'title' in notebook.metadata:
26
+ markdown_parts.append(f"# {notebook.metadata['title']}\n\n")
27
+
28
+ # Process cells
29
+ for i, cell in enumerate(notebook.cells):
30
+ cell_type = cell.cell_type
31
+
32
+ # For markdown cells, add the content directly
33
+ if cell_type == 'markdown':
34
+ markdown_parts.append(cell.source + "\n\n")
35
+
36
+ # Also add to document structure
37
+ section = DocumentSection(title=f"Markdown Cell {i+1}")
38
+ section.add_element(DocumentElement(
39
+ content=cell.source,
40
+ element_type="markdown"
41
+ ))
42
+ document.add_section(section)
43
+
44
+ # For code cells, format as code blocks with output
45
+ elif cell_type == 'code':
46
+ # Code
47
+ markdown_parts.append(f"```python\n{cell.source}\n```\n\n")
48
+
49
+ # Add code to document structure
50
+ section = DocumentSection(title=f"Code Cell {i+1}")
51
+ section.add_element(DocumentElement(
52
+ content=cell.source,
53
+ element_type="code",
54
+ metadata={"language": "python"}
55
+ ))
56
+
57
+ # Process output if available
58
+ if hasattr(cell, 'outputs') and cell.outputs:
59
+ outputs = []
60
+ for output in cell.outputs:
61
+ if output.output_type == 'stream':
62
+ outputs.append(f"```\n{output.text}\n```\n\n")
63
+ elif output.output_type == 'execute_result':
64
+ if 'text/plain' in output.data:
65
+ outputs.append(f"```\n{output.data['text/plain']}\n```\n\n")
66
+
67
+ if outputs:
68
+ output_content = "".join(outputs)
69
+ markdown_parts.append(f"**Output:**\n\n{output_content}")
70
+ section.add_element(DocumentElement(
71
+ content=output_content,
72
+ element_type="code_output"
73
+ ))
74
+
75
+ document.add_section(section)
76
+
77
+ # Store combined markdown in metadata
78
+ document.metadata["markdown"] = "".join(markdown_parts)
79
+
80
+ except Exception as e:
81
+ print(f"Error processing notebook: {e}")
82
+ # Create error section
83
+ error_section = DocumentSection(title="Error")
84
+ error_section.add_element(DocumentElement(
85
+ content=f"Failed to process notebook: {str(e)}",
86
+ element_type="paragraph"
87
+ ))
88
+ document.add_section(error_section)
89
+
90
+ return document
processors/text/markdown_processor.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+ import re
4
+
5
+ class MarkdownProcessor(BaseDocumentProcessor):
6
+ """Processor for Markdown and R Markdown files"""
7
+
8
+ def process(self, file_path: str) -> StructuredDocument:
9
+ """Process markdown file"""
10
+ doc_type = DocumentType.from_file_extension(file_path)
11
+
12
+ document = StructuredDocument(
13
+ title=Path(file_path).stem,
14
+ source_file=file_path,
15
+ doc_type=doc_type
16
+ )
17
+
18
+ try:
19
+ # Read markdown content
20
+ with open(file_path, 'r', encoding='utf-8') as f:
21
+ markdown_content = f.read()
22
+
23
+ # Extract sections based on headings
24
+ section_pattern = r'^(#{1,6})\s+(.+)$'
25
+ current_section = DocumentSection(title="Main Content")
26
+ current_content = []
27
+ last_position = 0
28
+
29
+ for match in re.finditer(section_pattern, markdown_content, re.MULTILINE):
30
+ # If we have content before this heading, add it to current section
31
+ if last_position < match.start():
32
+ content_before = markdown_content[last_position:match.start()].strip()
33
+ if content_before:
34
+ current_section.add_element(DocumentElement(
35
+ content=content_before,
36
+ element_type="markdown"
37
+ ))
38
+ current_content.append(content_before)
39
+
40
+ # Add current section to document if it has content
41
+ if current_content:
42
+ document.add_section(current_section)
43
+ current_content = []
44
+
45
+ # Create new section for this heading
46
+ level = len(match.group(1)) # Number of # characters
47
+ title = match.group(2)
48
+ current_section = DocumentSection(title=title, level=level)
49
+
50
+ last_position = match.end()
51
+
52
+ # Add any remaining content to the last section
53
+ if last_position < len(markdown_content):
54
+ remaining_content = markdown_content[last_position:].strip()
55
+ if remaining_content:
56
+ current_section.add_element(DocumentElement(
57
+ content=remaining_content,
58
+ element_type="markdown"
59
+ ))
60
+ current_content.append(remaining_content)
61
+
62
+ # Add the last section if it has content
63
+ if current_content:
64
+ document.add_section(current_section)
65
+
66
+ # For R Markdown, we could additionally parse and extract R code chunks
67
+ # but for now we'll just handle it as regular markdown
68
+
69
+ # Store the original markdown content
70
+ document.metadata["markdown"] = markdown_content
71
+
72
+ except Exception as e:
73
+ print(f"Error processing markdown file: {e}")
74
+ error_section = DocumentSection(title="Error")
75
+ error_section.add_element(DocumentElement(
76
+ content=f"Failed to process markdown file: {str(e)}",
77
+ element_type="paragraph"
78
+ ))
79
+ document.add_section(error_section)
80
+
81
+ return document
processors/text/pdf_processor.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, List, Optional, Union, Any
3
+ from pathlib import Path
4
+
5
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
6
+
7
+ class PDFProcessor(BaseDocumentProcessor):
8
+ """Processor for PDF documents"""
9
+
10
+ def __init__(self, use_vision_for_complex: bool = True):
11
+ """
12
+ Initialize PDF processor
13
+
14
+ Args:
15
+ use_vision_for_complex: Whether to use vision processing for complex PDFs
16
+ """
17
+ super().__init__()
18
+ self.use_vision_for_complex = use_vision_for_complex
19
+
20
+ def process(self, file_path: str) -> StructuredDocument:
21
+ """Process PDF document"""
22
+ # First try text extraction
23
+ document = self._extract_text(file_path)
24
+
25
+ # If text extraction didn't yield much content and vision processing is enabled,
26
+ # fall back to vision processing
27
+ content_size = sum(len(element.content) for section in document.sections for element in section.elements)
28
+ if content_size < 500 and self.use_vision_for_complex:
29
+ from processors.vision.vision_processor import VisionDocumentProcessor
30
+ try:
31
+ return VisionDocumentProcessor().process(file_path)
32
+ except Exception as e:
33
+ # If vision processing fails, return what we got from text extraction
34
+ print(f"Vision processing failed: {e}")
35
+ pass
36
+
37
+ return document
38
+
39
+ def _extract_text(self, file_path: str) -> StructuredDocument:
40
+ """Extract text from PDF using PyPDF2"""
41
+ from PyPDF2 import PdfReader
42
+
43
+ document = StructuredDocument(
44
+ title=Path(file_path).stem,
45
+ source_file=file_path,
46
+ doc_type=DocumentType.PDF
47
+ )
48
+
49
+ try:
50
+ reader = PdfReader(file_path)
51
+
52
+ # Simple processing - create one section per page
53
+ for i, page in enumerate(reader.pages):
54
+ text = page.extract_text()
55
+ if not text.strip():
56
+ continue
57
+
58
+ section = DocumentSection(title=f"Page {i+1}")
59
+ section.metadata["page"] = i+1
60
+
61
+ # Simple paragraph splitting
62
+ paragraphs = text.split("\n\n")
63
+ for para in paragraphs:
64
+ if not para.strip():
65
+ continue
66
+
67
+ # Try to detect if paragraph is a heading
68
+ element_type = "paragraph"
69
+ metadata = {}
70
+
71
+ # Simple heuristic: if paragraph is short and ends with colon, it might be a heading
72
+ if len(para) < 100 and para.strip().endswith(":"):
73
+ element_type = "heading"
74
+ metadata["level"] = 2
75
+
76
+ section.add_element(DocumentElement(
77
+ content=para.strip(),
78
+ element_type=element_type,
79
+ metadata=metadata
80
+ ))
81
+
82
+ document.add_section(section)
83
+
84
+ except Exception as e:
85
+ print(f"Error extracting text from PDF: {e}")
86
+ # Create error section
87
+ error_section = DocumentSection(title="Error")
88
+ error_section.add_element(DocumentElement(
89
+ content=f"Failed to extract text: {str(e)}",
90
+ element_type="paragraph"
91
+ ))
92
+ document.add_section(error_section)
93
+
94
+ return document
processors/text/pptx_processor.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+ from pptx import Presentation
4
+
5
+ class PowerPointProcessor(BaseDocumentProcessor):
6
+ """Processor for PowerPoint (PPTX) documents"""
7
+
8
+ def process(self, file_path: str) -> StructuredDocument:
9
+ """Process PowerPoint presentation"""
10
+ document = StructuredDocument(
11
+ title=Path(file_path).stem,
12
+ source_file=file_path,
13
+ doc_type=DocumentType.POWERPOINT
14
+ )
15
+
16
+ try:
17
+ # Open presentation
18
+ presentation = Presentation(file_path)
19
+
20
+ # Storage for markdown output
21
+ markdown_parts = []
22
+ markdown_parts.append(f"# {Path(file_path).stem}\n\n")
23
+
24
+ # Process each slide
25
+ for i, slide in enumerate(presentation.slides):
26
+ # Create section for each slide
27
+ slide_section = DocumentSection(title=f"Slide {i+1}", level=1)
28
+ slide_section.metadata["slide_number"] = i+1
29
+
30
+ # Add slide title to markdown
31
+ markdown_parts.append(f"## Slide {i+1}\n\n")
32
+
33
+ # Process slide title if available
34
+ if slide.shapes.title:
35
+ title_text = slide.shapes.title.text
36
+ markdown_parts.append(f"### {title_text}\n\n")
37
+
38
+ title_element = DocumentElement(
39
+ content=title_text,
40
+ element_type="heading",
41
+ metadata={"level": 3}
42
+ )
43
+ slide_section.add_element(title_element)
44
+
45
+ # Process text elements
46
+ for shape in slide.shapes:
47
+ if hasattr(shape, "text") and shape.text.strip() and shape != slide.shapes.title:
48
+ shape_text = shape.text.strip()
49
+ # Skip if it's same as title
50
+ if slide.shapes.title and shape_text == slide.shapes.title.text:
51
+ continue
52
+
53
+ markdown_parts.append(f"{shape_text}\n\n")
54
+
55
+ # Add text as paragraph
56
+ text_element = DocumentElement(
57
+ content=shape_text,
58
+ element_type="paragraph"
59
+ )
60
+ slide_section.add_element(text_element)
61
+
62
+ # Add notes if available
63
+ if slide.has_notes_slide and slide.notes_slide.notes_text_frame.text.strip():
64
+ notes_text = slide.notes_slide.notes_text_frame.text.strip()
65
+ markdown_parts.append(f"**Notes:**\n\n{notes_text}\n\n")
66
+
67
+ notes_element = DocumentElement(
68
+ content=notes_text,
69
+ element_type="paragraph",
70
+ metadata={"is_notes": True}
71
+ )
72
+ slide_section.add_element(notes_element)
73
+
74
+ # Add divider between slides
75
+ markdown_parts.append("---\n\n")
76
+
77
+ # Add section to document
78
+ document.add_section(slide_section)
79
+
80
+ # Store combined markdown
81
+ document.metadata["markdown"] = "".join(markdown_parts)
82
+
83
+ except Exception as e:
84
+ print(f"Error processing PowerPoint: {e}")
85
+ error_section = DocumentSection(title="Error")
86
+ error_section.add_element(DocumentElement(
87
+ content=f"Failed to process PowerPoint: {str(e)}",
88
+ element_type="paragraph"
89
+ ))
90
+ document.add_section(error_section)
91
+
92
+ return document
processors/text/text_processor.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
2
+ from pathlib import Path
3
+
4
+ class TextProcessor(BaseDocumentProcessor):
5
+ """Processor for plain text files"""
6
+
7
+ def process(self, file_path: str) -> StructuredDocument:
8
+ """Process text file"""
9
+ document = StructuredDocument(
10
+ title=Path(file_path).stem,
11
+ source_file=file_path,
12
+ doc_type=DocumentType.TEXT
13
+ )
14
+
15
+ try:
16
+ # Read text content
17
+ with open(file_path, 'r', encoding='utf-8') as f:
18
+ text_content = f.read()
19
+
20
+ # Create document structure
21
+ main_section = DocumentSection(title="Text Content")
22
+ main_section.add_element(DocumentElement(
23
+ content=text_content,
24
+ element_type="paragraph"
25
+ ))
26
+ document.add_section(main_section)
27
+
28
+ # Generate markdown representation
29
+ markdown = f"# {Path(file_path).name}\n\n{text_content}\n"
30
+ document.metadata["markdown"] = markdown
31
+
32
+ except Exception as e:
33
+ print(f"Error processing text file: {e}")
34
+ error_section = DocumentSection(title="Error")
35
+ error_section.add_element(DocumentElement(
36
+ content=f"Failed to process text file: {str(e)}",
37
+ element_type="paragraph"
38
+ ))
39
+ document.add_section(error_section)
40
+
41
+ return document
processors/vision/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ Vision-based document processors using Qwen2.5-VL.
3
+ """
4
+
5
+ from processors.vision.vision_processor import VisionDocumentProcessor
6
+
7
+ __all__ = ['VisionDocumentProcessor']
processors/vision/vision_processor.py ADDED
@@ -0,0 +1,658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ from typing import Dict, List, Optional, Union, Any, Tuple
4
+ from pathlib import Path
5
+ from io import BytesIO
6
+
7
+ from PIL import Image
8
+ from openai import OpenAI
9
+
10
+ from processors.base import BaseDocumentProcessor, StructuredDocument, DocumentSection, DocumentElement, DocumentType
11
+
12
+ class VisionDocumentProcessor(BaseDocumentProcessor):
13
+ """Document processor using Qwen2.5-VL via OpenAI-compatible API"""
14
+
15
+ # Configurable remote model and API settings
16
+ MODEL_PATH = "Qwen/Qwen2.5-VL-32B-Instruct:featherless-ai"
17
+ BASE_URL = "https://router.huggingface.co/v1"
18
+ API_KEY = None
19
+
20
+ @classmethod
21
+ def configure_api(
22
+ cls,
23
+ api_key=None,
24
+ base_url=None,
25
+ model=None,
26
+ ):
27
+ """
28
+ Configure class-level API settings for the remote vision model.
29
+ """
30
+ if api_key:
31
+ cls.API_KEY = api_key
32
+
33
+ if base_url:
34
+ cls.BASE_URL = base_url
35
+
36
+ if model:
37
+ cls.MODEL_PATH = model
38
+
39
+ def __init__(
40
+ self,
41
+ api_key: Optional[str] = None,
42
+ base_url: Optional[str] = None,
43
+ model: Optional[str] = None,
44
+ temperature: float = 0.0,
45
+ max_tokens: Optional[int] = None
46
+ ):
47
+ """
48
+ Initialize remote vision document processor with OpenAI SDK
49
+
50
+ Args:
51
+ api_key: API key for the remote service
52
+ base_url: Base URL for the OpenAI compatible endpoint
53
+ model: Remote model identifier
54
+ temperature: Temperature for generation (0.0-1.0).
55
+ max_tokens: Maximum tokens to generate.
56
+ """
57
+ super().__init__()
58
+
59
+ self.temperature = float(temperature) if temperature is not None else 0.0
60
+ self.max_tokens = int(max_tokens) if max_tokens is not None else 4000
61
+
62
+ self.model = model or self.MODEL_PATH
63
+ api_key = (
64
+ api_key
65
+ or self.API_KEY
66
+ or os.getenv("HF_TOKEN")
67
+ )
68
+ self.client = OpenAI(
69
+ base_url=base_url or self.BASE_URL,
70
+ api_key=api_key,
71
+ )
72
+
73
+ def process(self, file_path: str, max_concurrent: int = 2, images_per_batch: int = 1,
74
+ dynamic_batching: bool = True, max_tokens_per_batch: int = 4000) -> StructuredDocument:
75
+ """
76
+ Process document using remote vision model
77
+
78
+ Args:
79
+ file_path: Path to document file
80
+ max_concurrent: Maximum number of concurrent API calls for PDFs
81
+ images_per_batch: Number of consecutive pages to process in a single API call
82
+ (1 = traditional single page processing, 2+ = multi-image processing)
83
+ dynamic_batching: Whether to dynamically determine batch sizes based on image complexity
84
+ max_tokens_per_batch: Maximum tokens per batch when using dynamic batching
85
+
86
+ Returns:
87
+ StructuredDocument: Processed document
88
+ """
89
+ # Get document type
90
+ doc_type = DocumentType.from_file_extension(file_path)
91
+
92
+ # Create document with basic metadata
93
+ document = StructuredDocument(
94
+ title=Path(file_path).stem,
95
+ source_file=file_path,
96
+ doc_type=doc_type
97
+ )
98
+
99
+ # If file is PDF, process each page separately
100
+ if (doc_type == DocumentType.PDF):
101
+ if images_per_batch > 1:
102
+ # Use multi-image processing if specified
103
+ self._process_pdf_multi(file_path, document, max_concurrent, images_per_batch,
104
+ dynamic_batching, max_tokens_per_batch)
105
+ else:
106
+ # Use original single-page processing
107
+ self._process_pdf(file_path, document, max_concurrent)
108
+
109
+ # Ensure page markers are removed from final document output
110
+ if "markdown" in document.metadata:
111
+ document.metadata["markdown"] = self._remove_page_markers(document.metadata["markdown"])
112
+
113
+ # Also update section content to remove page markers
114
+ for section in document.sections:
115
+ for element in section.elements:
116
+ if element.element_type == "markdown":
117
+ element.content = self._remove_page_markers(element.content)
118
+
119
+ else:
120
+ # For images and other document types
121
+ image_content = self._prepare_image(file_path)
122
+
123
+ # Get markdown response directly
124
+ markdown_response = self._call_api(image_content)
125
+
126
+ # Create a section for the entire document
127
+ section = DocumentSection(title=Path(file_path).stem, level=1)
128
+ section.add_element(DocumentElement(
129
+ content=markdown_response,
130
+ element_type="markdown"
131
+ ))
132
+ document.add_section(section)
133
+
134
+ # Store raw markdown in metadata
135
+ document.metadata["markdown"] = markdown_response
136
+
137
+ return document
138
+
139
+ def _prepare_image(self, file_path: str) -> str:
140
+ """Read image file and encode as base64"""
141
+ with open(file_path, "rb") as image_file:
142
+ return base64.b64encode(image_file.read()).decode("utf-8")
143
+
144
+ def _process_pdf(self, file_path: str, document: StructuredDocument, max_concurrent: int = 2) -> None:
145
+ """
146
+ Process PDF document with parallel page processing
147
+ """
148
+ from pdf2image import convert_from_path
149
+ import tempfile
150
+ import concurrent.futures
151
+
152
+ # Storage for combined markdown from all pages
153
+ all_markdown = [None] * 0 # Will resize based on page count
154
+ all_sections = [None] * 0 # Will resize based on page count
155
+
156
+ try:
157
+ # Convert PDF to images
158
+ with tempfile.TemporaryDirectory() as path:
159
+ # Check if we can convert the PDF
160
+ try:
161
+ print("Converting PDF to images...")
162
+ images = convert_from_path(file_path)
163
+ page_count = len(images)
164
+ print(f"Converted {page_count} pages")
165
+
166
+ # Resize result arrays
167
+ all_markdown = [None] * page_count
168
+ all_sections = [None] * page_count
169
+
170
+ except Exception as e:
171
+ print(f"Error converting PDF to images: {str(e)}")
172
+ error_section = DocumentSection(title="Error")
173
+ error_section.add_element(DocumentElement(
174
+ content=f"Failed to convert PDF to images: {str(e)}",
175
+ element_type="paragraph"
176
+ ))
177
+ document.add_section(error_section)
178
+ return
179
+
180
+ # Save all images first
181
+ image_paths = []
182
+ for i, image in enumerate(images):
183
+ temp_image_path = os.path.join(path, f"page_{i+1}.jpg")
184
+ image.save(temp_image_path, "JPEG")
185
+ image_paths.append((i, temp_image_path))
186
+
187
+ # Process pages in parallel
188
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent) as executor:
189
+ # Submit all tasks and store the futures
190
+ future_to_page = {
191
+ executor.submit(self._process_single_page, path, index, temp_path): (index, temp_path)
192
+ for index, temp_path in image_paths
193
+ }
194
+
195
+ # Process completed tasks as they finish
196
+ completed = 0
197
+ for future in concurrent.futures.as_completed(future_to_page):
198
+ index, temp_path = future_to_page[future]
199
+ try:
200
+ section, markdown = future.result()
201
+ all_sections[index] = section
202
+ all_markdown[index] = markdown
203
+ completed += 1
204
+ print(f"Completed page {index+1}/{page_count} ({completed}/{page_count} done)")
205
+ except Exception as e:
206
+ print(f"Error processing page {index+1}: {str(e)}")
207
+ error_section = DocumentSection(title=f"Error on Page {index+1}")
208
+ error_section.add_element(DocumentElement(
209
+ content=f"Failed to process page: {str(e)}",
210
+ element_type="paragraph"
211
+ ))
212
+ all_sections[index] = error_section
213
+ all_markdown[index] = f"## Page {index+1}\n\nError processing page: {str(e)}"
214
+
215
+ # Add all sections to document in correct order
216
+ for section in all_sections:
217
+ if section:
218
+ document.add_section(section)
219
+
220
+ # Combine all markdown
221
+ valid_markdown = [md for md in all_markdown if md]
222
+ if valid_markdown:
223
+ document.metadata["markdown"] = "\n\n".join(valid_markdown)
224
+
225
+ except Exception as e:
226
+ print(f"Error in PDF processing: {str(e)}")
227
+ error_section = DocumentSection(title="Error")
228
+ error_section.add_element(DocumentElement(
229
+ content=f"Failed to process PDF: {str(e)}",
230
+ element_type="paragraph"
231
+ ))
232
+ document.add_section(error_section)
233
+
234
+ def _process_pdf_multi(self, file_path: str, document: StructuredDocument, max_concurrent: int = 2, images_per_batch: int = 2,
235
+ dynamic_batching: bool = True, max_tokens_per_batch: int = 4000) -> None:
236
+ """
237
+ Process PDF document with multi-image batches for improved context
238
+ """
239
+ from pdf2image import convert_from_path
240
+ import tempfile
241
+ import concurrent.futures
242
+
243
+ # Storage for combined markdown from all pages
244
+ all_markdown = []
245
+ all_sections = []
246
+
247
+ try:
248
+ # Convert PDF to images
249
+ with tempfile.TemporaryDirectory() as path:
250
+ # Check if we can convert the PDF
251
+ try:
252
+ print("Converting PDF to images...")
253
+ images = convert_from_path(file_path)
254
+ page_count = len(images)
255
+ print(f"Converted {page_count} pages")
256
+
257
+ # Initialize result arrays
258
+ all_markdown = [None] * page_count
259
+ all_sections = [None] * page_count
260
+
261
+ except Exception as e:
262
+ print(f"Error converting PDF to images: {str(e)}")
263
+ error_section = DocumentSection(title="Error")
264
+ error_section.add_element(DocumentElement(
265
+ content=f"Failed to convert PDF to images: {str(e)}",
266
+ element_type="paragraph"
267
+ ))
268
+ document.add_section(error_section)
269
+ return
270
+
271
+ # Save all images first
272
+ image_paths = []
273
+ for i, image in enumerate(images):
274
+ temp_image_path = os.path.join(path, f"page_{i+1}.jpg")
275
+ image.save(temp_image_path, "JPEG")
276
+ image_paths.append((i, temp_image_path))
277
+
278
+ # Create batch groups
279
+ batch_tasks = []
280
+
281
+ for i in range(0, len(image_paths), images_per_batch):
282
+ batch = image_paths[i:i+images_per_batch]
283
+ if batch:
284
+ # For each batch, all indices and paths
285
+ indices = [idx for idx, _ in batch]
286
+ paths = [p for _, p in batch]
287
+ batch_tasks.append((indices, paths))
288
+
289
+ print(f"Processing PDF with {len(batch_tasks)} batches of up to {images_per_batch} pages each")
290
+
291
+ # Process batches in parallel
292
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent) as executor:
293
+ # Submit all batch tasks
294
+ future_to_batch = {
295
+ executor.submit(self._process_multi_page_batch, path, indices, paths): (indices, paths)
296
+ for indices, paths in batch_tasks
297
+ }
298
+
299
+ # Process completed batch tasks
300
+ completed_batches = 0
301
+ for future in concurrent.futures.as_completed(future_to_batch):
302
+ indices, paths = future_to_batch[future]
303
+ try:
304
+ results = future.result() # List of (section, markdown) tuples
305
+
306
+ # Store results in correct positions
307
+ for i, (idx, result) in enumerate(zip(indices, results)):
308
+ if result:
309
+ section, markdown = result
310
+ all_sections[idx] = section
311
+ all_markdown[idx] = markdown
312
+
313
+ completed_batches += 1
314
+ processed_pages = min(completed_batches * images_per_batch, page_count)
315
+ print(f"Completed batch {completed_batches}/{len(batch_tasks)} ({processed_pages}/{page_count} pages)")
316
+
317
+ except Exception as e:
318
+ print(f"Error processing batch {indices}: {str(e)}")
319
+ # Create error sections for failed pages
320
+ for idx in indices:
321
+ page_num = idx + 1
322
+ error_section = DocumentSection(title=f"Error on Page {page_num}")
323
+ error_section.add_element(DocumentElement(
324
+ content=f"Failed to process page batch: {str(e)}",
325
+ element_type="paragraph"
326
+ ))
327
+ all_sections[idx] = error_section
328
+ all_markdown[idx] = f"## Page {page_num}\n\nError processing page: {str(e)}"
329
+
330
+ # Add all sections to document in correct order
331
+ for section in all_sections:
332
+ if section:
333
+ document.add_section(section)
334
+
335
+ # Combine all markdown
336
+ valid_markdown = [md for md in all_markdown if md]
337
+ if valid_markdown:
338
+ document.metadata["markdown"] = "\n\n".join(valid_markdown)
339
+
340
+ except Exception as e:
341
+ print(f"Error in PDF processing: {str(e)}")
342
+ error_section = DocumentSection(title="Error")
343
+ error_section.add_element(DocumentElement(
344
+ content=f"Failed to process PDF: {str(e)}",
345
+ element_type="paragraph"
346
+ ))
347
+ document.add_section(error_section)
348
+
349
+ def _process_single_page(self, temp_dir: str, page_index: int, image_path: str) -> tuple:
350
+ """
351
+ Process a single page in the PDF
352
+ """
353
+ print(f"Started processing page {page_index+1}")
354
+ page_num = page_index + 1 # Convert to 1-based page numbers for display
355
+
356
+ # Process image
357
+ image_content = self._prepare_image(image_path)
358
+ markdown_response = self._call_api(image_content)
359
+
360
+ # Create section for this page
361
+ section = DocumentSection(title=f"Page {page_num}", level=1)
362
+ section.metadata["page"] = page_num
363
+ section.add_element(DocumentElement(
364
+ content=markdown_response,
365
+ element_type="markdown"
366
+ ))
367
+
368
+ # Format markdown with page indicator
369
+ page_markdown = f"## Page {page_num}\n\n{markdown_response}"
370
+
371
+ return section, page_markdown
372
+
373
+ def _call_api(self, image_content: str) -> str:
374
+ """
375
+ Run remote vision model inference using OpenAI compatible API
376
+
377
+ Args:
378
+ image_content: Base64 encoded image
379
+
380
+ Returns:
381
+ str: Markdown representation of the document
382
+ """
383
+ prompt_text = (
384
+ "Convert this document into clean Markdown.\n"
385
+ "Requirements:\n"
386
+ "- preserve headings\n"
387
+ "- preserve tables\n"
388
+ "- preserve lists\n"
389
+ "- preserve code blocks\n"
390
+ "- preserve formatting\n"
391
+ "- preserve hierarchy\n"
392
+ "- do not include page numbers\n"
393
+ "- output Markdown only\n"
394
+ "- do not explain your reasoning"
395
+ )
396
+
397
+ print("Running remote model inference...")
398
+ try:
399
+ data_uri = f"data:image/jpeg;base64,{image_content}"
400
+
401
+ messages = [
402
+ {
403
+ "role": "user",
404
+ "content": [
405
+ {
406
+ "type": "text",
407
+ "text": prompt_text
408
+ },
409
+ {
410
+ "type": "image_url",
411
+ "image_url": {
412
+ "url": data_uri
413
+ }
414
+ }
415
+ ]
416
+ }
417
+ ]
418
+
419
+ response = self.client.chat.completions.create(
420
+ model=self.model,
421
+ messages=messages,
422
+ max_tokens=self.max_tokens,
423
+ temperature=self.temperature,
424
+ )
425
+
426
+ markdown_response = response.choices[0].message.content
427
+ print("\nMarkdown response generated successfully")
428
+ return markdown_response
429
+
430
+ except Exception as e:
431
+ print(f"Error during remote inference: {str(e)}")
432
+ return f"# Model Error\n\nFailed to process document: {str(e)}"
433
+
434
+ def _call_api_multi_image(self, image_contents: list, page_numbers: list) -> str:
435
+ """
436
+ Run remote vision model inference with multiple images
437
+
438
+ Args:
439
+ image_contents: List of base64 encoded images
440
+ page_numbers: List of page numbers for these images
441
+
442
+ Returns:
443
+ str: Markdown representation of the document across multiple pages
444
+ """
445
+ print(f"Processing {len(image_contents)} images remotely")
446
+
447
+ try:
448
+ content = []
449
+
450
+ for img in image_contents:
451
+ content.append({
452
+ "type": "image_url",
453
+ "image_url": {
454
+ "url": f"data:image/jpeg;base64,{img}"
455
+ }
456
+ })
457
+
458
+ prompt_text = (
459
+ f"These are consecutive pages from a document (pages {', '.join(map(str, page_numbers))}). "
460
+ "Convert this document into clean Markdown.\n"
461
+ "Requirements:\n"
462
+ "- preserve headings\n"
463
+ "- preserve tables\n"
464
+ "- preserve lists\n"
465
+ "- preserve code blocks\n"
466
+ "- preserve formatting\n"
467
+ "- preserve hierarchy\n"
468
+ "- do not include page numbers\n"
469
+ "- output Markdown only\n"
470
+ "- do not explain your reasoning"
471
+ )
472
+
473
+ content.append({
474
+ "type": "text",
475
+ "text": prompt_text
476
+ })
477
+
478
+ response = self.client.chat.completions.create(
479
+ model=self.model,
480
+ messages=[
481
+ {
482
+ "role": "user",
483
+ "content": content
484
+ }
485
+ ],
486
+ max_tokens=self.max_tokens,
487
+ temperature=self.temperature,
488
+ )
489
+
490
+ return response.choices[0].message.content
491
+
492
+ except Exception as e:
493
+ print(f"Multi-image inference failed ({str(e)}). Falling back to sequential processing.")
494
+
495
+ # Fallback to sequential execution if context window or multi-image processing fails
496
+ results = []
497
+ for img_content, page_num in zip(image_contents, page_numbers):
498
+ print(f"Running fallback inference for page {page_num}...")
499
+ page_markdown = self._call_api(img_content)
500
+ results.append(f"## Page {page_num}\n\n{page_markdown}")
501
+
502
+ return "\n\n".join(results)
503
+
504
+ def _process_multi_page_batch(self, temp_dir: str, page_indices: list, image_paths: list) -> list:
505
+ """
506
+ Process multiple pages in a single API call
507
+ """
508
+ page_numbers = [idx+1 for idx in page_indices] # Convert to 1-based page numbers
509
+ print(f"Processing batch with pages: {page_numbers}")
510
+
511
+ # Encode all images in the batch
512
+ image_contents = []
513
+ for path in image_paths:
514
+ image_contents.append(self._prepare_image(path))
515
+
516
+ # Call API with multiple images
517
+ markdown_response = self._call_api_multi_image(image_contents, page_numbers)
518
+
519
+ # Split response by page markers
520
+ results = []
521
+
522
+ try:
523
+ # Check if it's a multi-page response with page markers
524
+ if "## Page" in markdown_response:
525
+ # Attempt to split by page markers
526
+ parts = []
527
+ current_part = []
528
+ current_page_idx = None
529
+
530
+ for line in markdown_response.split("\n"):
531
+ if line.strip().startswith("## Page"):
532
+ # If we already have content for a page, save it
533
+ if current_part and current_page_idx is not None:
534
+ parts.append((current_page_idx, "\n".join(current_part)))
535
+
536
+ # Start a new page
537
+ current_part = [line]
538
+
539
+ # Extract page number from heading
540
+ try:
541
+ page_text = line.strip().replace("## Page", "").strip()
542
+ current_page_idx = int(page_text) - 1 # Convert to 0-based index
543
+ except:
544
+ # If we can't extract page number, use position in batch
545
+ current_page_idx = len(parts)
546
+ else:
547
+ current_part.append(line)
548
+
549
+ # Add the last part
550
+ if current_part and current_page_idx is not None:
551
+ parts.append((current_page_idx, "\n".join(current_part)))
552
+
553
+ # Create sections for each identified page
554
+ for page_idx, content in parts:
555
+ if 0 <= page_idx < len(page_indices):
556
+ idx = page_indices[page_idx]
557
+ page_num = idx + 1
558
+
559
+ section = DocumentSection(title=f"Page {page_num}", level=1)
560
+ section.metadata["page"] = page_num
561
+ section.add_element(DocumentElement(
562
+ content=content,
563
+ element_type="markdown"
564
+ ))
565
+
566
+ # Include page heading if not already there
567
+ if not content.strip().startswith("## Page"):
568
+ page_markdown = f"## Page {page_num}\n\n{content}"
569
+ else:
570
+ page_markdown = content
571
+
572
+ results.append((section, page_markdown))
573
+ else:
574
+ # If no page markers are found, distribute content evenly
575
+ # This is a fallback and likely to be less accurate
576
+ for i, idx in enumerate(page_indices):
577
+ page_num = idx + 1
578
+
579
+ # Simple approach - split content by number of pages
580
+ chunk_size = max(1, len(markdown_response) // len(page_indices))
581
+ start_pos = i * chunk_size
582
+ end_pos = start_pos + chunk_size if i < len(page_indices)-1 else len(markdown_response)
583
+
584
+ content = markdown_response[start_pos:end_pos]
585
+
586
+ section = DocumentSection(title=f"Page {page_num}", level=1)
587
+ section.metadata["page"] = page_num
588
+ section.add_element(DocumentElement(
589
+ content=content,
590
+ element_type="markdown"
591
+ ))
592
+
593
+ page_markdown = f"## Page {page_num}\n\n{content}"
594
+ results.append((section, page_markdown))
595
+ except Exception as e:
596
+ print(f"Error parsing multi-page response: {str(e)}")
597
+ # Fallback - create an error section for each page
598
+ for idx in page_indices:
599
+ page_num = idx + 1
600
+ error_content = f"Error parsing multi-page response: {str(e)}"
601
+
602
+ section = DocumentSection(title=f"Page {page_num}", level=1)
603
+ section.metadata["page"] = page_num
604
+ section.add_element(DocumentElement(
605
+ content=error_content,
606
+ element_type="paragraph"
607
+ ))
608
+
609
+ page_markdown = f"## Page {page_num}\n\n{error_content}"
610
+ results.append((section, page_markdown))
611
+
612
+ # Ensure we have a result for each page in the batch
613
+ while len(results) < len(page_indices):
614
+ idx = page_indices[len(results)]
615
+ page_num = idx + 1
616
+ error_message = "No content was generated for this page in the batch."
617
+
618
+ section = DocumentSection(title=f"Page {page_num}", level=1)
619
+ section.metadata["page"] = page_num
620
+ section.add_element(DocumentElement(
621
+ content=error_message,
622
+ element_type="paragraph"
623
+ ))
624
+
625
+ page_markdown = f"## Page {page_num}\n\n{error_message}"
626
+ results.append((section, page_markdown))
627
+
628
+ return results
629
+
630
+ def _remove_page_markers(self, content: str) -> str:
631
+ """
632
+ Remove page marker headings from the markdown content
633
+ """
634
+ if not content:
635
+ return content
636
+
637
+ # Split by lines and filter out page marker headings
638
+ lines = content.split('\n')
639
+ filtered_lines = []
640
+ skip_next_empty = False
641
+
642
+ for line in lines:
643
+ # Check if line is a page marker (## Page X)
644
+ if line.strip().startswith('## Page '):
645
+ skip_next_empty = True # Skip the next empty line if it exists
646
+ continue
647
+
648
+ # Skip empty line after page marker
649
+ if skip_next_empty and not line.strip():
650
+ skip_next_empty = False
651
+ continue
652
+
653
+ filtered_lines.append(line)
654
+
655
+ # Rejoin the filtered lines
656
+ cleaned_content = '\n'.join(filtered_lines)
657
+
658
+ return cleaned_content
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ requests>=2.31.0
3
+ pillow>=10.0.0
4
+ beautifulsoup4>=4.12.0
5
+ pydantic>=2.5.0
6
+ tqdm>=4.66.0
7
+ openai>=1.30.0
8
+
9
+ # Document handling
10
+ PyPDF2>=3.0.0
11
+ pdf2image>=1.16.3
12
+ python-docx>=1.0.0
13
+ python-pptx>=0.6.21
14
+ nbformat>=5.9.0
15
+ markdown>=3.5.1
16
+ tree-sitter>=0.20.1
17
+ mammoth>=1.6.0
18
+ docx2python>=1.0.0
19
+ pandas>=2.0.0
20
+ openpyxl>=3.1.0
21
+ tabulate>=0.9.0
22
+
23
+ # UI Framework
24
+ gradio>=4.13.0
ui/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # UI package
ui/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (140 Bytes). View file
 
ui/__pycache__/gradio_app.cpython-312.pyc ADDED
Binary file (2.57 kB). View file
 
ui/gradio_app.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from processors.base import BaseDocumentProcessor, DocumentType
4
+ from processors.vision.vision_processor import VisionDocumentProcessor
5
+
6
+ MODEL_NAME = "Qwen/Qwen2.5-VL-32B-Instruct:featherless-ai"
7
+
8
+
9
+ def convert_document(
10
+ uploaded_file,
11
+ force_vision,
12
+ temperature,
13
+ ):
14
+ if uploaded_file is None:
15
+ return "Please upload a document."
16
+
17
+ try:
18
+ doc_type = DocumentType.from_file_extension(uploaded_file)
19
+
20
+ if force_vision or doc_type == DocumentType.IMAGE:
21
+ processor = VisionDocumentProcessor(
22
+ temperature=temperature,
23
+ )
24
+ else:
25
+ processor = BaseDocumentProcessor.get_processor(uploaded_file)
26
+
27
+ document = processor.process(uploaded_file)
28
+
29
+ return document.to_markdown()
30
+
31
+ except Exception as e:
32
+ return f"Error:\n\n{str(e)}"
33
+
34
+
35
+ def create_ui():
36
+
37
+ with gr.Blocks(title="VisionMark") as demo:
38
+
39
+ gr.Markdown("# VisionMark")
40
+ gr.Markdown(
41
+ "Convert PDFs and images into structured Markdown using a Vision Language Model."
42
+ )
43
+
44
+ file_input = gr.File(
45
+ label="Upload Document",
46
+ type="filepath",
47
+ )
48
+
49
+ force_vision = gr.Checkbox(
50
+ value=False,
51
+ label="Force Vision Processing",
52
+ )
53
+
54
+ gr.Markdown(f"**Model:** `{MODEL_NAME}`")
55
+
56
+ temperature = gr.Slider(
57
+ minimum=0.0,
58
+ maximum=1.0,
59
+ value=0.0,
60
+ step=0.1,
61
+ label="Temperature",
62
+ )
63
+
64
+ convert_btn = gr.Button("Convert")
65
+
66
+ output = gr.Code(
67
+ label="Markdown Output",
68
+ language="markdown",
69
+ )
70
+
71
+ convert_btn.click(
72
+ fn=convert_document,
73
+ inputs=[
74
+ file_input,
75
+ force_vision,
76
+ temperature,
77
+ ],
78
+ outputs=output,
79
+ )
80
+
81
+ return demo