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.
- super_document_processor/__init__.py +28 -0
- super_document_processor/document_processors/process_docx.py +824 -0
- super_document_processor/document_processors/process_images.py +39 -0
- super_document_processor/document_processors/process_msg.py +62 -0
- super_document_processor/document_processors/process_pdf.py +715 -0
- super_document_processor/document_processors/process_plain_text.py +25 -0
- super_document_processor/document_processors/process_pptx.py +367 -0
- super_document_processor/main.py +826 -0
- super_document_processor/processor.py +135 -0
- super_document_processor/utils/aspose_slide_renderer.py +103 -0
- super_document_processor/utils/chunker.py +127 -0
- super_document_processor/utils/docx_image_utils.py +167 -0
- super_document_processor/utils/docx_xml_parser.py +73 -0
- super_document_processor/utils/optimizer.py +49 -0
- super_document_processor/utils/pdf_image_utils.py +90 -0
- super_document_processor/utils/pdf_table_utils.py +627 -0
- super_document_processor/utils/pptx_components_extract.py +2197 -0
- super_document_processor/utils/pptx_converter.py +170 -0
- super_document_processor/utils/vlm.py +176 -0
- super_document_processor-0.1.3.dist-info/METADATA +376 -0
- super_document_processor-0.1.3.dist-info/RECORD +25 -0
- super_document_processor-0.1.3.dist-info/WHEEL +5 -0
- super_document_processor-0.1.3.dist-info/entry_points.txt +2 -0
- super_document_processor-0.1.3.dist-info/licenses/LICENSE +21 -0
- super_document_processor-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -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()
|