super-document-processor 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ import os
2
+ try:
3
+ from super_document_processor.utils.chunker import collector
4
+ except ImportError:
5
+ from utils.chunker import collector
6
+
7
+ def process_plain_file(filename, original_filename=None):
8
+ display_name = os.path.basename(original_filename) if original_filename else os.path.basename(filename)
9
+ with open(filename, "r", encoding="utf-8", errors="replace") as f:
10
+ md_text = f.read()
11
+
12
+ # Add the raw content of the file
13
+ collector.add(md_text)
14
+
15
+ all_chunks = []
16
+ chunked_texts = collector.get_chunked_texts()
17
+ for i, chunk in enumerate(chunked_texts, 1):
18
+ all_chunks.append({
19
+ "content": f"--- Chunk {i} ---\n{chunk}",
20
+ "chunkIndex": i,
21
+ "filename": display_name
22
+ })
23
+
24
+ collector.clear()
25
+ return all_chunks
@@ -0,0 +1,367 @@
1
+ import xml.etree.ElementTree as ET
2
+ import os
3
+ import hashlib
4
+ import base64
5
+ import io
6
+ import re
7
+ import logging
8
+ import gc
9
+ import zipfile
10
+ import threading
11
+ import concurrent.futures
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Optional
14
+ from pptx import Presentation
15
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
16
+ from collections import defaultdict
17
+
18
+ logger = logging.getLogger("super_document_processor.pptx")
19
+ slides = None
20
+ drawing = None
21
+
22
+ def _ensure_aspose():
23
+ global slides, drawing
24
+ if slides is None or drawing is None:
25
+ import aspose.slides as _slides
26
+ import aspose.pydrawing as _drawing
27
+ slides = _slides
28
+ drawing = _drawing
29
+ try:
30
+ from super_document_processor.utils.vlm import generate_image_description
31
+ from super_document_processor.utils.pptx_components_extract import (
32
+ format_table_markdown,
33
+ extract_text_from_shape,
34
+ is_slide_header_shape,
35
+ extract_smartart_texts_per_slide,
36
+ extract_chart_data_from_pptx,
37
+ extract_embedded_excel_tables,
38
+ extract_emf_content_per_slide,
39
+ extract_emf_tables_and_text,
40
+ get_spatial_shape_reading_order,
41
+ get_pptx_card_boxes,
42
+ apply_pptx_box_decorations
43
+ )
44
+ from super_document_processor.utils.aspose_slide_renderer import render_slide_to_png_bytes
45
+ except ImportError:
46
+ from utils.vlm import generate_image_description
47
+ from utils.pptx_components_extract import (
48
+ format_table_markdown,
49
+ extract_text_from_shape,
50
+ is_slide_header_shape,
51
+ extract_smartart_texts_per_slide,
52
+ extract_chart_data_from_pptx,
53
+ extract_embedded_excel_tables,
54
+ extract_emf_content_per_slide,
55
+ extract_emf_tables_and_text,
56
+ get_spatial_shape_reading_order,
57
+ get_pptx_card_boxes,
58
+ apply_pptx_box_decorations
59
+ )
60
+ from utils.aspose_slide_renderer import render_slide_to_png_bytes
61
+
62
+ def clean_vlm_response(text: str) -> str:
63
+ """Removes Aspose evaluation watermarks from the text."""
64
+ if not text:
65
+ return text
66
+ patterns = [
67
+ r"Evaluation only\. Created with Aspose\..*?Copyright.*?\.",
68
+ r"Created with Aspose\..*?Copyright.*",
69
+ r"Aspose\.Slides for Python via \.NET.*",
70
+ r"Aspose\.Slides for \.NET.*",
71
+ r"Aspose\.Cells for \.NET.*",
72
+ r"Evaluation only\."
73
+ ]
74
+ cleaned = text
75
+ for pattern in patterns:
76
+ cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE)
77
+
78
+ cleaned = "\n".join([line for line in cleaned.splitlines() if line.strip()])
79
+ return cleaned.strip()
80
+
81
+ def get_slide_image_mapping(pptx_path):
82
+ prs = Presentation(pptx_path)
83
+ slide_partnames = [slide.part.partname.split('/')[-1] for slide in prs.slides]
84
+
85
+ slide_image_map = defaultdict(list)
86
+ with zipfile.ZipFile(pptx_path, 'r') as pptx:
87
+ for i, partname in enumerate(slide_partnames, start=1):
88
+ rels_path = f"ppt/slides/_rels/{partname}.rels"
89
+ slide_key = f"Slide {i}"
90
+ if rels_path not in pptx.namelist():
91
+ continue
92
+ rels_data = pptx.read(rels_path)
93
+ rels_root = ET.fromstring(rels_data)
94
+ ns = {"r": "http://schemas.openxmlformats.org/package/2006/relationships"}
95
+ for rel in rels_root.findall("r:Relationship", ns):
96
+ rel_type = rel.attrib.get("Type", "")
97
+ target = rel.attrib.get("Target", "").split("/")[-1]
98
+ ext = os.path.splitext(target)[1].lower()
99
+
100
+ if "relationships/image" in rel_type or ext in ['.png', '.jpg', '.jpeg', '.emf', '.wmf', '.svg', '.webp']:
101
+ slide_image_map[slide_key].append(target)
102
+ return slide_image_map
103
+
104
+ @dataclass
105
+ class SlideContext:
106
+ page_num: int
107
+ slide: Any
108
+ aspose_ppt: Any
109
+ slide_image_map: Dict[str, List[str]]
110
+ seen_image_hashes: Dict[str, str]
111
+ image_data_store: Dict[str, str]
112
+ image_page_map: Dict[str, List[int]]
113
+ filename: str
114
+ lock: threading.Lock
115
+ display_name: str
116
+ should_process_images: bool = False
117
+ model: Optional[str] = None
118
+
119
+ def process_pptx_slide(ctx: SlideContext):
120
+ slide_key = f"Slide {ctx.page_num}"
121
+ mapped_images = ctx.slide_image_map.get(slide_key, [])
122
+
123
+ # --- IMAGE HANDLING (when vision processing is requested) ---
124
+ image_descriptions = []
125
+ if ctx.should_process_images and mapped_images:
126
+ if len(mapped_images) > 5:
127
+ try:
128
+ aspose_slide = ctx.aspose_ppt.slides[ctx.page_num - 1]
129
+ image_bytes = render_slide_to_png_bytes(aspose_slide, scale=2.0)
130
+ raw_description = generate_image_description(image_bytes, model=ctx.model)
131
+ description = clean_vlm_response(raw_description)
132
+ if description:
133
+ image_descriptions.append(f"[Full-slide image description]:\n{description}")
134
+ except Exception as aspose_err:
135
+ if isinstance(aspose_err, ValueError):
136
+ raise
137
+ logger.warning(f"Aspose full slide render error on slide {ctx.page_num}: {aspose_err}")
138
+ else:
139
+ try:
140
+ aspose_slide = ctx.aspose_ppt.slides[ctx.page_num - 1]
141
+ except Exception as e:
142
+ aspose_slide = None
143
+ logger.warning(f"Could not load aspose_slide for slide {ctx.page_num}: {e}")
144
+
145
+ if aspose_slide is not None:
146
+ renderable_shapes = []
147
+ for shape_index, shape in enumerate(aspose_slide.shapes):
148
+ if isinstance(shape, (slides.PictureFrame, slides.OleObjectFrame)):
149
+ renderable_shapes.append((shape_index, shape))
150
+
151
+ def process_rendered_shape(shape_tuple):
152
+ idx, shape = shape_tuple
153
+ try:
154
+ shape_img = shape.get_image()
155
+ with io.BytesIO() as temp_stream:
156
+ shape_img.save(temp_stream, slides.ImageFormat.PNG)
157
+ temp_stream.seek(0)
158
+
159
+ src_img = drawing.Image.from_stream(temp_stream)
160
+ bitmap = drawing.Bitmap(src_img.width, src_img.height)
161
+ graphics = drawing.Graphics.from_image(bitmap)
162
+ graphics.clear(drawing.Color.white)
163
+ graphics.draw_image(src_img, 0, 0, src_img.width, src_img.height)
164
+
165
+ img_stream = io.BytesIO()
166
+ bitmap.save(img_stream, drawing.imaging.ImageFormat.png)
167
+ image_bytes = img_stream.getvalue()
168
+
169
+ if not image_bytes or len(image_bytes) < 10:
170
+ return (idx, None)
171
+
172
+ img_hash = hashlib.sha256(image_bytes).hexdigest()
173
+
174
+ with ctx.lock:
175
+ if ctx.page_num not in ctx.image_page_map[img_hash]:
176
+ ctx.image_page_map[img_hash].append(ctx.page_num)
177
+ if img_hash not in ctx.image_data_store:
178
+ ctx.image_data_store[img_hash] = base64.b64encode(image_bytes).decode("utf-8")
179
+ description = ctx.seen_image_hashes.get(img_hash)
180
+
181
+ if description is None:
182
+ try:
183
+ raw_description = generate_image_description(image_bytes, model=ctx.model)
184
+ description = clean_vlm_response(raw_description)
185
+ except Exception as e:
186
+ if isinstance(e, ValueError):
187
+ raise
188
+ description = f"[Error generating description: {e}]"
189
+ with ctx.lock:
190
+ ctx.seen_image_hashes[img_hash] = description
191
+
192
+ return (idx, description)
193
+ except Exception as e:
194
+ if isinstance(e, ValueError):
195
+ raise
196
+ logger.warning(f"Error rendering shape {idx} on slide {ctx.page_num}: {e}")
197
+ return (idx, None)
198
+
199
+ shape_workers = int(os.environ.get("MAX_WORKERS", 3))
200
+ with concurrent.futures.ThreadPoolExecutor(max_workers=shape_workers) as img_executor:
201
+ results = list(img_executor.map(process_rendered_shape, renderable_shapes))
202
+
203
+ for idx, desc in sorted(results, key=lambda x: x[0]):
204
+ if desc:
205
+ image_descriptions.append(desc)
206
+
207
+ # --- EXTRACT VISUAL CARD CONTAINERS ---
208
+ slide_h = getattr(ctx.slide, 'height', 6858000) or 6858000
209
+ slide_w = getattr(ctx.slide, 'width', 12192000) or 12192000
210
+ card_boxes = get_pptx_card_boxes(ctx.slide)
211
+
212
+ # --- 2D SPATIAL READING ORDER & IN-PLACE SHAPE RENDERING ---
213
+ ordered_shapes = get_spatial_shape_reading_order(ctx.slide.shapes, slide_h, slide_w)
214
+ raw_elements = []
215
+ extracted_shape_emfs = set()
216
+
217
+ for shape in ordered_shapes:
218
+ shape_rect = {
219
+ 'left': int(getattr(shape, 'left', 0) or 0),
220
+ 'top': int(getattr(shape, 'top', 0) or 0),
221
+ 'width': int(getattr(shape, 'width', 0) or 0),
222
+ 'height': int(getattr(shape, 'height', 0) or 0),
223
+ }
224
+ is_hdr = is_slide_header_shape(shape, slide_h, slide_w)
225
+ # Tables rendered in-place as markdown tables (not duplicated)
226
+ if getattr(shape, 'has_table', False) or getattr(shape, 'shape_type', None) == MSO_SHAPE_TYPE.TABLE:
227
+ table_md = format_table_markdown(shape.table)
228
+ if table_md:
229
+ raw_elements.append({**shape_rect, 'text': table_md, 'type': 'table'})
230
+ elif getattr(shape, 'has_text_frame', False):
231
+ extracted_text = extract_text_from_shape(shape, is_header=is_hdr)
232
+ if extracted_text:
233
+ raw_elements.append({**shape_rect, 'text': extracted_text, 'type': 'header' if is_hdr else 'text'})
234
+ elif getattr(shape, 'shape_type', None) == MSO_SHAPE_TYPE.GROUP or hasattr(shape, 'shapes'):
235
+ extracted_text = extract_text_from_shape(shape, is_header=is_hdr)
236
+ if extracted_text:
237
+ raw_elements.append({**shape_rect, 'text': extracted_text, 'type': 'header' if is_hdr else 'text'})
238
+
239
+ # Check for embedded EMF/WMF vector drawings/tables inside shape to preserve spatial order
240
+ shape_el = getattr(shape, '_element', None)
241
+ if shape_el is not None and hasattr(ctx.slide, 'part') and hasattr(ctx.slide.part, 'rels'):
242
+ for blip in shape_el.xpath('.//a:blip'):
243
+ embed = blip.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
244
+ if embed and embed in ctx.slide.part.rels:
245
+ rel = ctx.slide.part.rels[embed]
246
+ target_ref = getattr(rel, 'target_ref', '') or ''
247
+ target_lower = target_ref.lower()
248
+ if target_lower.endswith('.emf') or target_lower.endswith('.wmf'):
249
+ media_clean = 'ppt/' + target_ref.replace('../', '') if not target_ref.startswith('ppt/') else target_ref
250
+ extracted_shape_emfs.add(media_clean)
251
+ try:
252
+ emf_blob = rel.target_part.blob
253
+ emf_text = extract_emf_tables_and_text(emf_blob)
254
+ if emf_text and emf_text.strip():
255
+ raw_elements.append({**shape_rect, 'text': emf_text.strip(), 'type': 'emf_table'})
256
+ except Exception:
257
+ pass
258
+
259
+ # --- SmartArt / Diagrams ---
260
+ slide_diagrams_text = extract_smartart_texts_per_slide(ctx.filename, ctx.page_num)
261
+ if ctx.page_num in slide_diagrams_text:
262
+ smartart_items = slide_diagrams_text[ctx.page_num]
263
+ if smartart_items:
264
+ formatted_smartart = "\n".join(f"• {item}" if not item.startswith(('•', '-', '*', '#')) else item for item in smartart_items)
265
+ raw_elements.append({'text': formatted_smartart, 'type': 'smartart'})
266
+
267
+ # --- Charts ---
268
+ chart_data = extract_chart_data_from_pptx(ctx.filename, ctx.page_num)
269
+ if chart_data and ctx.page_num in chart_data:
270
+ for chart_md in chart_data[ctx.page_num]:
271
+ if chart_md:
272
+ raw_elements.append({'text': chart_md, 'type': 'chart'})
273
+
274
+ # --- EMF Vector Graphics Tables / Text (fallback for any slide EMFs not attached to shapes) ---
275
+ emf_content = extract_emf_content_per_slide(ctx.filename, ctx.page_num)
276
+ if emf_content and ctx.page_num in emf_content:
277
+ for emf_md in emf_content[ctx.page_num]:
278
+ if emf_md and not any(el.get('type') == 'emf_table' and el.get('text', '').strip() == emf_md.strip() for el in raw_elements):
279
+ raw_elements.append({'text': emf_md, 'type': 'emf_table'})
280
+
281
+ # --- Embedded Excel Worksheets (fallback if no EMF vector tables) ---
282
+ embedded_excel = extract_embedded_excel_tables(ctx.filename, ctx.page_num)
283
+ if embedded_excel and ctx.page_num in embedded_excel:
284
+ for excel_md in embedded_excel[ctx.page_num]:
285
+ if excel_md and not any(el.get('type') == 'emf_table' for el in raw_elements):
286
+ raw_elements.append({'text': excel_md, 'type': 'embedded_table'})
287
+
288
+ # --- Image Descriptions (if any) ---
289
+ if image_descriptions:
290
+ for desc in image_descriptions:
291
+ raw_elements.append({'text': desc, 'type': 'image'})
292
+
293
+ # --- Apply Visual Card / Box Decorations (---) ---
294
+ decorated_elements = apply_pptx_box_decorations(raw_elements, card_boxes)
295
+
296
+ # --- Slide Notes (Speaker Notes) ---
297
+ if ctx.slide.has_notes_slide and ctx.slide.notes_slide.notes_text_frame:
298
+ notes_text = ctx.slide.notes_slide.notes_text_frame.text.strip()
299
+ if notes_text:
300
+ decorated_elements.append(f"Notes:\n{notes_text}")
301
+
302
+ # Build clean page header and body
303
+ header = f"[Slide {ctx.page_num} | File: {ctx.display_name}]"
304
+ body_text = "\n\n".join(el for el in decorated_elements if el.strip())
305
+ full_content = header + "\n\n" + body_text if body_text else header
306
+
307
+ return {
308
+ "content": full_content,
309
+ "chunkIndex": ctx.page_num,
310
+ "filename": ctx.display_name
311
+ }
312
+
313
+ def process_pptx_file(filename, original_filename=None, process_images=None, model=None):
314
+ display_name = os.path.basename(original_filename) if original_filename else os.path.basename(filename)
315
+ should_process_images = bool(process_images)
316
+ ppt = Presentation(filename)
317
+ seen_image_hashes = {}
318
+ image_data_store = {}
319
+ image_page_map = defaultdict(list)
320
+
321
+ lock = threading.Lock()
322
+
323
+ if should_process_images:
324
+ _ensure_aspose()
325
+ aspose_ppt = slides.Presentation(filename)
326
+ else:
327
+ aspose_ppt = None
328
+ slide_image_map = get_slide_image_mapping(filename)
329
+
330
+ contexts = [
331
+ SlideContext(
332
+ page_num=page_num,
333
+ slide=slide,
334
+ aspose_ppt=aspose_ppt,
335
+ slide_image_map=slide_image_map,
336
+ seen_image_hashes=seen_image_hashes,
337
+ image_data_store=image_data_store,
338
+ image_page_map=image_page_map,
339
+ filename=filename,
340
+ lock=lock,
341
+ display_name=display_name,
342
+ should_process_images=should_process_images,
343
+ model=model
344
+ )
345
+ for page_num, slide in enumerate(ppt.slides, start=1)
346
+ ]
347
+
348
+ try:
349
+ workers = int(os.environ.get("MAX_WORKERS", 3))
350
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
351
+ results = list(executor.map(process_pptx_slide, contexts))
352
+
353
+ sorted_results = sorted(results, key=lambda x: x["chunkIndex"])
354
+ return sorted_results
355
+ finally:
356
+ if aspose_ppt is not None:
357
+ try:
358
+ aspose_ppt.dispose()
359
+ except Exception:
360
+ pass
361
+ aspose_ppt = None
362
+ del contexts
363
+ try:
364
+ del results
365
+ except NameError:
366
+ pass
367
+ gc.collect()